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()) {