4bb9e8caea515342ba98d3871da76cd4ec69916f
chmalee
  Fri May 1 14:10:00 2026 -0700
Initial myVariants implementation: a form on hgTracks where users can enter item details in one of three ways: hgvs/item search, simple bed form, advanced bed form where additional non-bed fields can dynamically created. Allows changing the color of items, writing descriptions, and editing the items after creation. Show overlaps with hardcoded tracks when hgc page is open (not in the hgc dialog). Next commit has implementation of sharing these tracks with other users

diff --git src/hg/hgTracks/hgTracks.c src/hg/hgTracks/hgTracks.c
index a7ebc4f7e5d..9063709f684 100644
--- src/hg/hgTracks/hgTracks.c
+++ src/hg/hgTracks/hgTracks.c
@@ -71,52 +71,54 @@
 #include "wigCommon.h"
 #include "knetUdc.h"
 #include "hex.h"
 #include <openssl/sha.h>
 #include "customComposite.h"
 #include "chromAlias.h"
 #include "jsonWrite.h"
 #include "cds.h"
 #include "cacheTwoBit.h"
 #include "cartJson.h"
 #include "wikiLink.h"
 #include "decorator.h"
 #include "decoratorUi.h"
 #include "mouseOver.h"
 #include "exportedDataHubs.h"
+#include "myVariants.h"
 
 //#include "bed3Sources.h"
 
 /* Other than submit and Submit all these vars should start with hgt.
  * to avoid weeding things out of other program's namespaces.
  * Because the browser is a central program, most of its cart
  * variables are not hgt. qualified.  It's a good idea if other
  * program's unique variables be qualified with a prefix though. */
 char *excludeVars[] = { "submit", "Submit", "dirty", "hgt.reset",
             "hgt.in1", "hgt.in2", "hgt.in3", "hgt.inBase",
             "hgt.out1", "hgt.out2", "hgt.out3", "hgt.out4",
             "hgt.left1", "hgt.left2", "hgt.left3",
             "hgt.right1", "hgt.right2", "hgt.right3",
             "hgt.dinkLL", "hgt.dinkLR", "hgt.dinkRL", "hgt.dinkRR",
             "hgt.tui", "hgt.hideAll", "hgt.visAllFromCt",
 	    "hgt.psOutput", "hideControls", "hgt.toggleRevCmplDisp",
 	    "hgt.collapseGroups", "hgt.expandGroups", "hgt.suggest",
 	    "hgt.jump", "hgt.refresh", "hgt.setWidth",
             "hgt.trackImgOnly", "hgt.ideogramToo", "hgt.trackNameFilter", "hgt.imageV1", "hgt.suggestTrack", "hgt.setWidth",
              TRACK_SEARCH,         TRACK_SEARCH_ADD_ROW,     TRACK_SEARCH_DEL_ROW, TRACK_SEARCH_PAGER,
             "hgt.contentType", "hgt.positionInput", "hgt.internal",
             "sortExp", "sortSim", "hideTracks", "ignoreCookie","dumpTracks",hgsMergeCart,"ctTest",
+            "myVarShare",
             NULL };
 
 boolean genomeIsRna = FALSE;    // is genome RNA instead of DNA
 
 /* These variables persist from one incarnation of this program to the
  * next - living mostly in the cart. */
 boolean baseShowPos;           /* TRUE if should display full position at top of base track */
 boolean baseShowAsm;           /* TRUE if should display assembly info at top of base track */
 boolean baseShowScaleBar;      /* TRUE if should display scale bar at very top of base track */
 boolean baseShowRuler;         /* TRUE if should display the basic ruler in the base track (default) */
 char *baseTitle = NULL;        /* Title it should display top of base track (optional)*/
 static char *userSeqString = NULL;  /* User sequence .fa/.psl file. */
 
 /* These variables are set by getPositionFromCustomTracks() at the very
  * beginning of tracksDisplay(), and then used by loadCustomTracks(). */
