2f0d3fa3d2bcbf3e23e834ca68565c470f73939d
chmalee
  Mon Oct 21 12:32:08 2024 -0700
Back end for auto-generating a hub with every user upload done

diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js
index 28442aa..a9ad6f1 100644
--- src/hg/js/hgMyData.js
+++ src/hg/js/hgMyData.js
@@ -199,30 +199,43 @@
             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);
                 }
             }
             // remove the file from the list the user can see
             let rowDivs = document.querySelectorAll("[id^='" + req.name+"']");
             let delIcon = rowDivs[0].previousElementSibling;
             rowDivs.forEach((div) => {div.remove();});
             delIcon.remove();
 
             // if nothing else we can close the dialog
             if (uiState.pendingQueue.length === 0) {
+                // first remove the grid headers
+                let headerEle = document.querySelectorAll(".fileListHeader");
+                headerEle.forEach( (header) => {
+                    if (header.style.display !== "none") {
+                        header.style.display = "none";
+                    }
+                });
+                //check if we can remove the batch change selects
+                if (uiState.pendingQueue.length > 1) {
+                    document.querySelectorAll("[id^=batchChangeSelect]").forEach( (select) => {
+                        select.remove();
+                    });
+                }
                 $("#filePickerModal").dialog("close");
             }
             const d = new Date(req.lastModified);
             newReqObj = {
                 "uploadTime": Date.now(),
                 "lastModified": d.toJSON(),
                 "fileName": metadata.fileName,
                 "fileSize": metadata.fileSize,
                 "fileType": metadata.fileType,
                 "genome": metadata.genome,
                 "hub": ""
             };
             addNewUploadedFileToTable(newReqObj);
         };
 
@@ -288,30 +301,33 @@
         let genomeInp = document.createElement("select");
         genomeInp.classList.add("genomePicker");
         genomeInp.name = `${fileName}#genomeInput`;
         genomeInp.id = `${fileName}#genomeInput`;
         let labelChoice = document.createElement("option");
         labelChoice.label = "Choose Genome";
         labelChoice.value = "Choose Genome";
         labelChoice.selected = true;
         labelChoice.disabled = true;
         genomeInp.appendChild(labelChoice);
         let choices = ["Human hg38", "Human T2T", "Human hg19", "Mouse mm39", "Mouse mm10"];
         let cartChoice = document.createElement("option");
         cartChoice.id = cartDb;
         cartChoice.label = cartDb;
         cartChoice.value = cartDb.split(" ").slice(-1);
+        if (cartChoice.value.startsWith("hub_")) {
+            cartChoice.label = cartDb.split(" ").slice(0,-1).join(" "); // take off the actual db value
+        }
         cartChoice.selected = true;
         genomeInp.appendChild(cartChoice);
         choices.forEach( (e) =>  {
             if (e === cartDb) {return;} // don't print the cart database twice
             let choice = document.createElement("option");
             choice.id = e;
             choice.label = e;
             choice.value = e.split(" ")[1];
             genomeInp.appendChild(choice);
         });
         return genomeInp;
     }
 
     function makeTypeSelect(fileName) {
         let typeInp = document.createElement("select");
@@ -334,57 +350,90 @@
             choice.id = e;
             choice.label = e;
             choice.value = e;
             typeInp.appendChild(choice);
         });
         return typeInp;
     }
 
 
     function createTypeAndDbDropdown(fileName) {
         typeInp = makeTypeSelect(fileName);
         genomeInp = makeGenomeSelect(fileName);
         return [typeInp, genomeInp];
     }
 
