55efeb031b7d8367e8be67cd05fad21c2e592170
chmalee
  Mon Aug 10 11:59:10 2026 -0700
hubSpace: take the batch genome from hub.txt, and always say why an upload was refused, refs #37972

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

diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js
index 13bbe361a37..61279a1f70d 100644
--- src/hg/js/hgMyData.js
+++ src/hg/js/hgMyData.js
@@ -112,51 +112,62 @@
     if (hubRoot in hubCreate.uiState.filesHash) {
         uppyInstance.info(`These files will be added to your existing hub "${hubRoot}", ` +
             `named by the hub.txt in this upload.`, "warning", 8000);
     }
     for (let f of uppyInstance.getFiles()) {
         let segments = ((f.meta && f.meta.parentDir) || "").split("/");
         let newParent;
         if (segments.length > 1) {
             newParent = hubRoot + "/" + segments.slice(1).join("/");
         } else {
             newParent = hubRoot;
         }
         uppyInstance.setFileMeta(f.id, {parentDir: newParent});
     }
     refreshBatchHubNameInput(uppyInstance);
+    refreshBatchSelects(uppyInstance);
 }
 
 function refreshBatchHubNameInput(uppyInstance) {
     // Point the batch Hub Name box at the hub the files are really set to. Leaves
     // the box alone when the batch spans more than one hub
     let input = document.getElementById("batchParentDir");
     if (!input) {
         return;
     }
     let roots = [];
     for (let f of uppyInstance.getFiles()) {
         let root = ((f.meta && f.meta.parentDir) || "").split("/")[0];
         if (root && !roots.includes(root)) {
             roots.push(root);
         }
     }
     if (roots.length === 1) {
         input.value = roots[0];
     }
 }
 
