b678946c7d246ae01d552242ac0296b8694d5fc3
max
  Tue Sep 8 06:15:43 2026 -0700
hgTrackUi: faceted composite says "Samples" when data types are on, and stops paging a short table

The two filter tabs were hardcoded to "All Tracks" and "Active Tracks", which is
wrong for a composite that uses dataTypes: there a row is a sample, standing for
as many tracks as there are active data types. The file already had an itemLabel
that resolves to "samples" or "tracks" and was feeding the DataTables strings, so
the tabs now use the same variable. A composite without dataTypes reads exactly
as before.

Page length was a flat 25, so a table of 41 samples hid a third of itself behind
a pager for no good reason. Tables under 50 rows now start out showing
everything; the length menu still offers 10/25/50/100/All for bigger ones.

refs #36210

diff --git src/hg/js/facetedComposite.js src/hg/js/facetedComposite.js
index b2d2f8448ce..4d33f92d011 100644
--- src/hg/js/facetedComposite.js
+++ src/hg/js/facetedComposite.js
@@ -295,30 +295,34 @@
 
         const checkboxColumn = {
             data: null,
             orderable: false,
             defaultContent: "",
             title: `
             <label title="Select all visible rows">
             <input type="checkbox" id="select-all"/></label>`,
             // 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);
+        // Capitalized, for the two filter tabs.  With data types a row is a
+        // sample rather than a track, since each row stands for as many tracks
+        // as there are active data types.
+        const itemLabelCap = itemLabel.charAt(0).toUpperCase() + itemLabel.slice(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, otherwise fall back to the first data column.
@@ -354,31 +358,35 @@
             data: metadata,
             deferRender: true,    // seems faster
             columns: columns,
             columnDefs: [ { targets:0, render: DataTable.render.select() } ],
             // 'responsive' (collapsing overflow columns) is intentionally off:
             // a wide table instead gets an internal horizontal scrollbar via
             // the .table-xscroll wrapper added at the end of initTable.
             responsive: false,
             layout: {
                 topStart: 'pageLength',
                 topEnd: null,        // omit global search
                 bottomStart: 'info',
                 bottomEnd: 'paging'
             },
             order: initialOrder,
-            pageLength: 25,       // show 25 rows per page by default
+            // Paginating a table that would nearly fit anyway just hides rows
+            // behind a menu, so show everything up to the first menu step that
+            // exceeds 25.  -1 is what DataTables reads as "all", the same value
+            // behind the "All" entry in the menu below.
+            pageLength: metadata.length < 50 ? -1 : 25,
             lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]],
             language: {
                 lengthMenu: `Show _MENU_ ${itemLabel}`,
                 select: {
                     rows: {
                         0: "",
                         1: `1 ${singularLabel} selected`,
                         _: `%d ${itemLabel} selected`
                     }
                 },
                 info: `Showing _START_ to _END_ of _TOTAL_ ${itemLabel}`,
                 infoFiltered: `(filtered from _MAX_ total ${itemLabel})`,
             },
             select: { style: "multi", selector: "td:not(:has(a))" },
             initComplete: function() {  // Check appropriate boxes
@@ -445,32 +453,32 @@
             {type: "button", className: "filter-tab"});
         const selectedTab = Object.assign(document.createElement("button"),
             {type: "button", className: "filter-tab"});
         toggleWrapper.appendChild(allTab);
         toggleWrapper.appendChild(selectedTab);
         lengthDiv.appendChild(toggleWrapper);
 
         // Refresh the tab labels and the active-tab highlight. Counts are grand
         // totals (default search:'none'), independent of the facet/search
         // filters, so "Selected" never misleadingly reads 0 when tracks are
         // selected but currently hidden by a facet. How many rows are actually
         // visible is reported by DataTables' bottom info line.
         function updateSelectedText() {
             const selCount = table.rows({selected: true}).count();
             const totalCount = table.rows().count();
-            allTab.textContent = `All Tracks (${totalCount})`;
-            selectedTab.textContent = `Active Tracks (${selCount})`;
+            allTab.textContent = `All ${itemLabelCap} (${totalCount})`;
+            selectedTab.textContent = `Active ${itemLabelCap} (${selCount})`;
             const showSelected = toggleCheckbox.checked;
             allTab.classList.toggle("active", !showSelected);
             selectedTab.classList.toggle("active", showSelected);
         }
         updateSelectedText();
 
         // Clicking a tab switches the selection filter and redraws. "Selected"
         // is always clickable; with nothing selected it just shows an empty list.
         function setFilterMode(showSelected) {
             toggleCheckbox.checked = showSelected;
             table.draw();
             updateSelectedText();
         }
         allTab.addEventListener("click", () => setFilterMode(false));
         selectedTab.addEventListener("click", () => setFilterMode(true));