b3ef92d687a2a170eab7758ff6b197e229675eee
braney
  Fri May 8 13:23:17 2026 -0700
add filterPriority and highlightPriority trackDb settings, refs #29223

New filterPriority.<fieldName> setting controls the display order of filter
controls on the track configuration page.  Lower values display first;
filters without an explicit priority sort after those that have one.  A
companion highlightPriority.<fieldName> setting orders highlight controls
the same way.  A single priority entry covers all filter styles on the
field (filter.*, filterText.*, filterValues.*).

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

diff --git src/hg/lib/hui.c src/hg/lib/hui.c
index fe7f06d8f3c..9335dccda83 100644
--- src/hg/lib/hui.c
+++ src/hg/lib/hui.c
@@ -4061,32 +4061,34 @@
 return filterBy;
 }
 
 filterBy_t *filterByValues(struct trackDb *tdb, struct cart *cart, struct trackDbFilter *trackDbFilters, char *name)
 /* Build a filterBy_t list from tdb variables of the form *FilterValues */
 {
 // Not every tdb has an autoSql: superTracks and tracks pointing at a
 // bigData file that isn't reachable at UI time both return NULL here.
 // That's fine for filterValues.* settings as long as a filterLabel.*
 // override is provided; buildFilterBy() already tolerates a NULL `as`.
 struct asObject *as = asForTdb(NULL, tdb);
 filterBy_t *filterByList = NULL, *filter;
 struct trackDbFilter *fieldFilter;
 while ((fieldFilter = slPopHead(&trackDbFilters)) != NULL)
     {
+    // slAddTail (not slAddHead) keeps the priority-sorted order from tdbGetTrackFilters
+    // since filterBySetCfgUi displays the list head-to-tail.
     if ((filter = buildFilterBy(tdb, cart, as, fieldFilter, name)) != NULL)
-        slAddHead(&filterByList, filter);
+        slAddTail(&filterByList, filter);
     }
 return filterByList;
 }
 
 filterBy_t *filterBySetGetGuts(struct trackDb *tdb, struct cart *cart, char *name, char *subName, char *settingName)
 // Gets one or more "filterBy" settings (ClosestToHome).  returns NULL if not found
 {
 // first check to see if this tdb is using "new" FilterValues cart variables
 if (differentString(subName, "highlightBy"))
     {
     struct trackDbFilter *trackDbFilters = tdbGetTrackFilterByFilters( tdb);
     if (trackDbFilters)
         return filterByValues(tdb, cart, trackDbFilters, name);
     }
 else
@@ -6785,73 +6787,114 @@
     safef(altLabel, sizeof(altLabel), "%s", (filterByRange?"": "colspan=3"));
     if (minLimit != NO_VALUE && maxLimit != NO_VALUE)
         printf("<TD align='left'%s> (%s to %s)",altLabel,shorterDouble(minLimit), shorterDouble(maxLimit));
     else if (minLimit != NO_VALUE)
         printf("<TD align='left'%s> (minimum %s)",altLabel,shorterDouble(minLimit));
     else if (maxLimit != NO_VALUE)
         printf("<TD align='left'%s> (maximum %s)",altLabel,shorterDouble(maxLimit));
     else
         printf("<TD align='left'%s",altLabel);
     puts("</TR>");
     return TRUE;
     }
 return FALSE;
 }
 
