diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 64f8e6e4c..d7749ada0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,12 +6,12 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black - rev: 23.10.1 + rev: 23.11.0 hooks: - id: black exclude: ^docs/ - repo: https://github.com/pre-commit/mirrors-prettier - rev: "v3.0.3" + rev: "v3.1.0" hooks: - id: prettier types_or: [css, javascript] diff --git a/docs/developer_guide.rst b/docs/developer_guide.rst index 21fa9cda5..ab7b43bc4 100644 --- a/docs/developer_guide.rst +++ b/docs/developer_guide.rst @@ -14,7 +14,7 @@ Start by cloning the repository .. code-block:: bash - git clone https://github.com/catalystneuro/nwb-guide + git clone https://github.com/NeurodataWithoutBorders/nwb-guide Install Python Dependencies @@ -23,28 +23,26 @@ Install Python Dependencies Install the appropriate Python dependencies for your operating system. -Windows -""""""" +**Windows** + .. code-block:: bash conda env create -f ./environments/environment-Windows.yml +**Mac with x64 architecture** -Mac -""" .. code-block:: bash conda env create -f ./environments/environment-MAC.yml +**Mac with arm64 architecture** -M1 Mac -"""""" .. code-block:: bash conda env create -f ./environments/environment-MAC-arm64.yml -Linux -""""" +**Linux** + .. code-block:: bash conda env create -f ./environments/environment-Linux.yml @@ -84,8 +82,8 @@ Repo Structure - `pages.js` - The main code that controls which pages are rendered and how they are linked together - `stories` - Contains all the Web Components and related Storybook stories - `electron` - Contains all the Electron-related code to enable conditional inclusion for development mode - - `assets` - Contains all the frontend-facing assets (e.g. images, css, etc.) -2. **pyflask** - Contains all the source code for the backend +2. **src/renderer/assets** - Contains all the frontend-facing assets (e.g. images, css, etc.) +3. **pyflask** - Contains all the source code for the backend @@ -118,7 +116,8 @@ Starting a New Feature Adding a New Page ^^^^^^^^^^^^^^^^^ -New pages can be added by linking a component in the ``src/pages.js`` file. For example, if you wanted to add a new page called ``NewPage``, you would add the following to the configuration file: +New pages can be added by linking a component in the ``src/pages.js`` file. For example, if you wanted to +add a new page called ``NewPage``, you would add the following to the configuration file: .. code-block:: javascript @@ -155,7 +154,9 @@ New pages can be added by linking a component in the ``src/pages.js`` file. For // ... -This will automatically add the new page to the sidebar. The page itself can be defined in the ``src/stories/pages/NewPage.js`` file. For example, if you wanted to add a new page that displays a simple message, you could add the following to the ``src/stories/pages/NewPage.js`` file: +This will automatically add the new page to the sidebar. The page itself can be defined in the +``src/stories/pages/NewPage.js`` file. For example, if you wanted to add a new page that displays +a simple message, you could add the following to the ``src/stories/pages/NewPage.js`` file: .. code-block:: javascript @@ -180,17 +181,23 @@ This will automatically add the new page to the sidebar. The page itself can be } } -Extending the ``Page`` class rather than the ``LitElement`` class provides each page with standard properties and methods that allow for uniform handling across the application. +Extending the ``Page`` class rather than the ``LitElement`` class provides each page with standard properties and +methods that allow for uniform handling across the application. Discover Existing Components ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -While developing NWB GUIDE, you may find that you need to use a component that already exists in the codebase. To find a component, you can manually peruse the ``src/stories`` directory or run the project's Storybook instance to see all of the components in action. +While developing NWB GUIDE, you may find that you need to use a component that already exists in the codebase. To +find a component, you can manually peruse the ``src/stories`` directory or run the project's Storybook instance to +see all of the components in action. -To run Storybook, simply run ``npm run storybook`` in the root directory of the repository. This will start a local server that you can access using the link provided on the command line. +To run Storybook, simply run ``npm run storybook`` in the root directory of the repository. This will start a local +server that you can access using the link provided on the command line. -To see if someone else has developed a third-party component to fit your needs, you can refer to :web-components:`WebComponents.org <>` and search based on your particular needs. :npm:`NPM` may also be useful to search for third-party packages (e.g. Handsontable) that implement the feature you need. +To see if someone else has developed a third-party component to fit your needs, you can refer to +:web-components:`WebComponents.org <>` and search based on your particular needs. :npm:`NPM` may also be +useful to search for third-party packages (e.g. Handsontable) that implement the feature you need. @@ -199,25 +206,32 @@ To see if someone else has developed a third-party component to fit your needs, Testing ------- -We use Chromatic on the Storybook to test changes to front-end components as well as to demonstrate example cases of what those components would look like on a real project. +We use Chromatic on the Storybook to test changes to front-end components as well as to demonstrate example cases of +what those components would look like on a real project. -We use :pytest:`pytest <>` for testing the back-end manager and REST API. To run the tests, simply run ``pytest`` in the root directory of the repository. +We use :pytest:`pytest <>` for testing the back-end manager and REST API. To run the tests, simply run ``pytest`` in +the root directory of the repository. .. _style: Coding Style ------------ -For all JavaScript code on the frontend, we use the :prettier-code-formatter:`prettier code formatter <>` with parameters defined in the ``prettier.config.js`` configuration file. +For all JavaScript code on the frontend, we use the :prettier-code-formatter:`prettier code formatter <>` with +parameters defined in the ``prettier.config.js`` configuration file. -For all Python code on the backend, we use the :black-coding-style:`black coding style <>` with parameters defined in the ``pyproject.toml`` configuration file. +For all Python code on the backend, we use the :black-coding-style:`black coding style <>` with parameters defined +in the ``pyproject.toml`` configuration file. Pre-Commit ^^^^^^^^^^ -We use an automated pre-commit bot to enforce these on the main repo, but contributions from external forks would either have to grant bot permissions on their own fork (via :pre-commit-bot:`the pre-commit bot website <>`) or run pre-commit manually. +We use an automated pre-commit bot to enforce these on the main repo, but contributions from external forks would +either have to grant bot permissions on their own fork (via :pre-commit-bot:`the pre-commit bot website <>`) or +run pre-commit manually. -For instructions to install pre-commit, as well as some other minor coding styles we follow, refer to the :neuroconv-coding-style:`NeuroConv style guide <>`. +For instructions to install pre-commit, as well as some other minor coding styles we follow, refer to the +:neuroconv-coding-style:`NeuroConv style guide <>`. Code signing on Mac OS ---------------------- diff --git a/src/renderer/assets/css/global.css b/src/renderer/assets/css/global.css index 936161b96..3d303d974 100755 --- a/src/renderer/assets/css/global.css +++ b/src/renderer/assets/css/global.css @@ -8,6 +8,10 @@ /* src: local('Source Code Pro'), local('SourceCodePro'), url(fonts/SourceCodePro-Regular.ttf) format('truetype'); */ } +.swal-conversion-actions { + margin-top: 0px !important; +} + /* Notfy */ .notyf__toast { max-width: 40vw !important; diff --git a/src/renderer/src/stories/FileSystemSelector.js b/src/renderer/src/stories/FileSystemSelector.js index 8eab4c6e1..1f6caac21 100644 --- a/src/renderer/src/stories/FileSystemSelector.js +++ b/src/renderer/src/stories/FileSystemSelector.js @@ -15,8 +15,8 @@ function getObjectTypeReferenceString(type, multiple, { nested, native } = {}) { ? "directories" : "files" : nested - ? type - : `a ${type}`; + ? type + : `a ${type}`; } const componentCSS = css` diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js index beb5e86d1..259f0644f 100644 --- a/src/renderer/src/stories/JSONSchemaForm.js +++ b/src/renderer/src/stories/JSONSchemaForm.js @@ -376,8 +376,8 @@ export class JSONSchemaForm extends LitElement { let message = isValid ? "" : requiredButNotSpecified.length === 1 - ? `${requiredButNotSpecified[0]} is not defined` - : `${requiredButNotSpecified.length} required inputs are not specified properly`; + ? `${requiredButNotSpecified[0]} is not defined` + : `${requiredButNotSpecified.length} required inputs are not specified properly`; if (requiredButNotSpecified.length !== nMissingRequired) console.warn("Disagreement about the correct error to throw..."); diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js index 4ad9a8742..97ea5729e 100644 --- a/src/renderer/src/stories/JSONSchemaInput.js +++ b/src/renderer/src/stories/JSONSchemaInput.js @@ -166,7 +166,7 @@ export class JSONSchemaInput extends LitElement { updated() { const el = this.getElement(); if (el) { - if (this.validateEmptyValue || (el.value ?? el.checked) !== "") el.dispatchEvent(new Event("change")); + el.dispatchEvent(new Event("change")); } } @@ -174,13 +174,16 @@ export class JSONSchemaInput extends LitElement { const { info } = this; const input = this.#render(); + return html` ${input} - ${info.description - ? html`

