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/hgTrackUi/hgTrackUi.c src/hg/hgTrackUi/hgTrackUi.c
index e664821afe3..32bafce637f 100644
--- src/hg/hgTrackUi/hgTrackUi.c
+++ src/hg/hgTrackUi/hgTrackUi.c
@@ -3268,30 +3268,54 @@
     hashElFreeList(&elList);
     }
 jsonWriteListEnd(jw);
 
 jsonWriteString(jw, "mdid", (char *)metaDataId);
 jsonWriteString(jw, "primaryKey", (char *)primaryKey);  // must exist
 if (maxCheckboxes) // only if present in trackDb.settings entry
     jsonWriteString(jw, "maxCheckboxes", (char *)maxCheckboxes);
 if (colorSettingsUrl) // only if present in trackDb.settings entry
     jsonWriteString(jw, "colorSettingsUrl", cgiEncode((char *)colorSettingsUrl));
 jsonWriteString(jw, "metadataUrl", cgiEncode((char *)metaDataUrl));
 jsonWriteString(jw, "track", tdb->track);
 char *defaultSortField = trackDbSetting(tdb, "defaultSortField");
 if (isNotEmpty(defaultSortField))
     jsonWriteString(jw, "defaultSortField", defaultSortField);
+// How the user last sorted the faceted table, if they have.  This overrides
+// defaultSortField in the javascript, which is the only side that can act on it -
+// turning field names into column positions needs the metadata file.  The value
+// comes back from the cart, so it's untrusted, and this JSON lands inside a
+// <script> block: only pass through the characters "field=+ field2=-" needs.
+char facetSortVar[1024];
+safef(facetSortVar, sizeof(facetSortVar), "%s.facetSortOrder", metaDataId);
+char *facetSortOrder = cartOptionalString(cart, facetSortVar);
+if (isNotEmpty(facetSortOrder))
+    {
+    boolean clean = TRUE;
+    char *c = facetSortOrder;
+    for ( ; *c != '\0'; c++)
+        {
+        if (!isalnum((unsigned char)*c) && *c != '_' && *c != '.' && *c != '-'
+            && *c != '+' && *c != '=' && *c != ' ')
+            {
+            clean = FALSE;
+            break;
+            }
+        }
+    if (clean)
+        jsonWriteString(jw, "facetSortOrder", facetSortOrder);
+    }
 if (isNotEmpty(subtrackUrls))
     {
     struct slPair *pairs = slPairListFromString((char *)subtrackUrls, TRUE);
     if (pairs)
         {
         jsonWriteObjectStart(jw, "subtrackUrls");
         for (struct slPair *p = pairs; p != NULL; p = p->next)
             {
             char *encoded = htmlEncode((char *)p->val);
             jsonWriteString(jw, p->name, encoded);
             freeMem(encoded);
             }
         jsonWriteObjectEnd(jw);
         }
     slPairFreeValsAndList(&pairs);