9f8d33c8b2b6bc61f6d02d781c4e02836f7099f9 max Fri Aug 21 02:05:42 2026 -0700 hgSession: new opt-in JavaScript "My Sessions" page; share gbModern.css with hgBlat. refs #38157 Applies the hgBlat facelift strategy (#37996) to hgSession: an opt-in, client-rendered "My Sessions" page gated by the sessionNewPage / sessionNewPageBanner hg.conf flags (mirroring blatNewForm / blatNewFormBanner), with a banner linking between the classic and new pages so neither is a one-way door. sessionNewPage also flips the site default. hgSession.c stays the data/action backend: it emits the session list and page config as an inline JSON global (hgSessionData) into an empty #sessionApp container, and the new hgSession.js builds the UI - a save-current-view card (name + optional description + "only I can load it"; empty name saves under a random share_ name), a "most recently saved session" one-click Update, a searchable/sortable/paged DataTable of sessions (assembly + position, created with last-used on hover, views, a lock icon on private sessions), inline Share (copy link / email / gallery), Edit (rename + description + private), Overwrite and Delete, and a bulk Select -> Delete-all-selected mode. The mutating actions POST to new JSON endpoints (hgS_doDeleteJson / doShareJson / doGalleryJson / doOverwriteJson / doDescribeJson) that run the same SQL as the classic full-page handlers and return JSON, so the table updates in place; loads, file up/downloads and custom-track backup stay as ordinary form submits/links. The Advanced panel keeps feature parity with the classic page (load another user's session, load from URL/file, save to file, back up custom tracks, reset), minus the login/ change-password links that now live in the top menu. Shared UCSC house-style components (design tokens, .gbPill, .gbCard, .gbStrip, .gbSection, .gbShareBox, .gbBanner, the .gbModal* dialog and a .gbTable) are factored into a new gbModern.css. hgBlat is migrated onto it: its generic .blat* classes are renamed to the shared .gb* names in hgBlat.css / hgBlat.js and the #blatResults / #blatFormBox containers get class="gbApp"; verified pixel-clean against the previous search form and results pages, including the rename modal. hgSession.css holds only session-specific layout. diff --git src/hg/js/hgBlat.js src/hg/js/hgBlat.js index d3f66718551..aae33b07a69 100644 --- src/hg/js/hgBlat.js +++ src/hg/js/hgBlat.js @@ -77,77 +77,77 @@ var c = blatIdColor(hit.identity); return `${hit.identity.toFixed(1)}%`; } function blatCoverageCell(hit) { var left = (hit.qStart - 1) / hit.qSize * 100; var width = (hit.qEnd - hit.qStart + 1) / hit.qSize * 100; var tip = `Query matches the genome at ${blatFmt(hit.qStart)}-${blatFmt(hit.qEnd)}bp out of ${blatFmt(hit.qSize)}bp`; return ``; } // ---- summary strip + detail panel --------------------------------------- function blatSummaryStrip(cfg, queryCount) { - var stat = (k, v) => `
${k}` + + var stat = (k, v) => `
${k}` + `${v}
`; - var div = ''; + var div = ''; var assembly = stat('Assembly', htmlEncode(cfg.organism) + ' / ' + htmlEncode(cfg.db)) + div + stat('Matches', blatFmt(cfg.hitCount)); var stats; if (cfg.multiQuery) { // With more than one query sequence a single query name/length would be wrong, so show the // number of distinct queries; each hit's own query is in the table's Query column. stats = stat('Queries', blatFmt(queryCount)) + div + assembly; } else { stats = stat('Query', htmlEncode(cfg.queryName)) + div + stat('Length', blatFmt(cfg.querySize) + ' bp') + div + assembly; } var actions = ''; // "View all in browser" is the primary action, so it comes first. if (cfg.viewAllUrl) { - actions += `View all in browser`; + actions += `View all in browser`; } // "Show Query Sequence" opens the query FASTA in a panel (with Download / Copy). Only on a fresh // search, where the uploaded sequence is available (cfg.querySeqs emitted by hgBlat.c). if (cfg.querySeqs && cfg.querySeqs.length) { - actions += ''; } // "Share a link" just reveals the page's stable URL (cfg.shareUrl, a trash-backed reopen link). // cfg.canShare covers old session-based links (?u=&s=), where the current URL is already shareable. if (cfg.shareUrl || cfg.canShare) { // A small share-nodes icon precedes the label so users learn to associate it with sharing. var shareIcon = ''; - actions += ''; } // "Rename BLAT Track" opens a modal to rename the results custom track. This is a JS-native // button (renders immediately with the strip) that replaces the old C-emitted inline form, which // only appeared after the buildBigPsl AJAX finished and reflowed the page when clicked. if (cfg.canRename) { - actions += ''; } - return `
${stats}${actions}
`; + return `
${stats}${actions}
`; } var BLAT_TILE_TIPS = { 'Score': 'BLAT score: matches minus mismatches and gap penalties. Higher is better.', 'Identity': 'Percent identity of the aligned bases.', 'Matches': 'Query bases that match the genome.', 'Mismatch': 'Bases that differ between query and genome.', 'Gaps': 'Number of gaps (insertions or deletions) in the alignment.', 'Blocks': 'Number of ungapped aligned blocks.', 'Strand': 'Genome strand the query matched (+ or -).', 'Q span': 'Range of the query sequence that aligned (1-based).' }; function blatTileSkeleton(label, id, color) { var style = color ? ` style="color:${color}"` : ''; @@ -162,36 +162,36 @@ var tiles = blatTileSkeleton('Score', 'dvScore') + blatTileSkeleton('Identity', 'dvIdentity') + blatTileSkeleton('Matches', 'dvMatches') + blatTileSkeleton('Mismatch', 'dvMismatch') + blatTileSkeleton('Gaps', 'dvGaps') + blatTileSkeleton('Blocks', 'dvBlocks') + blatTileSkeleton('Strand', 'dvStrand') + blatTileSkeleton('Q span', 'dvQspan'); document.getElementById('blatDetail').innerHTML = `
Selected hit` + `
` + `
` + `
${tiles}
` + `
` + + `Open in browser` + + `Open in new tab
` + `
` + `
Alignment
` + `
` + - `` + `View alignment
`; if (typeof convertTitleTagsToMouseovers === 'function') { convertTitleTagsToMouseovers(); } } function blatSet(id, prop, val) { var e = document.getElementById(id); if (!e) { return; } if (prop === 'text') { e.textContent = val; } else if (prop === 'href') { e.setAttribute('href', val); } else if (prop === 'color') { e.style.color = val; } } function blatRenderDetail(hit) { if (!hit || !document.getElementById('blatDetail')) { return; } @@ -258,106 +258,106 @@ var tip = BLAT_HEADER_TIPS[$(this).text().trim()]; if (tip) { $(this).attr('title', tip); } }); if (typeof convertTitleTagsToMouseovers === 'function') { convertTitleTagsToMouseovers(); } } // ---- share a link -------------------------------------------------------- function blatShareLink() { // No session, no AJAX: the results page already has a stable, shareable URL (hgBlat.c emits it as // cfg.shareUrl and blatBuild() pins it into the address bar with history.replaceState), so this // just shows/copies window.location. The link reopens straight from the trash .pslx/.fa, so it // works until those trash files are cleaned - hence the retention note. - var box = document.getElementById('blatShareBox'); + var box = document.getElementById('gbShareBox'); if (!box) { return; } if (box.style.display === 'flex') { box.style.display = 'none'; return; } // toggle off var url = window.location.href; box.style.display = 'flex'; box.innerHTML = - 'Shareable link — anyone with it can reopen ' + + 'Shareable link — anyone with it can reopen ' + 'these results. The results are stored temporarily, so the link works for at least 48 hours ' + 'after they were last viewed.' + - '' + - ''; - var inp = document.getElementById('blatShareInput'); + '' + + ''; + var inp = document.getElementById('gbShareInput'); inp.value = url; inp.focus(); inp.select(); $('#blatShareCopy').on('click', function() { inp.select(); if (navigator.clipboard) { navigator.clipboard.writeText(url); } else { document.execCommand('copy'); } this.textContent = 'Copied'; }); } // ---- Rename BLAT track (modal) ------------------------------------------- // The results custom track is built (and renamed) by hgBlat.c's inline code, which exposes a small // window.blatRenameCt(name, description) helper (it POSTs to hgc's buildBigPsl and rebuilds the // track). We reuse that helper (no new endpoint), just swapping its old inline toggle-form UI for a // proper modal dialog. The current name/description come from cfg (hgBlat.c), not a global, so this // does not depend on any generic page-global. function blatRenameModalHtml(cfg) { // hgSession link is relative (same /cgi-bin/), carrying db + hgsid so the session page opens in // this assembly and cart. var sessionUrl = `hgSession?db=${encodeURIComponent(cfg.db)}&hgsid=${encodeURIComponent(cfg.hgsid)}`; - return '