- ${unsafeHTML(capitalize(info.description))}${info.description.slice(-1)[0] === "." ? "" : "."} -

` - : ""} +

+ ${info.description + ? html`${unsafeHTML(capitalize(info.description))}${info.description.slice(-1)[0] === "." + ? "" + : "."}` + : ""} +

`; } @@ -236,8 +239,8 @@ export class JSONSchemaInput extends LitElement { (this.onValidate ? this.onValidate() : this.form - ? this.form.validateOnChange(key, parent, [...this.form.base, ...fullPath], v) - : "") + ? this.form.validateOnChange(key, parent, [...this.form.base, ...fullPath], v) + : "") ); }, diff --git a/src/renderer/src/stories/List.ts b/src/renderer/src/stories/List.ts index 1a77e666c..bb7521baa 100644 --- a/src/renderer/src/stories/List.ts +++ b/src/renderer/src/stories/List.ts @@ -266,7 +266,8 @@ export class List extends LitElement { } if (typeof content === 'string') { - const valueEl = editableElement = document.createElement("span"); + const valueEl = document.createElement("span"); + if (!key) editableElement = valueEl valueEl.innerText = content; div.appendChild(valueEl); } diff --git a/src/renderer/src/stories/Table.js b/src/renderer/src/stories/Table.js index e88abb036..3037eba08 100644 --- a/src/renderer/src/stories/Table.js +++ b/src/renderer/src/stories/Table.js @@ -406,8 +406,6 @@ export class Table extends LitElement { if (this.table) this.table.destroy(); - console.log("Rendered data", this.#getRenderedData(data)); - const table = new Handsontable(div, { data: this.#getRenderedData(data), // rowHeaders: rowHeaders.map(v => `sub-${v}`), @@ -434,65 +432,67 @@ export class Table extends LitElement { const initialCellsToUpdate = data.reduce((acc, v) => acc + v.length, 0); table.addHook("afterValidate", (isValid, value, row, prop) => { - const header = typeof prop === "number" ? colHeaders[prop] : prop; - let rowName = this.keyColumn ? rowHeaders[row] : row; - - // NOTE: We would like to allow invalid values to mutate the results - // if (isValid) { - const isResolved = rowName in this.data; - let target = this.data; - - if (!isResolved) { - if (!this.keyColumn) this.data[rowName] = {}; // Add new row to array - else { - rowName = row; - if (!unresolved[rowName]) unresolved[rowName] = {}; // Ensure row exists - target = unresolved; - } - } const isUserUpdate = initialCellsToUpdate <= validated; - - value = this.#getValue(value, entries[header]); - - // Transfer data to object (if valid) - if (header === this.keyColumn) { - if (isValid && value && value !== rowName) { - const old = target[rowName] ?? {}; - this.data[value] = old; - delete target[rowName]; - delete unresolved[row]; - rowHeaders[row] = value; + + if (isUserUpdate) { + const header = typeof prop === "number" ? colHeaders[prop] : prop; + let rowName = this.keyColumn ? rowHeaders[row] : row; + + // NOTE: We would like to allow invalid values to mutate the results + // if (isValid) { + const isResolved = rowName in this.data; + let target = this.data; + + if (!isResolved) { + if (!this.keyColumn) this.data[rowName] = {}; // Add new row to array + else { + rowName = row; + if (!unresolved[rowName]) unresolved[rowName] = {}; // Ensure row exists + target = unresolved; + } } - } - // Update data on passed object - else { - const globalValue = this.globals[header]; + value = this.#getValue(value, entries[header]); - if (value == undefined || value === "") { - if (globalValue) { - value = target[rowName][header] = globalValue; - table.setDataAtCell(row, prop, value); - this.onOverride(header, value, rowName); - } - target[rowName][header] = undefined; - } else { - // Correct for expected arrays (copy-paste issue) - if (entries[header]?.type === "array") { - if (value && !Array.isArray(value)) value = value.split(",").map((v) => v.trim()); + // Transfer data to object (if valid) + if (header === this.keyColumn) { + if (isValid && value && value !== rowName) { + const old = target[rowName] ?? {}; + this.data[value] = old; + delete target[rowName]; + delete unresolved[row]; + rowHeaders[row] = value; } + } - target[rowName][header] = value === globalValue ? undefined : value; + // Update data on passed object + else { + const globalValue = this.globals[header]; + + if (value == undefined || value === "") { + if (globalValue) { + value = target[rowName][header] = globalValue; + table.setDataAtCell(row, prop, value); + this.onOverride(header, value, rowName); + } + target[rowName][header] = undefined; + } else { + // Correct for expected arrays (copy-paste issue) + if (entries[header]?.type === "array") { + if (value && !Array.isArray(value)) value = value.split(",").map((v) => v.trim()); + } + + target[rowName][header] = value === globalValue ? undefined : value; + } } + + this.onUpdate(rowName, header, value); } validated++; - if (isUserUpdate) this.onUpdate(rowName, header, value); - if (typeof isValid === "function") isValid(); - // } }); // If only one row, do not allow deletion diff --git a/src/renderer/src/stories/pages/Page.js b/src/renderer/src/stories/pages/Page.js index ed26fed0b..45c987524 100644 --- a/src/renderer/src/stories/pages/Page.js +++ b/src/renderer/src/stories/pages/Page.js @@ -150,7 +150,16 @@ export class Page extends LitElement { const results = {}; - const popup = await openProgressSwal({ title: `Running conversion`, ...options }); + if (!("showCancelButton" in options)) { + options.showCancelButton = true; + options.customClass = { actions: "swal-conversion-actions" }; + } + + const cancelController = new AbortController(); + + const popup = await openProgressSwal({ title: `Running conversion`, ...options }, (result) => { + if (!result.isConfirmed) cancelController.abort(); + }); const isMultiple = toRun.length > 1; @@ -165,9 +174,8 @@ export class Page extends LitElement { element.append(progressBar); element.insertAdjacentHTML( "beforeend", - `Note: This may take a while to complete...` + `Note: This may take a while to complete...
` ); - // } let completed = 0; elements.progress.value = { b: completed, tsize: toRun.length }; @@ -195,9 +203,16 @@ export class Page extends LitElement { interfaces: globalState.interfaces, }, - { swal: popup, ...options } + { swal: popup, fetch: { signal: cancelController.signal }, ...options } ).catch((e) => { - this.notify(e.message, "error"); + let message = e.message; + + if (message.includes("The user aborted a request.")) { + this.notify("Conversion was cancelled.", "warning"); + throw e; + } + + this.notify(message, "error"); popup.close(); throw e; }); diff --git a/src/renderer/src/stories/pages/guided-mode/options/utils.js b/src/renderer/src/stories/pages/guided-mode/options/utils.js index 3eb939744..7057f83e0 100644 --- a/src/renderer/src/stories/pages/guided-mode/options/utils.js +++ b/src/renderer/src/stories/pages/guided-mode/options/utils.js @@ -1,12 +1,12 @@ import Swal from "sweetalert2"; import { baseUrl } from "../../../../globals.js"; import { sanitize } from "../../utils.js"; +import { Loader } from "../../../Loader"; -export const openProgressSwal = (options) => { +export const openProgressSwal = (options, callback) => { return new Promise((resolve) => { Swal.fire({ - title: options.title ?? "Requesting data from server", - html: `Please wait...`, + title: "Requesting data from server", allowEscapeKey: false, allowOutsideClick: false, showConfirmButton: false, @@ -17,13 +17,57 @@ export const openProgressSwal = (options) => { Swal.showLoading(); resolve(Swal); }, - }); + ...options, + }).then((result) => callback?.(result)); }); }; export const run = async (url, payload, options = {}) => { const needsSwal = !options.swal && options.swal !== false; - if (needsSwal) openProgressSwal(options).then((swal) => (options.onOpen ? options.onOpen(swal) : undefined)); + + if (needsSwal) { + + if (!("showCancelButton" in options)) { + options.showCancelButton = true; + options.customClass = { actions: "swal-conversion-actions" }; + } + + const cancelController = new AbortController(); + + options.fetch = { + signal: cancelController.signal, + } + + const popup = await openProgressSwal(options, (result) => { + if (!result.isConfirmed) cancelController.abort(); + }).then(async (swal) => { + if (options.onOpen) await options.onOpen(swal) + return swal + }); + + const element = popup.getHtmlContainer(); + const actions = popup.getActions() + const loader = actions.querySelector(".swal2-loader") + const container = document.createElement("div"); + container.append(loader) + element.innerText = ""; + Object.assign(container.style, { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + marginBottom: '25px' + }) + + element.appendChild(container); + + element.insertAdjacentHTML( + "beforeend", + `
` + ); + + + } if (!("base" in options)) options.base = "/neuroconv"; @@ -34,8 +78,9 @@ export const run = async (url, payload, options = {}) => { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), - }).then((res) => res.json()); - + ...(options.fetch ?? {}), + }).then((res) => res.json()) + if (needsSwal) Swal.close(); if (results?.message) throw new Error(`Request to ${url} failed: ${results.message}`); diff --git a/src/renderer/src/stories/pages/uploads/UploadsPage.js b/src/renderer/src/stories/pages/uploads/UploadsPage.js index 9aadcf8ea..a1b3adc5b 100644 --- a/src/renderer/src/stories/pages/uploads/UploadsPage.js +++ b/src/renderer/src/stories/pages/uploads/UploadsPage.js @@ -120,8 +120,6 @@ export async function uploadToDandi(info, type = "project" in info ? "project" : document.body.append(modal); }); - - console.log(api_key); } const result = await run( @@ -131,7 +129,7 @@ export async function uploadToDandi(info, type = "project" in info ? "project" : staging, api_key, }, - { title: "Uploading to DANDI" } + { title: "Uploading your files to DANDI" } ).catch((e) => { this.notify(e.message, "error"); throw e;