@@ -186,30 +188,31 @@
 void uglySnoopTrackList(int depth, struct track *trackList)
 /* Print out some info on track list. */
 {
 struct track *track;
 for (track = trackList; track != NULL; track = track->next)
     {
     if (stringIn("FaireH1h", track->track))
 	{
 	repeatCharOut(uglyOut, '+', depth);
 	}
     uglySnoopTrackList(depth+1, track->subtracks);
     }
 }
 #endif /* DEBUG */
 
+
 struct track *trackFindByName(struct track *tracks, char *trackName)
 /* find a track in tracks by name, recursively searching subtracks */
 {
 struct track *track;
 for (track = tracks; track != NULL; track = track->next)
     {
     if (sameString(track->track, trackName))
         return track;
     else if (track->subtracks != NULL)
         {
         struct track *st = trackFindByName(track->subtracks, trackName);
         if (st != NULL)
             return st;
         }
     }
@@ -6741,35 +6744,34 @@
     }
 else if (sameString(type, "vcfPhasedTrio"))
     {
     tg = trackFromTrackDb(tdb);
     tg->customPt = ct;
     vcfPhasedMethods(tg);
     tg->mapItemName = ctMapItemName;
     }
 else if (sameString(type, "vcf"))
     {
     tg = trackFromTrackDb(tdb);
     tg->customPt = ct;
     vcfMethods(tg);
     tg->mapItemName = ctMapItemName;
     }
-else if (sameString(type, "makeItems"))
+else if (sameString(type, "myVariants"))
     {
     tg = trackFromTrackDb(tdb);
-    makeItemsMethods(tg);
-    tg->nextItemButtonable = TRUE;
+    myVariantsMethods(tg);
     tg->customPt = ct;
     }
 else if (sameString(type, "bedTabix")  || sameString(type, "longTabix"))
     {
     knetUdcInstall();
     tg = trackFromTrackDb(tdb);
     tg->customPt = ct;
     tg->mapItemName = ctMapItemName; /* must be here to see ctMapItemName */
     }
 else if (sameString(type, "bedDetail"))
     {
     tg = trackFromTrackDb(tdb);
     bedDetailCtMethods(tg, ct);
     tg->mapItemName = ctMapItemName; /* must be here to see ctMapItemName */
     }
