b53a97d26633a1f174a7031559a69d37e393379f
chmalee
  Fri May 1 14:19:19 2026 -0700
Allow myVariants tracks to be shared similar to google docs, read only and read+edit access by username or for all. Last write wins. Currently no history of changes, leaving that for the next version of the feature

diff --git src/hg/hgTrackUi/hgTrackUi.c src/hg/hgTrackUi/hgTrackUi.c
index 844673f117d..d82c9429a94 100644
--- src/hg/hgTrackUi/hgTrackUi.c
+++ src/hg/hgTrackUi/hgTrackUi.c
@@ -18,30 +18,32 @@
 #include "snpUi.h"
 #include "snp125Ui.h"
 #include "snp125.h"
 #include "sample.h"
 #include "wiggle.h"
 #include "hgMaf.h"
 #include "obscure.h"
 #include "chainCart.h"
 #include "chainDb.h"
 #include "gvUi.h"
 #include "grp.h"
 #include "oregannoUi.h"
 #include "chromGraph.h"
 #include "hgConfig.h"
 #include "customTrack.h"
+#include "myVariants.h"
+#include "wikiLink.h"
 #include "dupTrack.h"
 #include "dbRIP.h"
 #include "tfbsConsSites.h"
 #include "hapmapSnps.h"
 #include "nonCodingUi.h"
 #include "expRecord.h"
 #include "wikiTrack.h"
 #include "hubConnect.h"
 #include "trackHub.h"
 #include "pcrResult.h"
 #include "dgv.h"
 #include "transMapStuff.h" 
 #include "vcfUi.h" 
 #include "bbiFile.h"
 #include "ensFace.h"
@@ -3282,30 +3284,86 @@
 
 jsIncludeFile("dataTables-2.2.2.min.js", NULL);
 jsIncludeFile("dataTables.select-3.0.0.min.js", NULL);
 jsIncludeFile("facetedComposite.js", NULL);
 
 webIncludeResourceFile("dataTables-2.2.2.min.css");
 webIncludeResourceFile("dataTables.select-3.0.0.min.css");
 webIncludeResourceFile("facetedComposite.css");
 
 
 // cleanup
 slPairFreeValsAndList(&dataTypes);
 hashFree(&defaultOn);
 }
 
+static void myVariantsShareUi(struct trackDb *tdb)
+/* Render the inline share management section on hgTrackUi for a user's own
+ * myVariants track. Emits HTML + calls into hui.js via jsInline to wire up
+ * the create/list/revoke API calls. */
+{
+char *userName = getUserName();
+if (userName == NULL)
+    {
+    printf("<p>Please <a href=\"./hgSession\">log in</a> to manage shares.</p>\n");
+    return;
+    }
+
+printf("<h3>Share this track</h3>\n");
+printf("<div id=\"shareCreateSection\">\n");
+printf("<h4 style=\"margin-top:0\">Create share link</h4>\n");
+printf("<table style=\"border-spacing:4px\"><tbody>\n");
+
+/* Project dropdown */
+printf("<tr><td>Project:</td><td><select id=\"shareProject\">\n");
+printf("<option value=\"*\">All projects</option>\n");
+struct slName *projects = myVariantsGetProjects(userName);
+struct slName *p;
+for (p = projects; p != NULL; p = p->next)
+    printf("<option value=\"%s\">%s</option>\n", p->name, p->name);
+slFreeList(&projects);
+printf("</select></td></tr>\n");
+
+/* Permission radios */
+printf("<tr><td>Permission:</td><td>"
+    "<label><input type=\"radio\" name=\"sharePerm\" value=\"0\" checked> Can view</label> "
+    "<label><input type=\"radio\" name=\"sharePerm\" value=\"1\"> Can edit</label>"
+    "</td></tr>\n");
+printf("<tr><td>Share with:</td>"
+    "<td><input type=\"text\" id=\"shareTargetUser\""
+    " placeholder=\"Username (blank = anyone with link)\" style=\"width:250px\"></td></tr>\n");
+printf("<tr><td>Label:</td>"
+    "<td><input type=\"text\" id=\"shareLabel\" placeholder=\"Optional label\""
+    " style=\"width:250px\"></td></tr>\n");
+printf("</tbody></table>\n");
+printf("<button type=\"button\" id=\"shareCreateBtn\">Create share link</button>\n");
+printf("<div id=\"shareResult\" style=\"display:none; margin-top:8px; padding:8px;"
+    " background:#e8f5e9; border-radius:4px\">"
+    "<span>Share URL: </span>"
+    "<input type=\"text\" id=\"shareUrlField\" readonly style=\"width:350px\">"
+    " <button type=\"button\" id=\"shareCopyBtn\">Copy</button></div>\n");
+printf("</div>\n");
+
+printf("<hr>\n");
+printf("<div id=\"shareListSection\">\n");
+printf("<h4>Active shares</h4>\n");
+printf("<div id=\"shareListContent\">Loading...</div>\n");
+printf("</div>\n");
+
+jsInline("if (typeof myVariantsShareInit === 'function') myVariantsShareInit();\n");
+}
+
 void specificUi(struct trackDb *tdb, struct trackDb *tdbList, struct customTrack *ct, boolean ajax)
 /* Draw track specific parts of UI. */
 {
 char *track = tdb->track;
 char *db = database;
 char *liftDb = cloneString(trackDbSetting(tdb, "quickLiftDb"));
 if (liftDb != NULL) 
     db = liftDb;
 // Ideally check cfgTypeFromTdb()/cfgByCfgType() first, but with all these special cases already in
 //    place, lets be cautious at this time.
 // NOTE: Developer, please try to use cfgTypeFromTdb()/cfgByCfgType().
 
 boolean boxed = trackDbSettingClosestToHomeOn(tdb, "boxedCfg");
 boolean isGencode3 = trackDbSettingOn(tdb, "isGencode3");
 // UI precedence:
@@ -3489,30 +3547,37 @@
     else if (startsWith("chromGraph", tdb->type))
         chromGraphUi(tdb);
     else if (startsWith("sample", tdb->type))
         genericWiggleUi(tdb,7);
     else if (startsWithWord("array",tdb->type)) // not quite the same as "expRatio" (custom tracks)
         expRatioCtUi(tdb);
     else if (startsWithWord("factorSource",tdb->type))
         factorSourceUi(db,tdb);
     else if (startsWithWord("bigBed",tdb->type))
         labelCfgUi(db, cart, tdb, tdb->track);
     }
 
 if (tdbSupportsColorOverride(tdb))
     colorTrackOption(cart, tdb->track, tdb);
 
