6627b1c33bdef1886f84fce4df14d0dd5c786657 chmalee Fri Mar 8 17:10:21 2024 -0800 Make the DataTable update dynamically. Correctly use a hidden file input to keep track of submitted and unsubmitted files diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js index 969c25a..9b47a96 100644 --- src/hg/js/hgMyData.js +++ src/hg/js/hgMyData.js @@ -2,33 +2,35 @@ debugCartJson = true; function prettyFileSize(num) { if (num < (1000 * 1024)) { return `${(num/1000).toFixed(1)}kb`; } else if (num < (1000 * 1000 * 1024)) { return `${((num/1000)/1000).toFixed(1)}mb`; } else { return `${(((num/1000)/1000)/1000).toFixed(1)}gb`; } } var hubCreate = (function() { let uiState = { // our object for keeping track of the current UI and what to do toUpload: {}, // set of file objects keyed by name - input: null, // the <input> element for picking files from the users machine + input: null, // the hidden input element pickedList: null, // the <div> for displaying files in toUpload - pendingQueue: [], // our queue of pending [xmlhttprequests, file], kind of like the toUpload object + pendingQueue: [], // our queue of pending [tus.Upload, file], kind of like the toUpload object + fileList: [], // the files this user has uploaded, initially populated by the server + // on page load, but gets updated as the user uploades/deletes files }; // We can use XMLHttpRequest if necessary or a mirror can't use tus var useTus = tus.isSupported && true; function getTusdEndpoint() { // return the port and basepath of the tusd server // NOTE: the port and basepath are specified in hg.conf //let currUrl = parseUrl(window.location.href); return "https://hgwdev-hubspace.gi.ucsc.edu/files"; } function togglePickStateMessage(showMsg = false) { if (showMsg) { let para = document.createElement("p"); @@ -46,38 +48,54 @@ function liForFile(file) { let liId = `${file.name}#li`; let li = document.getElementById(liId); return li; } function newButton(text) { /* Creates a new button with some text as the label */ let newBtn = document.createElement("label"); newBtn.classList.add("button"); newBtn.textContent = text; return newBtn; } + function createInput() { + /* Create a new input element for a file picker */ + let input = document.createElement("input"); + //input.style.opacity = 0; + //input.style.float = "right"; + input.multiple = true; + input.type = "file"; + input.id = "hiddenFileInput"; + input.style = "display: none"; + input.addEventListener("change", listPickedFiles); + return input; + } + function requestsPending() { /* Return true if requests are still pending, which means it needs to * have been sent(). aborted requests are considered done for this purpose */ for (let [req, f] of uiState.pendingQueue) { - if (req.readyState != XMLHttpRequest.DONE && req.readyState != XMLHttpRequest.UNSENT) { + if (req._req !== null) { + xreq = req._req._xhr; + if (xreq.readyState != XMLHttpRequest.DONE && xreq.readyState != XMLHttpRequest.UNSENT) { return true; } } + } return false; } function addCancelButton(file, req) { /* Add a button that cancels the request req */ let li = liForFile(file); let newBtn = newButton("Cancel upload"); newBtn.addEventListener("click", (e) => { req.abort(); li.removeChild(newBtn); // TODO: make this remove the cancel all button if it's the last pending // request stillPending = requestsPending(); if (!stillPending) { let btnRow = document.getElementById("chooseAndSendFilesRow"); @@ -89,34 +107,36 @@ } function removeCancelAllButton() { let btnRow = document.getElementById("chooseAndSendFilesRow"); if (btnRow.lastChild.textContent === "Cancel all") { btnRow.removeChild(btnRow.lastChild); } togglePickStateMessage(true); } function addCancelAllButton() { let btnRow = document.getElementById("chooseAndSendFilesRow"); let newBtn = newButton("Cancel all"); newBtn.addEventListener("click", (e) => { while (uiState.pendingQueue.length > 0) { - let [q, f] = uiState.pendingQueue.pop(); + let [req, f] = uiState.pendingQueue.pop(); // we only need to abort requests that haven't finished yet - if (q.readyState != XMLHttpRequest.DONE) { - q.abort(); + if (req._req !== null) { + if (_req._xhr.readyState != XMLHttpRequest.DONE) { + req.abort(true); + } } let li = liForFile(f); if (li !== null) { // the xhr event handlers should handle this but just in case li.removeChild(li.lastChild); } } }); btnRow.appendChild(newBtn); } function makeNewProgMeter(fileName) { // create a progress meter for this filename const progMeterWidth = 128; const progMeterHeight = 12; @@ -136,199 +156,255 @@ } progMeter.ctx.fillRect(0, 0, (progMeterWidth * percent) / 100, progMeterHeight); }; progMeter.updateProgress(0); return progMeter; } function submitPickedFiles() { let tusdServer = getTusdEndpoint(); let onBeforeRequest = function(req) { let xhr = req.getUnderlyingObject(req); xhr.withCredentials = true; }; - let onSuccess = function(reqFname) { + let onSuccess = function(req) { // remove the selected file from the input element and the ul list // FileList is a read only setting, so we have to make - // a new one without this reqFname - let i; - let newFileList = []; - for (i = 0; i < uiState.input.files.length; i++) { - fname = uiState.input.files[i].name; - if (fname !== reqFname) { - newFileList.unshift(uiState.input.files[0]); + // a new one without this req + delete uiState.toUpload[req.name]; + let i, newReqObj = {}; + for (i = 0; i < uiState.pendingQueue.length; i++) { + fname = uiState.pendingQueue[i][1].name; + if (fname === req.name) { + // remove the successful tusUpload off + uiState.pendingQueue.splice(i, 1); } } - newFileList.reverse(); - //uiState.input.files = newFileList; // remove the file from the list the user can see - let li = document.getElementById(reqFname+"#li"); + let li = document.getElementById(req.name+"#li"); li.parentNode.removeChild(li); - // add the file to the data table somehow? + if (uiState.pendingQueue.length === 0) { removeCancelAllButton(); + } + const d = new Date(req.lastModified); + newReqObj = {"createTime": d.toJSON(), "name": req.name, "size": req.size}; + addNewUploadedFileToTable(newReqObj); }; let onError = function(err) { - alert(err); + removeCancelAllButton(); + alert(err.originalResponse._xhr.responseText); }; for (let f in uiState.toUpload) { file = uiState.toUpload[f]; if (useTus) { let tusOptions = { endpoint: tusdServer, metadata: { filename: file.name, fileType: file.type, fileSize: file.size }, onBeforeRequest: onBeforeRequest, - onSuccess: onSuccess(file.name), + onSuccess: onSuccess.bind(null, file), onError: onError, retryDelays: [1000], }; // TODO: get the uploadUrl from the tusd server // use a pre-create hook to validate the user // and get an uploadUrl let tusUpload = new tus.Upload(file, tusOptions); + uiState.pendingQueue.push([tusUpload, file]); tusUpload.start(); } else { // make a new XMLHttpRequest for each file, if tusd-tusclient not supported new sendFile(file); } } addCancelAllButton(); return; } function clearPickedFiles() { while (uiState.pickedList.firstChild) { uiState.pickedList.removeChild(uiState.pickedList.firstChild); } - uiState.input.value = ""; + uiState.input = createInput(); uiState.toUpload = {}; togglePickStateMessage(true); } function addClearSubmitButtons() { let firstBtn = document.getElementById("btnForInput"); let btnRow = document.getElementById("chooseAndSendFilesRow"); if (!document.getElementById("clearPicked")) { - let newLabel = document.createElement("label"); - newLabel.classList.add("button"); - newLabel.id = "clearPicked"; - newLabel.textContent = "Clear"; - newLabel.addEventListener("click", clearPickedFiles); - btnRow.append(newLabel); + let clearBtn = document.createElement("button"); + clearBtn.classList.add("button"); + clearBtn.id = "clearPicked"; + clearBtn.textContent = "Clear"; + clearBtn.addEventListener("click", clearPickedFiles); + btnRow.append(clearBtn); } if (!document.getElementById("submitPicked")) { - newLabel = document.createElement("label"); - newLabel.id = "submitPicked"; - newLabel.classList.add("button"); - newLabel.textContent = "Submit"; - newLabel.addEventListener("click", submitPickedFiles); - btnRow.append(newLabel); + submitBtn = document.createElement("button"); + submitBtn.id = "submitPicked"; + submitBtn.classList.add("button"); + submitBtn.textContent = "Submit"; + submitBtn.addEventListener("click", submitPickedFiles); + btnRow.append(submitBtn); } } function removeClearSubmitButtons() { let btn = document.getElementById("clearPicked"); btn.parentNode.removeChild(btn); btn = document.getElementById("submitPicked"); btn.parentNode.removeChild(btn); } function listPickedFiles() { + //uiState.input.click(); // let the user choose files: if (uiState.input.files.length === 0) { - togglePickStateMessage(true); - removeClearSubmitButtons(); + console.log("not input"); + return; + //togglePickStateMessage(true); + //removeClearSubmitButtons(); } else { - let displayList = document.createElement("ul"); + let displayList = document.getElementsByClassName("pickedFiles"); + if (displayList.length === 0) { + displayList = document.createElement("ul"); displayList.classList.add("pickedFiles"); uiState.pickedList.appendChild(displayList); + } else { + displayList = displayList[0]; + } for (let file of uiState.input.files ) { if (file.name in uiState.toUpload) { continue; } // create a list for the user to see let li = document.createElement("li"); li.classList.add("pickedFile"); li.id = `${file.name}#li`; li.textContent = `File name: ${file.name}, file size: ${prettyFileSize(file.size)}`; displayList.appendChild(li); // finally add it for us uiState.toUpload[file.name] = file; } togglePickStateMessage(false); addClearSubmitButtons(); } + // always clear the input element + uiState.input = createInput(); } function dataTablePrintSize(data, type, row, meta) { return prettyFileSize(data); } - function deleteFile(fname) { + let pendingDeletes = {}; + function deleteFile(rowIx, fname) { // 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}`); - const xhr = new XMLHttpRequest(); const endpoint = "../cgi-bin/hgHubConnect?deleteFile=" + fname; + if (!(endpoint in pendingDeletes)) { + const xhr = new XMLHttpRequest(); + pendingDeletes[endpoint] = xhr; this.xhr = xhr; this.xhr.open("DELETE", endpoint, true); this.xhr.send(); - // TODO: on the correct return code, delete the row + deleteFileFromTable(rowIx, fname); + delete pendingDeletes[endpoint]; + } } - function showExistingFiles(fileList) { - // Make the DataTable for each file - //$(document).on("draw.dt", function() {alert("table redrawn");}); - let table = $("#filesTable").DataTable({ - data: fileList, - columnDefs: [{orderable: false, targets: [0,1]}], - columns: [ - {data: "", title: "<input type=\"checkbox\"></input>", + function deleteFileFromTable(rowIx, 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.name === fname); + row.remove().draw(); + } + + 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).draw(); + } else { + showExistingFiles([req]); + } + } + + let tableInitOptions = { + //columnDefs: [{orderable: false, targets: [0,1]}], + columnDefs: [ + { + orderable: false, targets: 0, + title: "<input type=\"checkbox\"></input>", render: function(data, type, row) { return "<input type=\"checkbox\"></input>"; } }, - {data: "action", title: "Action", + { + orderable: false, targets: 1, + data: "action", title: "Action", render: function(data, type, row) { // TODO: add event handler on delete button // click to call hgHubDelete file return "<button class='deleteFileBtn'>Delete</button"; } }, + { + targets: 3, + render: function(data, type, row) { + return dataTablePrintSize(data); + } + } + ], + columns: [ + {data: "", }, + {data: "", }, {data: "name", title: "File name"}, {data: "size", title: "File size", render: dataTablePrintSize}, {data: "createTime", title: "Creation Time"}, ], order: [[4, 'desc']], - initComplete: function(settings, json) { + drawCallback: function(settings) { let btns = document.querySelectorAll('.deleteFileBtn'); let i; for (i = 0; i < btns.length; i++) { let fnameNode = btns[i].parentNode.nextElementSibling.childNodes[0]; if (fnameNode.nodeName !== "#text") {continue;} let fname = fnameNode.nodeValue; btns[i].addEventListener("click", (e) => { - deleteFile(fname); + deleteFile(i, fname); }); } - } - }); + }, + }; + + function showExistingFiles(d) { + // Make the DataTable for each file + //$(document).on("draw.dt", function() {alert("table redrawn");}); + tableInitOptions.data = d; + let table = $("#filesTable").DataTable(tableInitOptions); } function checkJsonData(jsonData, callerName) { // Return true if jsonData isn't empty and doesn't contain an error; // otherwise complain on behalf of caller. if (! jsonData) { alert(callerName + ': empty response from server'); } else if (jsonData.error) { console.error(jsonData.error); alert(callerName + ': error from server: ' + jsonData.error); } else if (jsonData.warning) { alert("Warning: " + jsonData.warning); return true; } else { if (debugCartJson) { @@ -354,94 +430,84 @@ function handleRefreshState(jsonData) { if (checkJsonData(jsonData, 'handleRefreshState')) { updateStateAndPage(jsonData, true); } $("#spinner").remove(); } function init() { cart.setCgi('hgMyData'); cart.debug(debugCartJson); if (!useTus) { console.log("tus is not supported, falling back to XMLHttpRequest"); } let pickedFiles = document.getElementById("fileList"); - let input = document.getElementById("uploadedFiles"); + let inputBtn = document.getElementById("btnForInput"); if (pickedFiles !== null) { // this element should be an empty div upon loading the page uiState.pickedList = pickedFiles; if (pickedFiles.children.length === 0) { let para = document.createElement("p"); para.textContent = "No files chosen yet"; para.classList.add("noFiles"); pickedFiles.appendChild(para); } } else { // TODO: graceful handle of leaving the page and coming back? } - if (input !== null) { - uiState.input = input; - uiState.input.style.float = "right"; - uiState.input.style.opacity = 0; - uiState.input.value = ""; - } else { let parent = document.getElementById("chooseAndSendFilesRow"); - input = document.createElement("input"); - input.style.opacity = 0; - input.style.float = "right"; - input.multiple = true; - input.id = "uploadedFiles"; - input.type = "file"; + let input = createInput(); uiState.input = input; - parent.appendChild(input); - } + inputBtn.parentNode.appendChild(input); if (typeof cartJson !== "undefined") { if (typeof cartJson.warning !== "undefined") { alert("Warning: " + cartJson.warning); } var urlParts = {}; if (debugCartJson) { console.log('from server:\n', cartJson); } _.assign(uiState,cartJson); saveHistory(cartJson, urlParts, true); } else { // no cartJson object means we are coming to the page for the first time: //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 // TODO: make hgHubConnect respond to requests // TODO: initialize tus-client // TODO: get user name // TODO: send a request with username // TODO: have tusd respond on server - let uploadSection = document.getElementById("uploadedFilesSection"); + let uploadSection = document.getElementById("chosenFilesSection"); if (uploadSection.style.display === "none") { uploadSection.style.display = ""; } if (typeof userFiles !== 'undefined' && typeof userFiles.fileList !== 'undefined' && userFiles.fileList.length > 0) { - showExistingFiles(userFiles.fileList); + uiState.fileList= userFiles.fileList; + showExistingFiles(uiState.fileList); } - input.addEventListener("change", listPickedFiles); + inputBtn.addEventListener("click", (e) => uiState.input.click()); + //uiState.input.addEventListener("change", listPickedFiles); // TODO: add event handler for when file is succesful upload // TODO: add event handlers for editing defaults, grouping into hub // TODO: display quota somewhere // TODO: customize the li to remove the picked file } } return { init: init, uiState: uiState, }; }()); // when a user reaches this page from the back button we can display our saved state // instead of sending another network request