689349fca5a4865a1891db8cd39d392657b2b09b jcasper Wed Apr 22 02:54:09 2026 -0700 Replacing the subtrackUrl setting for faceted composites with subtrackUrls, which supports outlinks in multiple fields. refs #36320 diff --git src/hg/js/facetedComposite.js src/hg/js/facetedComposite.js index 912f6710d5f..0a54703ad1d 100644 --- src/hg/js/facetedComposite.js +++ src/hg/js/facetedComposite.js @@ -116,40 +116,70 @@ const selectedDataTypes = new Set( // get dataTypes selected initially Object.entries(embeddedData.dataTypes).filter(([_, val]) => val.active === 1) .map(([key]) => key) ); // initialize data type checkboxes (using class instead of 'name') document.querySelectorAll("input.cbgroup") .forEach(cb => { cb.checked = selectedDataTypes.has(cb.value); }); // Capture initial data type state initialState.dataTypes = new Set(selectedDataTypes); } function initTable(allData) { const { metadata, rowToIdx, colNames } = allData; + // Match subtrackUrls trackDb keys against metadata column names + // ignoring leading underscores on either side, so authors can toggle + // facet visibility by adding/removing a '_' prefix in the metadata + // file without having to re-edit trackDb. + const stripUnderscores = s => s.replace(/^_+/, ""); + const subtrackUrls = Object.fromEntries( + Object.entries(embeddedData.subtrackUrls || {}) + .map(([k, v]) => [stripUnderscores(k), v]) + ); + const ordinaryColumns = colNames.map(key => { const col = { data: key, title: toTitleCase(key.replace(/^_/, "")), }; - if (key === embeddedData.primaryKey && embeddedData.subtrackUrl) { + const urlTemplate = subtrackUrls[stripUnderscores(key)]; + if (urlTemplate) { + // Mirrors hgc/hgc.c:printIdOrLinks(): split cell on ',', each + // token may be 'id|label' (id substitutes $$, label is shown). + // urlTemplate is html-encoded server-side (htmlEncode in + // hgTrackUi.c), so it's safe to interpolate into an href. col.render = (data, type) => { if (type !== "display") return data; - const url = embeddedData.subtrackUrl.replace("$$", encodeURIComponent(data)); - return `${data}`; + if (data == null || data === "") return ""; + const parts = String(data).split(",") + .map(s => s.trim()) + .filter(Boolean); + return parts.map(tok => { + let idForUrl = tok, label = tok, encode = true; + const bar = tok.indexOf("|"); + if (bar >= 0) { + idForUrl = tok.slice(0, bar); + label = tok.slice(bar + 1); + encode = false; + } + if (/^https?:/i.test(label)) encode = false; + const sub = encode ? encodeURIComponent(idForUrl) : idForUrl; + const href = urlTemplate.replace(/\$\$/g, sub); + return `${label}`; + }).join(", "); }; } return col; }); const checkboxColumn = { data: null, orderable: false, defaultContent: "", title: ` `, // no render function needed };