1ab5d5566906c050e5bc8ce2bf2a21cf0d08e439
chmalee
  Mon Sep 21 15:23:53 2026 -0700
Fix hubspace UI race condition on file metadata editing, wait for the hub.txt parse to finish before offering file metadata edit, refs #38398

diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js
index 787629ec271..6a217422587 100644
--- src/hg/js/hgMyData.js
+++ src/hg/js/hgMyData.js
@@ -753,36 +753,40 @@
     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.
+    //
+    // Returns a promise that settles once the metadata is stamped. Reading a
+    // hub.txt is asynchronous, so callers that must see the stamped values wait
+    // on it. The promise never rejects.
     let files = uppyInstance.getFiles();
     let twoBit = files.find(looksLikeTwoBit);
     let hubTxt = files.find(looksLikeHubTxt);
     if (!twoBit && !hubTxt) {
         hubCreate.clearLastHubBatchDescriptor();
-        return;
+        return Promise.resolve();
     }
 
     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;
@@ -797,31 +801,31 @@
                 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) => {
+        return 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, 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.
@@ -834,35 +838,35 @@
                     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, "assemblyHub");
+    return Promise.resolve();
 }
 
 // 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;
@@ -1196,40 +1200,49 @@
                 let existing = hubCreate.uiState.filesHash[defaultMeta.parentDir];
                 if (existing && existing.hubType === "assemblyHub") {
                     this.uppy.setFileMeta(file.id, {
                         genome: existing.genome,
                         genomeLabel: existing.genome,
                         hubType: "assemblyHub",
                         genomeLocked: true,
                     });
                 }
             }
 
             // If a 2bit is in the batch, every sibling file in the same parentDir
             // adopts its genome and gets hubType=assemblyHub. Also handle hub.txt:
             // parse it client-side and, if it declares an assembly hub, mirror
             // those values onto every file (hub.txt wins).
-            propagateAssemblyHubMeta(this.uppy);
+            let stamped = propagateAssemblyHubMeta(this.uppy);
 
             if (this.uppy.getFiles().length > 1) {
                 this.addBatchSelectsToDashboard();
             } else {
-                // only open the file editor when there is one file
-                const dash = uppy.getPlugin("Dashboard");
+                // only open the file editor when there is one file, and only after
+                // the hub.txt parse. The card copies file.meta when it opens and
+                // writes that copy back on save, so an earlier open saves hg38.
+                stamped.then(() => {
+                    if (!this.uppy.getFile(file.id)) {
+                        return;
+                    }
+                    const dash = this.uppy.getPlugin("Dashboard");
+                    if (dash) {
                         dash.toggleFileCard(true, file.id);
                     }
                 });
+            }
+        });
         this.uppy.on("file-removed", (file) => {
             // remove the batch change selects if now <2 files present
             if (this.uppy.getFiles().length < 2) {
                 this.removeBatchSelectsFromDashboard();
             }
             if (this.uppy.getFiles().length === 0) {
                 userSetBatchHubName = false;
             }
             // If a hub-definition file leaves the batch, the cached split-hub
             // descriptor is no longer valid. Clear the cache and the per-file
             // stamps so pre-finish re-evaluates from scratch.
             if (looksLikeHubTxt(file) ||
                 (file.meta && file.meta.fileName === "genomes.txt")) {
                 hubCreate.clearLastHubBatchDescriptor();
                 for (let f of this.uppy.getFiles()) {