about summary refs log tree commit diff
path: root/uploader/static
diff options
context:
space:
mode:
Diffstat (limited to 'uploader/static')
-rw-r--r--uploader/static/css/layout-common.css3
-rw-r--r--uploader/static/css/layout-large.css62
-rw-r--r--uploader/static/css/layout-medium.css64
-rw-r--r--uploader/static/css/layout-small.css60
-rw-r--r--uploader/static/css/styles.css70
-rw-r--r--uploader/static/css/theme.css85
-rw-r--r--uploader/static/js/datatables.js69
-rw-r--r--uploader/static/js/debug.js40
-rw-r--r--uploader/static/js/files.js4
-rw-r--r--uploader/static/js/misc.js6
-rw-r--r--uploader/static/js/populations.js36
-rw-r--r--uploader/static/js/pubmed.js113
-rw-r--r--uploader/static/js/species.js34
-rw-r--r--uploader/static/js/upload_samples.js24
-rw-r--r--uploader/static/js/urls.js26
-rw-r--r--uploader/static/js/utils.js27
16 files changed, 694 insertions, 29 deletions
diff --git a/uploader/static/css/layout-common.css b/uploader/static/css/layout-common.css
new file mode 100644
index 0000000..36a5735
--- /dev/null
+++ b/uploader/static/css/layout-common.css
@@ -0,0 +1,3 @@
+* {
+    box-sizing: border-box;
+}
diff --git a/uploader/static/css/layout-large.css b/uploader/static/css/layout-large.css
new file mode 100644
index 0000000..8abd2dd
--- /dev/null
+++ b/uploader/static/css/layout-large.css
@@ -0,0 +1,62 @@
+@media screen and (min-width: 20.1in) {
+    body {
+        display: grid;
+        grid-template-columns: 7fr 3fr;
+        grid-gap: 1em;
+    }
+
+    #header {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 3;
+
+        /* Define layout for the children elements */
+        display: grid;
+        grid-template-columns: 8fr 2fr;
+    }
+
+    #header #header-text {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 2;
+
+        /* Content styling */
+        padding-left: 1em;
+    }
+
+    #header #header-nav {
+        /* Place it in the parent element */
+        grid-column-start: 2;
+        grid-column-end: 3;
+    }
+
+    #main {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 3;
+
+        /* Define layout for the children elements */
+        display: grid;
+        grid-template-columns: 7fr 3fr;
+        grid-gap: 1.5em;
+    }
+
+    #main #breadcrumbs {
+        grid-column-start: 1;
+        grid-column-end: 3;
+        padding: 0 3px;
+    }
+
+    #main #main-content {
+        max-width: 950px;
+
+        grid-column-start: 1;
+        grid-column-end: 2;
+    }
+
+    #main #sidebar-content {
+        grid-column-start: 2;
+        grid-column-end: 3;
+        padding: 1em 0 0 0;
+    }
+}
diff --git a/uploader/static/css/layout-medium.css b/uploader/static/css/layout-medium.css
new file mode 100644
index 0000000..2cca711
--- /dev/null
+++ b/uploader/static/css/layout-medium.css
@@ -0,0 +1,64 @@
+@media screen and (width > 8in) and (max-width: 20in) {
+    body {
+        display: grid;
+        grid-template-columns: 65fr 35fr;
+        grid-gap: 1em;
+    }
+
+    #header {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 3;
+
+        /* Define layout for the children elements */
+        display: grid;
+        grid-template-columns: 8fr 2fr;
+    }
+
+    #header #header-text {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 2;
+
+        /* Content styling */
+        padding-left: 1em;
+    }
+
+    #header #header-nav {
+        /* Place it in the parent element */
+        grid-column-start: 2;
+        grid-column-end: 3;
+    }
+
+    #main {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 3;
+
+        /* Define layout for the children elements */
+        display: grid;
+        grid-template-columns: 7fr 3fr;
+        grid-gap: 5px;
+    }
+
+    #main #breadcrumbs {
+        grid-column-start: 1;
+        grid-column-end: 3;
+        padding: 0 3px;
+    }
+
+    #main #main-content {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 2;
+        grid-gap: 5px;
+
+        /* Define layout for the children elements */
+        max-width: 100%;
+    }
+
+    #main #sidebar-content {
+        grid-column-start: 2;
+        grid-column-end: 3;
+    }
+}
diff --git a/uploader/static/css/layout-small.css b/uploader/static/css/layout-small.css
new file mode 100644
index 0000000..80a3759
--- /dev/null
+++ b/uploader/static/css/layout-small.css
@@ -0,0 +1,60 @@
+@media screen and (max-width: 8in) {
+    body {
+        display: grid;
+        grid-template-columns: 1fr;
+        grid-template-rows: 1fr 2fr 7fr;
+        grid-gap: 1em;
+    }
+
+    #header {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 3;
+
+        /* Define layout for the children elements */
+        display: grid;
+        grid-template-columns: 1fr;
+    }
+
+    #header #header-text {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 2;
+
+        /* Content styling */
+        padding-left: 1em;
+    }
+
+    #header #header-nav {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 2;
+    }
+
+    #main {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 2;
+        display: grid;
+
+        /* Define layout for the children elements */
+        grid-template-rows: 1.5em 80% 20%;
+        grid-template-columns: 1fr;
+    }
+
+    #main #breadcrumbs {
+        grid-row-start: 1;
+        grid-row-end: 2;
+        
+    }
+
+    #main #main-content {
+        grid-row-start: 2;
+        grid-row-end: 3;
+    }
+
+    #main #sidebar-content {
+        grid-row-start: 3;
+        grid-row-end: 4;
+    }
+}
diff --git a/uploader/static/css/styles.css b/uploader/static/css/styles.css
index 9bb4e65..df50dec 100644
--- a/uploader/static/css/styles.css
+++ b/uploader/static/css/styles.css
@@ -5,7 +5,7 @@
 body {
     margin: 0.7em;
     display: grid;
-    grid-template-columns: 1fr 9fr;
+    grid-template-columns: 2fr 8fr;
     grid-gap: 20px;
 
     font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
@@ -100,15 +100,32 @@ body {
     padding-left: 0.5em;
 }
 
-#main #all-content {
-    /* Place it in the parent element */
-    grid-column-start: 1;
-    grid-column-end: 3;
+@media screen and (max-width: 20in) {
+    #main #all-content {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 3;
 
-    /* Define layout for the children elements */
-    display: grid;
-    grid-template-columns: 7fr 3fr; /* For a maximum screen width of 1366 pixels */
-    grid-gap: 1.5em;
+        /* Define layout for the children elements */
+        max-width: 80%;
+    }
+
+    #sidebar-content {
+        display: none;
+    }
+}
+
+@media screen and (min-width: 20.1in) {
+    #main #all-content {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 3;
+
+        /* Define layout for the children elements */
+        display: grid;
+        grid-template-columns: 7fr 3fr;
+        grid-gap: 1.5em;
+    }
 }
 
 #main #all-content .row {
