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(),