47ea57080b515e5dad5f658c58feb8944a7e7d61 chmalee Thu Jan 29 15:30:26 2026 -0800 Replace clade/assembly dropdowns with a search bar on most CGIs. Add a recents list to hgGateway and to the species bar and to the 'Genomes' dropdown menu. Track recently selected species in localStorage. Add toGenome and fromGenome arguemnts to hubApi/liftOver in order to find appropriate liftover assemblies, refs #36232 diff --git src/hg/hgConvert/hgConvert.c src/hg/hgConvert/hgConvert.c index 325de74de1d..b6c0212324b 100644 --- src/hg/hgConvert/hgConvert.c +++ src/hg/hgConvert/hgConvert.c @@ -14,32 +14,33 @@ #include "htmshell.h" #include "hdb.h" #include "hui.h" #include "cart.h" #include "web.h" #include "chain.h" #include "liftOver.h" #include "liftOverChain.h" #include "chromInfo.h" #include "net.h" #include "genark.h" #include "trackHub.h" #include "hubConnect.h" #include "quickLift.h" #include "chromAlias.h" -#ifdef NOTNOW #include "jsHelper.h" +#include "hPrint.h" +#ifdef NOTNOW #include "bigChain.h" #include "bigLink.h" #endif /* CGI Variables */ #define HGLFT_TOORG_VAR "hglft_toOrg" /* TO organism */ #define HGLFT_TODB_VAR "hglft_toDb" /* TO assembly */ #define HGLFT_DO_CONVERT "hglft_doConvert" /* Do the actual conversion */ /* Global Variables */ static struct cart *cart; /* CGI and other variables */ static struct hash *oldVars = NULL; static char *organism = NULL; static char *database = NULL; @@ -58,91 +59,173 @@ return db; } struct dbDb *toDb = genarkLiftOverDb(name); if (toDb == NULL) errAbort("Can't find %s in matchingDb", name); return toDb; } static void askForDestination(struct liftOverChain *liftOver, char *fromPos, struct dbDb *fromDb, struct dbDb *toDb) /* set up page for entering data */ { struct dbDb *dbList; boolean askAboutQuickLift = FALSE; -//boolean quickLiftChainExists = (quickLiftGetChain(fromDb->name, toDb->name) != 0); +boolean quickLift = FALSE; if (quickLiftEnabled(cart)) + { askAboutQuickLift = TRUE; + quickLift = cartUsualBoolean(cart, "doQuickLift", FALSE); + } cartWebStart(cart, database, "Convert %s to New Assembly", fromPos); -/* create HMTL form */ +/* Include autocomplete libraries */ +jsIncludeAutoCompleteLibs(); + +/* create HTML form */ puts("
\n"); cartSaveSession(cart); -/* create HTML table for layout purposes */ -puts("\n\n"); - -/* top row -- labels */ -cgiSimpleTableRowStart(); -cgiTableField("Old genome: "); -cgiTableField("Old assembly: "); -cgiTableField("New genome: "); -cgiTableField("New assembly: "); -if (askAboutQuickLift) - cgiTableField("betaQuickLift tracks: "); -cgiTableField(" "); -cgiTableRowEnd(); - -/* Next row -- data and controls */ -cgiSimpleTableRowStart(); - -/* From organism and assembly. */ -cgiTableField(fromDb->organism); -cgiTableField(fromDb->description); - -/* Destination organism. */ -cgiSimpleTableFieldStart(); +/* CSS for two-section layout */ +puts("\n"); + +puts("
\n"); + +/* SOURCE SECTION (read-only) */ +puts("
\n"); +puts("\n"); +hPrintf("
Genome: %s
\n", fromDb->organism); +hPrintf("
Assembly: %s
\n", fromDb->description); +puts("
\n"); + +/* DESTINATION SECTION (editable) */ +puts("
\n"); +puts("\n"); + +/* Hidden fields for form submission */ +hPrintf("\n", HGLFT_TOORG_VAR, toDb->organism); +hPrintf("\n", HGLFT_TODB_VAR, liftOver->toDb); + +/* Search bar */ +char *searchBarId = "toGenomeSearch"; +puts("
\n"); +puts("Search:\n"); +printGenomeSearchBar(searchBarId, "Search for target genome...", NULL, TRUE, NULL, NULL); +puts("
\n"); + +/* Current selection display */ +char *selectedLabel = getCurrentGenomeLabel(liftOver->toDb); +hPrintf("
%s
\n", selectedLabel); + +/* Assembly dropdown (updates based on genome selection) */ +puts("
\n"); +puts("Assembly:\n"); dbList = hGetLiftOverToDatabases(liftOver->fromDb); -printSomeGenomeListHtmlNamed(HGLFT_TOORG_VAR, liftOver->toDb, dbList, "change", onChangeToOrg); -cgiTableFieldEnd(); - -/* Destination assembly */ -cgiSimpleTableFieldStart(); -printAllAssemblyListHtmlParm(liftOver->toDb, dbList, HGLFT_TODB_VAR, TRUE, NULL, NULL); -cgiTableFieldEnd(); +printAllAssemblyListHtmlParm(liftOver->toDb, dbList, HGLFT_TODB_VAR, TRUE, "change", onChangeToOrg); +puts("
\n"); +/* QuickLift option */ if (askAboutQuickLift) { - cgiSimpleTableFieldStart(); - boolean quickLift = cartUsualBoolean(cart, "doQuickLift", FALSE); + puts("
\n"); cgiMakeCheckBoxWithId("doQuickLift", quickLift, "doQuickLift"); - cgiTableFieldEnd(); + puts(" \n"); + puts(" beta\n"); + puts("
\n"); } -cgiSimpleTableFieldStart(); -cgiMakeButton(HGLFT_DO_CONVERT, "Submit"); -cgiTableFieldEnd(); +puts("
\n"); /* end destination section */ +puts("
\n"); /* end grid */ -cgiTableRowEnd(); -cgiTableEnd(); +/* Submit button centered below */ +puts("
\n"); +cgiMakeButton(HGLFT_DO_CONVERT, "Submit"); +puts("
\n"); + +/* JavaScript initialization for autocomplete with liftOver filtering */ +jsInlineF( + "let validTargets = new Set();\n" + "\n" + "fetch('../cgi-bin/hubApi/liftOver/listExisting?fromGenome=%s')\n" + " .then(response => response.json())\n" + " .then(data => {\n" + " if (data.existingLiftOvers) {\n" + " data.existingLiftOvers.forEach(chain => validTargets.add(chain.toDb));\n" + " }\n" + "\n" + " // Custom onServerReply that processes results and filters to valid targets\n" + " function processAndFilterResults(result, term) {\n" + " let processed = processFindGenome(result, term);\n" + " let filtered = processed.filter(item => validTargets.has(item.genome));\n" + " if (filtered.length === 0 && processed.length > 0) {\n" + " // Found genomes but none have liftOver from source\n" + " return [{label: 'No liftOver available for matching genomes', value: '', genome: '', disabled: true}];\n" + " } else if (filtered.length === 0) {\n" + " // No genomes matched the search at all\n" + " return [{label: 'No genomes found', value: '', genome: '', disabled: true}];\n" + " }\n" + " return filtered;\n" + " }\n" + "\n" + " // Error handler for API failures (e.g. HTTP 400)\n" + " function onSearchError(jqXHR, textStatus, errorThrown, term) {\n" + " return [{label: 'No genomes found', value: '', genome: '', disabled: true}];\n" + " }\n" + "\n" + " function onGenomeSelect(selectEle, item) {\n" + " // Ignore disabled/placeholder items\n" + " if (item.disabled || !item.genome) {\n" + " return;\n" + " }\n" + " selectEle.textContent = item.label;\n" + " document.mainForm.%s.value = item.commonName.split('(')[0].trim();\n" + " document.mainForm.%s.value = item.genome;\n" + " document.mainForm.submit();\n" + " }\n" + "\n" + " let selectEle = document.getElementById('toGenomeLabel');\n" + " initSpeciesAutoCompleteDropdown('%s', onGenomeSelect.bind(null, selectEle), null, null, processAndFilterResults, onSearchError);\n" + " });\n" + "\n" + "document.addEventListener('DOMContentLoaded', () => {\n" + " let btn = document.getElementById('%sButton');\n" + " if (btn) {\n" + " btn.addEventListener('click', () => {\n" + " let val = document.getElementById('%s').value;\n" + " $('[id=\\x27%s\\x27]').autocompleteCat('search', val);\n" + " });\n" + " }\n" + "});\n" + , liftOver->fromDb + , HGLFT_TOORG_VAR + , HGLFT_TODB_VAR + , searchBarId, searchBarId, searchBarId, searchBarId +); puts("\n"); cartWebEnd(); } static double scoreLiftOverChain(struct liftOverChain *chain, char *fromOrg, char *fromDb, char *toOrg, char *toDb, struct hash *dbRank ) /* Score the chain in terms of best match for cart settings */ { double score = 0; char *chainFromOrg = hOrganism(chain->fromDb); char *chainToOrg = hOrganism(chain->toDb); int fromRank = hashIntValDefault(dbRank, chain->fromDb, 0);