3f97b357c5967755f1e5c914663ce9c7cc385e70 max Wed Aug 26 03:01:34 2026 -0700 Share a link: create the session only when the user asks, and copy in one click Opening the top-right "Share a link" dialog used to save a session right away, so just looking at the dialog left an unused link in the user's session list. The dialog now explains what the link is and offers one "Create link & copy" button that saves the session and copies the URL in a single step. The random name for a machine-generated session is now made client-side and sent with the request, so there is one convention in one place: a leading underscore (kept verbatim by the short-link encoder, so /s/<user>/_XXXXXXXX stays clean) plus eight URL-safe characters. hgSession.c no longer auto-names; a logged-in save with no name is an error, which is what the callers already guarantee. refs #10138 diff --git src/hg/js/topLinks.js src/hg/js/topLinks.js index 0ffa1e94e6b..46669478c63 100644 --- src/hg/js/topLinks.js +++ src/hg/js/topLinks.js @@ -212,30 +212,36 @@ var urlBox = el("div", {id: "tlShareUrl", textContent: url}, {background: "#f0f0f0", padding: "6px 8px", borderRadius: "4px", wordBreak: "break-all", userSelect: "all", fontFamily: "monospace"}); urlBox.setAttribute("data-copy", url); // copyToClipboard() reads data-copy or innerText body.appendChild(urlBox); var btnRow = el("div", {}, {marginTop: "8px"}); var copyBtn = el("button", {title: "Copy URL to clipboard"}); copyBtn.setAttribute("data-target", "tlShareUrl"); copyBtn.innerHTML = clipboardSvg + "Copy to clipboard"; copyBtn.addEventListener("click", function(ev) { if (typeof copyToClipboard === "function") copyToClipboard(ev); }); btnRow.appendChild(copyBtn); + // One-click "Create link & copy": copy right after the session is created. execCommand copy + // still runs while the modal is focused; if a browser blocks it the URL box and Copy button + // above are the manual fallback. + if (opts.autoCopy) + copyBtn.click(); + if (canRename) { var nameBtn = el("button", {textContent: "Specify name"}, {marginLeft: "8px"}); nameBtn.addEventListener("click", function() { showRename(body, url, opts); }); btnRow.appendChild(nameBtn); } body.appendChild(btnRow); if (opts.session) appendManageNote(body, opts.loggedIn); else if (opts.pageNote) body.appendChild(el("p", {textContent: "This link shows the page only. It does not " + "restore the tracks you currently have displayed."}, {marginTop: "14px"})); } // The "Specify name" editor: rename the session, then show the updated link. @@ -307,42 +313,78 @@ var body = document.createElement("div"); showModal("Share a link", body, 720); showResult(body, clean, {pageNote: opts.pageNote}); } function showShareDialog(link) { var mode = link.getAttribute("data-sharemode") || "session"; // hgTrackUi etc.: the shareable thing is just this page's URL without the hgsid. Note // that it opens the page, not the user's session/tracks (like the hgc popup link). if (mode === "url") { shareUrlDialog(window.location.href, {pageNote: true}); return; } - // Session mode (hgTracks): create a link right away so the user can just copy it. + // Session mode (hgTracks): don't create the session yet. Opening the dialog and closing it + // should not litter the user's session list with unused links, so we only create the session + // when the user clicks the button (which then also copies the link in one step). var loggedIn = link.getAttribute("data-loggedin") === "1"; var body = document.createElement("div"); - var status = el("p", {textContent: "Creating linkā¦"}, {marginTop: "0"}); - body.appendChild(status); showModal("Share a link", body, 720); + showCreatePrompt(body, loggedIn); + } + + // A random internal session name: a leading "_" (marking it machine-generated, kept verbatim by + // the short-link encoder) plus 8 URL-safe alphanumeric chars. Mirrors sessRandomShareName() in + // hgSession.js; generated here so the server no longer needs to auto-name shared sessions. + function shareRandomName() { + var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + var s = ""; + for (var i = 0; i < 8; i++) + s += chars.charAt(Math.floor(Math.random() * chars.length)); + return "_" + s; + } + + // The initial session-mode view: explain the link, then a single button that creates the shared + // session and copies its link in one step. Logged in, we generate the name client-side and pass + // it; logged out, we ask the server for an anonymous token link. + function showCreatePrompt(body, loggedIn) { + body.innerHTML = ""; + body.appendChild(el("p", {textContent: "Create a stable link to your current view to share " + + "with collaborators or put into figure legends or manuscripts. Links never time out."}, + {marginTop: "0"})); + var status = el("div", {}, {margin: "6px 0"}); + var createBtn = el("button", {title: "Create the shareable link and copy it to your clipboard"}); + createBtn.innerHTML = clipboardSvg + "Create link & copy"; + createBtn.addEventListener("click", function() { + createBtn.disabled = true; var params = {hgsid: getHgsidSafe(), hgS_doSaveSessionJson: 1}; - if (!loggedIn) + if (loggedIn) + params.hgS_newSessionName = shareRandomName(); + else params.hgS_shareAnon = 1; // anonymous token link; no rename postJson(params, status, function(data) { - showResult(body, data.url, {name: data.name, session: true, loggedIn: loggedIn}); + showResult(body, data.url, {name: data.name, session: true, loggedIn: loggedIn, + autoCopy: true}); + }); }); + var row = el("div", {}); + row.appendChild(createBtn); + body.appendChild(row); + body.appendChild(status); + appendManageNote(body, loggedIn); } // ---- Wire up the menu items -------------------------------------------------------------- function init() { var login = document.getElementById("loginLink"); // Only intercept the click when logged in (data-username present); otherwise it is a // plain link to hgSession and should navigate normally. if (login && login.getAttribute("data-username")) { login.addEventListener("click", function(ev) { ev.preventDefault(); showLoginDialog(login); }); } var share = document.getElementById("shareLink");