d40ace87860e49440a8ffcd09a4a68a682ad07ec
chmalee
  Thu Apr 23 12:57:12 2026 -0700
add nowrap rules to more settings in the hubSpace data table to prevent row heights from growing when a filename is too long for the current window size and forces an adjustment by Data Tables. adjust columns when the window size grows dynamically. this should ensure the view in buttons are always the same size regardless of screen width or table content, refs Max/Baihe email

diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js
index 143b8b3f6be..d9298b42ef0 100644
--- src/hg/js/hgMyData.js
+++ src/hg/js/hgMyData.js
@@ -1185,31 +1185,30 @@
                 });
             }
         });
         updateSelectedFileDiv(data, selectedRow.data().fileType === "dir");
     }
 
     function createOneCrumb(table, dirName, dirFullPath, doAddEvent) {
         // make a new span that can be clicked to nav through the table
         let newSpan = document.createElement("span");
         newSpan.id = dirName;
         newSpan.textContent = decodeURIComponent(dirName);
         newSpan.classList.add("breadcrumb");
         if (doAddEvent) {
             newSpan.addEventListener("click", function(e) {
                 dataTableShowDir(table, dirName, dirFullPath);
-                table.draw();
                 dataTableCustomOrder(table, {"fullPath": dirFullPath});
                 table.draw();
             });
         } else {
             // can't click the final crumb so don't underline it
             newSpan.style.textDecoration = "unset";
         }
         return newSpan;
     }
 
     function dataTableEmptyBreadcrumb(table) {
         let currBreadcrumb = document.getElementById("breadcrumb");
         currBreadcrumb.replaceChildren(currBreadcrumb.firstChild);
     }
 
@@ -1248,32 +1247,32 @@
         clearSearch(table);
         // deselect any selected rows like Finder et al when moving into/upto a directory
         table.rows({selected: true}).deselect();
         table.search.fixed("showRoot", function(searchStr, rowData, rowIx) {
             return !rowData.parentDir;
         });
         uiState.currentHub = "";
         hideHubBanner();
     }
 
     function dataTableShowDir(table, dirName, dirFullPath) {
         // show the directory and all immediate children of the directory
         clearSearch(table);
         // deselect any selected rows like Finder et al when moving into/upto a directory
         table.rows({selected: true}).deselect();
-        table.draw();
-        // NOTE that the below does not actually render until the next table.draw() call
+        // Callers must call table.draw() after this so filter + order changes
+        // from showDir/customOrder render in a single redraw.
         table.search.fixed("oneHub", function(searchStr, rowData, rowIx) {
             // calculate the fullPath of this rows parentDir in case the dirName passed
             // to this function has the same name as a parentDir further up in the
             // listing. For example, consider a test/test/tmp.txt layout, where "test"
             // is the parentDir of tmp.txt and the test subdirectory
             let parentDirFull = rowData.fullPath.split("/").slice(0,-1).join("/");
             if (rowData.parentDir === dirName && parentDirFull === dirFullPath) {
                 return true;
             } else if (rowData.fullPath === dirFullPath) {
                 // also return the directory itself
                 return true;
             } else {
                 return false;
             }
         });
@@ -1431,30 +1430,31 @@
                 console.log("folder click");
                 let table = $("#filesTable").DataTable();
                 let trow = $(e.target).closest("tr");
                 let row = table.row(trow);
                 dataTableShowDir(table, rowData.fileName, rowData.fullPath);
                 dataTableCustomOrder(table, rowData);
                 table.draw();
             });
             return folderIcon;
         } else {
             // only offer the button if this is a track file
             if (rowData.fileType !== "hub.txt" && rowData.fileType !== "text" && rowData.fileType !== "tabixIndex" && rowData.fileType !== "bamIndex" && rowData.fileType !== "2bit" && rowData.fileType in extensionMap) {
                 let container = document.createElement("div");
                 let viewBtn = document.createElement("button");
                 viewBtn.textContent = "View in Genome Browser";
+                viewBtn.style.whiteSpace = "nowrap";
                 viewBtn.type = 'button';
                 viewBtn.addEventListener("click", function(e) {
                     e.stopPropagation();
                     viewInGenomeBrowser(rowData.fileName, rowData.fileType, rowData.genome, rowData.parentDir, rowData.hubType);
                 });
                 container.appendChild(viewBtn);
                 return container;
             } else {
                 return null;
             }
         }
     }
 
     function deleteFileFromTable(pathList) {
         // req is an object with properties of an uploaded file, make a new row
@@ -1536,63 +1536,59 @@
                 for (let j = 0; j < allRows.length; j++) {
                     let rowData = table.row(allRows[j]).data();
                     if (rowData.fullPath === obj.fullPath) {
                         table.row(allRows[j]).invalidate();
                         break;
                     }
                 }
             }
         }
 
         // show all the new rows we just added, note the double draw, we need
         // to have the new rows rendered to do the order because the order
         // will copy the actual DOM node
         parseFileListIntoHash(uiState.fileList);
         dataTableShowDir(table, hubDirData.fileName, hubDirData.fullPath);
