85372b41a99caf09ee398ab8ea7fc6707e3b6a0c max Fri Sep 18 09:20:10 2026 -0700 hgSession: privacy wording, wider description box, info bubbles, backup button #Preview2 week - bugs introduced now will need a build patch to fix Save-card privacy checkbox now reads "Private: Session can only be loaded by myself"; the Share dialog's gallery checkbox is normal-size text instead of a small form label. The description box is 50% wider. Info bubbles next to the Session name and Description labels explain the auto-generated name and where the description shows up. "Back up custom tracks" is a button again, at the top of the Advanced panel, and named first in that panel's summary line. refs #38311 diff --git src/hg/js/hgSession.js src/hg/js/hgSession.js index d78c515b506..10ae2247231 100644 --- src/hg/js/hgSession.js +++ src/hg/js/hgSession.js @@ -1,23 +1,23 @@ // hgSession.js - the experimental client-rendered "My Sessions" page. // // An opt-in modern alternative to the classic server-rendered hgSession page, applying hgBlat's // facelift strategy (#37996): hgSession.c emits the session list and page config as an inline JSON // global (hgSessionData) into an empty #sessionApp container, and this file builds the UI - a -// save-current-view card, a custom-track backup link, a searchable/sortable DataTable of saved -// sessions with inline Overwrite/Share/Edit/Delete, and an "Advanced" panel split into a load -// column and a save column. +// save-current-view card, a searchable/sortable DataTable of saved sessions with inline +// Overwrite/Share/Edit/Delete, and an "Advanced" panel (custom-track backup, a load column and a +// save column). // // The inline table actions POST to small JSON endpoints in hgSession.c (hgS_doDeleteJson, etc.) and // update the table in place. Navigation actions (load a session, load from URL/file, save to file, // reset) are ordinary form submits/links against the existing hgSession actions. // // Styling: shared house-style components in gbModern.css (.gbPill, .gbCard, .gbModal*, .gbTable, // .gbBanner, .gbSection), session-specific layout in hgSession.css. /* global $, hgSessionData, convertTitleTagsToMouseovers, titleTagToMouseover, addMouseover */ /* global htmlEncode, commify, gbShowTimingDialog */ // Cart action variables (must match the hgs* defines in hgSession.h; hgSessionPrefix is "hgS_"). var SESS_ACT = { save: 'hgS_doSaveSessionJson', rename: 'hgS_doRenameSessionJson', @@ -420,31 +420,31 @@ function sessOpenShare(row) { var mailBody = encodeURIComponent('Here is a UCSC Genome Browser session I would like to ' + 'share with you: ') + encodeURIComponent(row.shareUrl); var mailto = 'mailto:?subject=' + encodeURIComponent('UCSC Genome Browser session ' + row.name) + '&body=' + mailBody; var html = '<div class="gbModalTitle">Share “' + sessEnc(row.name) + '”</div>' + '<div class="gbShareBox" style="padding:0;border:0;background:none">' + '<input id="sessShareUrl" class="gbShareInput" type="text" readonly>' + '<button type="button" class="gbPill" id="sessShareCopy" ' + 'title="Copy the link to the clipboard">Copy</button>' + '<a class="gbPill" id="sessShareEmail" href="' + sessEnc(mailto) + '" ' + 'title="Compose an email with this link">Email</a></div>' + '<div class="gbModalText" style="margin-top:14px">Sessions are loadable by anyone with the ' + 'link by default; use <b>Edit</b> to make one private.</div>' + - '<label class="gbModalLabel" style="font-weight:400;color:inherit">' + + '<label class="gbModalLabel" style="font-weight:400;color:inherit;font-size:14px">' + '<input type="checkbox" id="sessGalleryChk"> List it in the ' + '<a href="' + sessEnc(sessData.config.publicSessionsUrl) + '" target="_blank">' + 'Public Sessions</a> gallery</label>' + '<div class="gbModalText err" id="sessShareErr" style="display:none"></div>' + '<div class="gbModalBtns">' + '<button type="button" class="gbPill primary" id="sessShareClose">Done</button></div>'; sessModalOpen(html); var inp = document.getElementById('sessShareUrl'); inp.value = row.shareUrl; document.getElementById('sessGalleryChk').checked = (row.shared >= 2); $('#sessShareClose').on('click', sessModalClose); $('#sessShareCopy').on('click', function() { inp.focus(); inp.select(); if (navigator.clipboard) { navigator.clipboard.writeText(row.shareUrl); } else { try { document.execCommand('copy'); } catch (e) { /* ignore */ } } @@ -611,55 +611,52 @@ var saveFile = '<div class="sessAdvItem"><span class="lab">Save settings to a file</span>' + '<form class="sessAdvRow" action="hgSession" method="POST">' + sid + '<input class="sessAdvInput" type="text" name="hgS_saveLocalFileName" ' + 'placeholder="File name (blank = show in browser)">' + '<label class="sessSaveCheck"><input type="checkbox" name="hgS_saveLocalFileCompress" ' + 'value="gzip"> gzip</label>' + '<button type="submit" class="gbPill" name="hgS_doSaveLocal" value="submit" ' + 'title="Download the current browser settings as a session file">Save</button></form></div>'; var other = '<div class="sessAdvItem"><span class="lab">Other</span>' + '<div class="sessAdvLinks">' + '<a href="' + sessEnc(C.resetUrl) + '" ' + 'title="Reset all browser settings to their defaults">Reset the browser to defaults</a>' + '</div></div>'; + // Back up custom tracks - a button again, across the top of the panel above both columns. + var backup = + '<div class="sessAdvTop"><form action="hgSession" method="POST">' + sid + + '<button type="submit" class="gbPill" name="hgS_showDownload_" value="Submit" ' + + 'title="Download your custom tracks as a .tar.gz archive you can reload later">' + + 'Back up custom tracks (.tar.gz)</button></form></div>'; // Two columns - what you load from, and what you save to - rather than one undifferentiated // list; "Other" (reset) belongs to neither, so it spans both underneath. return '<div class="sessAdv">' + '<div class="sessAdvHead" id="sessAdvHead"><span class="caret">▸</span>' + - '<span>Advanced — load another user’s session, load from a URL or file, ' + - 'save to a file, reset the browser</span></div>' + + '<span>Advanced — back up custom tracks, load another user’s session, load from a URL ' + + 'or file, save to a file, reset the browser</span></div>' + '<div class="sessAdvBody" id="sessAdvBody" style="display:none">' + + backup + '<div class="sessAdvCol"><div class="sessAdvColHead">Load</div>' + loadUser + loadUrl + loadFile + '</div>' + '<div class="sessAdvCol"><div class="sessAdvColHead">Save</div>' + saveFile + '</div>' + '<div class="sessAdvOther">' + other + '</div>' + '</div></div>'; } -function sessBackupHtml(C) { - // A single plain link, sitting on its own line right above the Advanced dropdown - visible - // without opening Advanced, but not a whole bar/button the way it once was. - if (!C.loggedIn) { return ''; } - return '<div class="sessBackupLink"><a href="hgSession?hgS_showDownload_=Submit&' + - sessEnc(C.cartVar) + '=' + sessEnc(C.hgsid) + '" ' + - 'title="Download your custom tracks as a .tar.gz archive you can reload later">' + - 'Back up custom tracks (.tar.gz)</a></div>'; -} - // ---- build the whole page ----------------------------------------------- function sessAccountHtml(C) { // The signed-in account line (Signed in as X · Sign out · Change password) now lives in the // top-right menu bar, so it is intentionally not rendered here. Kept commented out so QA can // add it back if wanted: /* if (C.loggedIn) { var s = 'Signed in as <b>' + sessEnc(C.userName) + '</b>'; if (C.logoutUrl) { s += ' · <a href="' + sessEnc(C.logoutUrl) + '">Sign out</a>'; } if (C.changePasswordUrl) { s += ' · <a href="' + sessEnc(C.changePasswordUrl) + '">Change password</a>'; } return s; } @@ -672,47 +669,53 @@ } function sessSaveCardHtml(C) { if (!C.loggedIn) { return ''; } // Assembly and position separated by a colon (e.g. "hg38: chr7:1-1,000"); assembly is already // the accession for hubs (trackHubSkipHubName on the server). var loc = ''; if (C.db && C.position) { loc = sessEnc(C.db) + ': ' + sessEnc(C.position); } else if (C.db) { loc = sessEnc(C.db); } else if (C.position) { loc = sessEnc(C.position); } if (loc && C.trackCount) { loc += ', ' + sessNum(C.trackCount) + ' track' + (C.trackCount === 1 ? '' : 's') + ' shown'; } var what = loc ? '<span class="sessSaveWhat">' + loc + '</span>' : ''; // Ghost text for the name field is a real generated name, not an explanation of the feature - - // the "Session name" label above the box already says what it is. + // the "Session name" label and its info bubble already say what it is. var randName = sessEnc(sessRandomShareName()); + var nameTip = 'Optional. Leave this blank and your session will be saved under an ' + + 'automatically generated random name, like the one shown here.'; + var descTip = 'Shown when the session is loaded, in the table below on mouseover, and in ' + + 'the Public Sessions gallery if this session is made public.'; return '<div class="sessSaveCard">' + '<div class="sessSaveHead"><span class="sessSaveTitle">' + 'Save the current view as a stable session link</span>' + what + '</div>' + '<div class="sessSaveRow">' + - '<div class="sessSaveField"><span class="lab">Session name</span>' + + '<div class="sessSaveField"><span class="lab">Session name ' + + '<span class="sessInfo" title="' + nameTip + '">ⓘ</span></span>' + '<input id="sessSaveName" class="sessSaveInput" type="text" maxlength="255" ' + 'placeholder="' + randName + '"></div>' + '<label class="sessSaveCheck"><input type="checkbox" id="sessSavePrivate"> ' + - 'Only I can load it</label>' + + 'Private: Session can only be loaded by myself</label>' + '<button type="button" class="gbPill primary" id="sessSaveBtn" ' + 'title="Save your current browser view as a named session">Save session</button>' + '</div>' + - '<div class="sessSaveField"><span class="lab">Description</span>' + + '<div class="sessSaveField"><span class="lab">Description ' + + '<span class="sessInfo" title="' + descTip + '">ⓘ</span></span>' + '<input id="sessSaveDesc" class="sessSaveInput" type="text" maxlength="512" ' + - 'placeholder="Optional — shown on hover and in the Public Sessions gallery"></div>' + + 'placeholder="Optional"></div>' + '</div>'; } function sessRecentHtml(recent) { // A quick shortcut to re-save the session the user most recently saved, keeping its name and // description. Hidden when there are no saved sessions. The button's own mouseover is easy to // miss, so a visible (i) bubble carries the same text. if (!recent) { return ''; } var tip = 'Overwrite this session with the currently active view; keeps the session name and ' + 'description identical'; return '<div class="sessRecent">Most recently saved session: <b>' + sessEnc(recent.name) + '</b> <span class="sessRecentTime">(' + sessEnc(recent.lastUse) + ')</span> ' + '<button type="button" class="gbPill" id="sessUpdateNow" title="' + tip + '">' + 'Overwrite now</button> <span class="sessInfo" title="' + tip + '">ⓘ</span></div>'; } @@ -824,31 +827,30 @@ }); // The session most recently saved/overwritten (max lastUse), for the one-click "Overwrite now". // Only this server's own sessions: "Overwrite now" saves through this server's cart. var recent = null; (sessData.sessions || []).forEach(function(s) { if (s.remote) { return; } if (!recent || s.lastUseEpoch > recent.lastUseEpoch) { recent = s; } }); app.html( intro + '<div id="sessMsg" class="sessMsg"></div>' + sessRecentHtml(recent) + sessSaveCardHtml(C) + - sessBackupHtml(C) + sessAdvancedHtml(C) + sessTableHtml(C) ); // Save current view. Enter in either the name or the description field saves. $('#sessSaveBtn').on('click', sessDoSave); $('#sessSaveName, #sessSaveDesc').on('keydown', function(ev) { if (ev.key === 'Enter') { ev.preventDefault(); sessDoSave(); } }); // One-click overwrite of the most recently saved session. if (recent) { $('#sessUpdateNow').on('click', function() { sessConfirm({ title: 'Overwrite session',