@@ -6819,30 +6821,48 @@
 tg->hasUi = TRUE;
 tg->customTrack = TRUE;// Explicitly declare this a custom track for flatTrack ordering
 
 freez(&typeDupe);
 return tg;
 }
 
 char *getPositionFromCustomTracks()
 /* Parses custom track data to get the position variable
  * return - The first chromosome position variable found in the
  * custom track data.  */
 {
 char *pos = NULL;
 struct slName *bl = NULL;
 
+// If the user has myVariants data for this database, write a custom track
+// stub file and set the ctfile_ cart variable so customTracksParseCart picks
+// it up.  This handles fresh carts and also refreshes after edits/deletes
+// done via jsCommandDispatch.  If the backing table no longer exists (e.g.
+// the user or an admin dropped it), remove the stale cart variable so the
+// custom track system doesn't try to load a non-existent table.
+if (cfgOptionBooleanDefault("doMyVariants", FALSE))
+    {
+    char *userName = getUserName();
+    char *ctFile = myVariantsWriteCtFile(userName, database, cart);
+    char mvVarName[256];
+    safef(mvVarName, sizeof mvVarName, CT_FILE_VAR_PREFIX "%s", database);
+    if (isNotEmpty(ctFile))
+        cartSetString(cart, mvVarName, ctFile);
+    else
+        cartRemove(cart, mvVarName);
+    }
+
 ctList = customTracksParseCart(database, cart, &browserLines, &ctFileName);
 
 for (bl = browserLines; bl != NULL; bl = bl->next)
     {
     char *words[96];
     int wordCount;
     char *dupe = cloneString(bl->name);
 
     wordCount = chopLine(dupe, words);
     if (wordCount >= 3)
         {
         char *command = words[1];
         if (sameString(command, "position"))
             pos = cloneString(words[2]);
         }
@@ -7879,32 +7899,32 @@
     tg->limitedVis = tvDense;
     tg->limitedVisSet = TRUE;
     }
 }
 
 void printTrackInitJavascript(struct track *trackList)
 {
 hPrintf("<input type='hidden' id='%s' name='%s' value=''>\n", hgtJsCommand, hgtJsCommand);
 }
 
 void jsCommandDispatch(char *command, struct track *trackList)
 /* Dispatch a command sent to us from some javaScript event.
  * This gets executed after the track list is built, but before
  * the track->loadItems methods are called.  */
 {
-if (startsWithWord("makeItems", command))
-    makeItemsJsCommand(command, trackList, trackHash);
+if (startsWithWord("myVariants", command))
+    myVariantsJsCommand(command, trackList, trackHash);
 else
     warn("Unrecognized jsCommand %s", command);
 }
 
 void parentChildCartCleanup(struct track *trackList,struct cart *newCart,struct hash *oldVars)
 /* When composite/view settings changes, remove subtrack specific vis
    When superTrackChild is found and selected, shape superTrack to match. */
 {
 struct lm *lm = lmInit(0);	/* Speed tweak cleanup with scatch memory pool. */
 struct track *track = trackList;
 for (;track != NULL; track = track->next)
     {
     boolean shapedByubtrackOverride = FALSE;
     boolean cleanedByContainerSettings = FALSE;
 
@@ -8834,30 +8854,78 @@
 hPrintf(" ");
 
 if (!multiRegionButtonTop)
     {
     printMultiRegionButton();
     hPrintf(" ");
     }
 hButtonMaybePressed("hgt.toggleRevCmplDisp", "Reverse",
                        revCmplDisp ? "Show forward strand at this location - keyboard shortcut: r, then v"
                                    : "Show reverse strand at this location - keyboard shortcut: r, then v",
                        NULL, revCmplDisp);
 hPrintf(" ");
 
 hButtonWithOnClick("hgt.setWidth", "Resize", "Resize image width to browser window size - keyboard shortcut: r, then s", "hgTracksSetWidth()");
 
+// put up the My Variants dialog if the hg.conf statement is present
+if (cfgOptionBooleanDefault("doMyVariants", FALSE))
+    {
+    hPrintf("<button id=\"myVariantsButton\" title=\"Add an item to the my variants track\">Make Item</button>");
+    jsInline("var doMyVariants = true;\n");
+    jsInlineF("var userIsLoggedIn = %s;\n", getUserName() != NULL ? "true" : "false");
+    // Get existing project values for dropdown
+    char *userName = getUserName();
+    if (userName)
+        {
+        struct slName *projects = myVariantsGetProjects(userName);
+        if (projects)
+            {
+            struct jsonElement *projectList = newJsonList(NULL);
+            struct slName *proj;
+            for (proj = projects; proj != NULL; proj = proj->next)
+                jsonListAdd(projectList, newJsonString(proj->name));
+            slReverse(&projectList->val.jeList);
+            jsonObjectAdd(jsonForClient, "myVariantsProjects", projectList);
+            slFreeList(&projects);
+            }
+        /* Get existing custom field names for the create dialog */
+        struct slName *customFields = myVariantsGetCustomFields(userName);
+        if (customFields)
+            {
+            struct jsonElement *cfList = newJsonList(NULL);
+            struct slName *cf;
+            for (cf = customFields; cf != NULL; cf = cf->next)
+                jsonListAdd(cfList, newJsonString(cf->name));
+            slReverse(&cfList->val.jeList);
+            jsonObjectAdd(jsonForClient, "myVariantsCustomFields", cfList);
+            slFreeList(&customFields);
+            }
+        /* Get hidden field names for the restore UI */
+        struct slName *hiddenFields = myVariantsGetHiddenFields(userName);
+        if (hiddenFields)
+            {
+            struct jsonElement *hfList = newJsonList(NULL);
+            struct slName *hf;
+            for (hf = hiddenFields; hf != NULL; hf = hf->next)
+                jsonListAdd(hfList, newJsonString(hf->name));
+            slReverse(&hfList->val.jeList);
+            jsonObjectAdd(jsonForClient, "myVariantsHiddenFields", hfList);
+            slFreeList(&hiddenFields);
+            }
+        }
+    }
+
 // put the track download interface behind hg.conf control
 if (cfgOptionBooleanDefault("showDownloadUi", TRUE))
     jsInline("var showDownloadButton = true;\n");
 
 // remove the hg.conf option once this feature is released
 if (cfgOptionBooleanDefault("showIgv", FALSE))
     {
     puts(" <button id='hgtIgv' type='button' "
             "title='Add an IGV.js window below the UCSC Browser, to open files from "
             "your local harddisk or server' >Add IGV Tracks</button>");
     //jsInline("document.getElementById('hgtIgv').addEventListener('click', onIgvClick);");
     }
 
 }
 
@@ -9025,38 +9093,30 @@
     changeTrackVis(groupList, NULL, vis);
     }
 
 if(!psOutput && !cartUsualBoolean(cart, "hgt.imageV1", FALSE))
     {
 
    // re-establish the enlarged portal
    if (imgBoxPortalDimensions(theImgBox,&virtWinStart,&virtWinEnd,&(tl.picWidth),NULL,NULL,NULL,NULL,NULL))
         {
         virtWinBaseCount = virtWinEnd - virtWinStart;
         fullInsideWidth = tl.picWidth - gfxBorder - fullInsideX;
         }
 
     }
 