-        table.draw();
         dataTableCustomOrder(table, hubDirData);
-        // Flush dataTableCustomOrder's row.remove() so DataTables internal state is clean,
-        // then defer columns.adjust() to allow browser reflow after header DOM manipulation
         table.draw();
-        setTimeout(function() {
-            table.columns.adjust();
-        }, 0);
     }
 
     function doRowSelect(evtype, table, indexes) {
         let selectedRow = table.row(indexes);
         let rowTr = selectedRow.node();
         if (rowTr) {
             handleCheckboxSelect(evtype, table, selectedRow);
         }
     }
 
     function indentActionButton(rowTr, rowData) {
         let numIndents = "0px"; //data.parentDir !== "" ? data.fullPath.split('/').length - 1: 0;
         if (rowData.fileType !== "dir") {
             numIndents = "10px";
         }
         rowTr.childNodes[1].style.textIndent = numIndents;
     }
 
     let tableInitOptions = {
         select: {
             items: 'row',
             selector: 'td:first-child',
             style: 'multi+shift', // default to a single click is all that's needed
         },
         pageLength: 25,
+        autoWidth: false,     // let the browser's default table-layout:auto size columns,
+                              // so they shrink/grow with the container on window resize
         scrollY: 600,
         scrollCollapse: true, // when less than scrollY height is needed, make the table shorter
         deferRender: true, // only draw into the DOM the nodes we need for each page
         orderCellsTop: true, // when viewing a subdirectory, the directory becomes a part of
                              // the header, this option prevents those cells from being used to
                              // sort the table
         layout: {
             top2Start: {
                 div: {
                     className: "",
                     id: "breadcrumb",
                     html: "<span id=\"rootBreadcrumb\" class=\"breadcrumb\">My Data</span>",
                 }
             },
             topStart: {
@@ -1622,63 +1618,63 @@
                     return '';
                 }
             },
             {
                 targets: 2,
                 render: function(data, type, row, meta) {
                     let decodedName = decodeURIComponent(data);
                     if (type !== "display" || row.fileType === "dir") {
                         return decodedName;
                     }
                     if (typeof uiState.userUrl === "undefined" || uiState.userUrl.length === 0) {
                         return decodedName;
                     }
                     let fileUrl = uiState.userUrl + cgiEncode(row.fullPath);
                     let copyIcon = '<svg class="copyLinkIcon" title="Copy file URL to clipboard" data-url="' + fileUrl + '" style="margin-left: 6px; cursor: pointer; vertical-align:baseline; width:0.8em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M502.6 70.63l-61.25-61.25C435.4 3.371 427.2 0 418.7 0H255.1c-35.35 0-64 28.66-64 64l.0195 256C192 355.4 220.7 384 256 384h192c35.2 0 64-28.8 64-64V93.25C512 84.77 508.6 76.63 502.6 70.63zM464 320c0 8.836-7.164 16-16 16H255.1c-8.838 0-16-7.164-16-16L239.1 64.13c0-8.836 7.164-16 16-16h128L384 96c0 17.67 14.33 32 32 32h47.1V320zM272 448c0 8.836-7.164 16-16 16H63.1c-8.838 0-16-7.164-16-16L47.98 192.1c0-8.836 7.164-16 16-16H160V128H63.99c-35.35 0-64 28.65-64 64l.0098 256C.002 483.3 28.66 512 64 512h192c35.2 0 64-28.8 64-64v-32h-47.1L272 448z"/></svg>';
-                    return '<a class="fileLink" href="' + fileUrl + '" target="_blank" rel="noopener">' + decodedName + '</a>' + copyIcon;
+                    return '<span style="white-space:nowrap"><a class="fileLink" href="' + fileUrl + '" target="_blank" rel="noopener">' + decodedName + '</a>' + copyIcon + '</span>';
                 }
             },
             {
                 targets: 3,
                 render: function(data, type, row, meta) {
                     if (type === "display") {
                          return dataTablePrintSize(data, type, row, meta);
                     }
                     return data;
                 }
             },
             {
                 targets: 5,
                 render: function(data, type, row) {
                     if (type === "display") {
                         return dataTablePrintGenome(data);
                     }
                     return data;
                 }
             },
             {
                 targets: 6,
                 render: function(data, type, row) {
                     if (type === "display") {
                         return cgiDecode(data);
                     }
                     return data;
                 }
             },
             {
-                // The upload time column
-                targets: 8,
+                targets: [7, 8],
+                className: "nowrap",
                 visible: true,
                 searchable: false,
                 orderable: true,
             },
             {
                 targets: 9,
                 visible: false,
                 searchable: false,
                 orderable: true,
             }
         ],
         columns: [
             {data: "", },
             {data: "", },
             {data: "fileName", title: "File name"},
@@ -1732,30 +1728,38 @@
         if (uiState.isLoggedIn) {
             tableInitOptions.language = {emptyTable: "Uploaded files will appear here. Click \"Upload\" to get started"};
         } else {
             tableInitOptions.language = {emptyTable: "You are not logged in, please <a href=\"../cgi-bin/hgSession\">log in or create an account</a> to begin uploading files"};
         }
         DataTable.feature.register('quota', function(settings, opts) {
             let options = Object.assign({option1: false, option2: false}, opts);
             let container = document.createElement("div");
             container.id = "quotaDiv";
             if (uiState.isLoggedIn) {
                 container.textContent = `Using ${prettyFileSize(uiState.userQuota)} of ${prettyFileSize(uiState.maxQuota)}`;
             }
             return container;
         });
         let table = new DataTable("#filesTable", tableInitOptions);
+        // Re-sync the scrollY head/body column widths after a resize settles.
+        let resizeTimer = null;
+        window.addEventListener("resize", function() {
+            clearTimeout(resizeTimer);
+            resizeTimer = setTimeout(function() {
+                table.columns.adjust();
+            }, 100);
+        });
         if (uiState.isLoggedIn) {
             table.buttons(".uploadButton").enable();
             document.getElementById("rootBreadcrumb").addEventListener("click", function(e) {
                 dataTableShowTopLevel(table);
                 dataTableCustomOrder(table);
                 dataTableEmptyBreadcrumb(table);
                 table.draw();
             });
         } else {
             table.buttons(".uploadButton").disable();
         }
         let hubBannerBtn = document.getElementById("hubBannerViewBtn");
         if (hubBannerBtn) {
             hubBannerBtn.addEventListener("click", function(e) {
                 viewHubInGenomeBrowser(uiState.currentHub);
@@ -1819,31 +1823,30 @@
                     }
                 }
             }
         });
         return table;
     }
 
     function handleGetFileList(jsonData, textStatus) {
         _.assign(uiState, jsonData.userFiles);
         if (uiState.fileList) {
             parseFileListIntoHash(uiState.fileList);
         }
 
         // first add the top level directories/files
         let table = showExistingFiles(uiState.fileList);
-        table.columns.adjust().draw();
 
         uppy.use(Uppy.Dashboard, uppyOptions);
 
         // define this in init so globals are available at runtime
         let tusOptions = {
             endpoint: getTusdEndpoint(),
             withCredentials: true,
             retryDelays: null,
             removeFingerprintOnSuccess: true, // clean up localStorage after successful upload
         };
 
         uppy.use(Uppy.Tus, tusOptions);
         uppy.use(BatchChangePlugin, {target: Uppy.Dashboard});
         uppy.on('upload-success', (file, response) => {
             const metadata = file.meta;