869e1a36ed23af200ec55157d1f2a2f2aea1ea19 chmalee Thu Apr 3 13:40:40 2025 -0700 Add search box to genome metadata field in hubSpace ui, allows searching for a genome like hgGateway, refs #31058 diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js index 48568f3b172..bbe536997e6 100644 --- src/hg/js/hgMyData.js +++ src/hg/js/hgMyData.js @@ -14,30 +14,111 @@ function cgiEncode(value) { // copy of cheapgi.c:cgiEncode except we are explicitly leaving '/' characters: let splitVal = value.split('/'); splitVal.forEach((ele, ix) => { splitVal[ix] = encodeURIComponent(ele); }); return splitVal.join('/'); } function cgiDecode(value) { // decode an encoded value return decodeURIComponent(value); } +function setDbSelectFromAutocomplete(selectEle, item) { + // this has been bound to the <select> we are going to add + // a new child option to + let newOpt = document.createElement("option"); + newOpt.value = item.genome; + newOpt.label = item.label; + newOpt.selected = true; + selectEle.appendChild(newOpt); + const event = new Event("change"); + selectEle.dispatchEvent(event); +} + +function processFindGenome(result, term) { + // process the hubApi/findGenome?q= result set into somthing + // jquery-ui autocomplete can use + let data = []; + let apiSkipList = new Set(["downloadTime", "downloadTimeStamp", "availableAssemblies", "browser", "elapsedTimeMs", "itemCount", "q", "totalMatchCount"]); + _.forEach(result, function(val, key) { + if (!(apiSkipList.has(key))) { + 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; +} + +function initAutocompleteForInput(inpIdStr, selectEle) { + // we must set this up for each input created, once per file chosen + // override the autocompleteCat.js _renderMenu to get the menu on top + // of the uppy widget + $.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("<li class='ui-autocomplete-category'>" + + item.category + "</li>" ); + currentCategory = item.category; + } + that._renderItem( ul, item ); + }); + }, + _renderItem: function(ul, item) { + // In order to use HTML markup in the autocomplete, one has to overwrite + // autocomplete's _renderItem method using .html instead of .text. + // http://forum.jquery.com/topic/using-html-in-autocomplete + // Hits to assembly hub top level (not individial db names) have no item label, + // so use the value instead + return $("<li></li>") + .data("ui-autocomplete-item", item) + .append($("<a></a>").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 + }); +} + 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; apiKeyInstr.style.display = "block"; let revokeDiv= document.getElementById("revokeDiv"); @@ -886,30 +967,31 @@ 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); @@ -1070,30 +1152,34 @@ } 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; + }); + 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) { uppy.info(`Error: Hub name has special characters, please rename hub: '${file.meta.parentDir}' to only include alpha-numeric characters, period, dash, underscore, or plus.`, 'error', 5000); } @@ -1110,75 +1196,108 @@ showProgressDetails: true, 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, + class: "uppy-u-reset uppy-c-textInput uppy-Dashboard-FileCard-input", onChange: e => { onChange(e.target.value); file.meta.fileType = detectFileType(e.target.value); + file.meta.name = e.target.value; }, required: required, form: form, } ); }, }, { id: 'genome', name: 'Genome', render: ({value, onChange}, h) => { - return h('select', { + // keep these as a variable so we can init the autocompleteCat + // code only after the elements have actually been rendered + // 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; } }, makeGenomeSelectOptions().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); + // 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', - render: ({value, onChange, required, form}, h) => { - return h('input', - {type: 'text', - value: value, - onChange: e => { - onChange(e.target.value); - }, - required: required, - form: form, - } - ); - }, }]; return fields; }, doneButtonHandler: function() { uppy.clear(); }, }; let tusOptions = { endpoint: getTusdEndpoint(), withCredentials: true, retryDelays: null, }; uppy.use(Uppy.Dashboard, uppyOptions); uppy.use(Uppy.Tus, tusOptions); uppy.use(BatchChangePlugin, {target: Uppy.Dashboard});