d2800e10690b66bf2ab04804e124958459f0ff0d chmalee Thu Nov 7 11:11:15 2024 -0800 Add a parentDir field to the hubSpace table and make an index on it diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js index 4baf424..2c6c995 100644 --- src/hg/js/hgMyData.js +++ src/hg/js/hgMyData.js @@ -1,1058 +1,1072 @@ /* jshint esnext: true */ var debugCartJson = true; +var hubNameDefault = "My First Hub"; function prettyFileSize(num) { if (!num) {return "n/a";} if (num < (1000 * 1024)) { return `${(num/1000).toFixed(1)}kb`; } else if (num < (1000 * 1000 * 1024)) { return `${((num/1000)/1000).toFixed(1)}mb`; } else { return `${(((num/1000)/1000)/1000).toFixed(1)}gb`; } } // make our Uppy instance: const uppy = new Uppy.Uppy({ debug: true, onBeforeUpload: (files) => { // set all the fileTypes and genomes from their selects for (let [key, file] of Object.entries(files)) { if (!file.meta.genome || !file.meta.fileType) { uppy.getPlugin("Dashboard").info("error!"); } uppy.setFileMeta(file.id, { fileName: file.name, fileSize: file.size, lastModified: file.data.lastModified, }); } }, }); var hubCreate = (function() { let uiState = { // our object for keeping track of the current UI and what to do toUpload: {}, // set of file objects keyed by name input: null, // the hidden input element pickedList: null, // the
for displaying files in toUpload pendingQueue: [], // our queue of pending [tus.Upload, file], kind of like the toUpload object fileList: [], // the files this user has uploaded, initially populated by the server // on page load, but gets updated as the user uploades/deletes files hubList: [], // the hubs this user has created/uploaded, initially populated by server // on page load, but gets updated as the user creates/deletes hubs userUrl: "", // the web accesible path where the uploads are stored for this user }; // We can use XMLHttpRequest if necessary or a mirror can't use tus var useTus = tus.isSupported && true; function getTusdEndpoint() { // return the port and basepath of the tusd server // NOTE: the port and basepath are specified in hg.conf //let currUrl = parseUrl(window.location.href); return "https://hgwdev-hubspace.gi.ucsc.edu/files"; } function liForFile(file) { let liId = `${file.name}#li`; let li = document.getElementById(liId); return li; } function newButton(text) { /* Creates a new button with some text as the label */ let newBtn = document.createElement("label"); newBtn.classList.add("button"); newBtn.textContent = text; return newBtn; } function createInput() { /* Create a new input element for a file picker */ let input = document.createElement("input"); input.multiple = true; input.type = "file"; input.id = "hiddenFileInput"; input.style = "display: none"; input.addEventListener("change", listPickedFiles); return input; } function requestsPending() { /* Return true if requests are still pending, which means it needs to * have been sent(). aborted requests are considered done for this purpose */ for (let [req, f] of uiState.pendingQueue) { if (req._req !== null) { xreq = req._req._xhr; if (xreq.readyState != XMLHttpRequest.DONE && xreq.readyState != XMLHttpRequest.UNSENT) { return true; } } } return false; } function addCancelButton(file, req) { /* Add a button that cancels the request req */ let li = liForFile(file); let newBtn = newButton("Cancel upload"); newBtn.addEventListener("click", (e) => { req.abort(); li.removeChild(newBtn); // TODO: make this remove the cancel all button if it's the last pending // request stillPending = requestsPending(); if (!stillPending) { let btnRow = document.getElementById("chooseAndSendFilesRow"); cAllBtn = btnRow.lastChild; btnRow.removeChild(cAllBtn); } }); li.append(newBtn); } function removeCancelAllButton() { let btnRow = document.getElementById("chooseAndSendFilesRow"); if (btnRow.lastChild.textContent === "Cancel all") { btnRow.removeChild(btnRow.lastChild); } } function addCancelAllButton() { let btnRow = document.getElementById("chooseAndSendFilesRow"); let newBtn = newButton("Cancel all"); newBtn.addEventListener("click", (e) => { while (uiState.pendingQueue.length > 0) { let [req, f] = uiState.pendingQueue.pop(); // we only need to abort requests that haven't finished yet if (req._req !== null) { if (req._req._xhr.readyState != XMLHttpRequest.DONE) { req.abort(true); } } let li = liForFile(f); if (li !== null) { // the xhr event handlers should handle this but just in case li.removeChild(li.lastChild); } } }); btnRow.appendChild(newBtn); } function makeNewProgMeter(fileName) { // create a progress meter for this filename const progMeterWidth = 128; const progMeterHeight = 12; const progMeter = document.createElement("canvas"); progMeter.classList.add("upload-progress"); progMeter.setAttribute("width", progMeterWidth); progMeter.setAttribute("height", progMeterHeight); idStr = `${fileName}#fileName`; ele = document.getElementById(idStr); ele.appendChild(progMeter); progMeter.ctx = progMeter.getContext('2d'); progMeter.ctx.fillStyle = 'orange'; progMeter.updateProgress = (percent) => { // update the progress meter for this elem if (percent === 100) { progMeter.ctx.fillStyle = 'green'; } progMeter.ctx.fillRect(0, 0, (progMeterWidth * percent) / 100, progMeterHeight); }; progMeter.updateProgress(0); return progMeter; } 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"], }; function detectFileType(fileName) { let fileLower = fileName.toLowerCase(); for (let fileType in extensionMap) { for (let extIx in extensionMap[fileType]) { let ext = extensionMap[fileType][extIx]; if (fileLower.endsWith(ext)) { return fileType; } } } //TODO: raise an error alert(`file extension for ${fileName} not found, please explicitly select it`); } /* function submitPickedFiles() { let tusdServer = getTusdEndpoint(); let onBeforeRequest = function(req) { let xhr = req.getUnderlyingObject(req); xhr.withCredentials = true; }; let onSuccess = function(req, metadata) { // remove the selected file from the input element and the ul list // FileList is a read only setting, so we have to make // a new one without this req delete uiState.toUpload[req.name]; let i, newReqObj = {}; for (i = 0; i < uiState.pendingQueue.length; i++) { fname = uiState.pendingQueue[i][1].name; if (fname === req.name) { // remove the successful tusUpload off uiState.pendingQueue.splice(i, 1); } } // remove the file from the list the user can see let rowDivs = document.querySelectorAll("[id^='" + req.name+"']"); let delIcon = rowDivs[0].previousElementSibling; rowDivs.forEach((div) => {div.remove();}); delIcon.remove(); // if nothing else we can close the dialog if (uiState.pendingQueue.length === 0) { // first remove the grid headers let headerEle = document.querySelectorAll(".fileListHeader"); headerEle.forEach( (header) => { if (header.style.display !== "none") { header.style.display = "none"; } }); //check if we can remove the batch change selects if (uiState.pendingQueue.length > 1) { document.querySelectorAll("[id^=batchChangeSelect]").forEach( (select) => { select.remove(); }); } $("#filePickerModal").dialog("close"); } }; let onError = function(metadata, err) { console.log("failing metadata:"); console.log(metadata); removeCancelAllButton(); if (err.originalResponse !== null) { alert(err.originalResponse._xhr.responseText); } else { alert(err); } }; let onProgress = function(bytesSent, bytesTotal) { this.updateProgress((bytesSent / bytesTotal) * 100); }; for (let f in uiState.toUpload) { file = uiState.toUpload[f]; if (useTus) { let progMeter = makeNewProgMeter(file.name); let metadata = { fileName: file.name, fileSize: file.size, fileType: detectFileType(file.name), genome: document.getElementById(`${file.name}#genomeInput`).selectedOptions[0].value, lastModified: file.lastModified, }; let tusOptions = { endpoint: tusdServer, metadata: metadata, onProgress: onProgress.bind(progMeter), onBeforeRequest: onBeforeRequest, onSuccess: onSuccess.bind(null, file, metadata), onError: onError.bind(null, metadata), retryDelays: null, }; // TODO: get the uploadUrl from the tusd server // use a pre-create hook to validate the user // and get an uploadUrl let tusUpload = new tus.Upload(file, tusOptions); uiState.pendingQueue.push([tusUpload, file]); tusUpload.start(); } else { // make a new XMLHttpRequest for each file, if tusd-tusclient not supported new sendFile(file); } } addCancelAllButton(); return; } */ /* function clearPickedFiles() { while (uiState.pickedList.firstChild) { uiState.pickedList.removeChild(uiState.pickedList.firstChild); } //uiState.input = createInput(); //uiState.toUpload = {}; } */ function defaultFileType(file) { return detectFileType(file); } function defaultDb() { return cartDb.split(" ").slice(-1)[0]; } function makeGenomeSelectOptions() { // Returns an array of options for genomes let ret = []; let choices = ["Human hg38", "Human T2T", "Human hg19", "Mouse mm39", "Mouse mm10"]; let cartChoice = {}; cartChoice.id = cartDb; cartChoice.label = cartDb; cartChoice.value = cartDb.split(" ").slice(-1)[0]; if (cartChoice.value.startsWith("hub_")) { cartChoice.label = cartDb.split(" ").slice(0,-1).join(" "); // take off the actual db value } cartChoice.selected = true; ret.push(cartChoice); choices.forEach( (e) => { if (e === cartDb) {return;} // don't print the cart database twice let choice = {}; choice.id = e; choice.label = e; choice.value = e.split(" ")[1]; ret.push(choice); }); return ret; } function makeTypeSelectOptions() { let ret = []; let autoChoice = {}; autoChoice.label = "Auto-detect from extension"; autoChoice.value = "Auto-detect from extension"; autoChoice.selected = true; ret.push(autoChoice); let choices = ["bigBed", "bam", "vcf", "vcf (bgzip or gzip compressed)", "bigWig", "hic", "cram", "bigBarChart", "bigGenePred", "bigMaf", "bigInteract", "bigPsl", "bigChain"]; choices.forEach( (e) => { let choice = {}; choice.id = e; choice.label = e; choice.value = e; ret.push(choice); }); return ret; } function createTypeAndDbDropdown(fileName) { typeInp = makeTypeSelect(fileName); genomeInp = makeGenomeSelectOptions(fileName); return [typeInp, genomeInp]; } /* function deletePickedFile(eventInst) { // called when the trash icon has been clicked to remove a file // the sibling text content is the file name, which we use to // find all the other elements to delete let trashIconDiv = eventInst.currentTarget; fname = trashIconDiv.nextSibling.textContent; document.querySelectorAll("[id^='" + fname + "']").forEach( (sib) => { sib.remove(); }); trashIconDiv.remove(); delete uiState.toUpload[fname]; // if there are no file rows left (1 row for the header) in the picker hide the headers: let container = document.getElementById("fileList"); if (getComputedStyle(container).getPropertyValue("grid-template-rows").split(" ").length === 1) { let headerEle = document.querySelectorAll(".fileListHeader"); headerEle.forEach( (header) => { if (header.style.display !== "none") { header.style.display = "none"; } }); } // if there is only one file picked hide the batch change selects, we may or may not have // hidden the headers yet, so we should have 3 or less rows if (getComputedStyle(container).getPropertyValue("grid-template-rows").split(" ").length <= 3) { let batchChange = document.querySelectorAll("[id^=batchChangeSelect]"); batchChange.forEach( (select) => { select.remove(); }); } } */ function listPickedFiles() { // displays the users chosen files in a grid: if (uiState.input.files.length === 0) { console.log("not input"); return; } else { let displayList = document.getElementById("fileList"); let deleteEle = document.createElement("div"); deleteEle.classList.add("deleteFileIcon"); deleteEle.innerHTML = ""; for (let file of uiState.input.files ) { if (file.name in uiState.toUpload) { continue; } // create a list for the user to see let nameCell = document.createElement("div"); nameCell.classList.add("pickedFile"); nameCell.id = `${file.name}#fileName`; nameCell.textContent = `${file.name}`; // Add the form controls for this file: let [typeCell, dbCell] = createTypeAndDbDropdown(file.name); let sizeCell = document.createElement("div"); sizeCell.classList.add("pickedFile"); sizeCell.id = `${file.name}#fileSize`; sizeCell.textContent = prettyFileSize(file.size); newDelIcon = deleteEle.cloneNode(true); newDelIcon.addEventListener("click", deletePickedFile); displayList.appendChild(newDelIcon); displayList.appendChild(nameCell); displayList.appendChild(typeCell); displayList.appendChild(dbCell); displayList.appendChild(sizeCell); // finally add it for us uiState.toUpload[file.name] = file; } let headerEle = document.querySelectorAll(".fileListHeader"); headerEle.forEach( (header) => { if (header.style.display !== "block") { header.style.display = "block"; } }); if (Object.keys(uiState.toUpload).length > 1) { // put up inputs to batch change all file inputs and dbs let batchType = document.createElement("div"); batchType = makeTypeSelect("batchChangeSelectType"); let batchDb = document.createElement("div"); batchDb = makeGenomeSelectOptions("batchChangeSelectDb"); // place into the grid in the right spot: batchType.classList.add('batchTypeSelect'); batchDb.classList.add('batchDbSelect'); // update each files select on change batchType.addEventListener("change", function(e) { let newVal = e.currentTarget.selectedOptions[0].value; document.querySelectorAll("[id$=typeInput]").forEach( (i) => { if (i === e.currentTarget) { return; } i.value = newVal; }); }); batchDb.addEventListener("change", function(e) { let newVal = e.currentTarget.selectedOptions[0].value; document.querySelectorAll("[id$=genomeInput]").forEach( (i) => { if (i === e.currentTarget) { return; } i.value = newVal; }); }); // append to the document displayList.appendChild(batchType); displayList.appendChild(batchDb); } document.querySelectorAll(".deleteFileIcon").forEach( (i) => { }); } // always clear the input element uiState.input = createInput(); } 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 deleteFileFromTable(fname) { // req is an object with properties of an uploaded file, make a new row // for it in the filesTable let table = $("#filesTable").DataTable(); let row = table.row((idx, data) => data.fileName === fname); row.remove().draw(); } function deleteFile(fname, fileType, hubNameList) { // Send an async request to hgHubConnect to delete the file // Note that repeated requests, like from a bot, will return 404 as a correct response console.log(`sending delete req for ${fname}`); cart.setCgi("hgHubConnect"); // a little complex, but the format is: // {commandToCgi: {arg: val, ...} // but we make val an object as well, becoming: // {commandToCgi: {fileList: [{propertyName1: propertyVal1, ...}, {propertName2: ...}]}} cart.send({deleteFile: {fileList: [{fileName: fname, fileType: fileType, hubNameList: hubNameList}]}}); cart.flush(); deleteFileFromTable(fname); } function deleteFileList() { } 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(); } function viewInGenomeBrowser(fname, ftype, genome, hubName) { // redirect to hgTracks with this track open in the hub if (typeof uiState.userUrl !== "undefined" && uiState.userUrl.length > 0) { if (ftype in extensionMap) { // TODO: tusd should return this location in it's response after // uploading a file and then we can look it up somehow, the cgi can // write the links directly into the html directly for prev uploaded files maybe? let url = "../cgi-bin/hgTracks?hgsid=" + getHgsid() + "&db=" + genome + "&hubUrl=" + uiState.userUrl + hubName + "/hub.txt"; window.location.assign(url); return false; } } } function addNewUploadedFileToTable(req) { // req is an object with properties of an uploaded file, make a new row // for it in the filesTable let table = null; if ($.fn.dataTable.isDataTable("#filesTable")) { table = $("#filesTable").DataTable(); let newRow = table.row.add(req).order([8, 'asc']).draw().node(); $(newRow).css('color','red').animate({color: 'black'}, 1000); } else { showExistingFiles([req]); } } function createHubSuccess(jqXhr, textStatus) { console.log(jqXhr); $("#newTrackHubDialog").dialog("close"); addNewUploadedFileToTable({ createTime: jqXhr.creationTime, fileType: "hub", fileName: jqXhr.hubName, genome: jqXhr.db, fileSize: null, hub: jqXhr.hubName }); } function createHub(db, hubName) { // send a request to hgHubConnect to create a hub for this user cart.setCgi("hgHubConnect"); cart.send({createHub: {db: db, name: hubName}}, createHubSuccess, null); cart.flush(); } /* function startUploadDialog() { // put up a dialog to walk a user through uploading data files and setting up a track hub console.log("create a hub button clicked!"); hubUploadButtons = { "Start": function() { submitPickedFiles(); let currBtns = $("#filePickerModal").dialog("option", "buttons"); // add a cancel button to stop current uploads if (!("Cancel" in currBtns)) { currBtns.Cancel = function() { clearPickedFiles(); $(this).dialog("close"); }; $("#filePickerModal").dialog("option", "buttons", currBtns); } }, "Cancel": function() { clearPickedFiles(); $(this).dialog("close"); }, "Close": function() { // delete everything that isn't the headers, which we set to hide: let fileList = document.getElementById("fileList"); let headers = document.querySelectorAll(".fileListHeader"); fileList.replaceChildren(...headers); headers.forEach( (header) => { header.style.display = "none"; }); uiState.input = createInput(); $(this).dialog("close"); } }; $("#filePickerModal").dialog({ modal: true, //buttons: hubUploadButtons, minWidth: $("#filePickerModal").width(), width: (window.innerWidth * 0.8), height: (window.innerHeight * 0.55), title: "Upload track data", open: function(e, ui) { $(e.target).parent().css("position", "fixed"); $(e.target).parent().css("top", "10%"); }, }); $("#filePickerModal").dialog("open"); } */ let tableInitOptions = { layout: { topStart: { buttons: [ { text: 'Upload', action: function() {return;}, className: 'uploadButton', enabled: false, // disable by default in case user is not logged in }, ] } }, columnDefs: [ { orderable: false, targets: 0, title: "", render: function(data, type, row) { return ""; } }, { orderable: false, targets: 1, data: "action", title: "Action", render: function(data, type, row) { /* Return a node for rendering the actions column */ // all of our actions will be buttons in this div: let container = document.createElement("div"); // click to call hgHubDelete file let delBtn = document.createElement("button"); delBtn.textContent = "Delete"; delBtn.type = 'button'; delBtn.addEventListener("click", function() { deleteFile(row.fileName, row.fileType, row.hub); }); // click to view hub/file in gb: let viewBtn = document.createElement("button"); viewBtn.textContent = "View in Genome Browser"; viewBtn.type = 'button'; viewBtn.addEventListener("click", function() { viewInGenomeBrowser(row.fileName, row.fileType, row.genome, row.hub); }); // click to rename file or hub: let renameBtn = document.createElement("button"); renameBtn.textContent = "Rename"; renameBtn.type = 'button'; renameBtn.addEventListener("click", function() { console.log("rename btn clicked!"); }); // click to associate this track to a hub let addToHubBtn = document.createElement("button"); addToHubBtn.textContent = "Add to hub"; addToHubBtn.type = 'button'; addToHubBtn.addEventListener("click", function() { addFileToHub(row); }); container.appendChild(delBtn); container.appendChild(viewBtn); container.appendChild(renameBtn); container.appendChild(addToHubBtn); return container; } }, { targets: 3, render: function(data, type, row) { return dataTablePrintSize(data); } }, { targets: 5, render: function(data, type, row) { return dataTablePrintGenome(data); } }, { // The upload time column, not visible but we use it to sort on new uploads targets: 8, visible: false, searchable: false } ], columns: [ {data: "", }, {data: "", }, {data: "fileName", title: "File name"}, {data: "fileSize", title: "File size", render: dataTablePrintSize}, {data: "fileType", title: "File type"}, {data: "genome", title: "Genome", render: dataTablePrintGenome}, {data: "hub", title: "Hubs"}, {data: "lastModified", title: "File Last Modified"}, {data: "uploadTime", title: "Upload Time"}, ], order: [[6, 'desc']], drawCallback: function(settings) { if (isLoggedIn) { settings.api.buttons(0).enable(); } } }; 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) { 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"}; } let table = new DataTable("#filesTable", tableInitOptions); } function showExistingHubs(d) { // Add the hubs to the files table if (!d) {return;} let table = $("#filesTable").DataTable(); d.forEach((hub) => { let hubName = hub.hubName; let db = hub.genome; let data = { fileName: hubName, fileSize: null, fileType: "hub", genome: db, hub: hubName, createTime: null, }; table.row.add(data).draw(); }); } function checkJsonData(jsonData, callerName) { // Return true if jsonData isn't empty and doesn't contain an error; // otherwise complain on behalf of caller. if (! jsonData) { alert(callerName + ': empty response from server'); } else if (jsonData.error) { console.error(jsonData.error); alert(callerName + ': error from server: ' + jsonData.error); } else if (jsonData.warning) { alert("Warning: " + jsonData.warning); return true; } else { if (debugCartJson) { console.log('from server:\n', jsonData); } return true; } return false; } function updateStateAndPage(jsonData, doSaveHistory) { // Update uiState with new values and update the page. _.assign(uiState, jsonData); } function handleRefreshState(jsonData) { if (checkJsonData(jsonData, 'handleRefreshState')) { updateStateAndPage(jsonData, true); } $("#spinner").remove(); } function init() { cart.setCgi('hgMyData'); cart.debug(debugCartJson); if (!useTus) { console.log("tus is not supported, falling back to XMLHttpRequest"); } let pickedFiles = document.getElementById("fileList"); let inputBtn = document.getElementById("btnForInput"); if (pickedFiles !== null) { // this element should be an empty div upon loading the page uiState.pickedList = pickedFiles; if (pickedFiles.children.length === 0) { let para = document.createElement("p"); para.textContent = "No files chosen yet"; para.classList.add("noFiles"); pickedFiles.parentNode.appendChild(para); } } else { // TODO: graceful handle of leaving the page and coming back? } /* let parent = document.getElementById("chooseAndSendFilesRow"); let input = createInput(); uiState.input = input; inputBtn.parentNode.appendChild(input); */ if (typeof cartJson !== "undefined") { if (typeof cartJson.warning !== "undefined") { alert("Warning: " + cartJson.warning); } var urlParts = {}; if (debugCartJson) { console.log('from server:\n', cartJson); } _.assign(uiState,cartJson); saveHistory(cartJson, urlParts, true); } else { // no cartJson object means we are coming to the page for the first time: //cart.send({ getUiState: {} }, handleRefreshState); //cart.flush(); // TODO: initialize buttons, check if there are already files // TODO: write functions for // after picking files // choosing file types // creating default trackDbs // editing trackDbs let fileDiv = document.getElementById('filesDiv'); if (typeof userFiles !== 'undefined' && Object.keys(userFiles).length > 0) { uiState.fileList = userFiles.fileList; uiState.hubList = userFiles.hubList; uiState.userUrl = userFiles.userUrl; } showExistingFiles(uiState.fileList.filter((row) => row.fileType !== "hub")); //inputBtn.addEventListener("click", (e) => uiState.input.click()); // TODO: add event handlers for editing defaults, grouping into hub // TODO: display quota somewhere } $("#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 { 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; option.label = opt.label; option.id = opt.id; option.selected = typeof opt.selected !== 'undefined' ? opt.selected : false; select.appendChild(option); }); } addSelectsForFile(file) { /* create two selects for the file object, to include the db and type */ const id = "uppy_" + file.id; let fileDiv = document.getElementById(id); // this might not exist yet depending on where we are in the render cycle if (fileDiv) { let dbSelectId = "db_select_" + file.id; if (!document.getElementById(dbSelectId)) { let dbSelect = document.createElement("select"); dbSelect.id = dbSelectId; let dbOpts = makeGenomeSelectOptions(); this.createOptsForSelect(dbSelect, dbOpts); fileDiv.appendChild(dbSelect); } let typeSelectId = "type_select_" + file.id; if (!document.getElementById(typeSelectId)) { let typeSelect = document.createElement("select"); typeSelect.id = typeSelectId; let typeOpts = makeTypeSelectOptions(); this.createOptsForSelect(typeSelect, typeOpts); fileDiv.appendChild(typeSelect); } } } removeBatchSelectsFromDashboard() { let batchSelectDiv = document.getElementById("batch-selector-div"); if (batchSelectDiv) { batchSelectDiv.remove(); } } addBatchSelectsToDashboard() { if (!document.getElementById("batch-selector-div")) { let batchSelectDiv = document.createElement("div"); batchSelectDiv.id = "batch-selector-div"; let batchDbSelect = document.createElement("select"); let batchTypeSelect = document.createElement("select"); this.createOptsForSelect(batchDbSelect, [{id: "batchChangeDb", name: "batchChangeDb"}]); this.createOptsForSelect(batchTypeSelect, [{id: "batchChangeType", name: "batchChangeType"}]); batchSelectDiv.textContent = "Change options for all files"; batchSelectDiv.appendChild(batchDbSelect); batchSelectDiv.appendChild(batchTypeSelect); batchSelectDiv.style.display = "flex"; batchSelectDiv.style.justifyContent = "center"; let titleBarText = document.querySelector(".uppy-DashboardContent-title"); if (titleBarText) { batchSelectDiv.style.color = getComputedStyle(titleBarText).color; } // 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": defaultFileType(file.name)}); + this.uppy.setFileMeta(file.id, {"genome": defaultDb(), "fileType": defaultFileType(file.name), "hubName": hubNameDefault}); if (this.uppy.getFiles().length > 1) { this.addBatchSelectsToDashboard(); } }); this.uppy.on("file-removed", (file) => { // remove the batch change selects if now <2 files present if (this.uppy.getFiles().length < 2) { this.removeBatchSelectsFromDashboard(); } }); this.uppy.on("dashboard:modal-open", () => { // check if there were already files chosen from before: if (this.uppy.getFiles().length > 2) { this.addBatchSelectsToDashboard(); } if (this.uppy.getFiles().length < 2) { this.removeBatchSelectsFromDashboard(); } }); this.uppy.on("dashboard:modal-close", () => { if (this.uppy.getFiles().length < 2) { this.removeBatchSelectsFromDashboard(); } }); } 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", meta: {"genome": null, "fileType": null}, metaFields: (file) => { const fields = [{id: 'name', name: 'File name'}]; fields.push({ id: 'genome', name: 'Genome', render: ({value, onChange}, h) => { return h('select', {onChange: e => onChange(e.target.value)}, makeGenomeSelectOptions().map( (genomeObj) => { return h('option', genomeObj, genomeObj.label); }) ); }, }); fields.push({ id: 'fileType', name: 'File Type', render: ({value, onChange}, h) => { return h( 'select', {onChange: e => { if (e.target.value === "Auto-detect from extension") { onChange(detectFileType(file.name)); } else { onChange(e.target.value); } }}, makeTypeSelectOptions().map( (typeObj) => { return h('option', typeObj, typeObj.label); }) ); }, }); + fields.push({ + id: 'hubName', + name: 'Hub Name', + render: ({value, onChange, required, form}, h) => { + return h('input', + {type: 'text', + required: required, + form: form, + value: hubNameDefault, + } + ); + }, + }); return fields; }, restricted: {requiredMetaFields: ["genome", "fileType"]}, closeModalOnClickOutside: true, closeAfterFinish: true, theme: 'auto', autoOpen: "metaEditor", }; let tusOptions = { endpoint: getTusdEndpoint(), withCredentials: true, retryDelays: null, }; uppy.use(Uppy.Dashboard, uppyOptions); uppy.use(Uppy.Tus, tusOptions); uppy.use(BatchChangePlugin, {target: Uppy.Dashboard}); uppy.on('upload-success', (file, response) => { const metadata = file.meta; const d = new Date(metadata.lastModified); newReqObj = { "uploadTime": Date.now(), "lastModified": d.toJSON(), "fileName": metadata.fileName, "fileSize": metadata.fileSize, "fileType": metadata.fileType, "genome": metadata.genome, - "hub": "" + "hub": metadata.hubName, }; addNewUploadedFileToTable(newReqObj); }); } return { init: init, uiState: uiState, }; }()); // when a user reaches this page from the back button we can display our saved state // instead of sending another network request window.onpopstate = function(event) { event.preventDefault(); hubCreate.updateStateAndPage(event.state, false); };