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/hubApi/findGenome.c src/hg/hubApi/findGenome.c
index 0e068e8467a..c7ff4b9935c 100644
--- src/hg/hubApi/findGenome.c
+++ src/hg/hubApi/findGenome.c
@@ -1,40 +1,42 @@
 /* findGenome search functions */
 
 #include "dataApi.h"
 #include "hgFind.h"
 #include "cartTrackDb.h"
 #include "cartJson.h"
 #include "genark.h"
 #include "asmAlias.h"
 #include "assemblyList.h"
+#include "liftOver.h"
 
 /* will be initialized as this function begins */
 static char *genarkTable = NULL;
 static char *asmListTable = NULL;
 static boolean statsOnly = FALSE;
 /* these three are radio button states, only one of these three can be TRUE */
 static boolean browserMustExist = TRUE;	/* default: browser must exist */
 static boolean browserMayExist = FALSE;
 static boolean browserNotExist = FALSE;
 static unsigned specificYear = 0;	/* from year=1234 argument */
 /* from category= reference or representative */
 static char *refSeqCategory = NULL;
 /* from status= one of: latest, replaced or suppressed */
 static char *versionStatus = NULL;
 /* from level= one of complete, chromosome, scaffold or contig */
 static char *assemblyLevel = NULL;
+static boolean liftable = FALSE;
 
 /*
 hgsql -e 'desc assemblyList;' hgcentraltest
 +----------------+---------------------+------+-----+---------+-------+
 | Field          | Type                | Null | Key | Default | Extra |
 +----------------+---------------------+------+-----+---------+-------+
 | name           | varchar(255)        | NO   | PRI | NULL    |       |
 | priority       | int(10) unsigned    | YES  |     | NULL    |       |
 | commonName     | varchar(511)        | YES  |     | NULL    |       |
 | scientificName | varchar(511)        | YES  |     | NULL    |       |
 | taxId          | int(10) unsigned    | YES  |     | NULL    |       |
 | clade          | varchar(255)        | YES  |     | NULL    |       |
 | description    | varchar(1023)       | YES  |     | NULL    |       |
 | browserExists  | tinyint(3) unsigned | YES  |     | NULL    |       |
 | hubUrl         | varchar(511)        | YES  |     | NULL    |       |
@@ -174,122 +176,130 @@
 
 static void addStatus(struct dyString *query)
 /* versionStatus = latest, replaced or suppressed */
 {
 if (isNotEmpty(versionStatus))
     sqlDyStringPrintf(query, " AND versionStatus='%s'", versionStatus);
 }
 
 static void addLevel(struct dyString *query)
 /* assemblyLevel = complete, chromosome, scaffold or contig */
 {
 if (isNotEmpty(assemblyLevel))
     sqlDyStringPrintf(query, " AND assemblyLevel='%s'", assemblyLevel);
 }
 
