7b7bb68187bb42ead29a9a3b26a950205820c9ae chmalee Wed Nov 20 15:54:48 2024 -0800 Make up to 25 rows show by default, make the table scroll down if it's more than 600 pixels, make the table collapse when there are less than 600 pixels height needed. Remove the 'Action' title, remove the delete button from each row, make the checkboxes select files, on checkbox select populate an info section about how many files have been selected and give the option to view or delete them Start of reworking the delete button to delete a list of files. The backend still needs to be smarter about creating the locations of files Put some place holder text where the selected file information would appear so the page doesn't jump around Start of work on back end for removing files. Changed the json returned for listing the files to list the full path to the file to make the requests to delete a file easier. Still need to decide on encoding vs decoding of the parent dirs during uploads diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js index 65f7a7c..46b6a8d 100644 --- src/hg/js/hgMyData.js +++ src/hg/js/hgMyData.js @@ -127,90 +127,219 @@ 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 dataTablePrintSize(data, type, row, meta) { - return prettyFileSize(data); + 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 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(); + // helper object so we don't need to use an AbortController to update + // the data this function is using + let selectedData = {}; + function viewAllInGenomeBrowser(ev) { + // redirect to hgTracks with these tracks/hubs open + let data = selectedData; + if (typeof uiState.userUrl !== "undefined" && uiState.userUrl.length > 0) { + let url = "../cgi-bin/hgTracks?hgsid=" + getHgsid(); + let genome; // may be multiple genomes in list, just redirect to the first one + // TODO: this should probably raise an alert to click through + let hubsAdded = {}; + _.forEach(data, (d) => { + if (!genome) { + genome = d.genome; + url += "&db=" + genome; + } + if (d.fileType 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? + if (!(d.parentDir in hubsAdded)) { + // NOTE: hubUrls get added regardless of whether they are on this assembly + // or not, because multiple genomes may have been requested. If this user + // switches to another genome we want this hub to be connected already + url += "&hubUrl=" + uiState.userUrl + d.parentDir + "hub.txt"; + } + hubsAdded[d.parentDir] = true; + if (d.genome == genome) { + // turn the track on if its for this db + url += "&" + d.fileName + "=pack"; + } + } + }); + window.location.assign(url); + return false; + } } - function deleteFile(fname, fileType, hubNameList) { + /* + function deleteFile(fname, fileType, parentDir, db) { // 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, ...} + // {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.send({ + deleteFile: { + fileList: [ + { + fileName: fname, + fileType: fileType, + parentDir: parentDir, + db: db, + } + ] + } + }); cart.flush(); deleteFileFromTable(fname); } + */ + + function deleteFileList(ev) { + // same as deleteFile() but acts on the selectedData variable + let data = selectedData; + let cartData = {deleteFile: {fileList: []}}; + cart.setCgi("hgHubConnect"); + _.forEach(data, (d) => { + cartData.deleteFile.fileList.push({ + fileName: d.fileName, + fileType: d.fileType, + parentDir: d.parentDir, + genome: d.genome, + fullPath: d.fullPath, + }); + }); + cart.send(cartData); + cart.flush(); + + } + + function updateSelectedFileDiv(data) { + // update the div that shows how many files are selected + let numSelected = Object.entries(data).length; + let infoDiv = document.getElementById("selectedFileInfo"); + let span = document.getElementById("numberSelectedFiles"); + let spanParentDiv = span.parentElement; + span.textContent = `${numSelected} ${numSelected > 1 ? "files" : "file"}`; + if (numSelected > 0) { + // (re) set up the handlers for the selected file info div: + let viewBtn = document.getElementById("viewSelectedFiles"); + selectedData= data; + viewBtn.addEventListener("click", viewAllInGenomeBrowser); + viewBtn.textContent = numSelected === 1 ? "View selected file in Genome Browser" : "View all selected files in Genome Browser"; + let deleteBtn = document.getElementById("deleteSelectedFiles"); + deleteBtn.addEventListener("click", deleteFileList); + deleteBtn.textContent = numSelected === 1 ? "Delete selected file" : "Delete selected files"; + + } + + // set the visibility of the placeholder text and info text + spanParentDiv.style.display = numSelected === 0 ? "none": "block"; + let placeholder = document.getElementById("placeHolderInfo"); + placeholder.style.display = numSelected === 0 ? "block" : "none"; + } + + function handleCheckboxSelect(ev) { + let checkbox = ev.target; + let table = $("#filesTable").DataTable(); + + // depending on the state of the checkbox, we will be adding information + // to the div, or removing information. We will also be potentially checking/unchecking + // all of the checkboxes if the selectAll box was clicked. The data variable + // will hold all the information we want to keep visible in the info div + let data = {}; + + // first check if we selected all or not + if (checkbox.classList.contains("selectAll")) { + // we can now turn off/on all the checkboxes + let state = checkbox.checked; + if (state) { + document.querySelectorAll(".filesTableCheckbox").forEach( (inp) => { + inp.checked = true; + }); + } else { + document.querySelectorAll(".filesTableCheckbox").forEach( (inp) => { + inp.checked = false; + }); + } + } + // get all of the currently selected rows (may be more than just the one that + // was most recently clicked) + let selected = document.querySelectorAll(".filesTableCheckbox:checked"); + selected.forEach((inp, ix) => { + if (inp.classList.contains("selectAll")) { + return; + } + data[ix] = table.row(inp.closest("tr")).data(); + }); + updateSelectedFileDiv(data); + } + + function dataTablePrintSize(data, type, row, meta) { + return prettyFileSize(data); + } - function deleteFileList() { + 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 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) { @@ -222,76 +351,75 @@ 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(); } let tableInitOptions = { + pageLength: 25, + scrollY: 600, + scrollCollapse: true, // when less than scrollY height is needed, make the table shorter layout: { topStart: { buttons: [ { text: 'Upload', action: function() {return;}, className: 'uploadButton', enabled: false, // disable by default in case user is not logged in }, ], quota: null, } }, columnDefs: [ { orderable: false, targets: 0, - title: "<input type=\"checkbox\"></input>", + // have to add the event handler for the selectAll later + title: "<input id=\"filesTableSelectAll\" class=\"selectAll filesTableCheckbox\" type=\"checkbox\"></input>", render: function(data, type, row) { - return "<input type=\"checkbox\"></input>"; + let ret = document.createElement("input"); + ret.classList.add("filesTableCheckbox"); + ret.type = "checkbox"; + ret.addEventListener("click", handleCheckboxSelect); + return ret; } }, { orderable: false, targets: 1, - data: "action", title: "Action", + data: "action", title: "", 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.parentDir); - }); - // 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.parentDir); }); - container.appendChild(delBtn); container.appendChild(viewBtn); return container; } }, { targets: 3, render: function(data, type, row) { return dataTablePrintSize(data); } }, { targets: 5, render: function(data, type, row) { return dataTablePrintGenome(data); @@ -431,30 +559,32 @@ //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")); + // initialize the selectAll handler here, because above we defined it as a string + document.getElementById("filesTableSelectAll").addEventListener("click", handleCheckboxSelect); // 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);