403ab7c9c2b204f487bb2f86260ffdab355e9517
jcasper
  Wed Aug 19 05:49:25 2026 -0700
Faceted composites should apply the active sort order to the tracks being
displayed; changing the sort changes the display order.  We also preserve that order when
returning to the page.  refs #36320

diff --git src/hg/cartDump/cartDump.c src/hg/cartDump/cartDump.c
index f959fb22f23..af5b1e9ddc3 100644
--- src/hg/cartDump/cartDump.c
+++ src/hg/cartDump/cartDump.c
@@ -27,51 +27,87 @@
  * IDs (if data types are also used) or just the ends of track names.
  *
  * If the user adds a new data element, we turn on all associated tracks for the
  * active data types.  If a user adds a new data type, we turn on all associated
  * tracks for the active data elements.  But if a data element/type combo was already
  * checked, we don't re-enable that track because the user might have hidden it
  * manually.
  */
 {
 char *mName = "cartDump.metaDataId";
 if (!cgiVarExists(mName))
     return;
 
 char *mdid = cgiOptionalString(mName);
 char mdid_de_was[1024], mdid_de_now[1024], mdid_dt_was[1024], mdid_dt_now[1024];
+char mdid_sort[1024];
 safef(mdid_de_was, sizeof(mdid_de_was), "%s.de_was", mdid);
 safef(mdid_de_now, sizeof(mdid_de_now), "%s.de_now", mdid);
 safef(mdid_dt_was, sizeof(mdid_dt_was), "%s.dt_was", mdid);
 safef(mdid_dt_now, sizeof(mdid_dt_now), "%s.dt_now", mdid);
+safef(mdid_sort, sizeof(mdid_sort), "%s.facetSortOrder", mdid);
 // Grab the lists of which de/dt elements were on before and after the user
 // changed settings around
 struct slName *de_was_list = cgiStringList(mdid_de_was);
 struct slName *de_now_list = cgiStringList(mdid_de_now);
 struct slName *dt_was_list = cgiStringList(mdid_dt_was);
 struct slName *dt_now_list = cgiStringList(mdid_dt_now);
 
 // For faster lookup
 struct hash *de_was_hash = hashFromSlNameList(de_was_list);
 struct hash *de_now_hash = hashFromSlNameList(de_now_list);
 struct hash *dt_was_hash = hashFromSlNameList(dt_was_list);
 struct hash *dt_now_hash = hashFromSlNameList(dt_now_list);
 
 // Check if we sent dt_was/now variables, indicating that this composite has data types
 boolean hasDataTypes = (dt_was_list != NULL || dt_now_list != NULL);
 
 char subtrackSetting[1024];
+char prioritySetting[1024];
+
+// Remember how the faceted table was sorted, so that the next visit to the track
+// UI page comes back in the same order.  The value is a list of metadata field
+// names with directions ("field=+ field2=-"), mirroring the classic composite
+// "<track>.sortOrder" cart variable.  It's opaque here - only the javascript can
+// turn field names into an ordering, since only it reads the metadata file.
+char *facetSortOrder = cgiOptionalString(mdid_sort);
+if (isNotEmpty(facetSortOrder))
+    cartSetString(cart, mdid_sort, facetSortOrder);
+else
+    cartRemove(cart, mdid_sort);
+
+// Any ".priority" values left over from an earlier submission are stale, since the
+// loops below assign a fresh, dense 1..N to exactly the subtracks that are on now.
+// Clearing them first means the subtracks the user just turned off don't keep a
+// priority that would place them oddly if they're ever turned on again elsewhere.
+// Note that "<mdid>_*" can't match the composite's own "<mdid>.priority" - the
+// subtrack names are joined with an underscore rather than a dot.
+char priorityWild[1024];
+safef(priorityWild, sizeof(priorityWild), "%s_*.priority", mdid);
+cartRemoveLike(cart, priorityWild);
+
+// A subtrack the user has dragged up or down in the hgTracks image carries an
+// "_imgOrd" value, and flatTracksCmp() in hgTracks/imageV2.c sorts on that before
+// it ever looks at priority - so without this the dragged position would win and
+// the sort established here would appear to be ignored.  cartJustify() in cart.c
+// normally clears these whenever a ".priority" arrives as a CGI variable, but ours
+// are set with cartSetInt() below, well after cartJustify() has run for this
+// request, so we have to do it ourselves.  This is what the wiggle sort in
+// hgTracks.c does when it establishes a new order for a composite.
+char imgOrdWild[1024];
+safef(imgOrdWild, sizeof(imgOrdWild), "%s_*_imgOrd", mdid);
+cartRemoveLike(cart, imgOrdWild);
 
 if (hasDataTypes)
     {
     // Cross-product mode: tracks are identified by mdid_de_dt
 
     // Turn ON: (de_on x dt_now), (de_now x dt_on)
     // where de_on = de_now - de_was, dt_on = dt_now - dt_was
     for (struct slName *de = de_now_list; de != NULL; de = de->next)
         {
         boolean de_is_new = (hashLookup(de_was_hash, de->name) == NULL);
         for (struct slName *dt = dt_now_list; dt != NULL; dt = dt->next)
             {
             boolean dt_is_new = (hashLookup(dt_was_hash, dt->name) == NULL);
             if (de_is_new || dt_is_new)
                 {
@@ -87,57 +123,82 @@
     for (struct slName *de = de_was_list; de != NULL; de = de->next)
         {
         boolean de_is_off = (hashLookup(de_now_hash, de->name) == NULL);
         for (struct slName *dt = dt_was_list; dt != NULL; dt = dt->next)
             {
             boolean dt_is_off = (hashLookup(dt_now_hash, dt->name) == NULL);
             if (de_is_off || dt_is_off)
                 {
                 safef(subtrackSetting, sizeof(subtrackSetting),
                       "%s_%s_%s_sel", mdid, de->name, dt->name);
                 cartSetString(cart, subtrackSetting, "0");
                 }
             }
         }
 
+    // Set each shown subtrack's priority to match the order the data elements
+    // are currently sorted in the faceted table (de_now arrives in that order).
+    // Data elements are the outer loop so a sample's data-type subtracks stay
+    // contiguous, in the sample's sorted position.
+    int priority = 0;
+    for (struct slName *de = de_now_list; de != NULL; de = de->next)
+        {
+        for (struct slName *dt = dt_now_list; dt != NULL; dt = dt->next)
+            {
+            safef(prioritySetting, sizeof(prioritySetting),
+                  "%s_%s_%s.priority", mdid, de->name, dt->name);
+            cartSetInt(cart, prioritySetting, ++priority);
+            }
+        }
+
     }
 else
     {
     // Data elements only mode: tracks are identified by mdid_de
 
     // Turn ON: de_now - de_was
     for (struct slName *de = de_now_list; de != NULL; de = de->next)
         {
         if (hashLookup(de_was_hash, de->name) == NULL)
             {
             safef(subtrackSetting, sizeof(subtrackSetting),
                   "%s_%s_sel", mdid, de->name);
             cartSetString(cart, subtrackSetting, "1");
             }
         }
 
     // Turn OFF: de_was - de_now
     for (struct slName *de = de_was_list; de != NULL; de = de->next)
         {
         if (hashLookup(de_now_hash, de->name) == NULL)
             {
             safef(subtrackSetting, sizeof(subtrackSetting),
                   "%s_%s_sel", mdid, de->name);
             cartSetString(cart, subtrackSetting, "0");
             }
         }
 
+    // Set each shown subtrack's priority to match the order the data elements
+    // are currently sorted in the faceted table (de_now arrives in that order).
+    int priority = 0;
+    for (struct slName *de = de_now_list; de != NULL; de = de->next)
+        {
+        safef(prioritySetting, sizeof(prioritySetting),
+              "%s_%s.priority", mdid, de->name);
+        cartSetInt(cart, prioritySetting, ++priority);
+        }
+
     }
 
 hashFree(&de_was_hash);
 hashFree(&de_now_hash);
 hashFree(&dt_was_hash);
 hashFree(&dt_now_hash);
 
 slFreeList(&de_was_list);
 slFreeList(&de_now_list);
 slFreeList(&dt_was_list);
 slFreeList(&dt_now_list);
 
 cartRemove(cart, mName);
 cartRemove(cart, mdid_de_was);
 cartRemove(cart, mdid_de_now);