d121edc0bd2809f1d6de6185a497e6a288958479
braney
  Tue May 12 09:18:29 2026 -0700
hgConvert quickLift: skip pre-lifted tracks, append-and-merge hub file, per-track remove UI, refs #37535

In hgConvert / trackHubBuild:
- Skip tracks that already came from a quickLift hub (quickLiftUrl / quickLifted setting) so they don't get re-lifted to a new destination.
- Append new track stanzas to an existing per-source hub file instead of overwriting it; new stanzas get priorities after the existing max; duplicate track names are skipped. Also avoids re-emitting a parent supertrack that's already in the file.

New public quickLiftHubRemoveTrack(cart, sourceDb, trackName) in trackHub.c rewrites the per-source hub file with the named stanza removed plus all descendant stanzas (parent reference cascade, transitive).

hgTrackUi: adds a "Remove from QuickLift" link next to "Duplicate track" for any tdb carrying a quickLiftDb setting. The link hits hgTrackUi_op=quickLiftRemove which calls quickLiftHubRemoveTrack, hides the track in the cart, and 302s to hgTracks. The op argument cart var is qlSourceDb (renamed from quickLiftSourceDb to avoid colliding with the quickLift.* prefix used elsewhere; values cloned out of the cart hash before cartRemove so the helper doesn't see freed strings).

hgTracks: adds a small "x" icon (printQuickLiftDelIcon) on tracks in a quickLift group, suppressed on the synthetic bigQuickLiftChain track. JS onQuickLiftDelIconClick fires the same hgTrackUi_op endpoint via synchronous XHR and removes every TD whose icon matches the deleted data-track, so the row goes away in both the QuickLift group and the Visible Tracks group.

hubConnect cart handling fixes shaken out by the above:
- hubConnectRemakeTrackHubVar's cart-var prefix is now "quickLift." (with the trailing dot) instead of "quickLift", so unrelated keys like qlSourceDb no longer get parsed as hubId/db and crash cart loading on every CGI. Also skips entries whose hubStatus lookup returned NULL.
- hubConnectStatusListFromCart no longer calls removeQuickListReference when the current db isn't the lift's destination. A side trip to another assembly between two lifts to the same destination was deleting the earlier attachment's cart var; just skip attaching this load and leave the cart alone so the lift survives the round trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

diff --git src/hg/js/hgTracks.js src/hg/js/hgTracks.js
index cd10f06316e..20bf397876c 100644
--- src/hg/js/hgTracks.js
+++ src/hg/js/hgTracks.js
@@ -4912,30 +4912,53 @@
 function onTrackDelIconClick (ev) {
     /* delete custom track if user clicks its trash icon */
     // https://genome.ucsc.edu/cgi-bin/hgCustom?hgsid=1645697744_i0Yp2Di71NytSDdb6r0vUbupIvKO&hgct_do_delete=delete&hgct_del_ct_UserTrack_3545=on
     var divEl = ev.target.closest("div"); // must use .closest(), as user can click on either the SVG or the DIV space.
     var trackName = divEl.getAttribute("data-track");
     var hgsid = getHgsid();
     var url = 'hgCustom?hgsid='+hgsid+'&hgct_do_delete=delete&hgct_del_'+trackName+'=on';
     xhttp = new XMLHttpRequest();
     // this cannot be asyncronous, as users can click quickly here and the hgCustom calls above cannot run in parallel
     // since we store custom tracks as a text file, not mysql tables
     xhttp.open("GET", url, false);
     xhttp.send();
     divEl.closest("td").remove();
 }
 
+function onQuickLiftDelIconClick (ev) {
+    /* Remove this track's stanza from the quickLift hub in trash, and
+     * remove all rows referencing it from the page (it can appear both in
+     * its hub group and in the Visible Tracks group). */
+    var divEl = ev.target.closest("div");
+    var trackName = divEl.getAttribute("data-track");
+    var sourceDb = divEl.getAttribute("data-sourcedb");
+    var hgsid = getHgsid();
+    var url = 'hgTrackUi?hgsid=' + hgsid +
+              '&hgTrackUi_op=quickLiftRemove' +
+              '&g=' + encodeURIComponent(trackName) +
+              '&qlSourceDb=' + encodeURIComponent(sourceDb);
+    var xhttp = new XMLHttpRequest();
+    // synchronous: the hub file is a shared text file, parallel rewrites would race
+    xhttp.open("GET", url, false);
+    xhttp.send();
+    var selector = 'div.quickLiftDelIcon[data-track="' + trackName + '"]';
+    document.querySelectorAll(selector).forEach(function(d) {
+        var td = d.closest("td");
+        if (td) td.remove();
+    });
+}
+
 
   //////////////////////////////////
  //// popup (aka modal dialog) ////
 //////////////////////////////////
 var popUp = {
 
     trackName:            "",
     trackDescriptionOnly: false,
     saveAllVars:          null,
 
     cleanup: function ()
     {  // Clean out the popup box on close
         if ($('#hgTrackUiDialog').html().length > 0 ) {
             // clear out html after close to prevent problems caused by duplicate html elements
             $('#hgTrackUiDialog').html("");
@@ -6885,30 +6908,33 @@
 };
 
   ///////////////
  //// READY ////
 ///////////////
 $(document).ready(function()
 {
     imageV2.moveTiming();
 
     // hg.conf will turn this on 2020-10 - Hiram
     if (window.mouseOverEnabled) { mouseOver.addListener(); }
 
     // custom tracks get little trash icons
     $("div.trackDeleteIcon").on("click", onTrackDelIconClick );
 
+    // quickLift tracks get a little 'x' icon
+    $("div.quickLiftDelIcon").on("click", onQuickLiftDelIconClick );
+
     // on Safari the back button doesn't call the ready function.  Reload the page if
     // the back button was pressed.
     $(window).on("pageshow", function(event) {
         if (event.originalEvent.persisted) {
                 window.location.reload() ;
         }
     });
 
     // The page may be reached via browser history (back button)
     // If so, then this code should detect if the image has been changed via js/ajax
     // and will reload the image if necessary.
     // NOTE: this is needed for IE but other browsers can detect the dirty page much earlier
     if (!imageV2.backSupport) {
         if (imageV2.isDirtyPage()) {
             // mark as non dirty to avoid infinite loop in chrome.