diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7b2f46b5dc..551a37799b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,7 +2,7 @@ on: [push, pull_request] jobs: run_tests: - runs-on: ubuntu-16.04 + runs-on: ubuntu-20.04 name: StochSS Continuous Testing steps: # Checkout diff --git a/.github/workflows/pylint_on_pull_request.yml b/.github/workflows/pylint_on_pull_request.yml index dfb9bf6f68..d881a16898 100644 --- a/.github/workflows/pylint_on_pull_request.yml +++ b/.github/workflows/pylint_on_pull_request.yml @@ -2,7 +2,7 @@ name: PyLint On Pull Request on: [pull_request] jobs: build: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 steps: - name: Set Up Python uses: actions/setup-python@v2 @@ -26,37 +26,50 @@ jobs: env: HEAD_REF: ${{ github.event.pull_request.head.ref }} if: always() + - name: Set Base Lint to 0 + run: echo BASE_LINT=0 >> $GITHUB_ENV + if: env.BASE_LINT == '' - name: Checkout Head run: git checkout $HEAD_REF env: HEAD_REF: ${{ github.event.pull_request.head.ref }} - - name: Get Lint Delta Sign (+/-) - run: | - echo PASSING_SCORE=$(git diff --name-only --diff-filter=M $BASE_REF | grep -E "\.py" | xargs pylint | grep -Eo "10, [+-]" | grep -Eo [+-] ) >> $GITHUB_ENV - env: - BASE_REF: ${{ github.event.pull_request.base.ref }} - if: always() - name: Get Head Lint Score run: | echo HEAD_LINT=$(git diff --name-only --diff-filter=M $BASE_REF | grep -E "\.py" | xargs pylint | grep -E -o "at [0-9.-]+" | grep -E -o [0-9.-]+) >> $GITHUB_ENV env: BASE_REF: ${{ github.event.pull_request.base.ref }} if: always() + - name: Set Head Lint to 0 + run: echo HEAD_LINT=0 >> $GITHUB_ENV + if: env.HEAD_LINT == '' - name: Get Added Files Lint Score run: | echo ADDED_LINT=$(git diff --name-only --diff-filter=A $BASE_REF | grep -E "\.py" | xargs pylint | grep -E -o "at [0-9.-]+" | grep -E -o [0-9.-]+) >> $GITHUB_ENV env: BASE_REF: ${{ github.event.pull_request.base.ref }} if: always() + - name: Get Delta + run: | + import os + base = float(os.environ['BASE_LINT']) + head = float(os.environ['HEAD_LINT']) + delta = head - base + os.popen(f"echo DELTA={round(delta, 2)} >> $GITHUB_ENV") + shell: python - name: Display Results run: | echo "Lint of modified files in base:" echo ${{ env.BASE_LINT }} echo "Lint of modified files in head:" echo ${{ env.HEAD_LINT }} + echo "Delta (+/-):" + echo ${{ env.DELTA }} echo "Lint of files added by head:" echo ${{ env.ADDED_LINT }} if: always() - name: Fail If Negative Delta - run: exit 1 - if: env.PASSING_SCORE == '-' + run: | + import os + if float(os.environ['HEAD_LINT']) < 9 and float(os.environ['DELTA']) < 0: + raise Exception("Head lint score < 9 and negative delta.") + shell: python diff --git a/.gitignore b/.gitignore index 73262f68be..101668ad28 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ stochss/dist/stochss-project-manager.html stochss/dist/multiple-plots-page.html stochss/dist/stochss-domain-editor.html stochss/dist/stochss-loading-page.html -stochss/dist/stochss-project-browser.html stochss/dist/stochss-quick-start.html stochss/dist/stochss-user-home.html jupyterhub/templates/page.html @@ -19,6 +18,8 @@ jupyterhub/templates/stochss-home.html jupyterhub/templates/stochss-job-presentation.html jupyterhub/templates/stochss-model-presentation.html jupyterhub/templates/stochss-notebook-presentation.html +jupyterhub/templates/multiple-plots-page.html + *.swp *.swo package-lock.json diff --git a/Dockerfile b/Dockerfile index 2dbc9102d1..27d57c6e4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,10 @@ COPY --chown=jovyan:users public_models/ /home/jovyan/Examples COPY --chown=jovyan:users . /stochss +COPY --chown=jovyan:users /stochss/dist/favicon.ico $JUPYTER_CONFIG_DIR/custom/favicon.ico + +COPY --chown=jovyan:users custom.js $JUPYTER_CONFIG_DIR/custom/custom.js + COPY --chown=jovyan:users stochss-logo.png $JUPYTER_CONFIG_DIR/custom/logo.png COPY --chown=jovyan:users custom.css $JUPYTER_CONFIG_DIR/custom/custom.css diff --git a/Makefile b/Makefile index 5fff122244..ee4955a7aa 100644 --- a/Makefile +++ b/Makefile @@ -65,15 +65,19 @@ jupyterhub/secrets/postgres.env: jupyterhub/userlist: @echo "You're missing a userlist file. We'll make a blank one for you." @echo "If you'd like to set admins to jupyterhub, add entries to the userlist file" - @echo "in the jupyterhub/ directory like this (one user per line):" - @echo "myuser@uni.edu admin" + @echo " in the jupyterhub/ directory like this (one user per line):" + @echo " myuser@uni.edu admin" + @echo "If you'd like to set power users who are exempt from resource constraints" + @echo " use entries like this:" + @echo " myuser@uni.edu power" + @echo "" @touch $@ check-files: jupyterhub/userlist jupyterhub/secrets/.oauth.dummy.env jupyterhub/secrets/postgres.env check_files_staging: check-files jupyterhub/secrets/.oauth.staging.env -check_files_prod: check-files jupyterhub/secrets/.oauth.prod.env +check_files_prod: check-files jupyterhub/secrets/.oauth.prod.env jupyterhub/.power_users cert: @echo "Generating certificate..." @@ -126,17 +130,17 @@ hub: build_hub build run_hub_dev build_clean: docker build \ --build-arg JUPYTER_CONFIG_DIR=$(JUPYTER_CONFIG_DIR) \ - --no-cache -t $(DOCKER_STOCHSS_IMAGE):latest . + --no-cache -t $(DOCKER_STOCHSS_IMAGE):latest . create_working_dir: $(DOCKER_SETUP_COMMAND) -build: +build: docker build \ --build-arg JUPYTER_CONFIG_DIR=$(JUPYTER_CONFIG_DIR) \ - -t $(DOCKER_STOCHSS_IMAGE):latest . + -t $(DOCKER_STOCHSS_IMAGE):latest . -test: create_working_dir +test: create_working_dir docker run --rm \ --name $(DOCKER_STOCHSS_IMAGE) \ --env-file .env \ @@ -144,11 +148,11 @@ test: create_working_dir -v $(DOCKER_WORKING_DIR):/home/jovyan/ \ -p 8888:8888 \ $(DOCKER_STOCHSS_IMAGE):latest \ - /stochss/stochss/tests/run_tests.py + /stochss/stochss/tests/run_tests.py build_and_test: build test -run: create_working_dir +run: create_working_dir $(PYTHON_EXE) launch_webbrowser.py & docker run --rm \ --name $(DOCKER_STOCHSS_IMAGE) \ @@ -156,7 +160,7 @@ run: create_working_dir -v $(DOCKER_WORKING_DIR):/home/jovyan/ \ -p 8888:8888 \ $(DOCKER_STOCHSS_IMAGE):latest \ - bash -c "cd /home/jovyan; start-notebook.sh " + bash -c "cd /home/jovyan; start-notebook.sh " build_and_run: build run diff --git a/README.md b/README.md index 8914e285de..758a0fdaf2 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ StochSS uses [JupyterHub](https://jupyterhub.readthedocs.io/en/stable/#) as the - [Optional] To set admins for JupyterHub, make a file called `userlist` in the `jupyterhub/` directory. On each line of this file place a username followed by the word 'admin'. For example: `myuser admin`. If using Google OAuth, the uesername will be a Gmail address. Navigate to `/hub/admin` to use the JupyterHub admin interface. -- [Optional] By default multi-user StochSS is set up to allocate 2 logical cpus per user, reserving 2 logical cpus for the hub container and underlying OS. You can define a list of "power users" that are excluded from resource limitations by adding a text file called `.power_users` (note the leading period) to the `jupyterhub/` directory with one username/email address on each line of the file. +- [Optional] By default multi-user StochSS is set up to allocate 2 logical cpus per user, reserving 2 logical cpus for the hub container and underlying OS. You can define "power users" that are excluded from resource limitations using the same method as above for adding an admin, but instead of following the username with 'admin', use the keyword 'power' instead. ### Run Locally diff --git a/__version__.py b/__version__.py index 294d16e551..124e6784c9 100644 --- a/__version__.py +++ b/__version__.py @@ -5,7 +5,7 @@ # @website https://github.com/stochss/stochss # ============================================================================= -__version__ = '2.3.12' +__version__ = '2.4' __title__ = 'StochSS' __description__ = 'StochSS is an integrated development environment (IDE) \ for simulation of biochemical networks.' diff --git a/client/app.js b/client/app.js index e249597e0d..6f72e7c34f 100644 --- a/client/app.js +++ b/client/app.js @@ -132,21 +132,21 @@ let getBrowser = () => { return {"name":BrowserDetect.browser,"version":BrowserDetect.version}; } -let validateName = (input, rename = false) => { - var error = "" +let validateName = (input, {rename=false, saveAs=true}={}) => { + var error = ""; if(input.endsWith('/')) { - error = 'forward' + error = 'forward'; } - var invalidChars = "`~!@#$%^&*=+[{]}\"|:;'<,>?\\" - if(rename) { - invalidChars += "/" + var invalidChars = "`~!@#$%^&*=+[{]}\"|:;'<,>?\\"; + if(rename || !saveAs) { + invalidChars += "/"; } for(var i = 0; i < input.length; i++) { if(invalidChars.includes(input.charAt(i))) { - error = error === "" || error === "special" ? "special" : "both" + error = error === "" || error === "special" ? "special" : "both"; } } - return error + return error; } let newWorkflow = (parent, mdlPath, isSpatial, type) => { @@ -157,7 +157,7 @@ let newWorkflow = (parent, mdlPath, isSpatial, type) => { let ext = isSpatial ? /.smdl/g : /.mdl/g let typeCode = type === "Ensemble Simulation" ? "_ES" : "_PS"; let name = mdlPath.split('/').pop().replace(ext, typeCode) - let modal = $(modals.newWorkflowHtml(name, type)).modal(); + let modal = $(modals.createWorkflowHtml(name, type)).modal(); let okBtn = document.querySelector('#newWorkflowModal .ok-model-btn'); let input = document.querySelector('#newWorkflowModal #workflowNameInput'); okBtn.disabled = false; @@ -213,12 +213,22 @@ documentSetup = () => { } copyToClipboard = (text, success, error) => { + fullURL = window.location.protocol + '//' + window.location.hostname + text; if (window.clipboardData && window.clipboardData.setData) { // Internet Explorer-specific code path to prevent textarea being shown while dialog is visible. - return window.clipboardData.setData("Text", text); + return window.clipboardData.setData("Text", fullURL); } else { - navigator.clipboard.writeText(text).then(success, error) + navigator.clipboard.writeText(fullURL).then(success, error) + } +} + +let switchToEditTab = (view, section) => { + let elementID = Boolean(view.model && view.model.elementID) ? view.model.elementID + "-" : ""; + if($(view.queryByHook(elementID + 'view-' + section)).hasClass('active')) { + $(view.queryByHook(elementID + section + '-edit-tab')).tab('show'); + $(view.queryByHook(elementID + 'edit-' + section)).addClass('active'); + $(view.queryByHook(elementID + 'view-' + section)).removeClass('active'); } } @@ -234,7 +244,9 @@ module.exports = { postXHR: postXHR, tooltipSetup: tooltipSetup, documentSetup: documentSetup, - copyToClipboard: copyToClipboard + copyToClipboard: copyToClipboard, + switchToEditTab: switchToEditTab, + validateName: validateName }; diff --git a/client/file-config.js b/client/file-config.js new file mode 100644 index 0000000000..c8730987a1 --- /dev/null +++ b/client/file-config.js @@ -0,0 +1,414 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2021 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +let path = require('path'); +//support files +let app = require('./app'); +let modals = require('./modals'); + +let contextZipTypes = ["workflow", "folder", "other", "project", "root"]; + +let doubleClick = (view, e) => { + let node = $('#files-jstree').jstree().get_node(e.target); + if(!(node.original._path.split("/")[0] === "trash")) { + if(node.type === "folder" && $('#files-jstree').jstree().is_open(node) && $('#files-jstree').jstree().is_loaded(node)){ + view.refreshJSTree(node); + }else if(node.type === "nonspatial" || node.type === "spatial"){ + view.openModel(node.original._path); + }else if(node.type === "notebook"){ + view.openNotebook(node.original._path); + }else if(node.type === "sbmlModel"){ + view.openSBML(node.original._path); + }else if(node.type === "project"){ + view.openProject(node.original._path); + }else if(node.type === "workflow"){ + view.openWorkflow(node.original._path); + }else if(node.type === "domain") { + view.openDomain(node.original._path); + }else if(node.type === "other"){ + if(node.text.endsWith(".zip")) { + view.extractAll(node); + }else{ + view.openFile(node.original._path); + } + } + } +} + +let getDomainContext = (view, node) => { + if(node.original._path.split("/")[0] === "trash") { // item in trash + return {delete: view.getDeleteContext(node, "directory")}; + } + let downloadOptions = {dataType: "json", identifier: "spatial-model/load-domain"}; + return { + open: view.buildContextBaseWithClass({ + label: "Open", + action: (data) => { + view.openDomain(node.original._path); + } + }), + download: view.getDownloadContext(node, downloadOptions), + rename: view.getRenameContext(node), + duplicate: view.getDuplicateContext(node, "file/duplicate"), + moveToTrash: view.getMoveToTrashContext(node, "domain") + } +} + +let getFolderContext = (view, node) => { + if(node.text === "trash") {//Trash node + return {refresh: view.getRefreshContext(node)}; + } + if(node.original._path.split("/")[0] === "trash") { // item in trash + return {delete: view.getDeleteContext(node, "directory")}; + } + let dirname = node.original._path; + let downloadOptions = {dataType: "zip", identifier: "file/download-zip"}; + let options = {asZip: true}; + return { + refresh: view.getRefreshContext(node), + newDirectory: view.getNewDirectoryContext(node), + newProject: view.buildContextBase({ + label: "New Project", + action: (data) => { + view.createProject(node, dirname); + } + }), + newModel: view.getNewModelContext(node, false), + newDomain: view.getNewDomainContext(node), + upload: view.getFullUploadContext(node, false), + download: view.getDownloadContext(node, downloadOptions, options), + rename: view.getRenameContext(node), + duplicate: view.getDuplicateContext(node, "directory/duplicate"), + moveToTrash: view.getMoveToTrashContext(node, "directory") + } +} + +let getModelContext = (view, node) => { + if(node.original._path.split("/")[0] === "trash") { // item in trash + return {delete: view.getDeleteContext(node, "model")}; + } + let downloadOptions = {dataType: "json", identifier: "file/json-data"}; + return { + edit: view.getEditModelContext(node), + newWorkflow: view.getFullNewWorkflowContext(node), + convert: view.getMdlConvertContext(node), + download: view.getDownloadContext(node, downloadOptions), + rename: view.getRenameContext(node), + duplicate: view.getDuplicateContext(node, "file/duplicate"), + moveToTrash: view.getMoveToTrashContext(node, "model") + } +} + +let getNotebookContext = (view, node) => { + if(node.original._path.split("/")[0] === "trash") { // item in trash + return {delete: view.getDeleteContext(node, "directory")}; + } + let open = view.getOpenNotebookContext(node); + let downloadOptions = {dataType: "json", identifier: "file/json-data"}; + let download = view.getDownloadContext(node, downloadOptions); + let rename = view.getRenameContext(node); + let duplicate = view.getDuplicateContext(node, "file/duplicate"); + let moveToTrash = view.getMoveToTrashContext(node, "notebook"); + if(app.getBasePath() === "/") { + return { + open: open, download: download, rename: rename, + duplicate: duplicate, moveToTrash: moveToTrash + } + } + return { + open: open, + publish: view.getPublishNotebookContext(node), + download: download, rename: rename, + duplicate: duplicate, moveToTrash: moveToTrash + } +} + +let getOtherContext = (view, node) => { + if(node.original._path.split("/")[0] === "trash") { // project in trash + return {delete: view.getDeleteContext(node, "file")}; + } + let open = view.getOpenFileContext(node); + let downloadOptions = {dataType: "zip", identifier: "file/download-zip"}; + let options = {asZip: true}; + let download = view.getDownloadContext(node, downloadOptions, options); + let rename = view.getRenameContext(node); + let duplicate = view.getDuplicateContext(node, "file/duplicate"); + let moveToTrash = view.getMoveToTrashContext(node, "file"); + if(node.text.endsWith(".zip")) { + return { + extractAll: view.getExtractAllContext(node), + download: download, rename: rename, + duplicate: duplicate, moveToTrash: moveToTrash + } + } + return { + open: open, download: download, rename: rename, + duplicate: duplicate, moveToTrash: moveToTrash + } +} + +let getOpenProjectContext = (view, node) => { + return view.buildContextBaseWithClass({ + label: "Open", + action: (data) => { + view.openProject(node.original._path); + } + }); +} + +let getProjectContext = (view, node) => { + if(node.original._path.split("/")[0] === "trash") { // project in trash + return {delete: view.getDeleteContext(node, "project")}; + } + return { + open: getOpenProjectContext(view, node), + addModel: view.getAddModelContext(node), + download: view.getDownloadWCombineContext(node), + rename: view.getRenameContext(node), + duplicate: view.getDuplicateContext(node, "directory/duplicate"), + moveToTrash: view.getMoveToTrashContext(node, "project") + } +} + +let getRootContext = (view, node) => { + let dirname = node.original._path === "/" ? "" : node.original._path; + return { + refresh: view.getRefreshContext(node), + newDirectory: view.getNewDirectoryContext(node), + newProject: view.buildContextBase({ + label: "New Project", + action: (data) => { + view.createProject(node, dirname); + } + }), + newModel: view.getNewModelContext(node, false), + newDomain: view.getNewDomainContext(node), + upload: view.getFullUploadContext(node, false) + } +} + +let getSBMLContext = (view, node) => { + if(node.original._path.split("/")[0] === "trash") { // item in trash + return {delete: view.getDeleteContext(node, "directory")}; + } + let downloadOptions = {dataType: "plain-text", identifier: "file/download"}; + return { + open: view.getOpenSBMLContext(node), + convert: view.getSBMLConvertContext(node, "sbml/to-model"), + download: view.getDownloadContext(node, downloadOptions), + rename: view.getRenameContext(node), + duplicate: view.getDuplicateContext(node, "file/duplicate"), + moveToTrash: view.getMoveToTrashContext(node, "sbml model") + } +} + +let getSpatialModelContext = (view, node) => { + if(node.original._path.split("/")[0] === "trash") { // project in trash + return {delete: view.getDeleteContext(node, "spatial model")}; + } + let downloadOptions = {dataType: "json", identifier: "file/json-data"}; + return { + edit: view.getEditModelContext(node), + newWorkflow: view.buildContextWithSubmenus({ + label: "New Workflow", + submenu: { + jupyterNotebook: view.getNotebookNewWorkflowContext(node) + } + }), + convert: view.getSmdlConvertContext(node, "spatial/to-model"), + download: view.getDownloadContext(node, downloadOptions), + rename: view.getRenameContext(node), + duplicate: view.getDuplicateContext(node, "file/duplicate"), + moveToTrash: view.getMoveToTrashContext(node, "spatial model") + } +} + +let getWorkflowContext = (view, node) => { + if(node.original._path.split("/")[0] === "trash") { // project in trash + return {delete: view.getDeleteContext(node, "workflow")}; + } + let duplicateOptions = {target: "workflow", cb: (body) => { + let title = `Model for ${body.File}`; + if(body.error){ + view.reportError({Reason: title, Message: body.error}); + }else{ + if(document.querySelector("#successModal")) { + document.querySelector("#successModal").remove(); + } + let message = `The model for ${body.File} is located here: ${body.mdlPath}`; + let modal = $(modals.successHtml(message, {title: title})).modal(); + } + }} + if(!node.original._newFormat) { + duplicateOptions['timeStamp'] = view.getTimeStamp(); + } + let downloadOptions = {dataType: "zip", identifier: "file/download-zip"}; + let options = {asZip: true}; + return { + open: view.getOpenWorkflowContext(node), + model: view.getWorkflowMdlContext(node), + download: view.getDownloadContext(node, downloadOptions, options), + rename: view.getRenameContext(node), + duplicate: view.getDuplicateContext(node, "workflow/duplicate", duplicateOptions), + moveToTrash: view.getMoveToTrashContext(node, "workflow") + } +} + +let move = (view, par, node) => { + let newDir = par.original._path !== "/" ? par.original._path : ""; + let file = node.original._path.split('/').pop(); + let oldPath = node.original._path; + let queryStr = `?srcPath=${oldPath}&dstPath=${path.join(newDir, file)}`; + let endpoint = path.join(app.getApiPath(), "file/move") + queryStr; + app.getXHR(endpoint, { + success: (err, response, body) => { + node.original._path = path.join(newDir, file); + if(newDir.endsWith("trash")) { + $(view.queryByHook('empty-trash')).prop('disabled', false); + } + view.refreshJSTree(par); + }, + error: (err, response, body) => { + body = JSON.parse(body); + view.refreshJSTree(par); + } + }); +} + +let setup = (view) => { + $(view.queryByHook("fb-proj-seperator")).css("display", "none"); + $(view.queryByHook("fb-import-model")).css("display", "none"); +} + +let toModel = (view, node, identifier) => { + let queryStr = `?path=${node.original._path}`; + let endpoint = path.join(app.getApiPath(), identifier) + queryStr; + app.getXHR(endpoint, { + success: (err, response, body) => { + let par = $('#files-jstree').jstree().get_node(node.parent); + view.refreshJSTree(par); + view.selectNode(par, body.File); + if(identifier.startsWith("sbml") && body.errors.length > 0){ + if(document.querySelector('#sbmlToModelModal')) { + document.querySelector('#sbmlToModelModal').remove(); + } + let modal = $(modals.sbmlToModelHtml(body.message, body.errors)).modal(); + } + } + }); +} + +let toSBML = (view, node) => { + let queryStr = `?path=${node.original._path}`; + let endpoint = path.join(app.getApiPath(), "model/to-sbml") + queryStr; + app.getXHR(endpoint, { + success: (err, response, body) => { + let par = $('#files-jstree').jstree().get_node(node.parent); + view.refreshJSTree(par); + view.selectNode(par, body.File); + } + }); +} + +let toSpatial = (view, node) => { + let queryStr = `?path=${node.original._path}`; + let endpoint = path.join(app.getApiPath(), "model/to-spatial") + queryStr; + app.getXHR(endpoint, { + success: (err, response, body) => { + let par = $('#files-jstree').jstree().get_node(node.parent); + view.refreshJSTree(par); + view.selectNode(par, body.File); + } + }); +} + +let types = { + 'root' : {"icon": "jstree-icon jstree-folder"}, + 'folder' : {"icon": "jstree-icon jstree-folder"}, + 'spatial' : {"icon": "jstree-icon jstree-file"}, + 'nonspatial' : {"icon": "jstree-icon jstree-file"}, + 'project' : {"icon": "jstree-icon jstree-file"}, + 'workflow' : {"icon": "jstree-icon jstree-file"}, + 'notebook' : {"icon": "jstree-icon jstree-file"}, + 'domain' : {"icon": "jstree-icon jstree-file"}, + 'sbmlModel' : {"icon": "jstree-icon jstree-file"}, + 'other' : {"icon": "jstree-icon jstree-file"} +} + +let updateParent = (view, type) => { + if(type === "project") { + view.parent.update("Projects"); + }else if(type === "Presentations") { + view.parent.update(type); + } +} + +let validateMove = (view, node, more, pos) => { + // Check if workflow is running + let validSrc = Boolean(node && node.type && node.original && node.original.text !== "trash"); + let isWorkflow = Boolean(validSrc && node.type === "workflow"); + if(isWorkflow && node.original._status && node.original._status === "running") { return false }; + + // Check if file is moving to a valid location + let validDsts = ["root", "folder"]; + let validDst = Boolean(more && more.ref && more.ref.type && more.ref.original); + if(validDst && !validDsts.includes(more.ref.type)) { return false }; + if(validDst && path.dirname(more.ref.original._path).split("/").includes("trash")) { return false }; + + // Check if file already exists with that name in folder + if(validDst && more.ref.type === 'folder' && more.ref.text !== "trash"){ + if(!more.ref.state.loaded) { return false }; + try{ + let BreakException = {}; + more.ref.children.forEach((child) => { + let child_node = $('#files-jstree').jstree().get_node(child); + let exists = child_node.text === node.text; + if(exists) { throw BreakException; }; + }); + }catch{ return false; }; + } + + // Check if curser is over the correct location + if(more && (pos != 0 || more.pos !== "i") && !more.core) { return false }; + return true; +} + +module.exports = { + contextZipTypes: contextZipTypes, + doubleClick: doubleClick, + getDomainContext: getDomainContext, + getFolderContext: getFolderContext, + getModelContext: getModelContext, + getNotebookContext: getNotebookContext, + getOtherContext: getOtherContext, + getProjectContext: getProjectContext, + getRootContext: getRootContext, + getSBMLContext: getSBMLContext, + getSpatialModelContext: getSpatialModelContext, + getWorkflowContext: getWorkflowContext, + getWorflowGroupContext: getOtherContext, + move: move, + setup: setup, + toModel: toModel, + toSBML: toSBML, + toSpatial: toSpatial, + types: types, + updateParent: updateParent, + validateMove: validateMove +} diff --git a/client/job-view/templates/gillespyResultsEnsembleView.pug b/client/job-view/templates/gillespyResultsEnsembleView.pug index 3bcdc2856e..0c8d595de5 100644 --- a/client/job-view/templates/gillespyResultsEnsembleView.pug +++ b/client/job-view/templates/gillespyResultsEnsembleView.pug @@ -50,19 +50,32 @@ div#workflow-results.card button.btn.btn-primary.box-shadow(data-hook="stddevran-edit-plot" data-target="edit-plot" disabled) Edit Plot - button.btn.btn-primary.box-shadow( - data-hook="stddevran-download-png-custom" - data-target="download-png-custom" - data-type="stddevran" + button.btn.btn-primary.box-shadow.dropdown-toggle( + id="stddevran-download" + data-hook="stddevran-download" + data-toggle="dropdown", + aria-haspopup="true", + aria-expanded="false", + type="button" disabled - ) Download PNG - - button.btn.btn-primary.box-shadow( - data-hook="stddevran-download-json" - data-target="download-json" - data-type="stddevran" - disabled - ) Download JSON + ) Download + + ul.dropdown-menu(aria-labelledby="#stddevran-download") + li.dropdown-item( + data-hook="stddevran-download-png-custom" + data-target="download-png-custom" + data-type="stddevran" + ) Plot as .png + li.dropdown-item( + data-hook="stddevran-download-json" + data-target="download-json" + data-type="stddevran" + ) Plot as .json + li.dropdown-item( + data-hook="stddevran-plot-csv" + data-target="download-plot-csv" + data-type="stddevran" + ) Plot Results as .csv div.card @@ -91,19 +104,32 @@ div#workflow-results.card button.btn.btn-primary.box-shadow(data-hook="multiple-plots", data-type="mltplplt" disabled) Multiple Plots - button.btn.btn-primary.box-shadow( - data-hook="trajectories-download-png-custom" - data-target="download-png-custom" - data-type="trajectories" + button.btn.btn-primary.box-shadow.dropdown-toggle( + id="trajectories-download" + data-hook="trajectories-download" + data-toggle="dropdown", + aria-haspopup="true", + aria-expanded="false", + type="button" disabled - ) Download PNG - - button.btn.btn-primary.box-shadow( - data-hook="trajectories-download-json" - data-target="download-json" - data-type="trajectories" - disabled - ) Download JSON + ) Download + + ul.dropdown-menu(aria-labelledby="#trajectories-download") + li.dropdown-item( + data-hook="trajectories-download-png-custom" + data-target="download-png-custom" + data-type="trajectories" + ) Plot as .png + li.dropdown-item( + data-hook="trajectories-download-json" + data-target="download-json" + data-type="trajectories" + ) Plot as .json + li.dropdown-item( + data-hook="trajectories-plot-csv" + data-target="download-plot-csv" + data-type="trajectories" + ) Plot Results as .csv div.card @@ -130,19 +156,32 @@ div#workflow-results.card button.btn.btn-primary.box-shadow(data-hook="stddev-edit-plot" data-target="edit-plot" disabled) Edit Plot - button.btn.btn-primary.box-shadow( - data-hook="stddev-download-png-custom" - data-target="download-png-custom" - data-type="stddev" + button.btn.btn-primary.box-shadow.dropdown-toggle( + id="stddev-download" + data-hook="stddev-download" + data-toggle="dropdown", + aria-haspopup="true", + aria-expanded="false", + type="button" disabled - ) Download PNG - - button.btn.btn-primary.box-shadow( - data-hook="stddev-download-json" - data-target="download-json" - data-type="stddev" - disabled - ) Download JSON + ) Download + + ul.dropdown-menu(aria-labelledby="#stddev-download") + li.dropdown-item( + data-hook="stddev-download-png-custom" + data-target="download-png-custom" + data-type="stddev" + ) Plot as .png + li.dropdown-item( + data-hook="stddev-download-json" + data-target="download-json" + data-type="stddev" + ) Plot as .json + li.dropdown-item( + data-hook="stddev-plot-csv" + data-target="download-plot-csv" + data-type="stddev" + ) Plot Results as .csv div.card @@ -169,36 +208,54 @@ div#workflow-results.card button.btn.btn-primary.box-shadow(data-hook="avg-edit-plot" data-target="edit-plot" disabled) Edit Plot - button.btn.btn-primary.box-shadow( - data-hook="avg-download-png-custom" - data-target="download-png-custom" - data-type="avg" + button.btn.btn-primary.box-shadow.dropdown-toggle( + id="avg-download" + data-hook="avg-download" + data-toggle="dropdown", + aria-haspopup="true", + aria-expanded="false", + type="button" disabled - ) Download PNG + ) Download - button.btn.btn-primary.box-shadow( - data-hook="avg-download-json" - data-target="download-json" - data-type="avg" - disabled - ) Download JSON + ul.dropdown-menu(aria-labelledby="#avg-download") + li.dropdown-item( + data-hook="avg-download-png-custom" + data-target="download-png-custom" + data-type="avg" + ) Plot as .png + li.dropdown-item( + data-hook="avg-download-json" + data-target="download-json" + data-type="avg" + ) Plot as .json + li.dropdown-item( + data-hook="avg-plot-csv" + data-target="download-plot-csv" + data-type="avg" + ) Plot Results as .csv + + div + + button.btn.btn-primary.box-shadow(id="convert-to-notebook" data-hook="convert-to-notebook") Convert to Notebook - button.btn.btn-primary.box-shadow(id="convert-to-notebook" data-hook="convert-to-notebook") Convert to Notebook + button.btn.btn-primary.box-shadow(id="download-results-csv" data-hook="download-results-csv") Download Full Results as .csv - button.btn.btn-primary.box-shadow(id="download-results-csv" data-hook="download-results-csv") Download Results as .csv + button.btn.btn-primary.box-shadow(id="job-presentation" data-hook="job-presentation") Publish - button.btn.btn-primary.box-shadow(id="job-presentation" data-hook="job-presentation") Publish + div.saving-status(data-hook="job-action-start") - div.saving-status(data-hook="job-action-start") + div.spinner-grow - div.spinner-grow + span Publishing ... - span Publishing ... + div.saved-status(data-hook="job-action-end") - div.saved-status(data-hook="job-action-end") + span Published - span Published + div.save-error-status(data-hook="job-action-err") - div.save-error-status(data-hook="job-action-err") + span Error - span Error + div.text-info(data-hook="update-format-message" style="display: none;") + | To publish you job the workflows format must be updated. diff --git a/client/job-view/templates/gillespyResultsView.pug b/client/job-view/templates/gillespyResultsView.pug index a7a9814814..3684e21ee1 100644 --- a/client/job-view/templates/gillespyResultsView.pug +++ b/client/job-view/templates/gillespyResultsView.pug @@ -50,36 +50,54 @@ div#workflow-results.card button.btn.btn-primary.box-shadow(data-hook="trajectories-edit-plot" data-target="edit-plot" disabled) Edit Plot - button.btn.btn-primary.box-shadow( - data-hook="trajectories-download-png-custom" - data-target="download-png-custom" - data-type="trajectories" + button.btn.btn-primary.box-shadow.dropdown-toggle( + id="trajectories-download" + data-hook="trajectories-download" + data-toggle="dropdown", + aria-haspopup="true", + aria-expanded="false", + type="button" disabled - ) Download PNG + ) Download - button.btn.btn-primary.box-shadow( - data-hook="trajectories-download-json" - data-target="download-json" - data-type="trajectories" - disabled - ) Download JSON + ul.dropdown-menu(aria-labelledby="#trajectories-download") + li.dropdown-item( + data-hook="trajectories-download-png-custom" + data-target="download-png-custom" + data-type="trajectories" + ) Plot as .png + li.dropdown-item( + data-hook="trajectories-download-json" + data-target="download-json" + data-type="trajectories" + ) Plot as .json + li.dropdown-item( + data-hook="trajectories-plot-csv" + data-target="download-plot-csv" + data-type="trajectories" + ) Plot Results as .csv + + div + + button.btn.btn-primary.box-shadow(id="convert-to-notebook" data-hook="convert-to-notebook") Convert to Notebook - button.btn.btn-primary.box-shadow(id="convert-to-notebook" data-hook="convert-to-notebook") Convert to Notebook + button.btn.btn-primary.box-shadow(id="download-results-csv" data-hook="download-results-csv") Download Full Results as .csv - button.btn.btn-primary.box-shadow(id="download-results-csv" data-hook="download-results-csv") Download Results as .csv + button.btn.btn-primary.box-shadow(id="job-presentation" data-hook="job-presentation") Publish - button.btn.btn-primary.box-shadow(id="job-presentation" data-hook="job-presentation") Publish + div.saving-status(data-hook="job-action-start") - div.saving-status(data-hook="job-action-start") + div.spinner-grow - div.spinner-grow + span Publishing ... - span Publishing ... + div.saved-status(data-hook="job-action-end") - div.saved-status(data-hook="job-action-end") + span Published - span Published + div.save-error-status(data-hook="job-action-err") - div.save-error-status(data-hook="job-action-err") + span Error - span Error + div.text-info(data-hook="update-format-message" style="display: none;") + | To publish you job the workflows format must be updated. diff --git a/client/job-view/templates/parameterScanResultsView.pug b/client/job-view/templates/parameterScanResultsView.pug index 94add97d48..a3704a375f 100644 --- a/client/job-view/templates/parameterScanResultsView.pug +++ b/client/job-view/templates/parameterScanResultsView.pug @@ -70,19 +70,32 @@ div#workflow-results.card button.btn.btn-primary.box-shadow(data-hook="multiple-plots" data-type="ts-psweep-mp" style="display: none;" disabled) Multiple Plots - button.btn.btn-primary.box-shadow( - data-hook="ts-psweep-download-png-custom" - data-target="download-png-custom" - data-type="ts-psweep" + button.btn.btn-primary.box-shadow.dropdown-toggle( + id="ts-psweep-download" + data-hook="ts-psweep-download" + data-toggle="dropdown", + aria-haspopup="true", + aria-expanded="false", + type="button" disabled - ) Download PNG + ) Download - button.btn.btn-primary.box-shadow( - data-hook="ts-psweep-download-json" - data-target="download-json" - data-type="ts-psweep" - disabled - ) Download JSON + ul.dropdown-menu(aria-labelledby="#ts-psweep-download") + li.dropdown-item( + data-hook="ts-psweep-download-png-custom" + data-target="download-png-custom" + data-type="ts-psweep" + ) Plot as .png + li.dropdown-item( + data-hook="ts-psweep-download-json" + data-target="download-json" + data-type="ts-psweep" + ) Plot as .json + li.dropdown-item( + data-hook="ts-psweep-plot-csv" + data-target="download-plot-csv" + data-type="ts-psweep" + ) Plot Results as .csv div.col-md-3 @@ -163,19 +176,32 @@ div#workflow-results.card button.btn.btn-primary.box-shadow(data-hook="psweep-edit-plot" data-target="edit-plot" disabled) Edit Plot - button.btn.btn-primary.box-shadow( - data-hook="psweep-download-png-custom" - data-target="download-png-custom" - data-type="psweep" - disabled - ) Download PNG - - button.btn.btn-primary.box-shadow( - data-hook="psweep-download-json" - data-target="download-json" - data-type="psweep" + button.btn.btn-primary.box-shadow.dropdown-toggle( + id="psweep-download" + data-hook="psweep-download" + data-toggle="dropdown", + aria-haspopup="true", + aria-expanded="false", + type="button" disabled - ) Download JSON + ) Download + + ul.dropdown-menu(aria-labelledby="#psweep-download") + li.dropdown-item( + data-hook="psweep-download-png-custom" + data-target="download-png-custom" + data-type="psweep" + ) Plot as .png + li.dropdown-item( + data-hook="psweep-download-json" + data-target="download-json" + data-type="psweep" + ) Plot as .json + li.dropdown-item( + data-hook="psweep-plot-csv" + data-target="download-plot-csv" + data-type="psweep" + ) Plot Results as .csv div.col-md-3 @@ -185,18 +211,25 @@ div#workflow-results.card div.mt-3(data-hook="ps-parameter-ranges") - button.btn.btn-primary.box-shadow(id="job-presentation" data-hook="job-presentation") Publish Presentation + div + + button.btn.btn-primary.box-shadow(id="download-results-csv" data-hook="download-results-csv") Download Full Results as .csv + + button.btn.btn-primary.box-shadow(id="job-presentation" data-hook="job-presentation") Publish Presentation + + div.saving-status(data-hook="job-action-start") - div.saving-status(data-hook="job-action-start") + div.spinner-grow - div.spinner-grow + span Publishing ... - span Publishing ... + div.saved-status(data-hook="job-action-end") - div.saved-status(data-hook="job-action-end") + span Published - span Published + div.save-error-status(data-hook="job-action-err") - div.save-error-status(data-hook="job-action-err") + span Error - span Error + div.text-info(data-hook="update-format-message" style="display: none;") + | To publish you job the workflows format must be updated. diff --git a/client/job-view/templates/parameterSweepResultsView.pug b/client/job-view/templates/parameterSweepResultsView.pug index 10676fc3eb..b0a670bb09 100644 --- a/client/job-view/templates/parameterSweepResultsView.pug +++ b/client/job-view/templates/parameterSweepResultsView.pug @@ -70,19 +70,32 @@ div#workflow-results.card button.btn.btn-primary.box-shadow(data-hook="multiple-plots" data-type="ts-psweep-mp" style="display: none;" disabled) Multiple Plots - button.btn.btn-primary.box-shadow( - data-hook="ts-psweep-download-png-custom" - data-target="download-png-custom" - data-type="ts-psweep" - disabled - ) Download PNG - - button.btn.btn-primary.box-shadow( - data-hook="ts-psweep-download-json" - data-target="download-json" - data-type="ts-psweep" + button.btn.btn-primary.box-shadow.dropdown-toggle( + id="ts-psweep-download" + data-hook="ts-psweep-download" + data-toggle="dropdown", + aria-haspopup="true", + aria-expanded="false", + type="button" disabled - ) Download JSON + ) Download + + ul.dropdown-menu(aria-labelledby="#ts-psweep-download") + li.dropdown-item( + data-hook="ts-psweep-download-png-custom" + data-target="download-png-custom" + data-type="ts-psweep" + ) Plot as .png + li.dropdown-item( + data-hook="ts-psweep-download-json" + data-target="download-json" + data-type="ts-psweep" + ) Plot as .json + li.dropdown-item( + data-hook="ts-psweep-plot-csv" + data-target="download-plot-csv" + data-type="ts-psweep" + ) Plot Results as .csv div.col-md-3 @@ -149,36 +162,54 @@ div#workflow-results.card button.btn.btn-primary.box-shadow(data-hook="psweep-edit-plot" data-target="edit-plot" disabled) Edit Plot - button.btn.btn-primary.box-shadow( - data-hook="psweep-download-png-custom" - data-target="download-png-custom" - data-type="psweep" + button.btn.btn-primary.box-shadow.dropdown-toggle( + id="psweep-download" + data-hook="psweep-download" + data-toggle="dropdown", + aria-haspopup="true", + aria-expanded="false", + type="button" disabled - ) Download PNG + ) Download - button.btn.btn-primary.box-shadow( - data-hook="psweep-download-json" - data-target="download-json" - data-type="psweep" - disabled - ) Download JSON - - button.btn.btn-primary.box-shadow(id="convert-to-notebook" data-hook="convert-to-notebook") Convert to Notebook + ul.dropdown-menu(aria-labelledby="#psweep-download") + li.dropdown-item( + data-hook="psweep-download-png-custom" + data-target="download-png-custom" + data-type="psweep" + ) Plot as .png + li.dropdown-item( + data-hook="psweep-download-json" + data-target="download-json" + data-type="psweep" + ) Plot as .json + li.dropdown-item( + data-hook="psweep-plot-csv" + data-target="download-plot-csv" + data-type="psweep" + ) Plot Results as .csv + + div + + button.btn.btn-primary.box-shadow(id="convert-to-notebook" data-hook="convert-to-notebook") Convert to Notebook + + button.btn.btn-primary.box-shadow(id="download-results-csv" data-hook="download-results-csv") Download Full Results as .csv - button.btn.btn-primary.box-shadow(id="download-results-csv" data-hook="download-results-csv") Download Results as .csv + button.btn.btn-primary.box-shadow(id="job-presentation" data-hook="job-presentation") Publish - button.btn.btn-primary.box-shadow(id="job-presentation" data-hook="job-presentation") Publish + div.saving-status(data-hook="job-action-start") - div.saving-status(data-hook="job-action-start") + div.spinner-grow - div.spinner-grow + span Publishing ... - span Publishing ... + div.saved-status(data-hook="job-action-end") - div.saved-status(data-hook="job-action-end") + span Published - span Published + div.save-error-status(data-hook="job-action-err") - div.save-error-status(data-hook="job-action-err") + span Error - span Error + div.text-info(data-hook="update-format-message" style="display: none;") + | To publish you job the workflows format must be updated. diff --git a/client/job-view/views/job-results-view.js b/client/job-view/views/job-results-view.js index a8284ed61f..3ff8518e59 100644 --- a/client/job-view/views/job-results-view.js +++ b/client/job-view/views/job-results-view.js @@ -49,9 +49,10 @@ module.exports = View.extend({ 'click [data-hook=multiple-plots]' : 'plotMultiplePlots', 'click [data-target=download-png-custom]' : 'handleDownloadPNGClick', 'click [data-target=download-json]' : 'handleDownloadJSONClick', + 'click [data-target=download-plot-csv]' : 'handlePlotCSVClick', 'click [data-hook=convert-to-notebook]' : 'handleConvertToNotebookClick', - 'click [data-hook=download-results-csv]' : 'handleDownloadResultsCsvClick', - // 'click [data-hook=job-presentation]' : 'handlePresentationClick' + 'click [data-hook=download-results-csv]' : 'handleFullCSVClick', + 'click [data-hook=job-presentation]' : 'handlePresentationClick' }, initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); @@ -79,8 +80,9 @@ module.exports = View.extend({ } }else if(app.getBasePath() === "/") { $(this.queryByHook("job-presentation")).css("display", "none"); - }else{ + }else if(!this.parent.newFormat) { $(this.queryByHook("job-presentation")).prop("disabled", true); + $(this.queryByHook("update-format-message")).css("display", "block"); } if(this.titleType === "Ensemble Simulation") { var type = isEnsemble ? "stddevran" : "trajectories"; @@ -128,13 +130,20 @@ module.exports = View.extend({ Plotly.purge(el); $(this.queryByHook(type + "-plot")).empty(); if(type === "ts-psweep" || type === "psweep"){ + $(this.queryByHook(type + "-download")).prop("disabled", true); $(this.queryByHook(type + "-edit-plot")).prop("disabled", true); - $(this.queryByHook(type + "-download-png-custom")).prop("disabled", true); - $(this.queryByHook(type + "-download-json")).prop("disabled", true); $(this.queryByHook("multiple-plots")).prop("disabled", true); } $(this.queryByHook(type + "-plot-spinner")).css("display", "block"); }, + downloadCSV: function (csvType, data) { + var queryStr = "?path=" + this.model.directory + "&type=" + csvType; + if(data) { + queryStr += "&data=" + JSON.stringify(data); + } + let endpoint = path.join(app.getApiPath(), "job/csv") + queryStr; + window.open(endpoint); + }, getPlot: function (type) { let self = this; this.cleanupPlotContainer(type); @@ -299,15 +308,22 @@ module.exports = View.extend({ let pngButton = $('div[data-hook=' + type + '-plot] a[data-title*="Download plot as a png"]')[0]; pngButton.click(); }, - handleDownloadResultsCsvClick: function (e) { - let self = this; - let queryStr = "?path=" + this.model.directory + "&action=resultscsv"; - let endpoint = path.join(app.getApiPath(), "file/download-zip") + queryStr; - app.getXHR(endpoint, { - success: function (err, response, body) { - window.open(path.join("files", body.Path)); + handleFullCSVClick: function (e) { + this.downloadCSV("full", null); + }, + handlePlotCSVClick: function (e) { + let type = e.target.dataset.type; + if(type !== "psweep") { + var data = { + data_keys: type === "ts-psweep" ? this.getDataKeys(true) : {}, + proc_key: type === "ts-psweep" ? this.tsPlotData.type : type } - }); + var csvType = "time series" + }else{ + var data = this.getDataKeys(false) + var csvType = "psweep" + } + this.downloadCSV(csvType, data); }, handlePresentationClick: function (e) { let self = this; @@ -317,11 +333,30 @@ module.exports = View.extend({ let endpoint = path.join(app.getApiPath(), "job/presentation") + queryStr; app.getXHR(endpoint, { success: function (err, response, body) { - self.endAction(); + self.endAction("publish"); + let title = body.message; + let linkHeaders = "Shareable Presentation Link"; + let links = body.links; + $(modals.presentationLinks(title, linkHeaders, links)).modal(); + let copyBtn = document.querySelector('#presentationLinksModal #copy-to-clipboard'); + copyBtn.addEventListener('click', function (e) { + let onFulfilled = (value) => { + $("#copy-link-success").css("display", "inline-block"); + } + let onReject = (reason) => { + let msg = $("#copy-link-failed"); + msg.html(reason); + msg.css("display", "inline-block"); + } + app.copyToClipboard(links.presentation, onFulfilled, onReject); + }); }, error: function (err, response, body) { + if(document.querySelector("#errorModal")) { + document.querySelector("#errorModal").remove(); + } self.errorAction(); - $(modals.newProjectModelErrorHtml(body.Reason, body.Message)).modal(); + $(modals.errorHtml(body.Reason, body.Message)).modal(); } }); }, @@ -340,8 +375,7 @@ module.exports = View.extend({ Plotly.newPlot(el, figure); $(this.queryByHook(type + "-plot-spinner")).css("display", "none"); $(this.queryByHook(type + "-edit-plot")).prop("disabled", false); - $(this.queryByHook(type + "-download-png-custom")).prop("disabled", false); - $(this.queryByHook(type + "-download-json")).prop("disabled", false); + $(this.queryByHook(type + "-download")).prop("disabled", false); if(type === "trajectories" || (this.tsPlotData && this.tsPlotData.type === "trajectories")) { $(this.queryByHook("multiple-plots")).prop("disabled", false); } diff --git a/client/modals.js b/client/modals.js index ac499bb04a..522c177435 100644 --- a/client/modals.js +++ b/client/modals.js @@ -19,564 +19,497 @@ along with this program. If not, see . let help = require('./page-help') let templates = { - input : (modalID, inputID, title, label, value) => { - return ` - ` - }, - input_long : (modalID, inputID, title, label, value) => { - return ` - ` - }, - message : (modalID, title, message) => { - return ` - ` - }, - confirmation : (modalID, title) => { - return ` - ` - }, - confirmation_with_message : (modalID, title, message) => { - return ` - ` - }, - upload : (modalID, title, accept, withName=true) => { - let displayNameField = withName ? "" : " style='display: none'" - return ` - ` - }, - select : (modalID, selectID, title, label, options) => { - return ` - ` - }, - fileSelect : (modalID, fileID, locationID, title, label, files) => { - return ` - ` - }, - presentationLinks : (modalID, title, headers, links) => { - return ` -