+static boolean isHighlightFilterPrefix(char *lowName)
+// Distinguish highlight filter passes from filter passes so we look up the right priority var.
+{
+return startsWith("highlight", lowName);
+}
+
+static double lookupFilterPriority(struct trackDb *tdb, char *fieldName, boolean isHighlight)
+// Find a filterPriority.<field> / <field>FilterPriority setting (or highlight equivalent) in tdb.
+// Returns TRACKDB_FILTER_DEFAULT_PRIORITY when no setting is found.
+{
+char setting[1024];
+char *priorityLow = isHighlight ? HIGHLIGHT_PRIORITY_NAME_LOW : FILTER_PRIORITY_NAME_LOW;
+char *priorityCap = isHighlight ? HIGHLIGHT_PRIORITY_NAME_CAP : FILTER_PRIORITY_NAME_CAP;
+safef(setting, sizeof setting, "%s.%s", priorityLow, fieldName);
+char *val = trackDbSetting(tdb, setting);
+if (val == NULL)
+    {
+    safef(setting, sizeof setting, "%s%s", fieldName, priorityCap);
+    val = trackDbSetting(tdb, setting);
+    }
+if (val == NULL)
+    return TRACKDB_FILTER_DEFAULT_PRIORITY;
+return atof(val);
+}
+
+static int trackDbFilterPriCmp(const void *va, const void *vb)
+// Sort trackDbFilters by priority ascending; tiebreak by name for determinism.
+{
+const struct trackDbFilter *a = *((struct trackDbFilter **)va);
+const struct trackDbFilter *b = *((struct trackDbFilter **)vb);
+if (a->priority < b->priority)
+    return -1;
+if (a->priority > b->priority)
+    return 1;
+return strcmp(a->name, b->name);
+}
+
 struct trackDbFilter *tdbGetTrackFilters( struct trackDb *tdb, char * lowWild, char * lowName, char * capWild, char * capName)
 // figure out which of the ways to specify trackDb filter variables we're using
 // and return the setting
 {
 struct trackDbFilter *trackDbFilterList = NULL;
+boolean isHighlight = isHighlightFilterPrefix(lowName);
 struct slName *filterSettings = trackDbSettingsWildMatch(tdb, lowWild);
 
 if (filterSettings)
     {
     struct trackDbFilter *tdbFilter;
     struct slName *filter = NULL;
     while ((filter = slPopHead(&filterSettings)) != NULL)
         {
         AllocVar(tdbFilter);
         slAddHead(&trackDbFilterList, tdbFilter);
         tdbFilter->name = cloneString(filter->name);
         tdbFilter->setting = trackDbSetting(tdb, filter->name);
         tdbFilter->fieldName = extractFieldNameNew(filter->name, lowName);
+        tdbFilter->priority = lookupFilterPriority(tdb, tdbFilter->fieldName, isHighlight);
         setAsNewFilterType(tdb, tdbFilter->name, tdbFilter->fieldName);
         }
     }
 filterSettings = trackDbSettingsWildMatch(tdb, capWild);
 
 if (filterSettings)
     {
     struct trackDbFilter *tdbFilter;
     struct slName *filter = NULL;
     while ((filter = slPopHead(&filterSettings)) != NULL)
         {
         if (differentString(filter->name,NO_SCORE_FILTER))
             {
             AllocVar(tdbFilter);
             slAddHead(&trackDbFilterList, tdbFilter);
             tdbFilter->name = cloneString(filter->name);
             tdbFilter->setting = trackDbSetting(tdb, filter->name);
             tdbFilter->fieldName = extractFieldNameOld(filter->name, capName);
+            tdbFilter->priority = lookupFilterPriority(tdb, tdbFilter->fieldName, isHighlight);
             char *name;
             if ((name = isNewFilterType(tdb, tdbFilter->fieldName) ) != NULL)
                 errAbort("error specifying a field's filters in both old (%s) and new format (%s).", tdbFilter->name, name);
             }
         }
     }
 
+slSort(&trackDbFilterList, trackDbFilterPriCmp);
 return trackDbFilterList;
 }
 
 struct trackDbFilter *tdbGetTrackNumFilters( struct trackDb *tdb)
 // get the number filters out of trackDb
 {
 return tdbGetTrackFilters( tdb, FILTER_NUMBER_WILDCARD_LOW, FILTER_NUMBER_NAME_LOW, FILTER_NUMBER_WILDCARD_CAP, FILTER_NUMBER_NAME_CAP);
 }
 
 struct trackDbFilter *tdbGetTrackTextFilters( struct trackDb *tdb)
 // get the text filters out of trackDb
 {
 return tdbGetTrackFilters( tdb, FILTER_TEXT_WILDCARD_LOW, FILTER_TEXT_NAME_LOW, FILTER_TEXT_WILDCARD_CAP, FILTER_TEXT_NAME_CAP);
 }