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("<li class='ui-autocomplete-category'>" +
@@ -93,30 +97,34 @@
                        // 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
         });
+        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(),