37aae4a2c5a919c329378f3da0792e168426537d
chmalee
  Tue Aug 11 11:31:52 2026 -0700
hgMyData: a blank hub genome means unset, not a mismatch, refs #37964

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js
index 61279a1f70d..9cc198a57b4 100644
--- src/hg/js/hgMyData.js
+++ src/hg/js/hgMyData.js
@@ -532,41 +532,45 @@
                 doUpload = false;
                 continue;
             }
             // Hub-level files in a split-hub batch intentionally carry empty genome.
             if (!file.meta.genome && file.meta.batchSplitHub !== "true") {
                 uppy.info(`Error: No genome selected for file ${file.meta.name}!`, 'error', 5000);
                 doUpload = false;
                 continue;
             }
             if  (!file.meta.fileType) {
                 uppy.info(`Error: File type not supported, file: ${file.meta.name}!`, 'error', 5000);
                 doUpload = false;
                 continue;
             }
             // check if this hub already exists and the genome is different from what was
-            // just selected, if so, make the user create a new hub
-            if (file.meta.parentDir in hubCreate.uiState.filesHash && hubCreate.uiState.filesHash[file.meta.parentDir].genome !== file.meta.genome) {
+            // just selected, if so, make the user create a new hub. A blank genome means
+            // none has been set yet, not a hub for a genome named "": a directory row is
+            // blank until an upload carrying a genome lands in it, and the hub level
+            // files of a hub that brings its own hub.txt have no genome of their own.
+            // So a blank on either side is not a mismatch.
             let existing = hubCreate.uiState.filesHash[file.meta.parentDir];
+            if (existing && existing.genome && existing.genome !== file.meta.genome) {
                 // If the existing hub is an assembly hub, adopt its genome
                 // automatically rather than erroring - the UI hid the picker
                 // for this case, so the mismatch is just stale metadata.
                 if (existing.hubType === "assemblyHub") {
                     file.meta.genome = existing.genome;
                     file.meta.genomeLabel = existing.genome;
                     file.meta.hubType = "assemblyHub";
-                } else {
+                } else if (file.meta.genome) {
                     uppy.info(`Error: the hub ${file.meta.parentDir} already exists and is for genome "${existing.genome}". Please select the correct genome, a different hub or make a new hub.`, 'error', 10000);
                     doUpload = false;
                     continue;
                 }
             }
             // check if the user is uploading a file that already exists in this hub
             if (file.meta.parentDir in hubCreate.uiState.filesHash) {
                 let hubFiles = hubCreate.uiState.filesHash[file.meta.parentDir].children;
                 for (let j = 0; j < hubFiles.length; j++) {
                     if (hubFiles[j].fileName === file.meta.name) {
                         filesToOverwrite.push(file);
                         break;
                     }
                 }
             }
@@ -1367,31 +1371,32 @@
     }
 }
 
 var hubCreate = (function() {
     let uiState = { // our object for keeping track of the current UI and what to do
         userUrl: "", // the web accesible path where the uploads are stored for this user
         hubNameDefault: "",
         currentHub: "", // if the user has a hub dir open, set the name here and use it as the default
                         // hub name when uploading a new file with the dir open, otherwise hubNameDefault
         currentHubPath: "", // full path of the open dir, so we can tell which hub it belongs to
                             // when it is a subdirectory like myHub/hg38
         isLoggedIn: "",
         maxQuota: 0,
         userQuota: 0,
         userFiles: {}, // same as uiData.userFiles on page load
-        filesHash: {}, // for each file, userFiles.fullPath is the key, and then the userFiles.fileList data as the value, with an extra key for the child fullPaths if the file is a directory
+        // Object.create(null) because a hub may be named 'constructor' or 'toString'
+        filesHash: Object.create(null), // for each file, userFiles.fullPath is the key, and then the userFiles.fileList data as the value, with an extra key for the child fullPaths if the file is a directory
     };
 
     let extensionMap = {
         "bigBed": [".bb", ".bigbed"],
         "bam": [".bam"],
         "vcf": [".vcf"],
         "vcfTabix": [".vcf.gz", "vcf.bgz"],
         "bigWig": [".bw", ".bigwig"],
         "hic": [".hic"],
         "cram": [".cram"],
         "bigBarChart": [".bigbarchart"],
         "bigGenePred": [".bgp", ".biggenepred"],
         "bigMaf": [".bigmaf"],
         "bigInteract": [".biginteract"],
         "bigPsl": [".bigpsl"],
@@ -2463,31 +2468,31 @@
         }
     }
 
     function deleteFileFromTable(pathList) {
         // req is an object with properties of an uploaded file, make a new row
         // for it in the filesTable
         let table = $("#filesTable").DataTable();
         let rows = table.rows((idx, data) => pathList.includes(data.fullPath));
         rows.remove().draw();
         let toKeep = (elem) => !pathList.includes(elem.fullPath);
         pathList.forEach((f) => {
             updateQuota(-uiState.filesHash[f].fileSize);
         });
         uiState.fileList = uiState.fileList.filter(toKeep);
         // Rebuild filesHash from remaining fileList to remove stale entries
-        uiState.filesHash = {};
+        uiState.filesHash = Object.create(null);
         parseFileListIntoHash(uiState.fileList);
         // If the currently viewed hub directory was deleted (its data is in oldRowData
         // because dataTableCustomOrder moved it to the header), clean up that stale state
         if (oldRowData && pathList.includes(oldRowData.fullPath)) {
             let thead = document.querySelector(
                 ".dt-scroll-headInner > table:nth-child(1) > thead:nth-child(1)");
             if (thead && thead.childNodes.length > 1) {
                 thead.removeChild(thead.lastChild);
             }
             oldRowData = null;
             dataTableShowTopLevel(table);
             dataTableEmptyBreadcrumb(table);
             table.order([{name: "uploadTime", dir: "desc"}]);
             table.draw();
         }