66ea6cb4eaf2036e464be55b295765ed5105a0fb max Wed Apr 22 09:42:25 2026 -0700 hgTrackUi/hui: render filter UI on supertrack configuration pages Supertracks (group of tracks with superTrack on, no data of their own) previously had no way to expose a shared filter: their trackDb stanza can declare filter.* / filterByRange.* / filterValues.*, but those settings were never drawn on the supertrack's hgTrackUi page. So users had to open each subtrack's own configuration page and set the same filter there, and the "lrSv.filter.svLen" cart namespace went unused. This change wires that up: - hgTrackUi.c (superTrackUi): after listing the subtracks, if the supertrack tdb declares any filter.* settings call scoreCfgUi() to render the standard filter UI. Cart variables land under the supertrack's own name (e.g. "lrSv.filter.svLen.min"), and subtracks already inherit them through cartOptionalStringClosestToHome() walking tdb->parent. Subtrack-level values continue to override. - hui.c: - buildFilterBy() / filterByValues(): tolerate a NULL autoSql object, so supertracks (which have no data table) don't errAbort when they declare filterValues.* of virtual aggregated fields. Missing-field errAbort still fires in the normal subtrack case. - scoreCfgUi() / cfgByCfgType(): when called with title == NULL (the supertrack filter path), suppress the default "

