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 = `
         <div id="dataTypeSelector"></div>
         <div id="container">
             <div id="filters"></div>
             <table id="theMetaDataTable">
                 <thead></thead>
                 <tfoot></tfoot>
             </table>
         </div>
         `;
         // Instead of appending to body, append into the placeholder div
         document.getElementById("metadata-placeholder").appendChild(container);
@@ -93,30 +99,33 @@
             innerHTML: "<b>Data type</b>",
         }));
         Object.keys(embeddedData.dataTypes).forEach(name => {
             const label = document.createElement("label");
             label.innerHTML = `
                 <input type="checkbox" class="cbgroup" value="${name}">${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;