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/hgTracks/hgTracks.c src/hg/hgTracks/hgTracks.c
index 2423e02916d..cd2668d9b1e 100644
--- src/hg/hgTracks/hgTracks.c
+++ src/hg/hgTracks/hgTracks.c
@@ -8741,36 +8741,61 @@
 char buf[256];
 safef(buf, sizeof buf, "Configure %s multi-region display mode", 
                         isPressed ? "or exit" : "");
 hButtonNoSubmitMaybePressed("hgTracksConfigMultiRegionPage", "Multi-region", buf,
             "popUpHgt.hgTracks('multi-region config'); return false;", isPressed);
 }
 
 static void printTrackDelIcon(struct track *track)
 /* little track icon after track name. Github uses SVG elements for all icons, apparently that is faster */
 /* we probably should have a library with all the icons, at least for the <svg> part */
 {
     hPrintf("<div title='Delete this custom track' data-track='%s' class='trackDeleteIcon'><svg xmlns='http://www.w3.org/2000/svg' height='0.8em' viewBox='0 0 448 512'><!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d='M135.2 17.7C140.6 6.8 151.7 0 163.8 0H284.2c12.1 0 23.2 6.8 28.6 17.7L320 32h96c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 96 0 81.7 0 64S14.3 32 32 32h96l7.2-14.3zM32 128H416V448c0 35.3-28.7 64-64 64H96c-35.3 0-64-28.7-64-64V128zm96 64c-8.8 0-16 7.2-16 16V432c0 8.8 7.2 16 16 16s16-7.2 16-16V208c0-8.8-7.2-16-16-16zm96 0c-8.8 0-16 7.2-16 16V432c0 8.8 7.2 16 16 16s16-7.2 16-16V208c0-8.8-7.2-16-16-16zm96 0c-8.8 0-16 7.2-16 16V432c0 8.8 7.2 16 16 16s16-7.2 16-16V208c0-8.8-7.2-16-16-16z'/></svg></div>", track->track);
 
 }
 
+static void printQuickLiftDelIcon(struct track *track, char *sourceDb)
+/* little 'x' icon next to a track in a quickLift group; clicking it removes
+ * the track from the quickLift hub. */
+{
+    hPrintf("<div title='Remove this track from the QuickLift group' "
+            "data-track='%s' data-sourcedb='%s' class='quickLiftDelIcon'>"
+            "<svg xmlns='http://www.w3.org/2000/svg' height='0.8em' viewBox='0 0 384 512'>"
+            "<!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License "
+            "- https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->"
+            "<path d='M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 "
+            "86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4"
+            "c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 "
+            "32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z'/></svg></div>",
+            track->track, sourceDb);
+}
+
 static void printTrackLink(struct track *track)
 /* print a link hgTrackUi with shortLabel and various icons and mouseOvers */
 {
 if (sameOk(track->groupName, "user"))
     printTrackDelIcon(track);
 
+char *quickLiftSourceDb = (track->tdb != NULL) ?
+        trackDbSetting(track->tdb, "quickLiftDb") : NULL;
+/* skip the synthetic "Alignment Differences" track -- it's added at runtime
+ * by hubConnect, not from the hub file, so removing it from the file is a
+ * no-op and it would reappear on the next page load. */
+if (quickLiftSourceDb != NULL &&
+    (track->tdb->type == NULL || !startsWith("bigQuickLiftChain", track->tdb->type)))
+    printQuickLiftDelIcon(track, quickLiftSourceDb);
+
 if (track->hasUi)
     {
     char *url = trackUrl(track->track, chromName);
     char *longLabel = replaceChars(track->longLabel, "\"", "&quot;");
 
     struct dyString *dsMouseOver = dyStringCreate("%s", longLabel);
     struct trackDb *tdb = track->tdb;
 
     if (tdbIsSuper(tdb))
         dyStringPrintf(dsMouseOver, " - container, %d tracks ", slCount(tdb->children));
     else if (tdbIsComposite(tdb))
         dyStringPrintf(dsMouseOver, " - container, %d subtracks ", slCount(tdb->subtracks));
 
     // Print icons before the title when any are defined
     hPrintIcons(track->tdb);