From 834b1c974d23dfe0b57aa1418c77d9bf742b4e3f Mon Sep 17 00:00:00 2001 From: Garrett Michael Flynn Date: Wed, 5 Jun 2024 13:10:05 -0700 Subject: [PATCH 01/13] Re-enable embargo and ensure all requests are made with a token --- .../frontend/core/components/DandiResults.js | 12 +- .../frontend/core/components/Search.js | 1 - .../pages/guided-mode/options/GuidedUpload.js | 2 +- .../components/pages/uploads/UploadsPage.js | 83 +------------ .../core/components/pages/uploads/utils.ts | 109 +++++++++++++++++- src/schemas/dandi-create.schema.ts | 2 +- 6 files changed, 117 insertions(+), 92 deletions(-) diff --git a/src/electron/frontend/core/components/DandiResults.js b/src/electron/frontend/core/components/DandiResults.js index 37229cc08a..826cdc610d 100644 --- a/src/electron/frontend/core/components/DandiResults.js +++ b/src/electron/frontend/core/components/DandiResults.js @@ -1,7 +1,7 @@ import { LitElement, css, html } from "lit"; import { get } from "dandi"; -import { isStaging } from "./pages/uploads/utils"; +import { isStaging, getAPIKey } from "./pages/uploads/utils"; export class DandiResults extends LitElement { static get styles() { @@ -38,8 +38,14 @@ export class DandiResults extends LitElement { const otherElIds = ["embargo_status"]; - const type = isStaging(this.id) ? "staging" : undefined; - const dandiset = await get(this.id, { type }); + const staging = isStaging(this.id); + const type = staging ? "staging" : undefined; + const api_key = await getAPIKey.call(this, staging); + + const dandiset = await get(this.id, { + type, + token: api_key, + }); otherElIds.forEach((str) => handleClass(str, dandiset)); elIds.forEach((str) => handleClass(str, dandiset.draft_version)); diff --git a/src/electron/frontend/core/components/Search.js b/src/electron/frontend/core/components/Search.js index bb7d889de2..66a988014e 100644 --- a/src/electron/frontend/core/components/Search.js +++ b/src/electron/frontend/core/components/Search.js @@ -35,7 +35,6 @@ export class Search extends LitElement { } #close = () => { - console.log("CLOSING", this.getSelectedOption()); if (this.listMode === "input" && this.getAttribute("interacted") === "true") { this.setAttribute("interacted", false); this.#onSelect(this.getSelectedOption()); diff --git a/src/electron/frontend/core/components/pages/guided-mode/options/GuidedUpload.js b/src/electron/frontend/core/components/pages/guided-mode/options/GuidedUpload.js index 075b1be03a..5c6506d57e 100644 --- a/src/electron/frontend/core/components/pages/guided-mode/options/GuidedUpload.js +++ b/src/electron/frontend/core/components/pages/guided-mode/options/GuidedUpload.js @@ -179,7 +179,7 @@ export class GuidedUploadPage extends Page { }, onUpdate: () => (this.unsavedUpdates = true), onThrow, - validateOnChange: validate, + validateOnChange: (...args) => validate.call(this, ...args) })); }) .catch((error) => html`

${error}

`); diff --git a/src/electron/frontend/core/components/pages/uploads/UploadsPage.js b/src/electron/frontend/core/components/pages/uploads/UploadsPage.js index 3d97bb6d30..77ceb93a00 100644 --- a/src/electron/frontend/core/components/pages/uploads/UploadsPage.js +++ b/src/electron/frontend/core/components/pages/uploads/UploadsPage.js @@ -25,16 +25,13 @@ import { Modal } from "../../Modal"; import { DandiResults } from "../../DandiResults.js"; import dandiGlobalSchema from "../../../../../../schemas/json/dandi/global.json"; -import { JSONSchemaInput } from "../../JSONSchemaInput.js"; -import { header } from "../../forms/utils"; - import { validateDANDIApiKey } from "../../../validation/dandi"; import * as dandi from "dandi"; import keyIcon from "../../../../assets/icons/key.svg?raw"; -import { AWARD_VALIDATION_FAIL_MESSAGE, awardNumberValidator, isStaging, validate } from "./utils"; +import { AWARD_VALIDATION_FAIL_MESSAGE, awardNumberValidator, isStaging, validate, getAPIKey } from "./utils"; import { createFormModal } from "../../forms/GlobalFormModal"; export async function createDandiset(results = {}) { @@ -167,82 +164,6 @@ export async function createDandiset(results = {}) { }); } -async function getAPIKey(staging = false) { - const whichAPIKey = staging ? "staging_api_key" : "main_api_key"; - const DANDI = global.data.DANDI; - let api_key = DANDI?.api_keys?.[whichAPIKey]; - - const errors = await validateDANDIApiKey(api_key, staging); - - const isInvalid = !errors || errors.length; - - if (isInvalid) { - const modal = new Modal({ - header: `${api_key ? "Update" : "Provide"} your ${header(whichAPIKey)}`, - open: true, - }); - - const input = new JSONSchemaInput({ - path: [whichAPIKey], - schema: dandiGlobalSchema.properties.api_keys.properties[whichAPIKey], - }); - - input.style.padding = "25px"; - - modal.append(input); - - let notification; - - const notify = (message, type) => { - if (notification) this.dismiss(notification); - return (notification = this.notify(message, type)); - }; - - modal.onClose = async () => notify("The updated DANDI API key was not set", "error"); - - api_key = await new Promise((resolve) => { - const button = new Button({ - label: "Save", - primary: true, - onClick: async () => { - const value = input.value; - if (value) { - const errors = await validateDANDIApiKey(input.value, staging); - if (!errors || !errors.length) { - modal.remove(); - - merge( - { - DANDI: { - api_keys: { - [whichAPIKey]: value, - }, - }, - }, - global.data - ); - - global.save(); - resolve(value); - } else { - notify(errors[0].message, "error"); - return false; - } - } else { - notify("Your DANDI API key was not set", "error"); - } - }, - }); - - modal.footer = button; - - document.body.append(modal); - }); - } - - return api_key; -} - export async function uploadToDandi(info, type = "project" in info ? "project" : "") { const { dandiset } = info; @@ -422,7 +343,7 @@ export class UploadsPage extends Page { error.message = "Please select at least one file or folder to upload."; }, - validateOnChange: validate, + validateOnChange: (...args) => validate.call(this, ...args) })); }) .catch((error) => html`

${error}

`); diff --git a/src/electron/frontend/core/components/pages/uploads/utils.ts b/src/electron/frontend/core/components/pages/uploads/utils.ts index 1cdff28223..7636f581cf 100644 --- a/src/electron/frontend/core/components/pages/uploads/utils.ts +++ b/src/electron/frontend/core/components/pages/uploads/utils.ts @@ -1,7 +1,18 @@ import { get } from "dandi"; import dandiUploadSchema from "../../../../../../schemas/dandi-upload.schema"; -import { html } from "lit"; + +import { validateDANDIApiKey } from "../../../validation/dandi"; +import { Modal } from "../../Modal"; +import { header } from "../../forms/utils"; + +import { JSONSchemaInput } from "../../JSONSchemaInput"; + +import { Button } from "../../Button.js"; +import { global } from "../../../progress/index.js"; +import { merge } from "../utils"; + +import dandiGlobalSchema from "../../../../../../schemas/json/dandi/global.json"; export const isStaging = (id: string) => parseInt(id) >= 100000; @@ -13,7 +24,7 @@ function isNumeric(str: string) { } - export const validate = async (name: string, parent: any) => { + export async function validate (name: string, parent: any) { const value = parent[name] @@ -33,8 +44,13 @@ function isNumeric(str: string) { }] const staging = isStaging(value) - - const dandiset = await get(value, { type: staging ? "staging" : undefined }) + const type = staging ? "staging" : undefined; + const token = await getAPIKey.call(this, staging); + + const dandiset = await get(value, { + type, + token + }) if (dandiset.detail) { if (dandiset.detail.includes('Not found')) return [{ @@ -44,12 +60,13 @@ function isNumeric(str: string) { if (dandiset.detail.includes('credentials were not provided')) return [{ type: 'warning', - message: `Authentication error – This Dandiset does not appear to be linked to your account.` + message: `Authentication error – This is an embargoed Dandiset and you haven't provided the correct credentials.` }] } // NOTE: This may not be thrown anymore with the above detail checks const { enum: enumValue } = dandiUploadSchema.properties.dandiset; + if (enumValue && !enumValue.includes(value)) return [{ type: 'error', message: `No Access – This Dandiset does not belong to you.` @@ -77,3 +94,85 @@ export function awardNumberValidator(awardNumber: string): boolean { } export const AWARD_VALIDATION_FAIL_MESSAGE = 'Award number must be properly space-delimited.\n\nExample (exclude quotes):\n"1 R01 CA 123456-01A1"'; + + +// this: +export async function getAPIKey( + // this: Page, + staging = false +) { + + const whichAPIKey = staging ? "staging_api_key" : "main_api_key"; + const DANDI = global.data.DANDI; + let api_key = DANDI?.api_keys?.[whichAPIKey]; + + const errors = await validateDANDIApiKey(api_key, staging); + + const isInvalid = !errors || errors.length; + + if (isInvalid) { + const modal = new Modal({ + header: `${api_key ? "Update" : "Provide"} your ${header(whichAPIKey)}`, + open: true, + }); + + const input = new JSONSchemaInput({ + path: [whichAPIKey], + schema: dandiGlobalSchema.properties.api_keys.properties[whichAPIKey], + }); + + input.style.padding = "25px"; + + modal.append(input); + + let notification; + + const notify = (message, type) => { + if (notification) this.dismiss(notification); + return (notification = this.notify(message, type)); + }; + + modal.onClose = async () => notify("The updated DANDI API key was not set", "error"); + + api_key = await new Promise((resolve) => { + const button = new Button({ + label: "Save", + primary: true, + onClick: async () => { + const value = input.value; + if (value) { + const errors = await validateDANDIApiKey(input.value, staging); + if (!errors || !errors.length) { + modal.remove(); + + merge( + { + DANDI: { + api_keys: { + [whichAPIKey]: value, + }, + }, + }, + global.data + ); + + global.save(); + resolve(value); + } else { + notify(errors[0].message, "error"); + return false; + } + } else { + notify("Your DANDI API key was not set", "error"); + } + }, + }); + + modal.footer = button; + + document.body.append(modal); + }); + } + + return api_key; +} diff --git a/src/schemas/dandi-create.schema.ts b/src/schemas/dandi-create.schema.ts index 8c44ce4477..7b08cd927e 100644 --- a/src/schemas/dandi-create.schema.ts +++ b/src/schemas/dandi-create.schema.ts @@ -1,4 +1,4 @@ -import create from './json/dandi/create_no_embargo.json' assert { type: "json" } +import create from './json/dandi/create.json' assert { type: "json" } const schema = structuredClone(create) export default schema From a05b0ee23c845ffe5370c4b13b0199cca396cd8c Mon Sep 17 00:00:00 2001 From: Garrett Michael Flynn Date: Wed, 5 Jun 2024 13:10:39 -0700 Subject: [PATCH 02/13] Delete create_no_embargo.json --- src/schemas/json/dandi/create_no_embargo.json | 60 ------------------- 1 file changed, 60 deletions(-) delete mode 100644 src/schemas/json/dandi/create_no_embargo.json diff --git a/src/schemas/json/dandi/create_no_embargo.json b/src/schemas/json/dandi/create_no_embargo.json deleted file mode 100644 index 2e5ec3d737..0000000000 --- a/src/schemas/json/dandi/create_no_embargo.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "order": [ - "title", - "archive", - "embargo_status", - "description", - "license" - ], - "properties": { - - "title": { - "type": "string", - "description": "Provide a title for this Dandiset. The title will appear in search results and at the top of the home page for this Dandiset, so make it concise and descriptive" - }, - - "archive": { - "type": "string", - "enumLabels": { - "staging": "Development Server", - "main": "Main Archive" - }, - "enum": ["main", "staging"], - "description": "Which DANDI server to upload to.
Note: The Development Server is recommended for developers, or users learning to use DANDI" - }, - - "description": { - "type": "string", - "description": "Provide a description for this Dandiset. This will appear prominently under the title in the home page for this Dandiset." - }, - - "license": { - "type": "array", - "description": "Provide a set of licenses for this Dandiset. Review the individual licenses and select the one that best fits your needs.", - "items": { - "type": "string", - "enumLinks": { - "spdx:CC0-1.0": "https://creativecommons.org/public-domain/cc0/", - "spdx:CC-BY-4.0": "https://creativecommons.org/licenses/by/4.0/deed.en" - }, - "enumKeywords": { - "spdx:CC0-1.0": ["No Rights Reserved"], - "spdx:CC-BY-4.0": ["Attribution 4.0 International"] - }, - "enumLabels": { - "spdx:CC0-1.0": "CC0 1.0", - "spdx:CC-BY-4.0": "CC BY 4.0" - }, - "enum": [ - "spdx:CC0-1.0", - "spdx:CC-BY-4.0" - ] - }, - "maxItems": 1, - "uniqueItems": true, - "strict": true - } - - }, - "required": ["title", "description", "license", "archive"] -} From 8d141afa638daf990c90c60d46262fba9f6782d8 Mon Sep 17 00:00:00 2001 From: Garrett Michael Flynn Date: Wed, 5 Jun 2024 13:15:22 -0700 Subject: [PATCH 03/13] Update utils.ts --- src/electron/frontend/core/components/pages/uploads/utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/electron/frontend/core/components/pages/uploads/utils.ts b/src/electron/frontend/core/components/pages/uploads/utils.ts index 7636f581cf..8d2e3c1670 100644 --- a/src/electron/frontend/core/components/pages/uploads/utils.ts +++ b/src/electron/frontend/core/components/pages/uploads/utils.ts @@ -102,7 +102,7 @@ export async function getAPIKey( staging = false ) { - const whichAPIKey = staging ? "staging_api_key" : "main_api_key"; + const whichAPIKey = staging ? "development_api_key" : "main_api_key"; const DANDI = global.data.DANDI; let api_key = DANDI?.api_keys?.[whichAPIKey]; @@ -114,6 +114,7 @@ export async function getAPIKey( const modal = new Modal({ header: `${api_key ? "Update" : "Provide"} your ${header(whichAPIKey)}`, open: true, + onClose: () => modal.remove(), }); const input = new JSONSchemaInput({ From 247c7423183857ddebc8e17a24ce4cc042167507 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 5 Jun 2024 20:15:33 +0000 Subject: [PATCH 04/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/electron/frontend/core/components/DandiResults.js | 2 +- .../components/pages/guided-mode/options/GuidedUpload.js | 2 +- .../frontend/core/components/pages/uploads/UploadsPage.js | 2 +- .../frontend/core/components/pages/uploads/utils.ts | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/electron/frontend/core/components/DandiResults.js b/src/electron/frontend/core/components/DandiResults.js index 826cdc610d..210757647b 100644 --- a/src/electron/frontend/core/components/DandiResults.js +++ b/src/electron/frontend/core/components/DandiResults.js @@ -42,7 +42,7 @@ export class DandiResults extends LitElement { const type = staging ? "staging" : undefined; const api_key = await getAPIKey.call(this, staging); - const dandiset = await get(this.id, { + const dandiset = await get(this.id, { type, token: api_key, }); diff --git a/src/electron/frontend/core/components/pages/guided-mode/options/GuidedUpload.js b/src/electron/frontend/core/components/pages/guided-mode/options/GuidedUpload.js index fe238c2512..50f400bc8c 100644 --- a/src/electron/frontend/core/components/pages/guided-mode/options/GuidedUpload.js +++ b/src/electron/frontend/core/components/pages/guided-mode/options/GuidedUpload.js @@ -182,7 +182,7 @@ export class GuidedUploadPage extends Page { }, onUpdate: () => (this.unsavedUpdates = true), onThrow, - validateOnChange: (...args) => validate.call(this, ...args) + validateOnChange: (...args) => validate.call(this, ...args), })); }) .catch((error) => html`

${error}

`); diff --git a/src/electron/frontend/core/components/pages/uploads/UploadsPage.js b/src/electron/frontend/core/components/pages/uploads/UploadsPage.js index f71fe66ab5..4a64867e5b 100644 --- a/src/electron/frontend/core/components/pages/uploads/UploadsPage.js +++ b/src/electron/frontend/core/components/pages/uploads/UploadsPage.js @@ -355,7 +355,7 @@ export class UploadsPage extends Page { error.message = "Please select at least one file or folder to upload."; }, - validateOnChange: (...args) => validate.call(this, ...args) + validateOnChange: (...args) => validate.call(this, ...args), })); }) .catch((error) => html`

${error}

`); diff --git a/src/electron/frontend/core/components/pages/uploads/utils.ts b/src/electron/frontend/core/components/pages/uploads/utils.ts index 8d2e3c1670..cc5b398c1c 100644 --- a/src/electron/frontend/core/components/pages/uploads/utils.ts +++ b/src/electron/frontend/core/components/pages/uploads/utils.ts @@ -46,8 +46,8 @@ function isNumeric(str: string) { const staging = isStaging(value) const type = staging ? "staging" : undefined; const token = await getAPIKey.call(this, staging); - - const dandiset = await get(value, { + + const dandiset = await get(value, { type, token }) @@ -96,7 +96,7 @@ export function awardNumberValidator(awardNumber: string): boolean { export const AWARD_VALIDATION_FAIL_MESSAGE = 'Award number must be properly space-delimited.\n\nExample (exclude quotes):\n"1 R01 CA 123456-01A1"'; -// this: +// this: export async function getAPIKey( // this: Page, staging = false From 775b636999bd3ffec8035ad827a2c0ea151cfc36 Mon Sep 17 00:00:00 2001 From: Garrett Michael Flynn Date: Wed, 5 Jun 2024 13:40:23 -0700 Subject: [PATCH 05/13] Update dandi api --- package-lock.json | 13 ++++++++----- package.json | 2 +- .../frontend/core/components/pages/uploads/utils.ts | 12 +++++++++--- src/electron/frontend/core/validation/dandi.ts | 10 ++++------ src/schemas/dandi-upload.schema.ts | 2 +- 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index fd3448426d..1322f0cac6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@vitest/coverage-v8": "^1.6.0", "chokidar": "^3.5.3", "concurrently": "^7.6.0", - "dandi": "^0.0.4", + "dandi": "^0.0.5", "find-free-port": "^2.0.0", "fomantic-ui": "^2.8.8", "fs-extra": "^10.0.0", @@ -8755,10 +8755,9 @@ } }, "node_modules/dandi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/dandi/-/dandi-0.0.4.tgz", - "integrity": "sha512-W0eM84uV87cCrfr9L3xKATbUYEITfCgG14Pgj9eEx1B1DS+lW/IRLmKHsWp8Noz9vegN7nDwfs43P1BCmZNWsg==", - "license": "MIT" + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/dandi/-/dandi-0.0.5.tgz", + "integrity": "sha512-612iKxHbz2qh95weOYMYAf0Y0HBXMoq53l/P7dnh5u2Qb7utxV4H69hrhsTdrtxXa2sENffs8wx0HeHmvolDcg==" }, "node_modules/data-uri-to-buffer": { "version": "6.0.1", @@ -20971,6 +20970,8 @@ }, "node_modules/through2-filter": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", "license": "MIT", "dependencies": { "through2": "~2.0.0", @@ -20979,6 +20980,8 @@ }, "node_modules/through2-filter/node_modules/through2": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "license": "MIT", "dependencies": { "readable-stream": "~2.3.6", diff --git a/package.json b/package.json index 64f15c4ec8..ab4a4e5330 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "@vitest/coverage-v8": "^1.6.0", "chokidar": "^3.5.3", "concurrently": "^7.6.0", - "dandi": "^0.0.4", + "dandi": "^0.0.5", "find-free-port": "^2.0.0", "fomantic-ui": "^2.8.8", "fs-extra": "^10.0.0", diff --git a/src/electron/frontend/core/components/pages/uploads/utils.ts b/src/electron/frontend/core/components/pages/uploads/utils.ts index 8d2e3c1670..178f2b5efc 100644 --- a/src/electron/frontend/core/components/pages/uploads/utils.ts +++ b/src/electron/frontend/core/components/pages/uploads/utils.ts @@ -1,6 +1,6 @@ import { get } from "dandi"; -import dandiUploadSchema from "../../../../../../schemas/dandi-upload.schema"; +import dandiUploadSchema, { regenerateDandisets } from "../../../../../../schemas/dandi-upload.schema"; import { validateDANDIApiKey } from "../../../validation/dandi"; import { Modal } from "../../Modal"; @@ -117,14 +117,18 @@ export async function getAPIKey( onClose: () => modal.remove(), }); + const container = document.createElement("div"); + const input = new JSONSchemaInput({ path: [whichAPIKey], schema: dandiGlobalSchema.properties.api_keys.properties[whichAPIKey], }); - input.style.padding = "25px"; + container.append(input); + + container.style.padding = "25px"; - modal.append(input); + modal.append(container); let notification; @@ -173,6 +177,8 @@ export async function getAPIKey( document.body.append(modal); }); + + await regenerateDandisets() } return api_key; diff --git a/src/electron/frontend/core/validation/dandi.ts b/src/electron/frontend/core/validation/dandi.ts index d64d017d8d..7829b68f83 100644 --- a/src/electron/frontend/core/validation/dandi.ts +++ b/src/electron/frontend/core/validation/dandi.ts @@ -1,6 +1,7 @@ const dandiAPITokenRegex = /^[a-f0-9]{40}$/; +import { validateToken } from 'dandi' export const validateDANDIApiKey = async (apiKey: string, staging = false) => { if (apiKey) { @@ -9,11 +10,8 @@ export const validateDANDIApiKey = async (apiKey: string, staging = false) => { const authFailedError = {type: 'error', message: `Authorization failed. Make sure you're providing an API key for the ${staging ? 'staging' : 'main'} archive.`} - return fetch(`https://api${staging ? '-staging' : ''}.dandiarchive.org/api/auth/token/`, {headers: {Authorization: `token ${apiKey}`}}) - .then((res) => { - if (!res.ok) return [authFailedError] - return true - }) - .catch(() => [authFailedError]) + const isValid = validateToken({ token: apiKey, type: staging ? 'staging' : undefined }).catch(e => false) + if (!isValid) return [ authFailedError ] + return true } } diff --git a/src/schemas/dandi-upload.schema.ts b/src/schemas/dandi-upload.schema.ts index 41c2edab2c..88cc8257e0 100644 --- a/src/schemas/dandi-upload.schema.ts +++ b/src/schemas/dandi-upload.schema.ts @@ -63,7 +63,7 @@ export const updateDandisets = async (main = true) => { if (!token) return [] - return await getMine({ token, type: staging ? 'staging' : undefined }) + return await getMine({ token, type: staging ? 'staging' : undefined }, { embargoed: true }) .then((results) => results ? Promise.all(results.map(addDandiset)) : []) .catch(() => { return [] From 6de9b15f4e08a3c81d36f916cb6d84952a1ac8ab Mon Sep 17 00:00:00 2001 From: Garrett Michael Flynn Date: Wed, 5 Jun 2024 14:52:13 -0700 Subject: [PATCH 06/13] Update dandi api version --- package-lock.json | 12 ++++++------ package.json | 2 +- .../core/components/pages/uploads/UploadsPage.js | 3 ++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1322f0cac6..e5bbb774e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nwb-guide", - "version": "0.0.16", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nwb-guide", - "version": "0.0.16", + "version": "1.0.0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -16,7 +16,7 @@ "@vitest/coverage-v8": "^1.6.0", "chokidar": "^3.5.3", "concurrently": "^7.6.0", - "dandi": "^0.0.5", + "dandi": "^0.0.6", "find-free-port": "^2.0.0", "fomantic-ui": "^2.8.8", "fs-extra": "^10.0.0", @@ -8755,9 +8755,9 @@ } }, "node_modules/dandi": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/dandi/-/dandi-0.0.5.tgz", - "integrity": "sha512-612iKxHbz2qh95weOYMYAf0Y0HBXMoq53l/P7dnh5u2Qb7utxV4H69hrhsTdrtxXa2sENffs8wx0HeHmvolDcg==" + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/dandi/-/dandi-0.0.6.tgz", + "integrity": "sha512-SiPqaFl7M/MYhWRdZQuAHu/8jgcgZYTMDg3X0IbplCyvWVO+yPVTCZmkY1PX2hPrz3hIkCgNVmYlL6IzNlBq2Q==" }, "node_modules/data-uri-to-buffer": { "version": "6.0.1", diff --git a/package.json b/package.json index cd385c3534..23e6e7005b 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "@vitest/coverage-v8": "^1.6.0", "chokidar": "^3.5.3", "concurrently": "^7.6.0", - "dandi": "^0.0.5", + "dandi": "^0.0.6", "find-free-port": "^2.0.0", "fomantic-ui": "^2.8.8", "fs-extra": "^10.0.0", diff --git a/src/electron/frontend/core/components/pages/uploads/UploadsPage.js b/src/electron/frontend/core/components/pages/uploads/UploadsPage.js index 4a64867e5b..7217d2d5ce 100644 --- a/src/electron/frontend/core/components/pages/uploads/UploadsPage.js +++ b/src/electron/frontend/core/components/pages/uploads/UploadsPage.js @@ -119,7 +119,8 @@ export function createDandiset(results = {}) { token: api_key, type: staging ? "staging" : undefined, }); - await api.init(); + + await api.authorize(); const metadata = { description: form.resolved.description, From e43da415f96365a39cc09c9a8327e573bd6b95d0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 5 Jun 2024 21:53:57 +0000 Subject: [PATCH 07/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../frontend/core/components/pages/uploads/UploadsPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/electron/frontend/core/components/pages/uploads/UploadsPage.js b/src/electron/frontend/core/components/pages/uploads/UploadsPage.js index 7217d2d5ce..83980621fc 100644 --- a/src/electron/frontend/core/components/pages/uploads/UploadsPage.js +++ b/src/electron/frontend/core/components/pages/uploads/UploadsPage.js @@ -119,7 +119,7 @@ export function createDandiset(results = {}) { token: api_key, type: staging ? "staging" : undefined, }); - + await api.authorize(); const metadata = { From 2bc1b75670f325e8525358e2f5c33e90bf09c33f Mon Sep 17 00:00:00 2001 From: Garrett Michael Flynn Date: Mon, 10 Jun 2024 09:42:33 -0700 Subject: [PATCH 08/13] Wait properly for asynchronous validation to occur before submitting incorrect form --- .../core/components/JSONSchemaForm.js | 49 ++++++++++--------- .../components/pages/uploads/UploadsPage.js | 5 +- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/electron/frontend/core/components/JSONSchemaForm.js b/src/electron/frontend/core/components/JSONSchemaForm.js index 075627d3ee..0aaa9ab118 100644 --- a/src/electron/frontend/core/components/JSONSchemaForm.js +++ b/src/electron/frontend/core/components/JSONSchemaForm.js @@ -554,7 +554,33 @@ export class JSONSchemaForm extends LitElement { }; validate = async (resolved = this.resolved) => { - if (this.validateEmptyValues === false) this.validateEmptyValues = true; + + if (this.validateEmptyValues === false) { + this.validateEmptyValues = true; + await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for next tick (re-render start) + await this.rendered // Wait for re-render + } + + // Validate nested forms (skip disabled) + for (let name in this.forms) { + const accordion = this.accordions[name]; + if (!accordion || !accordion.disabled) + await this.forms[name].validate(resolved ? resolved[name] : undefined); // Validate nested forms too + } + + for (let key in this.tables) { + try { + this.tables[key].validate(resolved ? resolved[key] : undefined); // Validate nested tables too + } catch (error) { + const title = this.tables[key].schema.title; + const message = error.message.replace( + "this table", + `the ${header(title ?? [...this.base, key].join("."))} table` + ); + this.throw(message); + break; + } + } // Validate against the entire JSON Schema const copy = structuredClone(resolved); @@ -616,27 +642,6 @@ export class JSONSchemaForm extends LitElement { if (message) this.throw(message); - // Validate nested forms (skip disabled) - for (let name in this.forms) { - const accordion = this.accordions[name]; - if (!accordion || !accordion.disabled) - await this.forms[name].validate(resolved ? resolved[name] : undefined); // Validate nested forms too - } - - for (let key in this.tables) { - try { - this.tables[key].validate(resolved ? resolved[key] : undefined); // Validate nested tables too - } catch (error) { - const title = this.tables[key].schema.title; - const message = error.message.replace( - "this table", - `the ${header(title ?? [...this.base, key].join("."))} table` - ); - this.throw(message); - break; - } - } - return true; }; diff --git a/src/electron/frontend/core/components/pages/uploads/UploadsPage.js b/src/electron/frontend/core/components/pages/uploads/UploadsPage.js index 83980621fc..b065323df7 100644 --- a/src/electron/frontend/core/components/pages/uploads/UploadsPage.js +++ b/src/electron/frontend/core/components/pages/uploads/UploadsPage.js @@ -69,6 +69,7 @@ export function createDandiset(results = {}) { schema: dandiCreateSchema, results, validateEmptyValues: false, // Only show errors after submission + validateOnChange: async (name, parent) => { const value = parent[name]; @@ -89,7 +90,8 @@ export function createDandiset(results = {}) { groups: [ { name: "Embargo your Data", - properties: [["embargo_status"], ["nih_award_number"]], + properties: [[ "embargo_status" ], [ "nih_award_number" ]], + link: true }, ], }); @@ -104,6 +106,7 @@ export function createDandiset(results = {}) { label: "Create", primary: true, onClick: async () => { + await form.validate().catch(() => { const message = "Please fill out all required fields"; notify("Dandiset was not set", "error"); From fc9cce2e642ae714929bc40dac4abe975f9fd965 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:42:50 +0000 Subject: [PATCH 09/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/electron/frontend/core/components/JSONSchemaForm.js | 3 +-- .../frontend/core/components/pages/uploads/UploadsPage.js | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/electron/frontend/core/components/JSONSchemaForm.js b/src/electron/frontend/core/components/JSONSchemaForm.js index 0aaa9ab118..c806a95262 100644 --- a/src/electron/frontend/core/components/JSONSchemaForm.js +++ b/src/electron/frontend/core/components/JSONSchemaForm.js @@ -554,11 +554,10 @@ export class JSONSchemaForm extends LitElement { }; validate = async (resolved = this.resolved) => { - if (this.validateEmptyValues === false) { this.validateEmptyValues = true; await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for next tick (re-render start) - await this.rendered // Wait for re-render + await this.rendered; // Wait for re-render } // Validate nested forms (skip disabled) diff --git a/src/electron/frontend/core/components/pages/uploads/UploadsPage.js b/src/electron/frontend/core/components/pages/uploads/UploadsPage.js index b065323df7..b389256450 100644 --- a/src/electron/frontend/core/components/pages/uploads/UploadsPage.js +++ b/src/electron/frontend/core/components/pages/uploads/UploadsPage.js @@ -69,7 +69,7 @@ export function createDandiset(results = {}) { schema: dandiCreateSchema, results, validateEmptyValues: false, // Only show errors after submission - + validateOnChange: async (name, parent) => { const value = parent[name]; @@ -90,8 +90,8 @@ export function createDandiset(results = {}) { groups: [ { name: "Embargo your Data", - properties: [[ "embargo_status" ], [ "nih_award_number" ]], - link: true + properties: [["embargo_status"], ["nih_award_number"]], + link: true, }, ], }); @@ -106,7 +106,6 @@ export function createDandiset(results = {}) { label: "Create", primary: true, onClick: async () => { - await form.validate().catch(() => { const message = "Please fill out all required fields"; notify("Dandiset was not set", "error"); From 80a5b47a91e344bffc9ee5ca560cc7124a55e42d Mon Sep 17 00:00:00 2001 From: Garrett Michael Flynn Date: Mon, 10 Jun 2024 09:54:40 -0700 Subject: [PATCH 10/13] Fix validation of groups --- .../frontend/core/components/JSONSchemaForm.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/electron/frontend/core/components/JSONSchemaForm.js b/src/electron/frontend/core/components/JSONSchemaForm.js index 0aaa9ab118..8353c7b386 100644 --- a/src/electron/frontend/core/components/JSONSchemaForm.js +++ b/src/electron/frontend/core/components/JSONSchemaForm.js @@ -1012,8 +1012,17 @@ export class JSONSchemaForm extends LitElement { const groupEl = this.#getGroupElement(externalPath); if (groupEl) { - groupEl.classList[resolvedErrors.length ? "add" : "remove"]("error"); - groupEl.classList[warnings.length ? "add" : "remove"]("warning"); + + groupEl.setAttribute(`data-${externalPath}-errors`, updatedErrors.length); + groupEl.setAttribute(`data-${externalPath}-warnings`, updatedWarnings.length); + + const allFormSections = groupEl.querySelectorAll(".form-section"); + const inputs = Array.from(allFormSections).map((section) => section.id); + const allErrors = inputs.reduce((acc, id) => acc + parseInt(groupEl.getAttribute(`data-${id}-errors`)), 0); + const allWarnings = inputs.reduce((acc, id) => acc + parseInt(groupEl.getAttribute(`data-${id}-warnings`)), 0); + + groupEl.classList[allErrors ? "add" : "remove"]("error"); + groupEl.classList[allWarnings ? "add" : "remove"]("warning"); } const clearAllErrors = isValid && updatedErrors.length === 0; From a3870f0518e958d9668ab4df33568bcbe4ce609c Mon Sep 17 00:00:00 2001 From: Garrett Michael Flynn Date: Mon, 10 Jun 2024 09:56:15 -0700 Subject: [PATCH 11/13] Update JSONSchemaForm.js --- src/electron/frontend/core/components/JSONSchemaForm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/electron/frontend/core/components/JSONSchemaForm.js b/src/electron/frontend/core/components/JSONSchemaForm.js index 814c4f054b..c18d308f6d 100644 --- a/src/electron/frontend/core/components/JSONSchemaForm.js +++ b/src/electron/frontend/core/components/JSONSchemaForm.js @@ -1012,8 +1012,8 @@ export class JSONSchemaForm extends LitElement { if (groupEl) { - groupEl.setAttribute(`data-${externalPath}-errors`, updatedErrors.length); - groupEl.setAttribute(`data-${externalPath}-warnings`, updatedWarnings.length); + groupEl.setAttribute(`data-${name}-errors`, updatedErrors.length); + groupEl.setAttribute(`data-${name}-warnings`, updatedWarnings.length); const allFormSections = groupEl.querySelectorAll(".form-section"); const inputs = Array.from(allFormSections).map((section) => section.id); From bef7529929c95e0aef63e4fe03b536702d09c75b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:56:32 +0000 Subject: [PATCH 12/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/electron/frontend/core/components/JSONSchemaForm.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/electron/frontend/core/components/JSONSchemaForm.js b/src/electron/frontend/core/components/JSONSchemaForm.js index c18d308f6d..2a3d9503ce 100644 --- a/src/electron/frontend/core/components/JSONSchemaForm.js +++ b/src/electron/frontend/core/components/JSONSchemaForm.js @@ -1011,15 +1011,17 @@ export class JSONSchemaForm extends LitElement { const groupEl = this.#getGroupElement(externalPath); if (groupEl) { - groupEl.setAttribute(`data-${name}-errors`, updatedErrors.length); groupEl.setAttribute(`data-${name}-warnings`, updatedWarnings.length); const allFormSections = groupEl.querySelectorAll(".form-section"); const inputs = Array.from(allFormSections).map((section) => section.id); const allErrors = inputs.reduce((acc, id) => acc + parseInt(groupEl.getAttribute(`data-${id}-errors`)), 0); - const allWarnings = inputs.reduce((acc, id) => acc + parseInt(groupEl.getAttribute(`data-${id}-warnings`)), 0); - + const allWarnings = inputs.reduce( + (acc, id) => acc + parseInt(groupEl.getAttribute(`data-${id}-warnings`)), + 0 + ); + groupEl.classList[allErrors ? "add" : "remove"]("error"); groupEl.classList[allWarnings ? "add" : "remove"]("warning"); } From 86fd05ca45c0bf6b5c96fc64195d32d81945e451 Mon Sep 17 00:00:00 2001 From: Garrett Michael Flynn Date: Mon, 10 Jun 2024 10:03:18 -0700 Subject: [PATCH 13/13] Fix tests --- tests/metadata.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/metadata.test.ts b/tests/metadata.test.ts index 4773b4d5ed..69b87a9b28 100644 --- a/tests/metadata.test.ts +++ b/tests/metadata.test.ts @@ -215,9 +215,10 @@ test('inter-table updates are triggered', async () => { }) await sleep(1000) // Wait for the ElectrodeGroup table to update properly - form.requestUpdate() // Re-render the form to update the Electrodes table + await form.rendered // Wait for the form to re-render and validate properly + // Validate that the new structure is correct const hasErrors = await form.validate().then(() => false).catch((e) => true)