+/* myVariants own track: render inline share management. Skip shared tracks
+ * (myVariants_shared_*) - you can't re-share someone else's data. */
+if (cfgOptionBooleanDefault("doMyVariants", FALSE)
+    && startsWith("myVariants_", tdb->track)
+    && !startsWith("myVariants_shared_", tdb->track))
+    myVariantsShareUi(tdb);
+
 if (!ajax) // ajax asks for a simple cfg dialog for right-click popup or hgTrackUi subtrack cfg
     {
     // Composites *might* have had their top level controls just printed,
     // but almost certainly have additional controls
     boolean isLogo = (trackDbSetting(tdb, "logo") != NULL);
     // It'd be nice to handle faceted composites as a separate container type, but practically so much
     // of the display features we want are identical to composites - it's easier to special case the UI.
     if (tdbIsComposite(tdb) && !isLogo) // for the moment generalizing this to include other containers...
         if (!sameOk(trackDbLocalSetting(tdb, "compositeTrack"), "faceted")) // but not faceted containers ...
             hCompositeUi(db, cart, tdb, NULL, NULL, MAIN_FORM);
 
     // Additional special case navigation links may be added
     extraUiLinks(db, tdb, cart);
     }
 }
@@ -4371,41 +4436,71 @@
 	    }
 	if (dupTdb != NULL)
 	    slAddHead(&tdbList, dupTdb);
 	}
     }
 
 if (sameWord(track, WIKI_TRACK_TABLE))
     tdb = trackDbForWikiTrack();
 else if (sameWord(track, RULER_TRACK_NAME))
     /* special handling -- it's not a full-fledged track */
     tdb = trackDbForRuler();
 else if (sameWord(track, OLIGO_MATCH_TRACK_NAME))
     tdb = trackDbForOligoMatch();
 else if (sameWord(track, CUTTERS_TRACK_NAME))
     tdb = trackDbForPseudoTrack(CUTTERS_TRACK_NAME, CUTTERS_TRACK_LABEL, CUTTERS_TRACK_LONGLABEL, tvHide, TRUE);
-else if (isCustomTrack(track))
+else if (isCustomTrack(track)
+         || (cfgOptionBooleanDefault("doMyVariants", FALSE) && startsWith("myVariants_", track)))
     {
+    /* myVariants tracks (own and shared) are built dynamically and live in
+     * the CT list rather than the SQL trackDb table, but their names don't
+     * carry the ct_ prefix, so we need to look them up alongside regular
+     * custom tracks. */
     ctList = customTracksParseCart(database, cart, NULL, NULL);
     for (ct = ctList; ct != NULL; ct = ct->next)
         {
         if (sameString(track, ct->tdb->track))
             {
             tdb = ct->tdb;
             break;
             }
         }
+    /* Fallback for direct hgTrackUi navigation without hgTracks: regenerate
+     * the myVariants CT file and re-parse. Normally the CT file was written
+     * during the preceding hgTracks visit, so this branch only fires for
+     * bookmarked URLs or direct links. */
+    if (tdb == NULL && startsWith("myVariants_", track))
+        {
+        char *userName = getUserName();
+        char *ctFile = myVariantsWriteCtFile(userName, database, cart);
+        if (isNotEmpty(ctFile))
+            {
+            char mvVarName[256];
+            safef(mvVarName, sizeof mvVarName, CT_FILE_VAR_PREFIX "%s", database);
+            cartSetString(cart, mvVarName, ctFile);
+            freeMem(ctFile);
+            ctList = customTracksParseCart(database, cart, NULL, NULL);
+            for (ct = ctList; ct != NULL; ct = ct->next)
+                {
+                if (sameString(track, ct->tdb->track))
+                    {
+                    tdb = ct->tdb;
+                    break;
+                    }
+                }
+            }
+        }
     }
 else if (isHubTrack(track))
     {
     tdb = hubConnectAddHubForTrackAndFindTdb(database, track, &tdbList, trackHash);
     }
 else if (sameString(track, "hgPcrResult"))
     tdb = pcrResultFakeTdb();
 else
     {
     tdb = tdbForTrack(database, track,&tdbList);
     }
 if (tdb == NULL)
    {
    errAbort("Can't find %s in track database %s chromosome %s",
 	    track, database, chromosome);