+static void addLiftover(struct dyString *query)
+/* liftable = are there liftover chains for this assembly */
+{
+if (liftable)
+    sqlDyStringPrintf(query, " AND exists (select 1 from %s where %s.name = %s.fromDb) ", liftOverChainTable(), asmListTable, liftOverChainTable());
+}
+
 static void addConditions(struct dyString *query)
 /* add any of the optional conditions */
 {
 addBrowserExists(query);
 addCategory(query);
 addStatus(query);
 addLevel(query);
+addLiftover(query);
 if (specificYear > 0)
     sqlDyStringPrintf(query, " AND year='%u'", specificYear);
 }
 
 static long long multipleWordSearch(struct sqlConnection *conn, char **words, int wordCount, struct jsonWrite *jw, long long *totalMatchCount)
 /* perform search on multiple words, prepare json and return number of matches */
 {
 long long itemCount = 0;
 *totalMatchCount = 0;
 if (wordCount < 0)
     return itemCount;
 
 /* get the words[] into a single string */
 struct dyString *queryDy = dyStringNew(128);
 dyStringPrintf(queryDy, "%s", words[0]);
 for (int i = 1; i < wordCount; ++i)
     dyStringPrintf(queryDy, " %s", words[i]);
 
 /* initial SELECT allows any browser exist status, existing or not */
 struct dyString *query = dyStringNew(64);
-sqlDyStringPrintf(query, "SELECT COUNT(*) FROM %s WHERE MATCH(name, commonName, scientificName, clade, description) AGAINST ('%s' IN BOOLEAN MODE)", asmListTable, queryDy->string);
+sqlDyStringPrintf(query, "SELECT COUNT(*) FROM %s ", asmListTable);
+sqlDyStringPrintf(query, "WHERE MATCH(name, commonName, scientificName, clade, description) AGAINST ('%s' IN BOOLEAN MODE)", queryDy->string);
 addConditions(query);	/* add optional SELECT options */
 
 long long matchCount = sqlQuickLongLong(conn, query->string);
 if (matchCount > 0)
     {
     *totalMatchCount = matchCount;
     if (statsOnly)	// only counting, nothing returned
 	{	// the LIMIT would limit results to maxItemsOutput
 	itemCount = min(maxItemsOutput, matchCount);
 	}	// when less than totalMatchCount
     else
 	{
 	dyStringFree(&query);
-	query = dyStringNew(64);
-	sqlDyStringPrintf(query, "SELECT * FROM %s WHERE MATCH(name, commonName, scientificName, clade, description, refSeqCategory, versionStatus, assemblyLevel) AGAINST ('%s' IN BOOLEAN MODE)", asmListTable, queryDy->string);
+	sqlDyStringPrintf(query, "SELECT * FROM %s ", asmListTable);
+        sqlDyStringPrintf(query, "WHERE MATCH(name, commonName, scientificName, clade, description, refSeqCategory, versionStatus, assemblyLevel) AGAINST ('%s' IN BOOLEAN MODE)", queryDy->string);
 	addConditions(query);	/* add optional SELECT options */
 	sqlDyStringPrintf(query, " ORDER BY priority LIMIT %d;", maxItemsOutput);
 	struct sqlResult *sr = sqlGetResult(conn, query->string);
 	itemCount = sqlJsonOut(jw, sr);
 	sqlFreeResult(&sr);
 	dyStringFree(&query);
 	}
     }
 return itemCount;
 }
 
 static long long oneWordSearch(struct sqlConnection *conn, char *searchWord, struct jsonWrite *jw, long long *totalMatchCount, boolean *prefixSearch)
 /* perform search on a single word, prepare json and return number of matches
  *   and number of potential matches totalMatchCount
  */
 {
 long long itemCount = 0;
 *totalMatchCount = 0;
 
-struct dyString *query = dyStringNew(64);
-sqlDyStringPrintf(query, "SELECT COUNT(*) FROM %s WHERE MATCH(name, commonName, scientificName, clade, description, refSeqCategory, versionStatus, assemblyLevel) AGAINST ('%s' IN BOOLEAN MODE)", asmListTable, searchWord);
+struct dyString *query = sqlDyStringCreate("SELECT COUNT(*) FROM %s ", asmListTable);
+sqlDyStringPrintf(query, "WHERE MATCH(name, commonName, scientificName, clade, description, refSeqCategory, versionStatus, assemblyLevel) AGAINST ('%s' IN BOOLEAN MODE)", searchWord);
 addConditions(query);	/* add optional SELECT options */
 
 long long matchCount = sqlQuickLongLong(conn, query->string);
 *prefixSearch = FALSE;	/* assume not */
 if (matchCount < 1)	/* no match, add the * wild card match to make a prefix match */
     {
-    dyStringFree(&query);
-    query = dyStringNew(64);
-    sqlDyStringPrintf(query, "SELECT COUNT(*) FROM %s WHERE MATCH(name, commonName, scientificName, clade, description, refSeqCategory, versionStatus, assemblyLevel) AGAINST ('%s*' IN BOOLEAN MODE)", asmListTable, searchWord);
+    dyStringClear(query);
+    sqlDyStringPrintf(query, "SELECT COUNT(*) FROM %s ", asmListTable);
+    sqlDyStringPrintf(query, "WHERE MATCH(name, commonName, scientificName, clade, description, refSeqCategory, versionStatus, assemblyLevel) AGAINST ('%s*' IN BOOLEAN MODE)", searchWord);
     addConditions(query);	/* add optional SELECT options */
     matchCount = sqlQuickLongLong(conn, query->string);
     if (matchCount > 0)
 	*prefixSearch = TRUE;
     }
 if (matchCount < 1)	// nothing found, returning zero
     return itemCount;
 *totalMatchCount = matchCount;
 
 if (statsOnly)	// only counting, nothing returned
     {	// the LIMIT would limit results to maxItemsOutput
     itemCount = min(maxItemsOutput, matchCount);
     }	// when less than totalMatchCount
 else
     {
-    dyStringFree(&query);
-    query = dyStringNew(64);
-
-    sqlDyStringPrintf(query, "SELECT * FROM %s WHERE MATCH(name, commonName, scientificName, clade, description, refSeqCategory, versionStatus, assemblyLevel) AGAINST ('%s%s' IN BOOLEAN MODE)", asmListTable, searchWord, *prefixSearch ? "*" : "");
+    dyStringClear(query);
+    sqlDyStringPrintf(query, "SELECT * FROM %s ", asmListTable);
+    sqlDyStringPrintf(query, "WHERE MATCH(name, commonName, scientificName, clade, description, refSeqCategory, versionStatus, assemblyLevel) AGAINST ('%s%s' IN BOOLEAN MODE)", searchWord, *prefixSearch ? "*" : "");
     addConditions(query);	/* add optional SELECT options */
     sqlDyStringPrintf(query, " ORDER BY priority LIMIT %d;", maxItemsOutput);
     struct sqlResult *sr = sqlGetResult(conn, query->string);
     itemCount = sqlJsonOut(jw, sr);
     sqlFreeResult(&sr);
     dyStringFree(&query);
     }
 
 return itemCount;
 }	/*	static long long oneWordSearch(struct sqlConnection *conn, char *searchWord, struct jsonWrite *jw, boolean *prefixSearch) */
 
 #ifdef NOT
 // disabled 2025-10-22
 static long elapsedTime(struct jsonWrite *jw)
 {
@@ -315,30 +325,31 @@
 if (extraArgs)
     apiErrAbort(err400, err400Msg, "extraneous arguments found for function /findGenome'%s'", extraArgs);
 
 boolean asmListExists = sqlTableExists(conn, asmListTable);
 if (!asmListExists)
     apiErrAbort(err400, err400Msg, "table central.assemblyList does not exist for /findGenome");
 
 boolean genArkExists = sqlTableExists(conn, genarkTable);
 if (!genArkExists)
     apiErrAbort(err400, err400Msg, "table central.%s does not exist for /findGenome", genarkTable);
 
 char *yearString = cgiOptionalString(argYear);
 char *categoryString = cgiOptionalString(argCategory);
 char *statusString = cgiOptionalString(argStatus);
 char *levelString = cgiOptionalString(argLevel);
+char *liftableStr = cgiOptionalString(argLiftable);
 /* protect sqlUnsigned from errors */
 if (isNotEmpty(yearString))
     {
     struct errCatch *errCatch = errCatchNew();
     if (errCatchStart(errCatch))
 	{
         specificYear = sqlUnsigned(yearString);
         if ((specificYear < 1800) || (specificYear > 2100))
 	    apiErrAbort(err400, err400Msg, "year specified '%s' must be >= 1800 and <= 2100", yearString);
 	}
     errCatchEnd(errCatch);
     if (errCatch->gotError)
 	apiErrAbort(err400, err400Msg, "can not recognize year '%s' as a number", yearString);
     }
 /* probably be better to place this arg checking business into a function
@@ -366,30 +377,42 @@
 	}
     }
 if (isNotEmpty(levelString))
     {
     assemblyLevel = cloneString(levelString);
     toLowerN(assemblyLevel, strlen(assemblyLevel));
     if (differentWord(assemblyLevel, "complete"))
 	{
 	if (differentWord(assemblyLevel, "chromosome"))
 	    if (differentWord(assemblyLevel, "scaffold"))
 		if (differentWord(assemblyLevel, "contig"))
 		    apiErrAbort(err400, err400Msg, "values for argument %s=%s must be one of: 'complete', 'chromosome', 'scaffold' or 'contig'", argLevel, levelString);
 	}
     }
 
+if (isNotEmpty(liftableStr))
+    {
+    char *lower = cloneString(liftableStr);
+    tolowers(lower);
+    if (sameWord(lower, "liftable") || sameWord(lower, "true") || sameWord(lower, "yes") ||sameWord(lower, "on"))
+        liftable = TRUE;
+    else if (sameWord(lower, "false") || sameWord(lower, "no") || sameWord(lower, "off"))
+        liftable = FALSE;
+    else
+        apiErrAbort(err400, err400Msg, "unrecognized '%s=%s' argument, must be either 'liftable' or 'true', or completely missing", argLiftable, liftableStr);
+    }
+
 char *browserExistString = cgiOptionalString(argBrowser);
 if (NULL == browserExistString)	/* set default if none given */
     browserExistString = cloneString("mustExist");
 
 if (isNotEmpty(browserExistString))
     {	/* from radio buttons, only one can be on */
     if (sameWord(browserExistString, "mustExist"))
 	{
 	browserMustExist = TRUE;	/* default: browser must exist */
         browserMayExist = FALSE;
         browserNotExist = FALSE;
 	}
     else if (sameWord(browserExistString, "mayExist"))
 	{
 	browserMustExist = FALSE;
@@ -432,30 +455,31 @@
 apiErrAbort(err400, err400Msg, "search term '%s=%s' should not have more than 5 words for function /findGenome", argQ, searchString);
 
 struct jsonWrite *jw = apiStartOutput();
 
 /* show options in effect in JSON return */
 
 jsonWriteString(jw, argBrowser, browserExistString);
 if (specificYear > 0)
     jsonWriteNumber(jw, argYear, specificYear);
 if (isNotEmpty(refSeqCategory))
     jsonWriteString(jw, argCategory, refSeqCategory);
 if (isNotEmpty(versionStatus))
     jsonWriteString(jw, argStatus, versionStatus);
 if (isNotEmpty(assemblyLevel))
     jsonWriteString(jw, argLevel, assemblyLevel);
+jsonWriteString(jw, argLiftable, liftableStr);
 
 long long itemCount = 0;
 long long totalMatchCount = 0;
 char **words;
 AllocArray(words, wordCount);
 (void) chopByWhite(searchString, words, wordCount);
 if (1 == wordCount)
     {
     boolean doQuote = TRUE;
     /* already quoted, let it go as-is */
     if (startsWith("\"", words[0]) && endsWith(words[0],"\""))
 	doQuote = FALSE;
     /* already wildcard, let it go as-is */
     if (endsWith(words[0],"*"))
 	doQuote = FALSE;