+    function deletePickedFile(eventInst) {
+        // called when the trash icon has been clicked to remove a file
+        // the sibling text content is the file name, which we use to
+        // find all the other elements to delete
+        let trashIconDiv = eventInst.currentTarget;
+        fname = trashIconDiv.nextSibling.textContent;
+        document.querySelectorAll("[id^='" + fname + "']").forEach( (sib) => {
+            sib.remove();
+        });
+        trashIconDiv.remove();
+        delete uiState.toUpload[fname];
+        // if there are no file rows left (1 row for the header) in the picker hide the headers:
+        let container = document.getElementById("fileList");
+        if (getComputedStyle(container).getPropertyValue("grid-template-rows").split(" ").length === 1) {
+            let headerEle = document.querySelectorAll(".fileListHeader");
+            headerEle.forEach( (header) => {
+                if (header.style.display !== "none") {
+                    header.style.display = "none";
+                }
+            });
+        }
+        // if there is only one file picked hide the batch change selects, we may or may not have
+        // hidden the headers yet, so we should have 3 or less rows
+        if (getComputedStyle(container).getPropertyValue("grid-template-rows").split(" ").length <= 3) {
+            let batchChange = document.querySelectorAll("[id^=batchChangeSelect]");
+            batchChange.forEach( (select) => {
+                select.remove();
+            });
+        }
+    }
+
     function listPickedFiles() {
         // displays the users chosen files in a grid:
         if (uiState.input.files.length === 0) {
             console.log("not input");
             return;
         } else {
             let displayList = document.getElementById("fileList");
             let deleteEle = document.createElement("div");
             deleteEle.classList.add("deleteFileIcon");
             deleteEle.innerHTML = "<svg xmlns='http://www.w3.org/2000/svg' height='0.8em' viewBox='0 0 448 512'><!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d='M135.2 17.7C140.6 6.8 151.7 0 163.8 0H284.2c12.1 0 23.2 6.8 28.6 17.7L320 32h96c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 96 0 81.7 0 64S14.3 32 32 32h96l7.2-14.3zM32 128H416V448c0 35.3-28.7 64-64 64H96c-35.3 0-64-28.7-64-64V128zm96 64c-8.8 0-16 7.2-16 16V432c0 8.8 7.2 16 16 16s16-7.2 16-16V208c0-8.8-7.2-16-16-16zm96 0c-8.8 0-16 7.2-16 16V432c0 8.8 7.2 16 16 16s16-7.2 16-16V208c0-8.8-7.2-16-16-16zm96 0c-8.8 0-16 7.2-16 16V432c0 8.8 7.2 16 16 16s16-7.2 16-16V208c0-8.8-7.2-16-16-16z'/></svg>";
             for (let file of uiState.input.files ) {
                 if (file.name in uiState.toUpload) {
                     continue;
                 }
                 // create a list for the user to see
                 let nameCell = document.createElement("div");
                 nameCell.classList.add("pickedFile");
                 nameCell.id = `${file.name}#fileName`;
                 nameCell.textContent = `${file.name}`;
                 // Add the form controls for this file:
                 let [typeCell, dbCell] = createTypeAndDbDropdown(file.name);
                 let sizeCell = document.createElement("div");
                 sizeCell.classList.add("pickedFile");
                 sizeCell.id = `${file.name}#fileSize`;
                 sizeCell.textContent = prettyFileSize(file.size);
 
-                displayList.appendChild(deleteEle.cloneNode(true));
+                newDelIcon = deleteEle.cloneNode(true);
+                newDelIcon.addEventListener("click", deletePickedFile);
+                displayList.appendChild(newDelIcon);
                 displayList.appendChild(nameCell);
                 displayList.appendChild(typeCell);
                 displayList.appendChild(dbCell);
                 displayList.appendChild(sizeCell);
 
                 // finally add it for us
                 uiState.toUpload[file.name] = file;
             }
             let headerEle = document.querySelectorAll(".fileListHeader");
             headerEle.forEach( (header) => {
                 if (header.style.display !== "block") {
                     header.style.display = "block";
                 }
             });
             if (Object.keys(uiState.toUpload).length > 1) {
@@ -411,102 +460,92 @@
                 batchDb.addEventListener("change", function(e) {
                     let newVal = e.currentTarget.selectedOptions[0].value;
                     document.querySelectorAll("[id$=genomeInput]").forEach( (i) => {
                         if (i === e.currentTarget) {
                             return;
                         }
                         i.value = newVal;
                     });
                 });
 
                 // append to the document
                 displayList.appendChild(batchType);
                 displayList.appendChild(batchDb);
             }
             document.querySelectorAll(".deleteFileIcon").forEach( (i) => {
-                i.addEventListener("click", function(e) {
-                    // the sibling text content is the file name, which we use to
-                    // find all the other elements to delete
-                    let trashIconDiv = e.currentTarget;
-                    fname = trashIconDiv.nextSibling.textContent;
-                    document.querySelectorAll("[id^='" + fname + "']").forEach( (sib) => {
-                        sib.remove();
-                    });
-                    trashIconDiv.remove();
-                    delete uiState.toUpload[fname];
-                    // if there are no rows left in the picker hide the headers:
-                    let container = document.getElementById("fileList");
-                    if (getComputedStyle(container).getPropertyValue("grid-template-rows").split(" ").length === 1) {
-                        let headerEle = document.querySelectorAll(".fileListHeader");
-                        headerEle.forEach( (header) => {
-                            if (header.style.display !== "none") {
-                                header.style.display = "none";
-                            }
-                        });
-                    }
-                });
             });
         }
         // always clear the input element
         uiState.input = createInput();
     }
 
     function dataTablePrintSize(data, type, row, meta) {
         return prettyFileSize(data);
     }
 
