c3788110e8d865774fff8197cf5b47350472ba64 chmalee Tue Aug 4 11:26:55 2026 -0700 Fix hubSpace hook error handling and nested hub path handling, 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 bdc7f01bdf4..cd292b3775f 100644 --- src/hg/js/hgMyData.js +++ src/hg/js/hgMyData.js @@ -911,32 +911,32 @@ "genome": hubCreate.defaultDb(), "fileType": ftype, "parentDir": defaultParentDir, "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); defaultMeta.genomeLabel = defaultMeta.genome; defaultMeta.hubType = "assemblyHub"; } this.uppy.setFileMeta(file.id, defaultMeta); // When drilled into an assembly hub, inherit and lock its genome. - if (hubCreate.uiState.currentHub && - hubCreate.uiState.currentHub === defaultMeta.parentDir) { + let openDir = hubCreate.uiState.currentHubPath || hubCreate.uiState.currentHub; + if (openDir && openDir === defaultMeta.parentDir) { 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). @@ -1012,60 +1012,72 @@ } }); } 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 + 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 }; 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"], "bigChain": [".bigchain"], "bamIndex": [".bam.bai", ".bai"], "tabixIndex": [".vcf.gz.tbi", "vcf.bgz.tbi"], "hub.txt": ["hub.txt"], "2bit": [".2bit"], "text": [".txt", ".text"], }; function getDefaultHubName() { - return uiState.currentHub.length > 0 ? uiState.currentHub : uiState.hubNameDefault; + // with a directory open, new files default into that directory, which for a + // subdirectory is the whole path like myHub/hg38 + let openDir = uiState.currentHubPath || uiState.currentHub; + return openDir.length > 0 ? openDir : uiState.hubNameDefault; + } + + function hubRootForCurrentDir() { + // the hub is the first path segment: hub.txt and the hub's own row live there + // even when the user has drilled down into a subdirectory of it + let path = uiState.currentHubPath || uiState.currentHub; + return path ? path.split("/")[0] : ""; } function sanitizeGenomeName(name) { // Strip .2bit, replace non-alphanumeric/_/-/. with _, drop hub_ prefix. // Returns empty string if nothing usable is left. // The allowed character class [A-Za-z0-9._-] must match the // server-side check in src/hg/hgHubConnect/hooks/pre-finish.c. if (!name) return ""; let stem = name.replace(/\.2bit$/i, ""); stem = stem.replace(/[^A-Za-z0-9._-]/g, "_"); stem = stem.replace(/^hub_/, ""); return stem; } function hubTxtPathForHub(hubName) { @@ -1847,59 +1859,61 @@ // clear any fixed searches so we can apply a new one let currSearches = table.search.fixed().toArray(); currSearches.forEach((name) => table.search.fixed(name, null)); } function dataTableShowTopLevel(table) { // show all the "root" files, which are files (probably mostly directories) // with no parentDir clearSearch(table); // deselect any selected rows like Finder et al when moving into/upto a directory table.rows({selected: true}).deselect(); table.search.fixed("showRoot", function(searchStr, rowData, rowIx) { return !rowData.parentDir; }); uiState.currentHub = ""; + uiState.currentHubPath = ""; hideHubBanner(); updateSelectedFileDiv(null); } function dataTableShowDir(table, dirName, dirFullPath) { // show the directory and all immediate children of the directory clearSearch(table); // deselect any selected rows like Finder et al when moving into/upto a directory table.rows({selected: true}).deselect(); // Callers must call table.draw() after this so filter + order changes // from showDir/customOrder render in a single redraw. table.search.fixed("oneHub", function(searchStr, rowData, rowIx) { // calculate the fullPath of this rows parentDir in case the dirName passed // to this function has the same name as a parentDir further up in the // listing. For example, consider a test/test/tmp.txt layout, where "test" // is the parentDir of tmp.txt and the test subdirectory let parentDirFull = rowData.fullPath.split("/").slice(0,-1).join("/"); if (rowData.parentDir === dirName && parentDirFull === dirFullPath) { return true; } else if (rowData.fullPath === dirFullPath) { // also return the directory itself return true; } else { return false; } }); uiState.currentHub = dirName; + uiState.currentHubPath = dirFullPath; dataTableCreateBreadcrumb(table, dirName, dirFullPath); - showHubBanner(dirName); + showHubBanner(hubRootForCurrentDir()); updateSelectedFileDiv(null); } // when we move into a new directory, we remove the row from the table // and add it's html into the header, keep the row object around so // we can add it back in later let oldRowData = null; function dataTableCustomOrder(table, dirData) { // figure out the order the rows of the table should be in // if dirData is null, sort on uploadTime first // if dirData exists, that is the first row, followed by everything else // in uploadTime order if (!dirData) { // make sure the old row can show up again in the table let thead = document.querySelector(".dt-scroll-headInner > table:nth-child(1) > thead:nth-child(1)"); @@ -2149,32 +2163,40 @@ let allRows = table.rows().indexes(); for (let j = 0; j < allRows.length; j++) { let rowData = table.row(allRows[j]).data(); if (rowData.fullPath === obj.fullPath) { table.row(allRows[j]).invalidate(); break; } } } } // show all the new rows we just added, note the double draw, we need // to have the new rows rendered to do the order because the order // will copy the actual DOM node parseFileListIntoHash(uiState.fileList); - dataTableShowDir(table, hubDirData.fileName, hubDirData.fullPath); - dataTableCustomOrder(table, hubDirData); + // stay in the directory the user has open, the upload may have gone into a + // subdirectory of the hub and would not be listed at the hub level. Both calls + // have to name the same directory, or the row moved into the header and the row + // dropped from the table are different ones + let showDirData = hubDirData; + if (uiState.currentHubPath && uiState.currentHubPath in uiState.filesHash) { + showDirData = uiState.filesHash[uiState.currentHubPath]; + } + dataTableShowDir(table, showDirData.fileName, showDirData.fullPath); + dataTableCustomOrder(table, showDirData); table.draw(); } function doRowSelect(evtype, table, indexes) { let selectedRow = table.row(indexes); let rowTr = selectedRow.node(); if (rowTr) { handleCheckboxSelect(evtype, table, selectedRow); } } function indentActionButton(rowTr, rowData) { let numIndents = "0px"; //data.parentDir !== "" ? data.fullPath.split('/').length - 1: 0; if (rowData.fileType !== "dir") { numIndents = "10px"; @@ -2375,31 +2397,31 @@ }); if (uiState.isLoggedIn) { table.buttons(".uploadButton").enable(); document.getElementById("rootBreadcrumb").addEventListener("click", function(e) { dataTableShowTopLevel(table); dataTableCustomOrder(table); dataTableEmptyBreadcrumb(table); table.draw(); }); } else { table.buttons(".uploadButton").disable(); } let hubBannerBtn = document.getElementById("hubBannerViewBtn"); if (hubBannerBtn) { hubBannerBtn.addEventListener("click", function(e) { - viewHubInGenomeBrowser(uiState.currentHub); + viewHubInGenomeBrowser(hubRootForCurrentDir()); }); } let hubBannerCopyBtn = document.getElementById("hubBannerCopyBtn"); if (hubBannerCopyBtn) { hubBannerCopyBtn.addEventListener("click", copyHubLinkFromBanner); } table.on("select", function(e, dt, type, indexes) { indexes.forEach(function(i) { doRowSelect(e.type, dt, i); }); }); table.on("deselect", function(e, dt, type, indexes) { indexes.forEach(function(i) { doRowSelect(e.type, dt, i); }); @@ -2500,64 +2522,66 @@ 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; let hubType = metadata.hubType || "trackHub"; // Multi-segment parentDir (split-hub uploads) needs per-segment rows. let parentSegments = metadata.parentDir.split("/"); let parentLeaf = parentSegments[parentSegments.length - 1]; + // the hub.txt lives at the hub, not in the subdirectory the file went into + let hubRoot = parentSegments[0]; newReqObj = { "fileName": cgiEncode(metadata.fileName), "fileSize": metadata.fileSize, "fileType": metadata.fileType, "genome": metadata.genome, "parentDir": cgiEncode(parentLeaf), "lastModified": dFormatted, "uploadTime": nowFormatted, "fullPath": cgiEncode(metadata.parentDir) + "/" + cgiEncode(metadata.fileName), "hubType": hubType, }; // from what I can tell, any response we would create in the pre-finish hook // is completely ignored for some reason, so we have to fake the other files // we would have created with this one file and add them to the table if they // weren't already there: // Only fabricate a hub.txt row when the backend actually synthesized // one. Skip if the user supplied their own *.hub.txt (either already // in filesHash from a prior upload, or coming in this same batch - // upload-success order is arbitrary so the hub.txt row may not be // in filesHash yet when a sibling's upload-success fires). - let dirHash = uiState.filesHash[cgiEncode(metadata.parentDir)]; + let dirHash = uiState.filesHash[cgiEncode(hubRoot)]; let hubTxtExists = !!(dirHash && dirHash.children && dirHash.children.some(c => c.fileType === "hub.txt")); let batchHasHubTxt = metadata.batchHasHubTxt === "true"; if (metadata.fileType !== "hub.txt" && !hubTxtExists && !batchHasHubTxt) { hubTxtObj = { "uploadTime": nowFormatted, "lastModified": dFormatted, "fileName": "hub.txt", "fileSize": 0, "fileType": "hub.txt", "genome": metadata.genome, - "parentDir": cgiEncode(parentLeaf), - "fullPath": cgiEncode(metadata.parentDir) + "/hub.txt", + "parentDir": cgiEncode(hubRoot), + "fullPath": cgiEncode(hubRoot) + "/hub.txt", "hubType": hubType, }; } // One dir row per path segment; leaf-only db, matching makeParentDirRows(). // For split hubs the hub-root dir stays empty regardless of layout. let isSplitHub = metadata.batchSplitHub === "true"; let dirRows = []; for (let i = 0; i < parentSegments.length; i++) { let dirFullPath = parentSegments.slice(0, i + 1) .map(cgiEncode).join("/"); let dirParent = i > 0 ? cgiEncode(parentSegments[i - 1]) : ""; let isLeaf = (i === parentSegments.length - 1); let dirDb; if (isSplitHub) { dirDb = (isLeaf && parentSegments.length > 1) ? metadata.genome : "";