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/js/hui.js src/hg/js/hui.js
index c028d2240a1..8bee32dd4c8 100644
--- src/hg/js/hui.js
+++ src/hg/js/hui.js
@@ -1634,15 +1634,160 @@
     for (var i = 0; i < selects.length; i++) {
         var sel = selects[i];
         if (sel.id==="superSubViz")
             continue;
         if (onlyVisible && sel.value === 'hide')
             continue;
         sel.value = newVal;
         // if the dropdown cannot be set to a value (e.g. bigWig has no pack), set it to full or dense
         if (sel.value === "")
             sel.value = 'full';
         if (sel.value === "")
             sel.value = 'dense';
         $(sel).trigger("change");
     }
 }
+
+// ---- myVariants share management (inline on hgTrackUi) ----
+
+function myVariantsShareApiUrl(action, params) {
+    // Build a URL for the share API
+    let url = "../cgi-bin/hgTracks?myVarShareCmd=" + action +
+        "&db=" + getDb() + "&hgsid=" + getHgsid();
+    if (params) {
+        Object.keys(params).forEach(function(key) {
+            if (params[key] !== undefined && params[key] !== null && params[key] !== "")
+                url += "&" + encodeURIComponent(key) + "=" + encodeURIComponent(params[key]);
+        });
+    }
+    return url;
+}
+
+function myVariantsShareLoad() {
+    let content = document.getElementById("shareListContent");
+    if (!content) return;
+    content.textContent = "Loading...";
+    fetch(myVariantsShareApiUrl("getShares", {}), { credentials: "same-origin" })
+    .then(function(response) { return response.json(); })
+    .then(function(data) {
+        // Build the share list with DOM APIs (textContent everywhere) so that
+        // owner-controlled fields like label/project/targetUser cannot inject
+        // markup or script.
+        while (content.firstChild) content.removeChild(content.firstChild);
+        if (data.error) {
+            content.textContent = "Error: " + data.error;
+            return;
+        }
+        if (!data.shares || data.shares.length === 0) {
+            let em = document.createElement("em");
+            em.textContent = "No active shares";
+            content.appendChild(em);
+            return;
+        }
+        let table = document.createElement("table");
+        table.style.width = "100%";
+        table.style.borderCollapse = "collapse";
+        table.style.fontSize = "0.9em";
+        let header = document.createElement("tr");
+        header.style.borderBottom = "1px solid #ccc";
+        ["Project", "Permission", "Shared With", "Label", "Created", ""].forEach(function(label) {
+            let th = document.createElement("th");
+            th.style.textAlign = "left";
+            th.textContent = label;
+            header.appendChild(th);
+        });
+        table.appendChild(header);
+        data.shares.forEach(function(s) {
+            let tr = document.createElement("tr");
+            tr.style.borderBottom = "1px solid #eee";
+            let cells = [
+                s.project === "*" ? "All" : s.project,
+                s.permission === 0 ? "View" : "Edit",
+                s.targetUser ? s.targetUser : "Anyone with link",
+                s.label ? s.label : "",
+                s.createdAt
+            ];
+            cells.forEach(function(text) {
+                let td = document.createElement("td");
+                td.textContent = text;
+                tr.appendChild(td);
+            });
+            let actionTd = document.createElement("td");
+            let revokeLink = document.createElement("a");
+            revokeLink.href = "#";
+            revokeLink.className = "shareRevokeLink";
+            revokeLink.dataset.token = s.shareToken;
+            revokeLink.textContent = "Revoke";
+            revokeLink.addEventListener("click", function(e) {
+                e.preventDefault();
+                myVariantsShareRevoke(this.dataset.token);
+            });
+            actionTd.appendChild(revokeLink);
+            tr.appendChild(actionTd);
+            table.appendChild(tr);
+        });
+        content.appendChild(table);
+    })
+    .catch(function(err) {
+        content.textContent = "Error loading shares: " + err.message;
+    });
+}
+
+function myVariantsShareCreate() {
+    let project = document.getElementById("shareProject").value;
+    let permission = document.querySelector('input[name="sharePerm"]:checked').value;
+    let targetUser = document.getElementById("shareTargetUser").value.trim();
+    let label = document.getElementById("shareLabel").value.trim();
+
+    let params = { project: project, permission: permission };
+    if (targetUser) params.targetUser = targetUser;
+    if (label) params.label = label;
+
+    fetch(myVariantsShareApiUrl("createShare", params), { credentials: "same-origin" })
+    .then(function(response) { return response.json(); })
+    .then(function(data) {
+        if (data.error) {
+            warn("Error creating share: " + data.error);
+            return;
+        }
+        let shareUrl = window.location.protocol + "//" + window.location.host +
+            "/cgi-bin/hgTracks?myVarShare=" + data.token + "&db=" + getDb();
+        document.getElementById("shareUrlField").value = shareUrl;
+        document.getElementById("shareResult").style.display = "block";
+        myVariantsShareLoad();
+    })
+    .catch(function(err) {
+        warn("Error creating share: " + err.message);
+    });
+}
+
+function myVariantsShareRevoke(token) {
+    fetch(myVariantsShareApiUrl("revokeShare", { shareToken: token }),
+          { credentials: "same-origin" })
+    .then(function(response) { return response.json(); })
+    .then(function(data) {
+        if (data.error) {
+            warn("Error revoking share: " + data.error);
+            return;
+        }
+        myVariantsShareLoad();
+    })
+    .catch(function(err) {
+        warn("Error revoking share: " + err.message);
+    });
+}
+
+function myVariantsShareInit() {
+    // Wire up the create/copy buttons and load existing shares
+    let createBtn = document.getElementById("shareCreateBtn");
+    if (!createBtn) return;
+    createBtn.addEventListener("click", myVariantsShareCreate);
+    let copyBtn = document.getElementById("shareCopyBtn");
+    if (copyBtn) {
+        copyBtn.addEventListener("click", function() {
+            let urlField = document.getElementById("shareUrlField");
+            urlField.select();
+            navigator.clipboard.writeText(urlField.value);
+        });
+    }
+    myVariantsShareLoad();
+}