+    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 deleteFile(fname, fileType) {
+    function deleteFile(fname, fileType, hubNameList) {
         // 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");
-        cart.send({deleteFile: {fileNameList: [fname, fileType]}});
+        // a little complex, but the format is:
+        // {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.flush();
         deleteFileFromTable(fname);
     }
 
     function deleteFileList() {
     }
 
     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) {
+    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?
-                window.location.assign("../cgi-bin/hgTracks?db=" + genome + "&hgt.customText=" + uiState.userUrl + fname);
+                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]);
@@ -544,90 +583,98 @@
                 if (!("Cancel" in currBtns)) {
                     currBtns.Cancel = function() {
                         clearPickedFiles();
                         $(this).dialog("close");
                     };
                     $("#filePickerModal").dialog("option", "buttons", currBtns);
                 }
             },
             /*
             "Cancel": function() {
                 clearPickedFiles();
                 $(this).dialog("close");
             },
             */
             "Close": function() {
+                // delete everything that isn't the headers, which we set to hide:
+                let fileList = document.getElementById("fileList");
+                let headers = document.querySelectorAll(".fileListHeader");
+                fileList.replaceChildren(...headers);
+                headers.forEach( (header) => {
+                    header.style.display = "none";
+                });
+                uiState.input = createInput();
                 $(this).dialog("close");
             }
         };
         $("#filePickerModal").dialog({
             modal: true,
             buttons: hubUploadButtons,
             minWidth: $("#filePickerModal").width(),
             width: (window.innerWidth * 0.8),
             height: (window.innerHeight * 0.55),
             title: "Upload track data",
             open: function(e, ui) {
                 $(e.target).parent().css("position", "fixed");
                 $(e.target).parent().css("top", "10%");
             },
         });
         $("#filePickerModal").dialog("open");
     }
 
     let tableInitOptions = {
-        language: {
-            emptyTable: "Uploaded files will appear here. Click \"Upload\" to get started",
-        },
         layout: {
             topStart: {
                 buttons: [
-                    {text: 'Upload',
-                     action: startUploadDialog},
+                    {
+                        text: 'Upload',
+                        action: startUploadDialog,
+                        enabled: false, // disable by default in case user is not logged in
+                    },
                 ]
             }
         },
         columnDefs: [
             {
                 orderable: false, targets: 0,
                 title: "<input type=\"checkbox\"></input>",
                 render: function(data, type, row) {
                     return "<input type=\"checkbox\"></input>";
                 }
             },
             {
                 orderable: false, targets: 1,
                 data: "action", title: "Action",
                 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);
+                        deleteFile(row.fileName, row.fileType, row.hub);
                     });
 
                     // 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);