" separator and the "
" between the title bar and the filter block; the caller renders its own section heading. - asForTdb(): handle conn == NULL by returning NULL rather than crashing, since supertrack filter rendering has no associated sqlConnection. refs #37426 diff --git src/hg/lib/hui.c src/hg/lib/hui.c index cac6a74ed83..9ce7959c8b6 100644 --- src/hg/lib/hui.c +++ src/hg/lib/hui.c @@ -4001,34 +4001,37 @@ static filterBy_t *buildFilterBy(struct trackDb *tdb, struct cart *cart, struct asObject *as, struct trackDbFilter *tdbFilter, char *name) /* Build a filterBy_t structure from a FilterValues statement. */ { boolean isHighlight = startsWith("highlightValues.", tdbFilter->name); char *field = tdbFilter->fieldName; if (isEmpty(tdbFilter->setting)) errAbort("track %s: FilterValues setting of field '%s' must have a value.", tdb->track, tdbFilter->fieldName); char *value = cartUsualStringClosestToHome(cart, tdb, FALSE, tdbFilter->name, tdbFilter->setting); filterBy_t *filterBy; AllocVar(filterBy); filterBy->column = cloneString(field); filterBy->title = cloneString(field); /// title should come from AS file, or trackDb variable -struct asColumn *asCol = asColumnFind(as, field); +struct asColumn *asCol = (as != NULL) ? asColumnFind(as, field) : NULL; if (asCol != NULL) filterBy->title = asCol->comment; -else +else if (as != NULL && getLabelSetting(cart, tdb, field) == NULL) + // Only error when there's an autoSql but the field is missing AND there's + // no filterLabel override. superTracks/noData tdbs have as==NULL and can + // legitimately declare filterValues on virtual fields they only aggregate. errAbort("Track %s: Building filter on field %s which is not in AS file.", tdb->track, field); char *trackDbLabel = getLabelSetting(cart, tdb, field); if (trackDbLabel) filterBy->title = trackDbLabel; filterBy->useIndex = FALSE; filterBy->slValues = slNameListFromCommaEscaped(value); chopUpValues(filterBy); if (cart != NULL) { char suffix[256]; safef(suffix, sizeof(suffix), "%s.%s", isHighlight ? "highlightBy" : "filterBy", filterBy->column); boolean parentLevel = isNameAtParentLevel(tdb,tdb->track); if (cartLookUpVariableClosestToHome(cart,tdb,parentLevel,suffix,&(filterBy->htmlName))) { filterBy->slChoices = cartOptionalSlNameList(cart,filterBy->htmlName); @@ -4040,33 +4043,35 @@ { char *setting = getFilterValueDefaultsSetting(cart, tdb, field); filterBy->slChoices = slNameListFromCommaEscaped(setting); } struct dyString *dy = dyStringNew(128); dyStringPrintf(dy, "%s.%s.%s", name, isHighlight ? "highlightBy": "filterBy", filterBy->column); filterBy->htmlName = dy->string; 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); -if (as == NULL) - errAbort("Track %s: Unable to get autoSql for %s", tdb->track, name); filterBy_t *filterByList = NULL, *filter; struct trackDbFilter *fieldFilter; while ((fieldFilter = slPopHead(&trackDbFilters)) != NULL) { if ((filter = buildFilterBy(tdb, cart, as, fieldFilter, name)) != NULL) slAddHead(&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")) @@ -5935,32 +5940,33 @@ if (boxed) printf("
"); } if (boxed) { printf("
", COLOR_BG_ALTDEFAULT); if (title) printf("
%s Configuration
\n", title); } else if (title) printf("

%s  ", title ); -else - printf("

"); +// When !boxed and title==NULL, emit nothing: the caller (e.g. supertrack +// filter UI) already renders its own heading and doesn't want a stray +// empty paragraph. return boxed; } void cfgEndBox(boolean boxed) // Handle end of box and title for individual track type settings { if (boxed) puts("

"); } void snakeOption(struct cart *cart, char *name, char *title, struct trackDb *tdb) /* let the user choose to see the track in snake mode */ { if (!cfgOptionBooleanDefault("canSnake", TRUE)) return; @@ -6905,30 +6911,34 @@ } static int numericFiltersShowAll(char *db, struct cart *cart, struct trackDb *tdb, boolean *opened, boolean boxed, boolean parentLevel,char *name, char *title, boolean isHighlight) // Shows all *Filter style filters. Note that these are in random order and have no graceful title { int count = 0; struct trackDbFilter *trackDbFilters = NULL; if (isHighlight) trackDbFilters = tdbGetTrackNumHighlights(tdb); else trackDbFilters = tdbGetTrackNumFilters(tdb); if (trackDbFilters) { + // The
is a separator under the track's "Configuration" block title. + // Callers that don't emit a title (e.g. the supertrack filter UI that + // owns its own heading) pass title==NULL and don't want the extra break. + if (title != NULL) puts("
"); struct trackDbFilter *filter = NULL; struct sqlConnection *conn = NULL; if (!isHubTrack(db)) conn = hAllocConnTrack(db, tdb); struct asObject *as = asForTdb(conn, tdb); hFreeConn(&conn); while ((filter = slPopHead(&trackDbFilters)) != NULL) { char *field = filter->fieldName; char *scoreName = cloneString(filter->name); char *trackDbLabel = getLabelSetting(cart, tdb, field); if (as != NULL) @@ -10301,30 +10311,34 @@ char *db = sqlGetDatabase(conn); int exists = hashIntValDefault(hash, db, -1); if (exists < 0) { exists = sqlTableExists(conn, "tableDescriptions"); hashAddInt(hash, db, exists); } return (boolean)exists; } struct asObject *asFromTableDescriptions(struct sqlConnection *conn, char *table) // If there is a tableDescriptions table and it has an entry for table, return // a parsed autoSql object; otherwise return NULL. { struct asObject *asObj = NULL; +// Callers occasionally invoke asForTdb with conn=NULL (e.g. superTrack filter +// rendering that isn't tied to a data table). Nothing to look up in that case. +if (conn == NULL) + return NULL; if (tableDescriptionsExists(conn)) { char query[PATH_LEN*2]; // Try unsplit table first. sqlSafef(query, sizeof(query), "select autoSqlDef from tableDescriptions where tableName='%s'", table); char *asText = sqlQuickString(conn, query); // If no result try split table. if (asText == NULL) { sqlSafef(query, sizeof(query), "select autoSqlDef from tableDescriptions where tableName='chrN_%s'", table); asText = sqlQuickString(conn, query); } if (isNotEmpty(asText))