abe6cb020492b82545875d59c570d0a3acc51376 jcasper Wed Feb 4 14:41:49 2026 -0800 Adjusting faceted composite cart logic to support tracks that are on by default, refs #36320 diff --git src/hg/js/facetedComposite.js src/hg/js/facetedComposite.js index 8c2298f5acc..e9da72619a2 100644 --- src/hg/js/facetedComposite.js +++ src/hg/js/facetedComposite.js @@ -33,30 +33,36 @@ : callback(); const toTitleCase = str => str.toLowerCase() .split(/[_\s-]+/) // Split on underscore, space, or hyphen .map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" "); const escapeRegex = str => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const embeddedData = (() => { // get data that was embedded in the HTML here to use them as globals 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); @@ -93,30 +99,33 @@ innerHTML: "Data type", })); Object.keys(embeddedData.dataTypes).forEach(name => { const label = document.createElement("label"); label.innerHTML = ` ${name}`; selector.appendChild(label); }); const selectedDataTypes = new Set( // get dataTypes selected initially Object.entries(embeddedData.dataTypes).filter(([_, val]) => val === 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; const ordinaryColumns = colNames.map(key => ({ // all but checkboxes data: key, title: toTitleCase(key), })); const checkboxColumn = { data: null, orderable: false, className: "select-checkbox", defaultContent: "", @@ -133,30 +142,32 @@ columns: columns, responsive: true, // autoWidth: true, // might help columns shrink to fit content order: [[1, "asc"]], // sort by the first data column, not checkbox pageLength: 50, // show 50 rows per page by default lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]], select: { style: "multi", selector: "td:first-child" }, initComplete: function() { // Check appropriate boxes const api = this.api(); embeddedData.dataElements.forEach(rowName => { const rowIndex = rowToIdx[rowName]; if (rowIndex !== undefined) { api.row(rowIndex).select(); } }); + // Capture initial data element state + initialState.dataElements = new Set(embeddedData.dataElements); }, drawCallback: function() { // Reset header "select all" checkbox $("#select-all") .prop("checked", false) .prop("indeterminate", false); }, }); // define inputs for search functionality for each column in the table const row = document.querySelector("#theMetaDataTable thead").insertRow(); columns.forEach((col) => { const cell = row.insertCell(); if (col.className === "select-checkbox") { // show selected items const label = document.createElement("label"); label.title = "Show only selected rows"; @@ -313,58 +324,89 @@ clearBtn.addEventListener("click", () => { cboxes.forEach(cb => cb.checked = false); // Uncheck all // Recalculate the (now cleared) search term and update table table.column(colIdx + 1).search("", true, false).draw(); }); // Prepend the "clear" button attrDiv.insertBefore(clearBtn, attrDiv.children[1] || null); }); return table; // to chain calls } // end initFilters function initSubmit(table) { // logic for the submit event const { mdid, primaryKey } = embeddedData; // mdid: metadata identifier const hasDataTypes = embeddedData.dataTypes && Object.keys(embeddedData.dataTypes).length > 0; - document.getElementById("Submit").addEventListener("click", (submitBtnEvent) => { submitBtnEvent.preventDefault(); // hold the submit button event - const selectedRows = table.rows({selected: true}).data().toArray(); - const uriForUpdate = new URLSearchParams({ "cartDump.metaDataId": mdid, "noDisplay": 1 }); - selectedRows.forEach(obj => // 'de' for data element - uriForUpdate.append(`${mdid}.de`, obj[primaryKey])); - + const currentDataTypes = []; if (hasDataTypes) { - // Collect checked data types - const selectedDataTypes = []; + // Get current data type selections document.querySelectorAll("input.cbgroup").forEach(cb => { if (cb.checked) { - selectedDataTypes.push(cb.value); + currentDataTypes.push(cb.value); } }); // Require at least one data type when the selector exists - if (selectedDataTypes.length === 0) { + if (currentDataTypes.length === 0) { alert("Please select at least one data type."); return; // abort submission } - selectedDataTypes.forEach(dat => // 'dt' for data type - uriForUpdate.append(`${mdid}.dt`, dat)); + } + + // Get current data element selections + const currentDataElements = table.rows({selected: true}).data().toArray() + .map(obj => obj[primaryKey]); + + // Build the parameters for the cart update + const uriForUpdate = new URLSearchParams({ + "cartDump.metaDataId": mdid, + "noDisplay": 1 + }); + + // Data elements: was and now + if (initialState.dataElements.size > 0) { + initialState.dataElements.forEach(de => + uriForUpdate.append(`${mdid}.de_was`, de)); + } else { + uriForUpdate.append(`${mdid}.de_was`, ""); + } + if (currentDataElements.length > 0) { + currentDataElements.forEach(de => + uriForUpdate.append(`${mdid}.de_now`, de)); + } else { + uriForUpdate.append(`${mdid}.de_now`, ""); + } + + if (hasDataTypes) { + // Data types: was and now + if (initialState.dataTypes.size > 0) { + initialState.dataTypes.forEach(dt => { + uriForUpdate.append(`${mdid}.dt_was`, dt);}); + } else { + uriForUpdate.append(`${mdid}.dt_was`, ""); + } + if (currentDataTypes.length > 0) { + currentDataTypes.forEach(dt => { + uriForUpdate.append(`${mdid}.dt_now`, dt);}); } else { - // No data types configured for this track: send empty-string sentinel - uriForUpdate.append(`${mdid}.dt`, ""); + uriForUpdate.append(`${mdid}.dt_now`, ""); } + } + // No ${mdid}.dt* variables indicates that the composite doesn't use data types + updateVisibilities(uriForUpdate, submitBtnEvent); }); } // end initSubmit function initAll(dataForTable) { initDataTypeSelector(); const table = initTable(dataForTable); initFilters(table, dataForTable); initSubmit(table); } function loadDataAndInit() { // load data and call init functions const { mdid, primaryKey, metadataUrl, colorSettingsUrl } = embeddedData; const CACHE_KEY = mdid;