cd0b41f1116bd93328ab34e9eceaa5f9afa8b825 chmalee Thu Feb 6 13:36:29 2025 -0800 Lots of changes to hub space UI after code review and QA feedback. 1) Make the table interface more like a file explorer where you view the 'root' directory by default and then can click into 'folders'. Also adds a breadcrumb nav so you can go backwards. 2) Fix bug in delete command to remove directories correctly. 3) cgiEncode the parentDir of incoming requests to prevent moving files to weird locations diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js index 1c3e4130e38..c5594b84f79 100644 --- src/hg/js/hgMyData.js +++ src/hg/js/hgMyData.js @@ -67,80 +67,93 @@ cart.setCgi("hgHubConnect"); cart.send(cartData, handleSuccess); cart.flush(); } const fileNameRegex = /[0-9a-zA-Z._\-+]+/g; // allowed characters in file names // make our Uppy instance: const uppy = new Uppy.Uppy({ debug: true, allowMultipleUploadBatches: false, onBeforeUpload: (files) => { // set all the fileTypes and genomes from their selects let doUpload = true; for (let [key, file] of Object.entries(files)) { let fileNameMatch = file.meta.name.match(fileNameRegex); + let parentDirMatch = file.meta.parentDir.match(fileNameRegex); if (!fileNameMatch || fileNameMatch[0] !== file.meta.name) { uppy.info(`Error: File name has special characters, please rename file: ${file.meta.name} to only include alpha-numeric characters, period, dash, underscore or plus.`, 'error', 2000); doUpload = false; continue; } + if (!parentDirMatch || parentDirMatch[0] !== file.meta.parentDir) { + uppy.info(`Error: File name has special characters, please rename file: ${file.meta.name} to only include alpha-numeric characters, period, dash, underscore or plus.`, 'error', 2000); + doUpload = false; + continue; + } if (!file.meta.genome) { uppy.info(`Error: No genome selected for file ${file.meta.name}!`, 'error', 2000); doUpload = false; continue; } else if (!file.meta.fileType) { uppy.info(`Error: File type not supported, file: ${file.meta.name}!`, 'error', 2000); doUpload = false; continue; } uppy.setFileMeta(file.id, { fileName: file.meta.name, fileSize: file.size, lastModified: file.data.lastModified, }); } return doUpload; }, }); 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: "", + isLoggedIn: "", + maxQuota: 0, + userQuota: 0, + userFiles: {}, }; function getTusdEndpoint() { // this variable is set by hgHubConnect and comes from hg.conf value return tusdEndpoint; } 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"], + "text": [".txt", ".text"], }; function detectFileType(fileName) { let fileLower = fileName.toLowerCase(); for (let fileType in extensionMap) { for (let ext of extensionMap[fileType]) { if (fileLower.endsWith(ext)) { return fileType; } } } //we could alert here but instead just explicitly set the value to null //and let the backend reject it instead, forcing the user to rename their //file //alert(`file extension for ${fileName} not found, please explicitly select it`); @@ -285,227 +298,341 @@ // update the div that shows how many files are selected let numSelected = data !== null ? Object.entries(data).length : 0; let infoDiv = document.getElementById("selectedFileInfo"); let span = document.getElementById("numberSelectedFiles"); let spanParentDiv = span.parentElement; span.textContent = `${numSelected} ${numSelected > 1 ? "files" : "file"}`; if (numSelected > 0) { // (re) set up the handlers for the selected file info div: let viewBtn = document.getElementById("viewSelectedFiles"); selectedData = data; viewBtn.addEventListener("click", viewAllInGenomeBrowser); viewBtn.textContent = numSelected === 1 ? "View selected file in Genome Browser" : "View all selected files in Genome Browser"; let deleteBtn = document.getElementById("deleteSelectedFiles"); deleteBtn.addEventListener("click", deleteFileList); deleteBtn.textContent = numSelected === 1 ? "Delete selected file" : "Delete selected files"; - } // set the visibility of the placeholder text and info text spanParentDiv.style.display = numSelected === 0 ? "none": "block"; let placeholder = document.getElementById("placeHolderInfo"); placeholder.style.display = numSelected === 0 ? "block" : "none"; } function handleCheckboxSelect(ev, table, row) { // depending on the state of the checkbox, we will be adding information // to the div, or removing information. We will also be potentially checking/unchecking // all of the checkboxes if the selectAll box was clicked. The data variable // will hold all the information we want to keep visible in the info div let data = {}; // get all of the currently selected rows (may be more than just the one that // was most recently clicked) table.rows({selected: true}).data().each(function(row, ix) { data[ix] = row; }); updateSelectedFileDiv(data); } + function createOneCrumb(table, dirName, dirFullPath) { + // make a new span that can be clicked to nav through the table + let newSpan = document.createElement("span"); + newSpan.id = dirName; + newSpan.textContent = dirName; + newSpan.classList.add("breadcrumb"); + newSpan.addEventListener("click", function(e) { + dataTableShowDir(table, dirName, dirFullPath); + dataTableCustomOrder(table, {"fullPath": dirFullPath}); + table.draw(); + }); + return newSpan; + } + + function dataTableEmptyBreadcrumb(table) { + let currBreadcrumb = document.getElementById("breadcrumb"); + currBreadcrumb.replaceChildren(currBreadcrumb.firstChild); + currBreadcrumb.firstChild.style.cursor = "unset"; + currBreadcrumb.firstChild.style.textDecoration = "unset"; + } + + function dataTableCreateBreadcrumb(table, dirName, dirFullPath) { + // Re-create the breadcrumb nav to move back through directories + let currBreadcrumb = document.getElementById("breadcrumb"); + // empty the node but leave the first "My Data" span + if (currBreadcrumb.children.length > 1) { + currBreadcrumb.replaceChildren(currBreadcrumb.firstChild); + } + currBreadcrumb.firstChild.style.cursor = "pointer"; + currBreadcrumb.firstChild.style.textDecoration = "underline"; + let components = dirFullPath.split("/"); + components.forEach(function(dirName, dirNameIx) { + if (!dirName) { + return; + } + let path = components.slice(0, dirNameIx+1); + componentFullPath = path.join('/'); + componentFullPath += "/"; // need a final '/' on there + let newSpan = createOneCrumb(table, dirName, componentFullPath); + currBreadcrumb.appendChild(document.createTextNode(" > ")); + currBreadcrumb.appendChild(newSpan); + }); + } + + // search related functions: + function clearSearch(table) { + // 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); + table.search.fixed("showRoot", function(searchStr, rowData, rowIx) { + return !rowData.parentDir; + }); + } + + function dataTableShowDir(table, dirName, dirFullPath) { + // show the directory and all immediate children of the directory + clearSearch(table); + table.search.fixed("oneHub", function(searchStr, rowData, rowIx) { + return rowData.parentDir === dirName || rowData.fullPath === dirFullPath; + }); + dataTableCreateBreadcrumb(table, dirName, dirFullPath); + } + + // 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)"); + if (thead.childNodes.length > 1) { + let old = thead.removeChild(thead.lastChild); + if (oldRowData) { + table.row.add(oldRowData); + oldRowData = null; + } + } + table.order([{name: "uploadTime", dir: "desc"}]); + } else { + // move the dirName row into the header, then the other files can + // sort normally + let row = table.row((idx,data) => data.fullPath === dirData.fullPath); + let rowNode = row.node(); + if (oldRowData) { + // restore the previous row, which will be not displayed by the search anyways: + table.row.add(oldRowData); + oldRowData = null; + } + if (!rowNode) { + // if we are using the breadcrumb to jump back 2 directories, we won't + // have a rowNode because the row will not have been rendered yet + table.draw(); + rowNode = row.node(); + } + oldRowData = row.data(); + // put the data in the header: + let rowClone = rowNode.cloneNode(true); + // match the background color of the normal rows: + rowNode.style.backgroundColor = "#f9f9f9"; + let thead = document.querySelector(".dt-scroll-headInner > table:nth-child(1) > thead:nth-child(1)"); + if (thead.childNodes.length === 1) { + thead.appendChild(rowClone); + } else { + thead.replaceChild(rowClone, thead.lastChild); + } + // remove the row + row.remove(); + // now do a regular order + table.order([{name: "uploadTime", dir: "desc"}]); + } + } + function dataTablePrintSize(data, type, row, meta) { return prettyFileSize(data); } function dataTablePrintGenome(data, type, row, meta) { if (data.startsWith("hub_")) return data.split("_").slice(2).join("_"); return data; } function dataTablePrintAction(rowData) { /* Return a node for rendering the actions column */ if (rowData.fileType === "dir") { let folderIcon = document.createElement("i"); folderIcon.style.display = "inline-block"; folderIcon.style.backgroundImage = "url(\"../images/folderC.png\")"; folderIcon.style.backgroundPosition = "left center"; folderIcon.style.backgroundRepeat = "no-repeat"; folderIcon.style.width = "24px"; folderIcon.style.height = "24px"; folderIcon.classList.add("folderIcon"); - folderIcon.addEventListener("dblclick", function(e) { + folderIcon.addEventListener("click", function(e) { e.stopPropagation(); - console.log("dblclick"); + console.log("folder click"); let table = $("#filesTable").DataTable(); let trow = $(e.target).closest("tr"); let row = table.row(trow); - if (row.child.isShown()) { - row.child.hide(); - } else { - row.child.show(); - } - }); - folderIcon.addEventListener("click", (e) => { - e.stopPropagation(); - console.log("click"); + dataTableShowDir(table, rowData.fileName, rowData.fullPath); + dataTableCustomOrder(table, rowData); + table.draw(); }); return folderIcon; } else { + // only offer the button if this is a track file + if (rowData.fileType !== "hub.txt" && rowData.fileType !== "text" && rowData.fileType in extensionMap) { let container = document.createElement("div"); - // click to view hub.txt or track file in gb: let viewBtn = document.createElement("button"); viewBtn.textContent = "View in Genome Browser"; viewBtn.type = 'button'; viewBtn.addEventListener("click", function(e) { e.stopPropagation(); viewInGenomeBrowser(rowData.fileName, rowData.fileType, rowData.genome, rowData.parentDir); }); container.appendChild(viewBtn); return container; + } else { + return null; + } } } function deleteFileFromTable(pathList) { // req is an object with properties of an uploaded file, make a new row // for it in the filesTable let table = $("#filesTable").DataTable(); let rows = table.rows((idx, data) => pathList.includes(data.fullPath)); rows.remove().draw(); let toKeep = (elem) => !pathList.includes(elem.fullPath); uiState.fileList = uiState.fileList.filter(toKeep); history.replaceState(uiState, "", document.location.href); } function addFileToHub(rowData) { // a file has been uploaded and a hub has been created, present a modal // to choose which hub to associate this track to // backend wise: move the file into the hub directory // update the hubSpace row with the hub name // frontend wise: move the file row into a 'child' of the hub row console.log(`sending addToHub req for ${rowData.fileName} to `); cart.setCgi("hgHubConnect"); cart.send({addToHub: {hubName: "", dataFile: ""}}); cart.flush(); } - // hash of file paths to their objects, starts as userFiles + // hash of file paths to their objects, starts as uiState.userFiles let filesHash = {}; - function addNewUploadedFileToTable(req) { - // req is an object with properties of an uploaded file, make a new row - // for it in the filesTable - if (!(req.fullPath in filesHash)) { - if ($.fn.dataTable.isDataTable("#filesTable")) { + function addNewUploadedHubToTable(hub) { + // hub is a list of objects representing the file just uploaded, the associated + // hub.txt, and directory. Make a new row for each in the filesTable, except for + // maybe the hub directory row and hub.txt which we may have already seen before let table = $("#filesTable").DataTable(); - let rowObj = table.row.add(req); - rowVis[req.fullPath] = true; - table.search.fixed("defaultView", function(searchStr, data, rowIx) { - return rowVis[data.fileName] || rowVis[data.fullPath]; - }).draw(); - indentActionButton(rowObj); - let newRow = rowObj.node(); - $(newRow).css('color','red').animate({color: 'black'}, 1500); - } else { - showExistingFiles([req]); + let justUploaded = {}; // hash of contents of hub but keyed by fullPath + let hubDirData = {}; // the data for the parentDir of the uploaded file + for (let obj of hub) { + if (!obj.parentDir) { + hubDirData = obj; + } + let rowObj; + if (!(obj.fullPath in filesHash)) { + justUploaded[obj.fullPath] = obj; + rowObj = table.row.add(obj); + filesHash[obj.fullPath] = obj; + uiState.fileList.push(obj); } - filesHash[req.fullPath] = req; - uiState.fileList.push(req); } + + // 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 + dataTableShowDir(table, hubDirData.fileName, hubDirData.fullPath); + table.draw(); + dataTableCustomOrder(table, hubDirData); + table.draw(); } function doRowSelect(ev, table, indexes) { let row = table.row(indexes); let rowTr = row.node(); + if (rowTr) { let rowCheckbox = rowTr.childNodes[0].firstChild; if (rowTr.classList.contains("parentRow")) { // we need to manually check the children table.rows((idx,rowData) => rowData.fullPath.startsWith(row.data().fullPath) && rowData.parentDir === row.data().fileName).every(function(rowIdx, tableLoop, rowLoop) { if (ev.type === "select") { this.select(); } else { this.deselect(); } }); } rowCheckbox.checked = ev.type === "select"; handleCheckboxSelect(ev, table, rowTr); } - - let rowVis = {}; // heirarchy of files and their row visibility - function makeFileHeirarchy(table) { - function rowHeirarchy(data) { - if (rowVis[data.fullPath]) {return;} - - // first check for the top levels which are always open by default - if (!data.parentDir) { - rowVis[data.fileName] = true; - return; - } - - // get the parent row and check it's status: - let parentName = data.parentDir[-1] === "/" ? data.parentDir.slice(0,-1) : data.parentDir; - let parentData = table.row((idx, rowData) => rowData.fileName === parentName).data(); - if (!(parentName in rowVis) && !(parentData.fullPath in rowVis)) { - // get the data for the parent and recurse "up": - rowHeirarchy(parentData); - } - parentVis = rowVis[parentName] || rowVis[parentData.fullPath]; - rowVis[data.fullPath] = parentVis; } - table.rows().every(function(rowIdx, tableLoop, rowLoop) { - let data = this.data(); - rowHeirarchy(data); - }); + function indentActionButton(rowTr, rowData) { + let numIndents = "0px"; //data.parentDir !== "" ? data.fullPath.split('/').length - 1: 0; + if (rowData.fileType !== "dir") { + numIndents = "10px"; } - - function indentActionButton(rowObj) { - let data = rowObj.data(); - let numIndents = data.parentDir !== "" ? data.fullPath.split('/').length - 1: 0; - rowObj.node().childNodes[1].style.textIndent = (numIndents * 10) + "px"; + rowTr.childNodes[1].style.textIndent = numIndents; } let tableInitOptions = { select: { items: 'row', - style: 'multi', // default to a single click is all that's needed + style: 'multi+shift', // default to a single click is all that's needed }, pageLength: 25, scrollY: 600, scrollCollapse: true, // when less than scrollY height is needed, make the table shorter + deferRender: true, // only draw into the DOM the nodes we need for each page + orderCellsTop: true, // when viewing a subdirectory, the directory becomes a part of + // the header, this option prevents those cells from being used to + // sort the table layout: { + top2Start: { + div: { + className: "", + id: "breadcrumb", + html: "My Data", + } + }, topStart: { buttons: [ { text: 'Upload', action: function() {return;}, className: 'uploadButton', enabled: false, // disable by default in case user is not logged in }, ], quota: null, - } + }, }, columnDefs: [ { orderable: false, targets: 0, render: DataTable.render.select(), }, { orderable: false, targets: 1, data: "action", title: "", render: function(data, type, row) { if (type === "display") { return dataTablePrintAction(row); } return ''; } @@ -517,138 +644,136 @@ dataTablePrintSize(data); } return data; } }, { targets: 5, render: function(data, type, row) { if (type === "display") { return dataTablePrintGenome(data); } return data; } }, { - // The upload time column, not visible but we use it to sort on new uploads + // The upload time column targets: 8, - visible: false, + visible: true, searchable: false, orderable: true, }, { targets: 9, visible: false, searchable: false, orderable: true, } ], columns: [ {data: "", }, {data: "", }, {data: "fileName", title: "File name"}, {data: "fileSize", title: "File size"}, {data: "fileType", title: "File type"}, {data: "genome", title: "Genome"}, {data: "parentDir", title: "Hubs"}, {data: "lastModified", title: "File Last Modified"}, {data: "uploadTime", title: "Upload Time", name: "uploadTime"}, {data: "fullPath", title: "fullPath", name: "fullPath"}, ], - order: [{name: "fullPath", dir: "asc"},{name: "uploadTime", dir: "asc"}], drawCallback: function(settings) { console.log("table draw"); - if (isLoggedIn) { - settings.api.buttons(0).enable(); - } }, rowCallback: function(row, data, displayNum, displayIndex, dataIndex) { + // row is a tr element, data is the td values // a row can represent one of three things: // a 'folder', with no parents, but with children // a folder with parents and children (can only come from hubtools // a 'file' with no children, but with parentDir // we assign the appropriate classes which are used later to // collapse/expand and select rows for viewing or deletion if (!data.parentDir) { row.className = "topLevelRow"; } else { row.className = "childRow"; } if (data.fileType === "dir") { row.className += " parentRow"; } + indentActionButton(row, data); }, initComplete: function(settings, json) { - console.log("data loaded, hiding directories"); + console.log("data loaded, only showing directories"); let table = new $.fn.dataTable.Api(settings); - makeFileHeirarchy(table); - table.rows().every(function(rowIdx, rowLoop, tableLoop) { - indentActionButton(this); - }); - table.order.fixed({pre: [{name: "fullPath", dir: "asc"}, {name: "uploadTime", dir: "asc"}]}); - // only show the top level and one layer of children by default - table.search.fixed("defaultView", function(searchStr, data, rowIx) { - return rowVis[data.fileName] || rowVis[data.fullPath]; - }).draw(); + dataTableShowTopLevel(table); + dataTableCustomOrder(table); + table.draw(); } }; function showExistingFiles(d) { // Make the DataTable for each file // make buttons have the same style as other buttons $.fn.dataTable.Buttons.defaults.dom.button.className = 'button'; tableInitOptions.data = d; - if (isLoggedIn) { + if (uiState.isLoggedIn) { tableInitOptions.language = {emptyTable: "Uploaded files will appear here. Click \"Upload\" to get started"}; } else { tableInitOptions.language = {emptyTable: "You are not logged in, please navigate to \"My Data\" > \"My Sessions\" and log in or create an account to begin uploading files"}; } DataTable.feature.register('quota', function(settings, opts) { let options = Object.assign({option1: false, option2: false}, opts); let container = document.createElement("div"); - if (isLoggedIn) { - container.textContent = `Using ${prettyFileSize(userQuota)} of ${prettyFileSize(maxQuota)}`; + if (uiState.isLoggedIn) { + container.textContent = `Using ${prettyFileSize(uiState.userQuota)} of ${prettyFileSize(uiState.maxQuota)}`; } return container; }); let table = new DataTable("#filesTable", tableInitOptions); + 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(); + } table.on("select", function(e, dt, type, indexes) { doRowSelect(e, dt, indexes); }); table.on("deselect", function(e, dt, type, indexes) { doRowSelect(e, dt, indexes); }); _.each(d, function(f) { filesHash[f.fullPath] = f; }); return table; } function init() { cart.setCgi('hgMyData'); cart.debug(debugCartJson); // TODO: write functions for // creating default trackDbs // editing trackDbs - let fileDiv = document.getElementById('filesDiv'); // get the state from the history stack if it exists - if (history.state) { - uiState = history.state; - } else if (typeof userFiles !== 'undefined' && Object.keys(userFiles).length > 0) { - uiState.fileList = userFiles.fileList; - uiState.hubList = userFiles.hubList; - uiState.userUrl = userFiles.userUrl; + if (typeof uiData !== 'undefined' && typeof uiState.userFiles !== 'undefined') { + _.assign(uiState, uiData.userFiles); } // first add the top level directories/files let table = showExistingFiles(uiState.fileList); // TODO: add event handlers for editing defaults, grouping into hub $("#newTrackHubDialog").dialog({ modal: true, autoOpen: false, title: "Create new track hub", closeOnEscape: true, minWidth: 400, minHeight: 120 }); // create a custom uppy plugin to batch change the type and db fields class BatchChangePlugin extends Uppy.BasePlugin { @@ -716,31 +841,31 @@ // the batch change db select let batchDbSelect = document.createElement("select"); this.createOptsForSelect(batchDbSelect, makeGenomeSelectOptions()); batchDbSelect.id = "batchDbSelect"; batchDbSelect.style.gridArea = "2 / 2 / 2 / 2"; batchDbSelect.style.margin = "1px 1px auto"; let batchDbLabel = document.createElement("label"); batchDbLabel.textContent = "Genome"; batchDbLabel.for = "batchDbSelect"; batchDbLabel.style.gridArea = "2 / 1 / 2 / 1"; // the batch change hub name let batchParentDirInput = document.createElement("input"); batchParentDirInput.id = "batchParentDir"; - batchParentDirInput.value = hubNameDefault; + batchParentDirInput.value = uiState.hubNameDefault; batchParentDirInput.style.gridArea = "3 / 2 / 3 / 2"; batchParentDirInput.style.margin= "1px 1px auto"; let batchParentDirLabel = document.createElement("label"); batchParentDirLabel.textContent = "Hub Name"; batchParentDirLabel.for = "batchParentDir"; batchParentDirLabel.style.gridArea = "3 / 1 / 3 / 1"; // add event handlers to change metadata, use an arrow function // because otherwise 'this' keyword will be the element instead of // our class batchDbSelect.addEventListener("change", (ev) => { let files = this.uppy.getFiles(); let val = ev.target.value; for (let [key, file] of Object.entries(files)) { this.uppy.setFileMeta(file.id, {genome: val}); @@ -762,31 +887,31 @@ // 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); } } } install() { this.uppy.on("file-added", (file) => { // add default meta data for genome and fileType console.log("file-added"); - this.uppy.setFileMeta(file.id, {"genome": defaultDb(), "fileType": detectFileType(file.name), "parentDir": hubNameDefault}); + this.uppy.setFileMeta(file.id, {"genome": defaultDb(), "fileType": detectFileType(file.name), "parentDir": uiState.hubNameDefault}); if (this.uppy.getFiles().length > 1) { this.addBatchSelectsToDashboard(); } else { // only open the file editor when there is one file const dash = uppy.getPlugin("Dashboard"); 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(); } }); @@ -804,35 +929,33 @@ this.removeBatchSelectsFromDashboard(); } let allFiles = this.uppy.getFiles(); let completeFiles = this.uppy.getFiles().filter((f) => f.progress.uploadComplete === true); if (allFiles.length === completeFiles.length) { this.uppy.clear(); } }); } uninstall() { // not really used because we aren't ever uninstalling the uppy instance this.uppy.off("file-added"); } } let uppyOptions = { - //target: "#filePickerModal", // this seems nice but then the jquery css interferes with - // the uppy css trigger: ".uploadButton", showProgressDetails: true, - note: "Example text in the note field", + note: "The UCSC Genome Browser is not a HIPAA compliant data store. Do not upload patient information or other sensitive data files here, as anyone with the URL can view them.", meta: {"genome": null, "fileType": null}, restricted: {requiredMetaFields: ["genome"]}, closeModalOnClickOutside: true, closeAfterFinish: true, theme: 'auto', metaFields: (file) => { const fields = [{ id: 'name', name: 'File name', render: ({value, onChange, required, form}, h) => { return h('input', {type: "text", value: value, onChange: e => { onChange(e.target.value); @@ -920,28 +1043,28 @@ "fileType": "hub.txt", "genome": metadata.genome, "parentDir": metadata.parentDir, "fullPath": metadata.parentDir + "/hub.txt", }; parentDirObj = { "uploadTime": now.toLocaleString(), "lastModified": d.toLocaleString(), "fileName": metadata.parentDir, "fileSize": 0, "fileType": "dir", "genome": metadata.genome, "parentDir": "", "fullPath": metadata.parentDir + "/", }; - addNewUploadedFileToTable(parentDirObj); - addNewUploadedFileToTable(hubTxtObj); - addNewUploadedFileToTable(newReqObj); + // package the three objects together as one "hub" and display it + let hub = [parentDirObj, hubTxtObj, newReqObj]; + addNewUploadedHubToTable(hub); }); uppy.on('complete', (result) => { history.replaceState(uiState, "", document.location.href); console.log("replace history with uiState"); }); } return { init: init, uiState: uiState, }; }());