83858083785a6b2b1f3c05f93e818e5a89318692 max Tue Aug 25 01:42:34 2026 -0700 hgSession: show cytoBand and gene/locus annotations for each saved session; date-only Created/Last used columns with full timestamp on hover. Factor locusName lookup into reusable hLocusName/hLocusNameExpand in hdb.c and reuse from hgBlat. refs #38157 The Assembly column now shows band, gene/locus name and Mbp position (separated by whitespace, normal text size). hLocusNameExpand centralizes the ex:/in:/ig: and | to - expansion that hgBlat's results page did inline. diff --git src/hg/js/hgSession.js src/hg/js/hgSession.js index 090936a3cb4..cc2942efb23 100644 --- src/hg/js/hgSession.js +++ src/hg/js/hgSession.js @@ -46,35 +46,40 @@ return (typeof commify === 'function') ? commify(n) : String(n); } function sessRandomShareName() { // Mirror the server's auto/anonymous share-name convention ("share_" + 8 URL-safe alphanumeric // chars). hgSession.c's doSaveSessionJson generates the same style server-side for the top-right // "Share a link"; we generate it here so the confirm dialog can show the name before saving. var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; var s = ''; for (var i = 0; i < 8; i++) { s += chars.charAt(Math.floor(Math.random() * chars.length)); } return 'share_' + s; } -function sessCommifyPos(pos) { - // Add thousands separators to each number in a position string ("chr7:155799529-155812871" -> - // "chr7:155,799,529-155,812,871") using utils.js commify. - if (typeof commify !== 'function') { return String(pos); } - return String(pos).replace(/\d+/g, function(n) { return commify(n); }); +function sessMbpPos(pos) { + // Shorten a position to megabases to save horizontal space: + // "chr7:155799529-155812871" -> "chr7:155.80-155.81 Mbp". Falls back to the raw string + // if it doesn't parse. + var m = /^(.+):([0-9,]+)-([0-9,]+)$/.exec(String(pos)); + if (!m) { return String(pos); } + var s = parseInt(m[2].replace(/,/g, ''), 10); + var e = parseInt(m[3].replace(/,/g, ''), 10); + if (isNaN(s) || isNaN(e)) { return String(pos); } + return m[1] + ':' + (s / 1e6).toFixed(2) + '-' + (e / 1e6).toFixed(2) + ' Mbp'; } function sessMsg(text, cls) { // Show a transient status line at the top of the app (cls: 'ok' | 'err' | ''). var el = document.getElementById('sessMsg'); if (!el) { return; } el.className = 'sessMsg' + (cls ? ' ' + cls : ''); el.innerHTML = text ? sessEnc(text) : ''; } // ---- AJAX ---------------------------------------------------------------- // Post an action to hgSession and get JSON back. Always carry the session id so the CGI loads the // right cart. onOk(resp) is called for {success:true,...} or {name,url}; onErr(msg) for {error}. function sessAjax(params, onOk, onErr) { @@ -715,41 +720,49 @@ language: { searchPlaceholder: 'Search sessions', search: '', lengthMenu: 'Show _MENU_', emptyTable: 'No saved sessions yet.' }, columns: [ { title: '<input type="checkbox" id="sessSelAll" title="Select all on this page">', className: 'sessSelCol', data: null, orderable: false, searchable: false, visible: false, render: function(d, type, row) { return '<input type="checkbox" class="sessSelChk" data-enc="' + sessEnc(row.encName) + '">'; } }, { title: 'Session', className: 'sessNameCol', data: 'name', render: function(d, type, row) { return (type === 'display') ? sessNameCellHtml(row) : row.name; } }, { title: 'Assembly', data: 'db', render: function(d, type, row) { if (type !== 'display') { return row.db || ''; } var html = sessEnc(row.db || 'n/a'); - if (row.position) { - html += ' <span class="sessPos">' + sessEnc(sessCommifyPos(row.position)) + - '</span>'; + // Region: band, locus (gene) and the position shortened to Mbp, so it all fits. + var region = []; + if (row.band) { region.push(sessEnc(row.band)); } + if (row.locus) { region.push(sessEnc(row.locus)); } + if (row.position) { region.push(sessEnc(sessMbpPos(row.position))); } + if (region.length) { + html += ' <span class="sessPos">' + region.join(' ') + '</span>'; } return html; } }, { title: 'Created', data: 'createdEpoch', render: function(d, type, row) { - return (type === 'display') ? sessEnc(row.created) : row.createdEpoch; } }, + return (type === 'display') ? + '<span title="' + sessEnc(row.createdFull || row.created) + '">' + + sessEnc(row.created) + '</span>' : row.createdEpoch; } }, { title: 'Last used', data: 'lastUseEpoch', render: function(d, type, row) { - return (type === 'display') ? sessEnc(row.lastUse) : row.lastUseEpoch; } }, + return (type === 'display') ? + '<span title="' + sessEnc(row.lastUse) + '">' + + sessEnc(row.lastUseDate || row.lastUse) + '</span>' : row.lastUseEpoch; } }, { title: 'Views', data: 'useCount', render: function(d, type) { return (type === 'display') ? sessNum(d) : d; } }, { title: 'Actions', className: 'sessActionsCol', data: null, orderable: false, searchable: false, render: function(d, type, row) { return (type === 'display') ? sessActionsHtml(row) : ''; } } ] }); // Delegate the row action buttons. $('#sessionAppTable tbody').on('click', 'button[data-act]', function() { var act = this.getAttribute('data-act'); var row = sessByEnc(this.getAttribute('data-enc')); if (!row) { return; } if (act === 'overwrite') { sessOpenOverwrite(row); } else if (act === 'share') { sessOpenShare(row); }