@@ -133,3 +150,38 @@ body {
 #pagetitle .breadcrumb a {
     color: #666666;
 }
+
+.heading {
+    border-bottom: solid #EEBB88;
+    text-transform: capitalize;
+}
+
+.subheading {
+    padding: 1em 0 0.1em 0.5em;
+    border-bottom: solid #88BBEE;
+    text-transform: capitalize;
+}
+
+input[type="search"] {
+    border-radius: 5px;
+}
+
+.btn {
+    text-transform: Capitalize;
+}
+
+table.dataTable thead th, table.dataTable tfoot th{
+  border-right: 1px solid white;
+  color: white;
+  background-color: #369 !important;
+}
+
+table.dataTable tbody tr.selected td {
+    background-color: #ffee99 !important;
+}
+
+.form-group {
+    margin-bottom: 2em;
+    padding-bottom: 0.2em;
+    border-bottom: solid gray 1px;
+}
diff --git a/uploader/static/css/theme.css b/uploader/static/css/theme.css
new file mode 100644
index 0000000..99b7af3
--- /dev/null
+++ b/uploader/static/css/theme.css
@@ -0,0 +1,85 @@
+body {
+    margin: 0.7em;
+    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+    font-style: normal;
+    font-size: 20px;
+}
+
+#header {
+    background-color: #336699;
+    color: #FFFFFF;
+    border-radius: 3px;
+    min-height: 30px;
+}
+
+#header #header-nav .nav li a {
+    /* Content styling */
+    color: #FFFFFF;
+    background: #4477AA;
+    border: solid 5px #336699;
+    border-radius: 5px;
+    font-size: 0.7em;
+    text-align: center;
+    padding: 1px 7px;
+}
+
+#main #breadcrumbs {
+    border-radius:3px;
+    text-align: center;
+}
+
+#main #main-content {
+    border-radius: 5px;
+    padding: 0 5px;
+}
+
+#main #sidebar-content {
+    background: #EEEEEE;
+
+    border-radius: 5px;
+    padding: 10px 5px;
+}
+
+#main .row {
+    margin: 0 2px;
+}
+
+
+.heading {
+    border-bottom: solid #EEBB88;
+    text-transform: capitalize;
+}
+
+.subheading {
+    padding: 1em 0 0.1em 0.5em;
+    border-bottom: solid #88BBEE;
+    text-transform: capitalize;
+}
+
+label {
+    text-transform: Capitalize;
+}
+
+input[type="search"] {
+    border-radius: 5px;
+}
+
+.btn {
+    text-transform: Capitalize;
+}
+
+table.dataTable thead th, table.dataTable tfoot th{
+    border-right: 1px solid white;
+    color: white;
+    background-color: #369 !important;
+}
+
+table.dataTable tbody tr.selected td {
+    background-color: #ffee99 !important;
+}
+
+.form-group {
+    margin-bottom: 2em;
+    padding-bottom: 0.2em;
+    border-bottom: solid gray 1px;
+}
diff --git a/uploader/static/js/datatables.js b/uploader/static/js/datatables.js
new file mode 100644
index 0000000..82fd696
--- /dev/null
+++ b/uploader/static/js/datatables.js
@@ -0,0 +1,69 @@
+/** Handlers for events in datatables **/
+
+var addTableLength = (menuList, lengthToAdd, dataLength) => {
+    if(dataLength >= lengthToAdd) {
+        newList = structuredClone(menuList);//menuList.slice(0, menuList.length); // shallow copy
+        newList.push(lengthToAdd);
+        return newList;
+    }
+    return menuList;
+};
+
+var defaultLengthMenu = (data) => {
+    menuList = []
+    var lengths = [10, 25, 50, 100, 1000, data.length];
+    lengths.forEach((len) => {
+        menuList = addTableLength(menuList, len, data.length);
+    });
+    return menuList;
+};
+
+var buildDataTable = (tableId, data = [], columns = [], userSettings = {}) => {
+    var defaultSettings = {
+        responsive: true,
+        layout: {
+            topStart: null,
+            topEnd: null,
+            bottomStart: null,
+            bottomEnd: null,
+        },
+        select: true,
+        lengthMenu: defaultLengthMenu(data),
+        language: {
+            processing: "Processing… Please wait.",
+            loadingRecords: "Loading table data… Please wait.",
+            lengthMenu: "",
+            info: ""
+        },
+        data: data,
+        columns: columns,
+        drawCallback: (settings) => {
+            $(this[0]).find("tbody tr").each((idx, row) => {
+                var arow = $(row);
+                var checkboxOrRadio = arow.find(".chk-row-select");
+                if (checkboxOrRadio) {
+                    if (arow.hasClass("selected")) {
+                        checkboxOrRadio.prop("checked", true);
+                    } else {
+                        checkboxOrRadio.prop("checked", false);
+                    }
+                }
+            });
+        }
+    }
+    var theDataTable = $(tableId).DataTable({
+        ...defaultSettings,
+        ...userSettings
+    });
+    theDataTable.on("select", (event, datatable, type, cell, originalEvent) => {
+        datatable.rows({selected: true}).nodes().each((node, index) => {
+            $(node).find(".chk-row-select").prop("checked", true)
+        });
+    });
+    theDataTable.on("deselect", (event, datatable, type, cell, originalEvent) => {
+        datatable.rows({selected: false}).nodes().each((node, index) => {
+            $(node).find(".chk-row-select").prop("checked", false)
+        });
+    });
+    return theDataTable;
+};
diff --git a/uploader/static/js/debug.js b/uploader/static/js/debug.js
new file mode 100644
index 0000000..eb01209
--- /dev/null
+++ b/uploader/static/js/debug.js
@@ -0,0 +1,40 @@
+/**
+ * The entire purpose of this function is for use to debug values inline
+ * without changing the flow of the code too much.
+ *
+ * This **MUST** be a non-arrow function to allow access to the `arguments`
+ * object.
+ *
+ * This function expects at least one argument.
+ *
+ * If more than one argument is provided, then:
+ * a) the last argument is considered the value, and will be returned
+ * b) all other arguments will be converted to string and output
+ *
+ * If only one argument is provided, it is considered the value, and will be
+ * returned.
+ *
+ * Zero arguments is an error condition.
+ **/
+function __pk__(val) {
+    /* Handle zero arguments */
+    if (arguments.length < 1) {
+        throw new Error("Invalid arguments: Expected at least one argument.");
+    }
+
+    msg = "/********** DEBUG **********/";
+    if (arguments.length > 1) {
+        msg = Array.from(
+            arguments
+        ).slice(
+            0,
+            arguments.length - 1
+        ).map((val) => {
+            return String(val);
+        }).join("; ")
+    }
+
+    value = arguments[arguments.length - 1];
+    console.debug("/********** " + msg + " **********/", value);
+    return value;
+}
diff --git a/uploader/static/js/files.js b/uploader/static/js/files.js
index 9d6bca1..0bde6f7 100644
--- a/uploader/static/js/files.js
+++ b/uploader/static/js/files.js
@@ -84,8 +84,8 @@ var errorHandler = makeResumableHandler("error");
 var markResumableDragAndDropElement = (resumable, fileinput, droparea, browsebutton) => {
     if(resumable.support) {
         //Hide file input element and display drag&drop UI
-        add_class(fileinput, "hidden");
-        remove_class(droparea, "hidden");
+        add_class(fileinput, "visually-hidden");
+        remove_class(droparea, "visually-hidden");
 
         // Define UI elements for browse and drag&drop
         resumable.assignDrop(droparea);
diff --git a/uploader/static/js/misc.js b/uploader/static/js/misc.js
deleted file mode 100644
index cf7b39e..0000000
--- a/uploader/static/js/misc.js
+++ /dev/null
@@ -1,6 +0,0 @@
-"Miscellaneous functions and event-handlers"
-
-$(".not-implemented").click((event) => {
-    event.preventDefault();
-    alert("This feature is not implemented yet. Please bear with us.");
-});
diff --git a/uploader/static/js/populations.js b/uploader/static/js/populations.js
new file mode 100644
index 0000000..111ebb7
--- /dev/null
+++ b/uploader/static/js/populations.js
@@ -0,0 +1,36 @@
+$(() => {
+    var populationsDataTable = buildDataTable(
+        "#tbl-select-population",
+        JSON.parse(
+            $("#tbl-select-population").attr("data-populations-list")),
+        [
+            {
+                data: (apopulation) => {
+                    return `<input type="radio" name="population_id"`
+                        + `id="rdo_population_id_${apopulation.InbredSetId}" `
+                        + `value="${apopulation.InbredSetId}" `
+                        + `class="chk-row-select">`;
+                }
+            },
+            {
+                searchable: true,
+                data: (apopulation) => {
+                    return `${apopulation.FullName} (${apopulation.InbredSetName})`;
+                }
+            }
+        ],
+        {
+            select: "single",
+            paging: true,
+            scrollY: 500,
+            deferRender: true,
+            scroller: true,
+            scrollCollapse: true,
+            layout: {
+                topStart: "info",
+                topEnd: "search",
+                bottomStart: "pageLength",
+                bottomEnd: false
+            }
+        });
+});
diff --git a/uploader/static/js/pubmed.js b/uploader/static/js/pubmed.js
new file mode 100644
index 0000000..f425f49
--- /dev/null
+++ b/uploader/static/js/pubmed.js
@@ -0,0 +1,113 @@
+var extract_details = (pubmed_id, details) => {
+    var months = {
+        "jan": "January",
+        "feb": "February",
+        "mar": "March",
+        "apr": "April",
+        "may": "May",
+        "jun": "June",
+        "jul": "July",
+        "aug": "August",
+        "sep": "September",
+        "oct": "October",
+        "nov": "November",
+        "dec": "December"
+    };
+    var _date = details[pubmed_id].pubdate.split(" ");
+    return {
+        "authors": details[pubmed_id].authors.map((authobj) => {
+            return authobj.name;
+        }),
+        "title": details[pubmed_id].title,
+        "journal": details[pubmed_id].fulljournalname,
+        "volume": details[pubmed_id].volume,
+        "pages": details[pubmed_id].pages,
+        "month": _date.length > 1 ? (months[_date[1].toLowerCase()] || "January") : "January",
+        "year": _date[0],
+    };
+};
+
+var update_publication_details = (details) => {
+    Object.entries(details).forEach((entry) => {;
+                                                switch(entry[0]) {
+                                                case "authors":
+                                                    $("#txt-publication-authors").val(entry[1].join(", "));
+                                                    break;
+                                                case "month":
+                                                    $("#select-publication-month")
+                                                        .children("option")
+                                                        .each((index, child) => {
+                                                            console.debug(entry[1].toLowerCase());
+                                                            child.selected = child.value == entry[1].toLowerCase();
+                                                        });
+                                                default:
+                                                    $("#txt-publication-" + entry[0]).val(entry[1]);
+                                                    break;
+                                                }
+                                               });
+};
+
+var fetch_publication_abstract = (pubmed_id, pub_details) => {
+    $.ajax("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi",
+           {
+               "method": "GET",
+               "data": {
+                   "db": "pubmed",
+                   "id": pubmed_id,
+                   "rettype": "abstract",
+                   "retmode": "xml"
+               },
+               "success": (data, textStatus, jqXHR) => {
+                   update_publication_details({
+                       ...pub_details,
+                       ...{
+                           "abstract": Array.from(data
+                                                  .getElementsByTagName(
+                                                      "Abstract")[0]
+                                                  .children)
+                               .map((elt) => {return elt.textContent.trim();})
+                               .join("\r\n")
+                       }});
+               },
+               "error": (jqXHR, textStatus, errorThrown) => {},
+               "complete": (jqXHR, textStatus) => {},
+               "dataType": "xml"
+           });
+};
+
+var fetch_publication_details = (pubmed_id, complete_thunks) => {
+    error_display = $("#search-pubmed-id-error");
+    error_display.text("");
+    add_class(error_display, "visually-hidden");
+    $.ajax("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi",
+           {
+               "method": "GET",
+               "data": {"db": "pubmed", "id": pubmed_id, "format": "json"},
+               "success": (data, textStatus, jqXHR) => {
+                   // process and update publication details
+                   hasError = (
+                       Object.hasOwn(data, "error") ||
+                           Object.hasOwn(data.result[pubmed_id], "error"));
+                   if(hasError) {
+                       error_display.text(
+                           "There was an error fetching a publication with " +
+                               "the given PubMed ID! The error received " +
+                               "was: '" + (
+                                   data.error ||
+                                       data.result[pubmed_id].error) +
+                               "'. Please check ID you provided and try " +
+                               "again.");
+                       remove_class(error_display, "visually-hidden");
+                   } else {
+                       fetch_publication_abstract(
+                           pubmed_id,
+                           extract_details(pubmed_id, data.result));
+                   }
+               },
+               "error": (jqXHR, textStatus, errorThrown) => {},
+               "complete": () => {
+                   complete_thunks.forEach((thunk) => {thunk()});
+               },
+               "dataType": "json"
+           });
+};
diff --git a/uploader/static/js/species.js b/uploader/static/js/species.js
new file mode 100644
index 0000000..fb0d2d2
--- /dev/null
+++ b/uploader/static/js/species.js
@@ -0,0 +1,34 @@
+$(() => {
+    var speciesDataTable = buildDataTable(
+        "#tbl-select-species",
+        JSON.parse(
+            $("#tbl-select-species").attr("data-species-list")),
+        [
+            {
+                data: (aspecies) => {
+                    return `<input type="radio" name="species_id"`
+                        + `id="rdo_species_id_${aspecies.SpeciesId}" `
+                        + `value="${aspecies.SpeciesId}" class="chk-row-select">`;
+                }
+            },
+            {
+                data: (aspecies) => {
+                    return `${aspecies.FullName} (${aspecies.SpeciesName})`;
+                }
+            }
+        ],
+        {
+            select: "single",
+            paging: true,
+            scrollY: 500,
+            deferRender: true,
+            scroller: true,
+            scrollCollapse: true,
+            layout: {
+                topStart: "info",
+                topEnd: "search",
+                bottomStart: "pageLength",
+                bottomEnd: false
+            }
+        });
+});
diff --git a/uploader/static/js/upload_samples.js b/uploader/static/js/upload_samples.js
index aed536f..1c25a1d 100644
--- a/uploader/static/js/upload_samples.js
+++ b/uploader/static/js/upload_samples.js
@@ -87,20 +87,20 @@ function display_preview(event) {
     var data_preview_table = document.getElementById("tbl:samples-preview");
     remove_rows(data_preview_table);
 
-    var separator = document.getElementById("select:separator").value;
+    var separator = document.getElementById("select-separator").value;
     if(separator === "other") {
-	separator = document.getElementById("txt:separator").value;
+	separator = document.getElementById("txt-separator").value;
     }
     if(separator == "") {
 	display_error_row(data_preview_table, "Please provide a separator.");
 	return false;
     }
 
-    var delimiter = document.getElementById("txt:delimiter").value;
+    var delimiter = document.getElementById("txt-delimiter").value;
 
-    var firstlineheading = document.getElementById("chk:heading").checked;
+    var firstlineheading = document.getElementById("chk-heading").checked;
 
-    var fileelement = document.getElementById("file:samples");
+    var fileelement = document.getElementById("file-samples");
     var preview_data = JSON.parse(
 	fileelement.getAttribute("data-preview-content") || "[]");
     if(preview_data.length == 0) {
@@ -115,18 +115,18 @@ function display_preview(event) {
 	delimiter));
 }
 
-document.getElementById("chk:heading").addEventListener(
+document.getElementById("chk-heading").addEventListener(
     "change", display_preview);
-document.getElementById("select:separator").addEventListener(
+document.getElementById("select-separator").addEventListener(
     "change", display_preview);
-document.getElementById("txt:separator").addEventListener(
+document.getElementById("txt-separator").addEventListener(
     "keyup", display_preview);
-document.getElementById("txt:delimiter").addEventListener(
+document.getElementById("txt-delimiter").addEventListener(
     "keyup", display_preview);
-document.getElementById("file:samples").addEventListener(
+document.getElementById("file-samples").addEventListener(
     "change", (event) => {
 	read_first_n_lines(event,
-			   document.getElementById("file:samples"),
+			   document.getElementById("file-samples"),
 			   30,
-			   document.getElementById("chk:heading").checked);
+			   document.getElementById("chk-heading").checked);
     });
diff --git a/uploader/static/js/urls.js b/uploader/static/js/urls.js
new file mode 100644
index 0000000..e3fb7c6
--- /dev/null
+++ b/uploader/static/js/urls.js
@@ -0,0 +1,26 @@
+function baseURL() {
+    return new URL(`${window.location.protocol}//${window.location.host}`);
+};
+
+function buildURLFromCurrentURL(pathname, searchParams = new URLSearchParams()) {
+    var uri = baseURL();
+    uri.pathname=pathname;
+    var _search = new URLSearchParams(window.location.search);
+    searchParams.forEach(function(value, key) {
+        _search.set(key, value);
+    });
+    uri.search = _search.toString();
+    return uri
+};
+
+function deleteSearchParams(url, listOfParams = []) {
+    _params = new URLSearchParams(url.search);
+    listOfParams.forEach(function(paramName) {
+        _params.delete(paramName);
+    });
+
+    
+    newUrl = new URL(url.toString());
+    newUrl.search = _params.toString();
+    return newUrl;
+}
diff --git a/uploader/static/js/utils.js b/uploader/static/js/utils.js
index 045dd47..1b31661 100644
--- a/uploader/static/js/utils.js
+++ b/uploader/static/js/utils.js
@@ -8,3 +8,30 @@ function trigger_change_event(element) {
     evt = new Event("change");
     element.dispatchEvent(evt);
 }
+
+
+var remove_class = (element, classvalue) => {
+    new_classes = (element.attr("class") || "").split(" ").map((val) => {
+        return val.trim();
+    }).filter((val) => {
+        return ((val !== classvalue) &&
+                (val !== ""))
+    }).join(" ");
+
+    if(new_classes === "") {
+        element.removeAttr("class");
+    } else {
+        element.attr("class", new_classes);
+    }
+};
+
+
+var add_class = (element, classvalue) => {
+    remove_class(element, classvalue);
+    element.attr("class", (element.attr("class") || "") + " " + classvalue);
+};
+
+$(".not-implemented").click((event) => {
+    event.preventDefault();
+    alert("This feature is not implemented yet. Please bear with us.");
+});