-char *jsCommand = cartCgiUsualString(cart, hgtJsCommand, "");
-if (!isEmpty(jsCommand))
-   {
-   cartRemove(cart, hgtJsCommand);
-   jsCommandDispatch(jsCommand, trackList);
-   }
-
-
 /* adjust visibility */
 for (track = trackList; track != NULL; track = track->next)
     {
     /* adjust track visibility based on supertrack just before load loop */
     if (tdbIsSuperTrackChild(track->tdb))
         limitSuperTrackVis(track);
 
     /* hide tracks not on any windows chromNames */
     boolean hideIt = TRUE;
     struct window *w;
     for (w = windows; w; w=w->next)
         {
         if (hTrackOnChrom(track->tdb, w->chromName))
             hideIt = FALSE;
         }
@@ -10653,30 +10713,47 @@
 jsInline(dy->string);
 dyStringFree(&dy);
 }
 
 
 void tracksDisplay()
 /* Put up main tracks display. This routine handles zooming and
  * scrolling. */
 {
 char titleVar[256];
 char *oldPosition = cartUsualString(cart, "oldPosition", "");
 boolean findNearest = cartUsualBoolean(cart, "findNearest", FALSE);
 cartRemove(cart, "findNearest");
 
 boolean positionIsVirt = FALSE;
+
+/* Process JS commands (e.g., myVariants insert) BEFORE building track list/CTs */
+char *jsEarly = cartCgiUsualString(cart, hgtJsCommand, "");
+if (!isEmpty(jsEarly))
+    {
+    cartRemove(cart, hgtJsCommand);
+    jsCommandDispatch(jsEarly, NULL);
+    }
+
+/* Accept incoming share links before CT file generation so the cart var is
+ * visible to shared track rendering in the same request. */
+if (cfgOptionBooleanDefault("doMyVariants", FALSE))
+    {
+    myVariantsProcessShareParam();
+    myVariantsProcessSharedEdits();
+    }
+
 position = getPositionFromCustomTracks();
 if (NULL == position)
     {
     position = cartGetPosition(cart, database, &lastDbPosCart);
     if (sameOk(cgiOptionalString("position"), "lastDbPos"))
 	{
         restoreSavedVirtPosition();
 	}
     if (startsWith(OLD_MULTI_REGION_CHROM, position))
         position = replaceChars(position, OLD_MULTI_REGION_CHROM, MULTI_REGION_CHROM);
     if (startsWith(MULTI_REGION_CHROM, position))
 	{
 	position = stripCommas(position); // sometimes the position string arrives with commas in it.
 	positionIsVirt = TRUE;
 	}