403ab7c9c2b204f487bb2f86260ffdab355e9517
jcasper
Wed Aug 19 05:49:25 2026 -0700
Faceted composites should apply the active sort order to the tracks being
displayed; changing the sort changes the display order. We also preserve that order when
returning to the page. refs #36320
diff --git src/hg/js/facetedComposite.js src/hg/js/facetedComposite.js
index b6796214e5e..f19ff0657bf 100644
--- src/hg/js/facetedComposite.js
+++ src/hg/js/facetedComposite.js
@@ -1,23 +1,33 @@
// SPDX-License-Identifier: MIT; (c) 2025 Andrew D Smith (author)
/* jshint esversion: 11 */
$(function() {
/* ADS: Uncomment below to force confirm on unload/reload */
// window.addEventListener("beforeunload", function (e) {
// e.preventDefault(); e.returnValue = ""; });
const DEFAULT_MAX_CHECKBOXES = 20; // ADS: without default, can get crazy
+ // Hover help for the sort note above the table. addMouseover() in utils.js
+ // renders this with innerHTML, so simple tags are fine.
+ const SORT_ORDER_HELP =
+ "The row order of this table sets the order the subtracks appear in the " +
+ "Genome Browser image.
" +
+ "Click a column heading to sort by that column; click it again to reverse " +
+ "the direction.
" +
+ "To sort on more than one column, click the first heading, then " +
+ "shift-click each additional heading, in the order you want them applied.";
+
const isValidColorMap = obj => // check the whole thing and ignore if invalid
typeof obj === "object" && obj !== null && !Array.isArray(obj) &&
Object.values(obj).every(x =>
typeof x === "object" && x !== null && !Array.isArray(x) &&
Object.values(x).every(value => typeof value === "string"));
// fetch file dynamically
const loadOptional = (url, hgsid, track) => { // load if possible otherwise carry on
if (!url) return Promise.resolve(null);
let fetchBody = `fileUrl=${url}&track=${track}`;
if (hgsid !== null) {
fetchBody = fetchBody + `&hgsid=${hgsid}`;
}
const fetchUrl = `/cgi-bin/hgTrackUi?${fetchBody}`;
const req = (fetchUrl.length > 2048 || embeddedData.udcTimeout) ?
@@ -134,40 +144,52 @@
const dataTag = document.getElementById("app-data");
return dataTag ? JSON.parse(dataTag.innerText) : "";
})();
// Store initial checkbox states for delta computation on server
const initialState = {
dataElements: new Set(),
dataTypes: new Set()
};
function generateHTML() {
const container = document.createElement("div");
container.id = "myTag";
container.innerHTML = `
+
`;
// Instead of appending to body, append into the placeholder div
document.getElementById("metadata-placeholder").appendChild(container);
+
+ // The table's row order drives the order tracks are drawn in the browser
+ // image. That's easy to miss (the classic composite UI never says so
+ // either), so state it in one line and put the details in the hover.
+ // The icon is appended as a node because createInfoIcon() returns an
+ // element that already has its mouseover listeners attached.
+ const note = document.getElementById("sortNote");
+ note.appendChild(document.createTextNode(
+ "Tracks appear in the Genome Browser in the same order as the table " +
+ "below - click a column heading to re-sort."));
+ note.appendChild(createInfoIcon(SORT_ORDER_HELP));
}
function updateVisibilities(uriForUpdate, submitBtnEvent) {
// get query params from URL
const paramsFromUrl = new URLSearchParams(window.location.search);
const db = paramsFromUrl.get("db");
const hgsid = paramsFromUrl.get("hgsid");
let body = `${uriForUpdate}`;
if (db !== null) {
body = body + `&db=${db}`;
}
if (hgsid !== null) {
body = body + `&hgsid=${hgsid}`;
}
fetch("/cgi-bin/cartDump", {
@@ -269,58 +291,86 @@
orderable: false,
defaultContent: "",
title: `
`,
// no render function needed
};
const hasDataTypes = embeddedData.dataTypes &&
Object.keys(embeddedData.dataTypes).length > 0;
const itemLabel = hasDataTypes ? "samples" : "tracks";
const singularLabel = itemLabel.slice(0, -1);
const columns = [checkboxColumn, ...ordinaryColumns];
+ // Map a metadata field name to its DataTables column index, matching
+ // case-insensitively and ignoring leading underscores. Returns -1 when
+ // the name isn't one of the metadata columns.
+ const colIdxForName = name => {
+ const target = name.replace(/^_+/, "").toLowerCase();
+ const idx = colNames.findIndex(
+ c => c.replace(/^_+/, "").toLowerCase() === target);
+ return idx >= 0 ? idx + 1 : -1; // +1 for the checkbox column
+ };
+
// Determine which column to sort by: use defaultSortField if it matches
- // a metadata column (case-insensitive, ignoring leading underscores),
- // otherwise fall back to the first data column.
+ // a metadata column, otherwise fall back to the first data column.
let defaultSortCol = 1; // column 0 is checkboxes, 1 is first data col
if (embeddedData.defaultSortField) {
- const target = embeddedData.defaultSortField.replace(/^_+/, "").toLowerCase();
- const idx = colNames.findIndex(
- c => c.replace(/^_+/, "").toLowerCase() === target);
- if (idx >= 0)
- defaultSortCol = idx + 1; // +1 for the checkbox column
+ const idx = colIdxForName(embeddedData.defaultSortField);
+ if (idx > 0)
+ defaultSortCol = idx;
+ }
+
+ // A sort the user established on an earlier visit wins over
+ // defaultSortField. facetSortOrder mirrors the classic composite
+ // '