46a7db400ddc1280555e0bc32ed09311e19efc65
chmalee
  Thu May 7 10:53:58 2026 -0700
Reword some hubSpace assembly hub error message. Make the duration of some error message notifications longer, change some text around input about why certain inputs are disabled in certain situations, refs #37411

diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js
index 5c23bbf4183..8bd3e531a63 100644
--- src/hg/js/hgMyData.js
+++ src/hg/js/hgMyData.js
@@ -155,40 +155,42 @@
                     },
                     required: required,
                     form: form,
                     }
                 );
             },
         },
         {
             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 (isTwoBit) {
-                        label = "Genome (set by hub.txt in this batch):";
+                    } else if (isHubTxt || (isTwoBit && batchHasHubTxt)) {
+                        label = "Genome (locked by hub.txt - edit hub.txt locally and re-add to change):";
                     } else {
-                        label = "Genome (set by the assembly hub):";
+                        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 || "",
                             disabled: !editable2bit,
                             onChange: e => {
                                 let v = hubCreate.sanitizeGenomeName(e.target.value);
@@ -600,31 +602,31 @@
 
             if (assemblyHubGenome) {
                 // Assembly hub: show the custom genome name 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.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";
 
                 let note = document.createElement("div");
-                note.textContent = "(assembly hub - genome is set by the 2bit in this batch)";
+                note.textContent = "(assembly hub - genome locked; 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";
                 batchDbSelect.style.margin = "2px";
@@ -716,39 +718,43 @@
         }
     }
 
     install() {
         this.uppy.on("file-added", (file) => {
             // Only one 2bit per hub is supported. If this batch already has
             // a 2bit, reject the new one before any meta-setting runs (which
             // would propagate the wrong genome to it). onBeforeUpload also
             // enforces this; the duplicate check here keeps the per-file card
             // UI from drifting.
             if (looksLikeTwoBit(file)) {
                 let existingTwoBits = this.uppy.getFiles().filter(
                     f => f.id !== file.id && looksLikeTwoBit(f));
                 if (existingTwoBits.length > 0) {
                     this.uppy.removeFile(file.id);
-                    // setState({error}) puts the message in the StatusBar at
-                    // the bottom of the dashboard where it stays until the
-                    // user clicks Upload (which clears state.error) or closes
-                    // the dashboard. Persistent, unlike uppy.info().
-                    this.uppy.setState({
-                        error: `Only one 2bit file per hub is supported. ` +
-                               `Already have "${existingTwoBits[0].name}"; ignoring ` +
-                               `"${file.name}". Split them into separate hubs.`
-                    });
+                    // Close the file card if it auto-opened for the first 2bit;
+                    // otherwise it covers the error banner.
+                    const dash = this.uppy.getPlugin("Dashboard");
+                    if (dash) dash.toggleFileCard(false);
+                    // Long duration so the user has time to read it; the
+                    // StatusBar (setState.error) truncates to "Upload failed"
+                    // and hides the message behind a "?" icon.
+                    this.uppy.info(
+                        `Only one 2bit file per hub is allowed. ` +
+                        `"${existingTwoBits[0].name}" was added; ` +
+                        `"${file.name}" was not. To create a separate ` +
+                        `hub for "${file.name}", upload it on its own.`,
+                        'error', 15000);
                     return;
                 }
             }
             // add default meta data for genome and fileType
             let ftype = hubCreate.detectFileType(file.name);
             let defaultMeta = {
                 "genome": hubCreate.defaultDb(),
                 "fileType": ftype,
                 "parentDir": hubCreate.getDefaultHubName(),
                 "hubType": "trackHub",
             };
             if (ftype === "2bit") {
                 // This file defines an assembly hub. Default the genome to the
                 // sanitized filename stem; the user can edit it in the file card.
                 defaultMeta.genome = hubCreate.sanitizeGenomeName(file.name);
@@ -1171,31 +1177,31 @@
         // twoBitPath pointing at a missing file and the surviving rows still
         // flagged hubType=assemblyHub. The user must delete the entire hub
         // instead, or replace the 2bit by uploading a new one with the same name.
         let selectedValues = Object.values(data);
         let selectedHubDirs = new Set(
             selectedValues.filter(x => x.fileType === "dir").map(x => x.fullPath));
         let blockedTwoBits = [];
         for (let d of selectedValues) {
             if (d.fileType !== "2bit") continue;
             let hub = uiState.filesHash[d.parentDir];
             if (!hub || hub.hubType !== "assemblyHub") continue;
             if (!selectedHubDirs.has(d.parentDir)) blockedTwoBits.push(d);
         }
         if (blockedTwoBits.length > 0) {
             let names = blockedTwoBits.map(d => d.fullPath).join("\n  ");
-            alert(`Cannot delete the following 2bit file(s) because they define ` +
+            alert(`Cannot delete the following 2bit file(s) because they are part of ` +
                   `an assembly hub:\n  ${names}\n\nDelete the whole hub instead, ` +
                   `or replace the 2bit by uploading a new one with the same name.`);
             return;
         }
         // Only warn about hub.txt deletion if the user directly selected the hub.txt file,
         // not if it's being deleted as part of selecting a whole hub/directory
         let hasDirectlySelectedHubTxt = Object.values(directlySelected).some(d => d.fileType === "hub.txt");
         if (hasDirectlySelectedHubTxt) {
             if (!confirm("Warning: Deleting a hub.txt file will remove your hub and its shareable URL. Are you sure?")) {
                 return;
             }
         }
         let cartData = {deleteFile: {fileList: []}};
         cart.setCgiAndUrl(fileListEndpoint);
         _.forEach(data, (d) => {
@@ -1930,30 +1936,40 @@
 
         uppy.use(Uppy.Tus, tusOptions);
         uppy.use(BatchChangePlugin, {target: Uppy.Dashboard});
         uppy.on('upload-error', (file, error, response) => {
             // Replace tus's verbose default ("tus: unexpected response while
             // uploading chunk, originated from request (method: PATCH, ...)")
             // with the message our hook actually sent. Overwrite per-file
             // state, global state.error (read by the StatusBar), and the
             // info[] array (transient banner) - Uppy core populates all three
             // with the wrapped message before this handler runs.
             let cleanMsg = extractHookErrorMessage(error, response);
             if (file) {
                 uppy.setFileState(file.id, {error: cleanMsg});
             }
             uppy.setState({error: cleanMsg, info: []});
+            // Long-duration banner so the user has time to read the message;
+            // the StatusBar truncates to "Upload failed" and hides the rest
+            // behind a "?" icon.
+            uppy.info(cleanMsg, 'error', 30000);
+            // Genome-name collision is fixable in place by editing the 2bit's
+            // genome field, so reopen the file card.
+            if (file && cleanMsg && cleanMsg.includes(hubGenomeCollisionErrFrag)) {
+                const dash = uppy.getPlugin("Dashboard");
+                if (dash) dash.toggleFileCard(true, file.id);
+            }
         });
         uppy.on('upload-success', (file, response) => {
             const metadata = file.meta;
             const d = new Date(metadata.lastModified);
             const pad = (num) => String(num).padStart(2, '0');
             const dFormatted = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
             const now = new Date(Date.now());
             const nowFormatted = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
             let newReqObj, hubTxtObj, parentDirObj;
             let hubType = metadata.hubType || "trackHub";
             newReqObj = {
                 "fileName": cgiEncode(metadata.fileName),
                 "fileSize": metadata.fileSize,
                 "fileType": metadata.fileType,
                 "genome": metadata.genome,