+                        viewInGenomeBrowser(row.fileName, row.fileType, row.genome, row.hub);
                     });
 
                     // click to rename file or hub:
                     let renameBtn = document.createElement("button");
                     renameBtn.textContent = "Rename";
                     renameBtn.type = 'button';
                     renameBtn.addEventListener("click", function() {
                         console.log("rename btn clicked!");
                     });
 
                     // click to associate this track to a hub
                     let addToHubBtn = document.createElement("button");
                     addToHubBtn.textContent = "Add to hub";
                     addToHubBtn.type = 'button';
                     addToHubBtn.addEventListener("click", function() {
@@ -637,55 +684,71 @@
                     container.appendChild(delBtn);
                     container.appendChild(viewBtn);
                     container.appendChild(renameBtn);
                     container.appendChild(addToHubBtn);
 
                     return container;
                 }
             },
             {
                 targets: 3,
                 render: function(data, type, row) {
                     return dataTablePrintSize(data);
                 }
             },
             {
+                targets: 5,
+                render: function(data, type, row) {
+                    return dataTablePrintGenome(data);
+                }
+            },
+            {
                 // The upload time column, not visible but we use it to sort on new uploads
                 targets: 8,
                 visible: false,
                 searchable: false
             }
         ],
         columns: [
             {data: "", },
             {data: "", },
             {data: "fileName", title: "File name"},
             {data: "fileSize", title: "File size", render: dataTablePrintSize},
             {data: "fileType", title: "File type"},
-            {data: "genome", title: "Genome"},
+            {data: "genome", title: "Genome", render: dataTablePrintGenome},
             {data: "hub", title: "Hubs"},
             {data: "lastModified", title: "File Last Modified"},
             {data: "uploadTime", title: "Upload Time"},
         ],
         order: [[6, 'desc']],
+        drawCallback: function(settings) {
+            if (isLoggedIn) {
+                settings.api.buttons(0).enable();
+            }
+        }
     };
 
     function showExistingFiles(d) {
         // Make the DataTable for each file
         // make buttons have the same style as other buttons
         $.fn.dataTable.Buttons.defaults.dom.button.className = 'button';
         tableInitOptions.data = d;
+        if (isLoggedIn) {
+            tableInitOptions.language = {emptyTable: "Uploaded files will appear here. Click \"Upload\" to get started"};
+        } else {
+            tableInitOptions.language = {emptyTable: "You are not logged in, please navigate to \"My Data\" > \"My Sessions\" and log in or create an account to begin uploading files"};
+        }
         let table = new DataTable("#filesTable", tableInitOptions);
     }
 
     function showExistingHubs(d) {
         // Add the hubs to the files table
         if (!d) {return;}
         let table = $("#filesTable").DataTable();
         d.forEach((hub) => {
             let hubName = hub.hubName;
             let db = hub.genome;
             let data = {
                 fileName: hubName,
                 fileSize: null,
                 fileType: "hub",
                 genome: db,
@@ -762,43 +825,39 @@
                 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
             let fileDiv = document.getElementById('filesDiv');
-            if (typeof userFiles !== 'undefined') {
+            if (typeof userFiles !== 'undefined' && Object.keys(userFiles).length > 0) {
                 uiState.fileList = userFiles.fileList;
                 uiState.hubList = userFiles.hubList;
                 uiState.userUrl = userFiles.userUrl;
             }
-            showExistingFiles(uiState.fileList);
-            showExistingHubs(uiState.hubList);
+            showExistingFiles(uiState.fileList.filter((row) => row.fileType !== "hub"));
             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
         }
         $("#newTrackHubDialog").dialog({
             modal: true,
             autoOpen: false,
             title: "Create new track hub",
             closeOnEscape: true,
             minWidth: 400,
             minHeight: 120
         });
     }
     return { init: init,
              uiState: uiState,
            };
 
 }());