4157b5a16cd39c1f6af20dd321cb69e113574993 chmalee Tue Apr 8 16:55:52 2025 -0700 Make the uppy window larger, add a search bar to the batch select options. TODO: make the batch change options present a plain option if there are differing metadata for each file, refs #31058 diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js index bbe536997e6..d390af65b91 100644 --- src/hg/js/hgMyData.js +++ src/hg/js/hgMyData.js @@ -48,34 +48,38 @@ let d = { "genome": key, "label": `${val.commonName} (${key})`, }; if (val.hubUrl !== null) { d.category = "UCSC GenArk - bulk annotated assemblies from NCBI GenBank / Refseq"; } else { d.category = "UCSC Genome Browser assemblies - annotation tracks curated by UCSC"; } data.push(d); } }); return data; } +let autocompletes = {}; function initAutocompleteForInput(inpIdStr, selectEle) { - // we must set this up for each input created, once per file chosen + // we must set up the autocompleteCat for each input created, once per file chosen // override the autocompleteCat.js _renderMenu to get the menu on top - // of the uppy widget + // of the uppy widget. + // Return true if we actually set up the autocomplete, false if we have already + // set it up previously + if ( !(inpIdStr in autocompletes) || autocompletes[inpIdStr] === false) { $.widget("custom.autocompleteCat", $.ui.autocomplete, { _renderMenu: function(ul, items) { var that = this; var currentCategory = ""; // There's no this._super as shown in the doc, so I can't override // _create as shown in the doc -- just do this every time we render... this.widget().menu("option", "items", "> :not(.ui-autocomplete-category)"); $(ul).css("z-index", "99999999"); $.each(items, function(index, item) { // Add a heading each time we see a new category: if (item.category && item.category !== currentCategory) { ul.append("
  • " + @@ -93,30 +97,34 @@ // so use the value instead return $("
  • ") .data("ui-autocomplete-item", item) .append($("").html((item.label !== null ? item.label : item.value))) .appendTo(ul); } }); let selectFunction = setDbSelectFromAutocomplete.bind(null, selectEle); autocompleteCat.init($("[id='"+inpIdStr+"']"), { baseUrl: "hubApi/findGenome?browser=mustExist&q=", //watermark: "Enter species name, common name, etc", onSelect: selectFunction, onServerReply: processFindGenome, enterSelectsIdentical: false }); + autocompletes[inpIdStr] = true; + return true; + } + return false; } function generateApiKey() { let apiKeyInstr = document.getElementById("apiKeyInstructions"); let apiKeyDiv = document.getElementById("apiKey"); if (!document.getElementById("spinner")) { let spinner = document.createElement("i"); spinner.id = "spinner"; spinner.classList.add("fa", "fa-spinner", "fa-spin"); document.getElementById("generateApiKey").after(spinner); } let handleSuccess = function(reqObj) { apiKeyDiv.textContent = reqObj.apiKey; @@ -255,51 +263,58 @@ 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`); return null; } function defaultDb() { return cartDb.split(" ").slice(-1)[0]; } - function makeGenomeSelectOptions() { - // Returns an array of options for genomes + let defaultGenomeChoices = { + "Human hg38": {value: "hg38", label: "Human hg38"}, + "Human T2T": {value: "T2T", label: "Human T2T"}, + "Human hg19": {value: "hg19", label: "Human hg19"}, + "Mouse mm39": {value: "mm39", label: "Mouse mm39"}, + "Mouse mm10": {value: "mm10", label: "Mouse mm10"} + }; + + function makeGenomeSelectOptions(value, label) { + // Returns an array of options for genomes, if value and label exist, add that + // as an additional option 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); - }); + cartChoice.selected = value && label ? false: true; + defaultGenomeChoices[cartChoice.label] = cartChoice; + + // next time around our value/label pair will be a default. this time around we + // want it selected because it was explicitly asked for, but it may not be next time + ret = Object.values(defaultGenomeChoices); + if (value && label && !(label in defaultGenomeChoices)) { + defaultGenomeChoices[label] = {value: value, label: label, selected: true}; + } 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; @@ -967,31 +982,30 @@ table.draw(); } else { if (row.selected()) { row.deselect(); doRowSelect("deselect", table, row.index()); } else { row.select(); doRowSelect("select", table, row.index()); } } } }); return table; } - let autocompleteInitComplete = false; function init() { cart.setCgi('hgMyData'); cart.debug(debugCartJson); // TODO: write functions for // creating default trackDbs // editing trackDbs // get the state from the history stack if it exists if (typeof uiData !== 'undefined' && typeof uiState.userFiles !== 'undefined') { _.assign(uiState, uiData.userFiles); if (uiState.fileList) { parseFileListIntoHash(uiState.fileList); } } // first add the top level directories/files let table = showExistingFiles(uiState.fileList); @@ -1044,96 +1058,133 @@ 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"; batchSelectDiv.style.display = "grid"; batchSelectDiv.style.width = "80%"; // the grid syntax is 2 columns, 3 rows - batchSelectDiv.style.gridTemplateColumns = "50% 50%"; - batchSelectDiv.style.gridTemplateRows = "25px 25px 25px"; + batchSelectDiv.style.gridTemplateColumns = "max-content minmax(0, 200px) max-content 1fr min-content"; + batchSelectDiv.style.gridTemplateRows = "repest(3, auto)"; batchSelectDiv.style.margin = "10px auto"; // centers this div + batchSelectDiv.style.fontSize = "14px"; + batchSelectDiv.style.gap = "8px"; if (window.matchMedia("(prefers-color-scheme: dark)").matches) { batchSelectDiv.style.color = "#eaeaea"; } // first just explanatory text: let batchSelectText = document.createElement("div"); batchSelectText.textContent = "Change options for all files:"; // syntax here is rowStart / columnStart / rowEnd / columnEnd batchSelectText.style.gridArea = "1 / 1 / 1 / 2"; // 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"; + batchDbSelect.style.margin = "2px"; let batchDbLabel = document.createElement("label"); batchDbLabel.textContent = "Genome"; batchDbLabel.for = "batchDbSelect"; batchDbLabel.style.gridArea = "2 / 1 / 2 / 1"; + // the search bar for db selection + let batchDbSearchBarLabel= document.createElement("label"); + batchDbSearchBarLabel.textContent = "or search for your genome:"; + batchDbSearchBarLabel.style.gridArea = "2 / 3 /2 / 3"; + batchDbSearchBarLabel.style.margin = "auto"; + + let batchDbGenomeSearchBar = document.createElement("input"); + batchDbGenomeSearchBar.classList.add("uppy-u-reset", "uppy-c-textInput"); + batchDbGenomeSearchBar.type = "text"; + batchDbGenomeSearchBar.id = "batchDbSearchBar"; + batchDbGenomeSearchBar.style.gridArea = "2 / 4 / 2 / 4"; + let batchDbGenomeSearchButton = document.createElement("input"); + batchDbGenomeSearchButton.type = "button"; + batchDbGenomeSearchButton.value = "search"; + batchDbGenomeSearchButton.id = "batchDbSearchBarButton"; + batchDbGenomeSearchButton.style.gridArea = "2 / 5 / 2 / 5"; + // the batch change hub name + let batchParentDirLabel = document.createElement("label"); + batchParentDirLabel.textContent = "Hub Name"; + batchParentDirLabel.for = "batchParentDir"; + batchParentDirLabel.style.gridArea = "3 / 1 / 3 / 1"; + let batchParentDirInput = document.createElement("input"); batchParentDirInput.id = "batchParentDir"; 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"; + batchParentDirInput.classList.add("uppy-u-reset", "uppy-c-textInput"); // 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}); + this.uppy.setFileMeta(file.id, {genomeLabel: ev.target.selectedOptions[0].label}); } }); batchParentDirInput.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, {parentDir: val}); } }); + batchSelectDiv.appendChild(batchSelectText); batchSelectDiv.appendChild(batchDbLabel); batchSelectDiv.appendChild(batchDbSelect); + batchSelectDiv.appendChild(batchDbSearchBarLabel); + batchSelectDiv.appendChild(batchDbGenomeSearchBar); + batchSelectDiv.appendChild(batchDbGenomeSearchButton); batchSelectDiv.appendChild(batchParentDirLabel); batchSelectDiv.appendChild(batchParentDirInput); // 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); } + + // everything has to exist already for autocompleteCat to initialize + let justInitted = initAutocompleteForInput(batchDbGenomeSearchBar.id, batchDbSelect); + if (justInitted) { + // only do this once per batch setup + batchDbGenomeSearchButton.addEventListener("click", (e) => { + let inp = document.getElementById(batchDbSearchBar.id).value; + let selector = "[id='"+batchDbGenomeSearchBar.id+"']"; + $(selector).autocompleteCat("search", inp); + }); + } } } 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": 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); } @@ -1153,31 +1204,31 @@ if (this.uppy.getFiles().length < 2) { this.removeBatchSelectsFromDashboard(); } }); this.uppy.on("dashboard:modal-closed", () => { if (this.uppy.getFiles().length < 2) { 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(); } }); this.uppy.on("dashboard:file-edit-start", (file) => { - autocompleteInitComplete = false; + autocompletes[`${file.name}DbInput`] = false; }); this.uppy.on("dashboard:file-edit-complete", (file) => { // check the filename and hubname metadata and warn the user // to edit them if they are wrong. unfortunately I cannot // figure out how to force the file card to re-toggle // and jump back into the editor from here if (file) { let fileNameMatch = file.meta.name.match(fileNameRegex); let parentDirMatch = file.meta.parentDir.match(parentDirRegex); const dash = uppy.getPlugin("Dashboard"); 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', 5000); } if (!parentDirMatch || parentDirMatch[0] !== file.meta.parentDir) { @@ -1229,69 +1280,71 @@ // there are multiple rendering passes and only eventually // do the elements actually make it into the DOM let ret = h('div', { class: "uppy-Dashboard-FileCard-label", style: "display: inline-block; width: 78%" }, // first child of div "Select from popular assemblies:", // second div child h('select', { id: `${file.meta.name}DbSelect`, style: "margin-left: 5px", onChange: e => { onChange(e.target.value); file.meta.genome = e.target.value; + file.meta.genomeLabel = e.target.selectedOptions[0].label; } }, - makeGenomeSelectOptions().map( (genomeObj) => { + makeGenomeSelectOptions(file.meta.genome, file.meta.genomeLabel).map( (genomeObj) => { return h('option', { value: genomeObj.value, label: genomeObj.label, selected: file.meta.genome !== null ? genomeObj.value === file.meta.genome : genomeObj.value === defaultDb() }); }) ), h('p', { class: "uppy-Dashboard-FileCard-label", style: "display: block; width: 78%", }, "or search for your genome:"), // third div child h('input', { id: `${file.meta.name}DbInput`, type: 'text', class: "uppy-u-reset uppy-c-textInput uppy-Dashboard-FileCard-input", }), h('input', { id: `${file.meta.name}DbSearchButton`, type: 'button', value: 'search', style: "margin-left: 5px", }) ); let selectToChange = document.getElementById(`${file.meta.name}DbSelect`); - if (selectToChange && !autocompleteInitComplete ) { - initAutocompleteForInput(`${file.meta.name}DbInput`, selectToChange); + if (selectToChange) { + let justInitted = initAutocompleteForInput(`${file.meta.name}DbInput`, selectToChange); + if (justInitted) { // only do this once per file - autocompleteInitComplete = true; document.getElementById(`${file.meta.name}DbSearchButton`) .addEventListener("click", (e) => { let inp = document.getElementById(`${file.meta.name}DbInput`).value; let selector = `[id='${file.meta.name}DbInput']`; $(selector).autocompleteCat("search", inp); }); } + } return ret; } }, { id: 'parentDir', name: 'Hub Name', }]; return fields; }, doneButtonHandler: function() { uppy.clear(); }, }; let tusOptions = { endpoint: getTusdEndpoint(),