+function refreshBatchSelects(uppyInstance) {
+    // Rebuild the batch controls so the genome box shows what the files actually
+    // carry. addBatchSelectsToDashboard only rebuilds when the batch changed shape,
+    // so this is cheap to call after anything that restamps genome metadata
+    let plugin = uppyInstance.getPlugin("BatchChangePlugin");
+    if (plugin && uppyInstance.getFiles().length > 1) {
+        plugin.addBatchSelectsToDashboard();
+    }
+}
+
 function generateApiKey() {
     let apiKeyInstr = document.getElementById("apiKeyInstructions");
     let apiKeyDiv = document.getElementById("apiKey");
 
     if (!document.getElementById("spinner")) {
         let spinner = document.createElement("i");
         spinner.id = "spinner";
         spinner.classList.add("fa", "fa-spinner", "fa-spin");
         document.getElementById("generateApiKey").after(spinner);
     }
 
     let handleSuccess = function(reqObj) {
         apiKeyDiv.textContent = reqObj.apiKey;
         apiKeyInstr.style.display = "block";
         let revokeDiv= document.getElementById("revokeDiv");
@@ -270,31 +281,31 @@
             id: 'genome',
             name: 'Genome',
             render: ({value, onChange}, h) => {
                 // 2bit files name a new assembly hub (editable). Other files
                 // with genomeLocked are pinned by a hub-defining sibling or
                 // the hub they were drilled into.
                 let isTwoBit = file.meta.fileType === "2bit";
                 let isHubTxt = looksLikeHubTxt(file);
                 let isLocked = !!file.meta.genomeLocked;
                 if (isTwoBit || isLocked) {
                     let editable2bit = isTwoBit && !isLocked;
                     let batchHasHubTxt = uppy.getFiles().some(looksLikeHubTxt);
                     let label;
                     if (editable2bit) {
                         label = "Genome name for your assembly hub:";
-                    } else if (isHubTxt || (isTwoBit && batchHasHubTxt)) {
+                    } else if (isHubTxt || batchHasHubTxt) {
                         label = "Genome (locked by hub.txt - edit hub.txt locally and re-add to change):";
                     } else {
                         label = "Genome (locked by this assembly hub):";
                     }
                     return h('div', {
                             class: "uppy-Dashboard-FileCard-label",
                             style: "display: inline-block; width: 78%"
                             },
                         label,
                         h('input', {
                             id: `${file.meta.name}AsmHubInput`,
                             type: 'text',
                             class: "uppy-u-reset uppy-c-textInput uppy-Dashboard-FileCard-input",
                             style: "margin-left: 5px",
                             value: file.meta.genome || "",
@@ -532,81 +543,86 @@
                 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) {
                 let existing = hubCreate.uiState.filesHash[file.meta.parentDir];
                 // 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 {
-                    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.`);
+                    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;
                     }
                 }
             }
 
             // Set metadata directly on the file object since we're returning a modified files object
             // (using setFileMeta would be overwritten when we return the files object)
             file.meta.fileName = file.meta.name;
             file.meta.fileSize = file.size;
             file.meta.lastModified = file.data.lastModified;
             thisQuota += file.size;
 
         }
         // If any files will overwrite existing ones, show a single confirmation dialog
         if (filesToOverwrite.length > 0) {
-            let fileNames = filesToOverwrite.map(f => f.meta.name).join("\n  ");
+            let names = filesToOverwrite.map(f => f.meta.name);
+            let fileNames = names.join("\n  ");
             if (!confirm(`The following file(s) already exist and will be overwritten:\n  ${fileNames}\n\nContinue?`)) {
+                // the confirm is the only thing that stopped the upload, so say so
+                // rather than leave the Upload button sitting there with no reason
+                uppy.info(`Upload cancelled. It would have overwritten: ${names.join(", ")}. ` +
+                          `Rename those files or use a different hub name.`, 'warning', 10000);
                 doUpload = false;
             } else {
                 // Set metadata flag to allow overwrite on backend for each file
                 filesToOverwrite.forEach(f => f.meta.allowOverwrite = "true");
             }
         }
         // A hub we synthesize gets one genome line, so everything going into it has to
         // agree. Runs after the loop above, which trims parentDir, stamps a 2bit's
         // genome onto its siblings and adopts an existing assembly hub's genome, so
         // this sees the values the server will. A batch bringing its own hub.txt
         // states its own genomes, and hubtools does not come through here at all
         if (!isSplitHub && !hubTxtInBatch) {
             for (let m of hubsWithMixedGenomes(Object.values(files))) {
                 uppy.info(`Error: the hub "${m.hub}" would hold files for more than ` +
                     `one genome (${m.genomes.join(", ")}). The hub.txt this page ` +
                     `writes for you can only name one genome. Give each genome its ` +
                     `own hub name, or include your own hub.txt. hubtools can upload ` +
                     `a hub covering several genomes.`, "error", 10000);
                 doUpload = false;
             }
         }
         if (thisQuota + hubCreate.uiState.userQuota > hubCreate.uiState.maxQuota) {
-            uppy.info(`Error: this file batch exceeds your quota. Please delete some files to make space or email genome-www@soe.ucsc.edu if you feel you need more space.`);
+            uppy.info(`Error: this file batch exceeds your quota. Please delete some files to make space or email genome-www@soe.ucsc.edu if you feel you need more space.`, 'error', 10000);
             doUpload = false;
         }
         return doUpload ? files : false;
     },
 });
 
 function extractHookErrorMessage(error, response) {
     // Our hooks exit 0 + RejectUpload=true, so the response body is the raw
     // errAbort message. tus-js-client still wraps error.message with
     // "tus: unexpected response while ..., response text: <ours>, request
     // id: n/a" when the status code is 4xx/5xx.
     if (response && response.body) return String(response.body).trim();
     let body = null;
     try { body = error && error.originalResponse && error.originalResponse.getBody(); }
     catch (e) { /* ignore */ }
@@ -747,125 +763,131 @@
             hubType: descriptor.isAssemblyHub ? "assemblyHub" : "trackHub",
         };
         if (hubParentDir && !isNestedLayout) meta.parentDir = hubParentDir;
         if (assignedGenome) {
             meta.genome = assignedGenome;
             meta.genomeLabel = assignedGenome;
             meta.genomeLocked = true;
         } else if (descriptor.fileGenome.has(f.id)) {
             // Hub-level files (hub.txt, genomes.txt): empty db.
             meta.genome = "";
             meta.genomeLabel = "";
             meta.genomeLocked = true;
         }
         uppyInstance.setFileMeta(f.id, meta);
     }
+    refreshBatchSelects(uppyInstance);
     let names = descriptor.genomes.map(g => g.name).join(", ");
     if (names) {
         uppyInstance.info(`Split hub detected. Genomes: ${names}`, "info", 4000);
     }
 }
 
 function propagateAssemblyHubMeta(uppyInstance) {
     // When a batch contains a 2bit (and/or an assembly-hub hub.txt), mirror the
     // custom genome name onto every file sharing that parentDir and mark every
     // file hubType=assemblyHub. hub.txt wins over the 2bit's default.
     //
     // We detect the hub-defining files by filename rather than by meta.fileType,
     // because setFileMeta updates Uppy's state immutably - file objects captured
     // from getFiles() earlier in this event may still carry old meta.
     let files = uppyInstance.getFiles();
     let twoBit = files.find(looksLikeTwoBit);
     let hubTxt = files.find(looksLikeHubTxt);
     if (!twoBit && !hubTxt) {
         hubCreate.clearLastHubBatchDescriptor();
         return;
     }
 
-    function applyGenomeToSiblings(genome, alsoLockHubDefiners) {
+    function applyGenomeToSiblings(genome, alsoLockHubDefiners, hubType) {
         // Set genome/hubType on every file in the batch. Non-hub-defining
         // files (i.e. the sibling tracks) are always locked to this genome so
         // the user can't drift them. The hub-defining files (2bit, hub.txt)
         // are locked only when alsoLockHubDefiners is true - used by the
         // hub.txt path to pin the 2bit's editable field too.
         if (!genome) return;
         // All files in this batch belong to one new hub, so they must share
         // one parentDir. Take it from the hub-defining file - its parentDir
         // came from getDefaultHubName(), while a track that was added first
         // may have been pointed at an existing assembly hub.
         let hubDefiner = hubTxt || twoBit;
         let syncParentDir = hubDefiner && hubDefiner.meta && hubDefiner.meta.parentDir;
         // Folder drops carry their own multi-segment parentDir; don't overwrite.
         let isNestedLayout = uppyInstance.getFiles().some(
             f => f.meta && f.meta.parentDir && f.meta.parentDir.includes("/"));
         for (let f of uppyInstance.getFiles()) {
             let isHubDefining = looksLikeTwoBit(f) || looksLikeHubTxt(f);
             let meta = {
                 genome: genome,
                 genomeLabel: genome,
-                hubType: "assemblyHub",
+                hubType: hubType,
                 genomeLocked: !isHubDefining || alsoLockHubDefiners,
             };
             if (syncParentDir && !isNestedLayout) meta.parentDir = syncParentDir;
             uppyInstance.setFileMeta(f.id, meta);
         }
         // keep the batch Hub Name box showing where the files are really going
         refreshBatchHubNameInput(uppyInstance);
+        // and the genome box showing the genome they just picked up
+        refreshBatchSelects(uppyInstance);
     }
 
     if (hubTxt) {
         hubBatchParsesInFlight++;
         setUploadButtonEnabled(false);
         hubCreate.parseHubBatch(uppyInstance.getFiles()).then((descriptor) => {
             // Skip stale parses; only the latest-completed one applies.
             if (descriptor !== hubCreate.getLastHubBatchDescriptor()) return;
             for (let e of descriptor.errors) {
                 uppyInstance.info(e, "error", 8000);
             }
             for (let w of descriptor.warnings) {
                 uppyInstance.info(w, "warning", 6000);
             }
             if (descriptor.isSplit) {
                 applySplitHubDescriptor(uppyInstance, descriptor);
             } else {
                 // Single-file hub: hub.txt is authoritative for the one genome
-                // it declares. Lock all siblings to that genome.
+                // it declares, whether or not it is an assembly hub. Without
+                // this the batch keeps the session's assembly and the rows are
+                // written for a genome the hub.txt never mentions.
                 let parsed = descriptor.hubMeta || {};
-                if (parsed.isAssemblyHub && parsed.genome) {
-                    applyGenomeToSiblings(parsed.genome, true);
-                    uppyInstance.info(`Using genome "${parsed.genome}" from hub.txt`, "info", 4000);
-                } else if (parsed.genome && twoBit) {
-                    let twoBitGenome = twoBit.meta.genome || hubCreate.sanitizeGenomeName(twoBit.name);
-                    if (parsed.genome !== twoBitGenome) {
-                        applyGenomeToSiblings(parsed.genome, true);
+                if (parsed.genome) {
+                    let batchHubType = (parsed.isAssemblyHub || twoBit) ? "assemblyHub" : "trackHub";
+                    applyGenomeToSiblings(parsed.genome, true, batchHubType);
+                    let twoBitGenome = twoBit ?
+                        (twoBit.meta.genome || hubCreate.sanitizeGenomeName(twoBit.name)) : null;
+                    if (twoBitGenome && parsed.genome !== twoBitGenome) {
                         uppyInstance.info(`Using genome "${parsed.genome}" from hub.txt (overrides 2bit default)`, "warning", 5000);
+                    } else {
+                        uppyInstance.info(`Using genome "${parsed.genome}" from hub.txt`, "info", 4000);
                     }
                 }
             }
             // last, so it wins over the parentDir the other two stamp
             applyHubTxtHubName(uppyInstance, descriptor);
         }).catch((err) => {
             console.warn("Could not read hub.txt for genome detection:", err);
         }).finally(() => {
             hubBatchParsesInFlight--;
             if (hubBatchParsesInFlight === 0) setUploadButtonEnabled(true);
         });
         return;
     }
 
     let asmGenome = twoBit.meta.genome || hubCreate.sanitizeGenomeName(twoBit.name);
-    applyGenomeToSiblings(asmGenome, false);
+    applyGenomeToSiblings(asmGenome, false, "assemblyHub");
 }
 
 // create a custom uppy plugin to batch change the type and db fields
 class BatchChangePlugin extends Uppy.BasePlugin {
     constructor(uppy, opts) {
         super(uppy, opts);
         this.id = "BatchChangePlugin";
         this.type = "progressindicator";
         this.opts = opts;
     }
 
     createOptsForSelect(select, opts) {
         opts.forEach( (opt) => {
             let option = document.createElement("option");
             option.value = opt.value;
@@ -886,62 +908,67 @@
             if (!document.getElementById(dbSelectId)) {
                 let dbSelect = document.createElement("select");
                 dbSelect.id = dbSelectId;
                 let dbOpts = hubCreate.makeGenomeSelectOptions();
                 this.createOptsForSelect(dbSelect, dbOpts);
                 fileDiv.appendChild(dbSelect);
             }
         }
     }
 
     removeBatchSelectsFromDashboard() {
         removeBatchSelectDiv();
     }
 
     addBatchSelectsToDashboard() {
-        // If the batch contains a 2bit, the UCSC genome picker makes no
-        // sense - show the custom genome name read-only instead. Detect by
-        // filename rather than meta.hubType because setFileMeta updates
-        // Uppy state immutably and the meta may not be visible on file
-        // objects captured from getFiles() earlier in this event. A split
-        // assembly hub can declare more than one 2bit (one per genome);
-        // join all of them.
-        let assemblyHubGenomes = [];
+        // When the batch's genome is decided for it - by a 2bit, or by a hub.txt
+        // that names one - the UCSC picker makes no sense, so show the genome
+        // read-only instead. A 2bit is detected by filename rather than
+        // meta.hubType because setFileMeta updates Uppy state immutably and the
+        // meta may not be visible on file objects captured from getFiles()
+        // earlier in this event. A split assembly hub can declare more than one
+        // genome; join all of them.
+        // Only a hub-defining file in the batch locks the box. A file drilled into an
+        // existing assembly hub also carries genomeLocked, but the user can still
+        // retarget that batch at another hub, and then the picker has to come back.
+        let lockedGenomes = [];
+        let hubDefined = this.uppy.getFiles().some(
+            f => looksLikeTwoBit(f) || looksLikeHubTxt(f));
+        if (hubDefined) {
             for (let f of this.uppy.getFiles()) {
-            if (looksLikeTwoBit(f)) {
-                let g = f.meta.genome || hubCreate.sanitizeGenomeName(f.name);
-                if (g && !assemblyHubGenomes.includes(g)) {
-                    assemblyHubGenomes.push(g);
+                let g = looksLikeTwoBit(f) ?
+                    (f.meta.genome || hubCreate.sanitizeGenomeName(f.name)) : f.meta.genome;
+                if (g && !lockedGenomes.includes(g)) {
+                    lockedGenomes.push(g);
                 }
             }
         }
-        // The genome row is built one way for an assembly hub and another for a
-        // track hub, so a 2bit joining or leaving an existing batch has to
-        // rebuild the whole thing rather than leave the old row in place
-        let asmSignature = assemblyHubGenomes.join(", ");
+        // The genome row is built one way for a locked genome and another for a
+        // free one, so a 2bit or hub.txt joining or leaving an existing batch has
+        // to rebuild the whole thing rather than leave the old row in place. The
+        // signature also changes when the locked genome is renamed, which is what
+        // keeps the read-only box from showing a stale name
+        let asmSignature = lockedGenomes.join(", ");
         let staleDiv = document.getElementById("batch-selector-div");
         if (staleDiv) {
             if (staleDiv.dataset.asmGenome === asmSignature) {
                 refreshBatchHubNameInput(this.uppy);
                 return;
             }
             removeBatchSelectDiv();
         }
-        let assemblyHubGenome = null;
-        if (assemblyHubGenomes.length) {
-            assemblyHubGenome = assemblyHubGenomes.join(", ");
-        }
+        let lockedGenome = lockedGenomes.length ? asmSignature : null;
 
         let batchSelectDiv = document.createElement("div");
         batchSelectDiv.id = "batch-selector-div";
         batchSelectDiv.dataset.asmGenome = asmSignature;
         batchSelectDiv.style.display = "grid";
         batchSelectDiv.style.width = "80%";
         // the grid syntax is 2 columns, 3 rows
         batchSelectDiv.style.gridTemplateColumns = "max-content minmax(0, 200px) max-content 1fr min-content";
         batchSelectDiv.style.gridTemplateRows = "repest(3, auto)";
         batchSelectDiv.style.margin = "10px auto"; // centers this div
         batchSelectDiv.style.fontSize = "14px";
         batchSelectDiv.style.gap = "8px";
         if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
             batchSelectDiv.style.color = "#eaeaea";
         }
@@ -949,48 +976,52 @@
         // first just explanatory text:
         let batchSelectText = document.createElement("div");
         batchSelectText.textContent = "Change options for all files:";
         // syntax here is rowStart / columnStart / rowEnd / columnEnd
         batchSelectText.style.gridArea = "1 / 1 / 1 / 2";
 
         let batchDbLabel = document.createElement("label");
         batchDbLabel.textContent = "Genome";
         batchDbLabel.style.gridArea = "2 / 1 / 2 / 1";
 
         let batchDbSelect = null;
         let batchDbGenomeSearchBar = null;
         let batchDbGenomeSearchButton = null;
         let batchDbSearchBarLabel = null;
 
-        if (assemblyHubGenome) {
-            // Assembly hub: show the custom genome name as a locked text
-            // field, no UCSC picker or search.
+        if (lockedGenome) {
+            // The genome is decided by a 2bit or a hub.txt: show it as a locked
+            // text field, no UCSC picker or search.
             let locked = document.createElement("input");
             locked.type = "text";
             locked.id = "batchAsmHubGenome";
-            locked.value = assemblyHubGenome;
+            locked.value = lockedGenome;
             locked.disabled = true;
             locked.classList.add("uppy-u-reset", "uppy-c-textInput");
             locked.style.gridArea = "2 / 2 / 2 / 2";
             locked.style.margin = "2px";
             batchDbLabel.for = "batchAsmHubGenome";
 
+            // say which file decided the genome, so the box is not just read-only
+            // with no explanation
             let note = document.createElement("div");
-            if (assemblyHubGenomes.length > 1) {
-                note.textContent = "(assembly hub - genome per file is set by genomes.txt; this list shows all genomes in the hub)";
-            } else {
+            if (lockedGenomes.length > 1) {
+                note.textContent = "(genome per file is set by genomes.txt; this list shows all genomes in the hub)";
+            } else if (this.uppy.getFiles().some(looksLikeTwoBit)) {
                 note.textContent = "(assembly hub - genome locked; shared by all files in this batch)";
+            } else {
+                note.textContent = "(genome locked by hub.txt; shared by all files in this batch)";
             }
             note.style.gridArea = "2 / 3 / 2 / 5";
             note.style.margin = "auto 0";
             note.style.fontStyle = "italic";
 
             batchSelectDiv.appendChild(batchSelectText);
             batchSelectDiv.appendChild(batchDbLabel);
             batchSelectDiv.appendChild(locked);
             batchSelectDiv.appendChild(note);
         } else {
             // Track hub: the usual UCSC picker + autocomplete.
             batchDbSelect = document.createElement("select");
             this.createOptsForSelect(batchDbSelect, hubCreate.makeGenomeSelectOptions());
             batchDbSelect.id = "batchDbSelect";
             batchDbSelect.style.gridArea = "2 / 2 / 2 / 2";
@@ -1100,33 +1131,37 @@
                     newParent = newRoot;
                 }
                 this.uppy.setFileMeta(file.id, {parentDir: newParent});
             }
             // merging separate hubs under one name can bring two genomes together
             warnOnMixedGenomes(this.uppy);
         });
 
         batchSelectDiv.appendChild(batchParentDirLabel);
         batchSelectDiv.appendChild(batchParentDirInput);
 
         // append the batch changes to the bottom of the file list, for some reason
         // I can't append to the actual Dashboard-files, it must be getting emptied
         // and re-rendered or something
         let uppyFilesDiv = document.querySelector(".uppy-Dashboard-progressindicators");
-        if (uppyFilesDiv) {
-            uppyFilesDiv.insertBefore(batchSelectDiv, uppyFilesDiv.firstChild);
+        if (!uppyFilesDiv) {
+            // nothing to attach to yet. Bail rather than fall through to the
+            // autocomplete setup below, which would memoize an id belonging to a
+            // detached element and leave the search box dead for the rest of the page
+            return;
         }
+        uppyFilesDiv.insertBefore(batchSelectDiv, uppyFilesDiv.firstChild);
         refreshBatchHubNameInput(this.uppy);
 
         // autocomplete only applies in the track-hub path
         if (batchDbSelect && batchDbGenomeSearchBar && batchDbGenomeSearchButton) {
             initAutocompleteForInput(batchDbGenomeSearchBar.id, batchDbSelect);
             // this button belongs to the element just built, so it is bound
             // every time, unlike the autocomplete which is memoized by id
             batchDbGenomeSearchButton.addEventListener("click", (e) => {
                 let inp = document.getElementById(batchDbGenomeSearchBar.id).value;
                 let selector = "[id='"+batchDbGenomeSearchBar.id+"']";
                 $(selector).autocompleteCat("search", inp);
             });
         }
     }
 
@@ -1307,32 +1342,34 @@
                 let asmGenome = file.meta.genome || hubCreate.sanitizeGenomeName(file.name);
                 let renamed = this.uppy.getFiles().filter(
                     f => f.id !== file.id && f.meta && f.meta.genome !== asmGenome);
                 if (asmGenome && renamed.length) {
                     let lead;
                     if (renamed.length === 1) {
                         lead = "The other file in this batch now uses";
                     } else {
                         lead = `The other ${renamed.length} files in this batch now use`;
                     }
                     uppy.info(`${lead} the genome "${asmGenome}", since every file ` +
                               `in the batch goes into this one assembly hub.`, "info", 5000);
                 }
                 propagateAssemblyHubMeta(this.uppy);
             }
-            // a hub name edited on a file card has to reach the batch box too
+            // a hub name or genome edited on a file card has to reach the batch
+            // boxes too, or they keep showing what the batch used to say
             refreshBatchHubNameInput(this.uppy);
+            refreshBatchSelects(this.uppy);
             warnOnMixedGenomes(this.uppy);
         });
     }
     uninstall() {
         // not really used because we aren't ever uninstalling the uppy instance
         this.uppy.off("file-added");
     }
 }
 
 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