24fece8377dedf184e8e872325ff2af175076552 max Mon May 4 05:51:05 2026 -0700 hui: fix duplicated filter UI on bigGenePred; tighten supertrack filter changes Two issues, both surfaced reviewing #37426: 1. bigGenePred tracks (e.g. openprot) rendered every filterBy/highlightBy <select> twice with the same id. dropdownchecklist.js binds the first instance only, leaving the second copy display:none — users saw filter labels under the second copy with no input fields under them. The duplication was pre-existing (since 7d9e5c172d1b reorganized genePredCfgUi/scoreCfgUi): genePredCfgUi rendered filterBy inline, then for bigGenePred called scoreCfgUi which rendered filterBy again. Skip the inline rendering for bigGenePred and let scoreCfgUi own it; plain genePred (which doesn't reach scoreCfgUi here) is unchanged. 2. Tighten the supertrack filter changes from 66ea6cb4eaf: - cfgBeginBoxAndTitle: revert the title==NULL "emit nothing" branch. The supertrack flow exits scoreCfgUi before reaching this anyway, so the change was only affecting other title==NULL callers like interactUi.c — pure unintended side effect. - numericFiltersShowAll: revert the title==NULL <BR> suppression for the same reason. The supertrack <h3> heading already provides enough separation. - buildFilterBy: tighten the errAbort relaxation. The previous condition (as != NULL && getLabelSetting == NULL) let a typo'd filterValues.<field> on a regular subtrack silently render a useless filter whenever a matching filterLabel.<field> was present. Restrict the relaxation to as==NULL (the supertrack / noData case). Add a warn() before errAbort so hub makers reading the page see an actionable hint, not just an opaque abort. refs #37426 diff --git src/hg/lib/hui.c src/hg/lib/hui.c index 3304df6a7b9..fe7f06d8f3c 100644 --- src/hg/lib/hui.c +++ src/hg/lib/hui.c @@ -4004,35 +4004,44 @@ { 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 = (as != NULL) ? asColumnFind(as, field) : NULL; if (asCol != NULL) filterBy->title = asCol->comment; -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); +else if (as != NULL) + { + // We have autoSql but the field is missing — that's a typo or an + // outdated trackDb. Emit a visible warning so a hub maker reading the + // page sees a clear hint, then abort. (The as==NULL case is reserved + // for superTracks/noData tdbs that legitimately declare filterValues + // on virtual fields they only aggregate over.) + warn("Track %s: filter on field '%s' is not in the autoSql file. " + "Check the field name in your trackDb filterValues.* / " + "filterByRange.* settings against the .as schema.", + tdb->track, field); + 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); freeMem(filterBy->htmlName); @@ -5940,33 +5949,32 @@ if (boxed) printf("<BR>"); } if (boxed) { printf("<TABLE class='blueBox"); char *view = tdbGetViewName(tdb); if (view != NULL) printf(" %s",view); printf("' style='background-color:%s;'><TR><TD>", COLOR_BG_ALTDEFAULT); if (title) printf("<CENTER><B>%s Configuration</B></CENTER>\n", title); } else if (title) printf("<p><B>%s </b>", title ); -// 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. +else + printf("<p>"); return boxed; } void cfgEndBox(boolean boxed) // Handle end of box and title for individual track type settings { if (boxed) puts("</td></tr></table>"); } 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; @@ -6911,34 +6919,30 @@ } 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 <BR> 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("<BR>"); 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) @@ -7940,52 +7944,61 @@ if (trackDbSettingClosestToHomeOn(tdb, "nmdFilter")) { boolean nmdDefault = FALSE; safef(varName, sizeof(varName), "hgt.%s.nmdFilter", name); nmdDefault = cartUsualBoolean(cart,varName, FALSE); // TODO: var name (hgt prefix) needs changing before ClosesToHome can be used printf("<p><b>Filter out NMD targets.</b>"); cgiMakeCheckBox(varName, nmdDefault); } if (!sameString(tdb->track, "tigrGeneIndex") && !sameString(tdb->track, "ensGeneNonCoding") && !sameString(tdb->track, "encodeGencodeRaceFrags")) baseColorDropLists(cart, tdb, name); +// For bigGenePred, scoreCfgUi() (called below) renders the filterBy/highlightBy +// controls itself. Rendering them here too produces two <SELECT> elements with +// the same id, and dropdownchecklist.js only binds the first; the second copy +// stays display:none and shows no input fields. So skip the inline pass for +// bigGenePred and let scoreCfgUi own it. +boolean isBigGenePred = startsWith("bigGenePred", tdb->type); +if (!isBigGenePred) + { filterBy_t *filterBySet = filterBySetGet(tdb,cart,name); if (filterBySet != NULL) { printf("<BR>"); filterBySetCfgUi(cart,tdb,filterBySet,FALSE, name); filterBySetFree(&filterBySet); } filterBy_t *highlightBySet = highlightBySetGet(tdb,cart,name); if (highlightBySet != NULL) { printf("<BR>"); highlightBySetCfgUi(cart,tdb,highlightBySet,FALSE, name, TRUE); filterBySetFree(&highlightBySet); } + } squishyPackOption(cart, name, title, tdb); filterNameOption(cart, name, tdb); wigOption(cart, name, title, tdb); cfgEndBox(boxed); // N.B. scoreCfgUi maybe creates a box, so this is called after cfgEndBox // unclear what the logic is with box creation here -if (startsWith("bigGenePred", tdb->type)) +if (isBigGenePred) { char *scoreMax = trackDbSettingClosestToHome(tdb, SCORE_FILTER _MAX); int maxScore = (scoreMax ? sqlUnsigned(scoreMax):1000); scoreCfgUi(db, cart,tdb,name,title,maxScore,boxed); } } static boolean isSpeciesOn(struct cart *cart, struct trackDb *tdb, char *species, char *option, int optionSize, boolean defaultState) /* check the cart to see if species is turned off or on (default is defaultState) */ { boolean parentLevel = isNameAtParentLevel(tdb,option); if (*option == '\0') safef(option, optionSize, "%s.%s", tdb->track, species); else {