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
};