From 8e0589bfd6b952827a42d5c0f593b461ee04b299 Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Thu, 29 Feb 2024 07:36:52 -0500 Subject: [PATCH 1/3] Version bump (#631) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ceea93333..dc69a9e93 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nwb-guide", "productName": "NWB GUIDE", - "version": "0.0.14", + "version": "0.0.15", "description": "NWB GUIDE is a desktop app that provides a no-code user interface for converting neurophysiology data to NWB.", "main": "./build/main/main.js", "engine": { From 110900906babb03be65800898ffc6b9403190bfe Mon Sep 17 00:00:00 2001 From: Garrett Michael Flynn Date: Fri, 8 Mar 2024 10:58:36 -0800 Subject: [PATCH 2/3] Path Expansion Autocomplete Improvements (#632) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- src/renderer/src/stories/JSONSchemaForm.js | 93 ++++++++++--------- src/renderer/src/stories/JSONSchemaInput.js | 1 + .../src/stories/forms/GlobalFormModal.ts | 2 +- .../guided-mode/data/GuidedPathExpansion.js | 23 +++-- .../guided-mode/setup/GuidedNewDatasetInfo.js | 2 +- .../pages/guided-mode/setup/GuidedSubjects.js | 2 +- .../preview/inspector/InspectorList.js | 3 +- 7 files changed, 72 insertions(+), 54 deletions(-) diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js index ed318d187..d0b3a592d 100644 --- a/src/renderer/src/stories/JSONSchemaForm.js +++ b/src/renderer/src/stories/JSONSchemaForm.js @@ -21,6 +21,47 @@ const encode = (str) => { } }; +export const get = (path, object, omitted = [], skipped = []) => { + // path = path.slice(this.base.length); // Correct for base path + if (!path) throw new Error("Path not specified"); + return path.reduce((acc, curr, i) => { + const tempAcc = acc?.[curr] ?? acc?.[omitted.find((str) => acc[str] && acc[str][curr])]?.[curr]; + if (tempAcc) return tempAcc; + else { + const level1 = acc?.[skipped.find((str) => acc[str])]; + if (level1) { + // Handle items-like objects + const result = get(path.slice(i), level1, omitted, skipped); + if (result) return result; + + // Handle pattern properties objects + const got = Object.keys(level1).find((key) => { + const result = get(path.slice(i + 1), level1[key], omitted, skipped); + if (result && typeof result === "object") return result; // Schema are objects... + }); + + if (got) return level1[got]; + } + } + }, object); +}; + +export const getSchema = (path, schema, base = []) => { + if (typeof path === "string") path = path.split("."); + + // NOTE: Still must correct for the base here + if (base.length) { + const indexOf = path.indexOf(base.slice(-1)[0]); + if (indexOf !== -1) path = path.slice(indexOf + 1); + } + + // NOTE: Refs are now pre-resolved + const resolved = get(path, schema, ["properties", "patternProperties"], ["patternProperties", "items"]); + // if (resolved?.["$ref"]) return this.getSchema(resolved["$ref"].split("/").slice(1)); // NOTE: This assumes reference to the root of the schema + + return resolved; +}; + const additionalPropPattern = "additional"; const templateNaNMessage = `
Type NaN to represent an unknown value.`; @@ -235,7 +276,7 @@ export class JSONSchemaForm extends LitElement { this.groups = props.groups ?? []; // NOTE: We assume properties only belong to one conditional requirement group - this.validateEmptyValues = props.validateEmptyValues ?? true; + this.validateEmptyValues = props.validateEmptyValues === undefined ? true : props.validateEmptyValues; // false = validate when not empty, true = always validate, null = never validate if (props.onInvalid) this.onInvalid = props.onInvalid; if (props.sort) this.sort = props.sort; @@ -396,7 +437,7 @@ export class JSONSchemaForm extends LitElement { return; // Ignore required errors if value is empty - if (e.name === "required" && !this.validateEmptyValues && !(e.property in e.instance)) return; + if (e.name === "required" && this.validateEmptyValues === null && !(e.property in e.instance)) return; // Non-Strict Rule if (schema.strict === false && e.message.includes("is not one of enum values")) return; @@ -422,6 +463,8 @@ export class JSONSchemaForm extends LitElement { }; validate = async (resolved = this.resolved) => { + if (this.validateEmptyValues === false) this.validateEmptyValues = true; + // Validate against the entire JSON Schema const copy = structuredClone(resolved); delete copy.__disabled; @@ -505,30 +548,7 @@ export class JSONSchemaForm extends LitElement { return true; }; - #get = (path, object = this.resolved, omitted = [], skipped = []) => { - // path = path.slice(this.base.length); // Correct for base path - if (!path) throw new Error("Path not specified"); - return path.reduce((acc, curr, i) => { - const tempAcc = acc?.[curr] ?? acc?.[omitted.find((str) => acc[str] && acc[str][curr])]?.[curr]; - if (tempAcc) return tempAcc; - else { - const level1 = acc?.[skipped.find((str) => acc[str])]; - if (level1) { - // Handle items-like objects - const result = this.#get(path.slice(i), level1, omitted, skipped); - if (result) return result; - - // Handle pattern properties objects - const got = Object.keys(level1).find((key) => { - const result = this.#get(path.slice(i + 1), level1[key], omitted, skipped); - if (result && typeof result === "object") return result; // Schema are objects... - }); - - if (got) return level1[got]; - } - } - }, object); - }; + #get = (path, object = this.resolved, omitted = [], skipped = []) => get(path, object, omitted, skipped); #checkRequiredAfterChange = async (localPath) => { const path = [...localPath]; @@ -549,22 +569,7 @@ export class JSONSchemaForm extends LitElement { return this.#schema; } - getSchema(path, schema = this.schema) { - if (typeof path === "string") path = path.split("."); - - // NOTE: Still must correct for the base here - if (this.base.length) { - const base = this.base.slice(-1)[0]; - const indexOf = path.indexOf(base); - if (indexOf !== -1) path = path.slice(indexOf + 1); - } - - // NOTE: Refs are now pre-resolved - const resolved = this.#get(path, schema, ["properties", "patternProperties"], ["patternProperties", "items"]); - // if (resolved?.["$ref"]) return this.getSchema(resolved["$ref"].split("/").slice(1)); // NOTE: This assumes reference to the root of the schema - - return resolved; - } + getSchema = (path, schema = this.schema) => getSchema(path, schema, this.base); #renderInteractiveElement = (name, info, required, path = [], value, propertyType) => { let isRequired = this.#isRequired([...path, name]); @@ -652,7 +657,7 @@ export class JSONSchemaForm extends LitElement { // if (typeof isRequired === "object" && !Array.isArray(isRequired)) // invalid.push(...(await this.#validateRequirements(resolved[name], isRequired, path))); // else - if (this.isUndefined(resolved[name]) && this.validateEmptyValues) invalid.push(path); + if (this.isUndefined(resolved[name]) && this.validateEmptyValues !== null) invalid.push(path); } } @@ -874,7 +879,7 @@ export class JSONSchemaForm extends LitElement { type: "error", missing: true, }); - } else { + } else if (this.validateEmptyValues === null) { warnings.push({ message: `${schema.title ?? header(name)} is a suggested property.`, type: "warning", diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js index 281c281df..b9a884e34 100644 --- a/src/renderer/src/stories/JSONSchemaInput.js +++ b/src/renderer/src/stories/JSONSchemaInput.js @@ -505,6 +505,7 @@ export class JSONSchemaInput extends LitElement { constructor(props) { super(); Object.assign(this, props); + if (props.validateEmptyValue === false) this.validateEmptyValue = true; // False is treated as required but not triggered if empty } // onUpdate = () => {} diff --git a/src/renderer/src/stories/forms/GlobalFormModal.ts b/src/renderer/src/stories/forms/GlobalFormModal.ts index 11232285d..9292adc01 100644 --- a/src/renderer/src/stories/forms/GlobalFormModal.ts +++ b/src/renderer/src/stories/forms/GlobalFormModal.ts @@ -70,7 +70,7 @@ export function createFormModal ({ else removeProperties(schemaCopy.properties, propsToRemove) const globalForm = new JSONSchemaForm({ - validateEmptyValues: false, + validateEmptyValues: null, schema: schemaCopy, emptyMessage: "No properties to edit globally.", onThrow, diff --git a/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js b/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js index 30380b8bc..8f88aec22 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js @@ -2,7 +2,7 @@ import { html } from "lit"; import { Page } from "../../Page.js"; // For Multi-Select Form -import { JSONSchemaForm } from "../../../JSONSchemaForm.js"; +import { JSONSchemaForm, getSchema } from "../../../JSONSchemaForm.js"; import { OptionalSection } from "../../../OptionalSection.js"; import { run } from "../options/utils.js"; import { onThrow } from "../../../../errors"; @@ -25,6 +25,13 @@ export async function autocompleteFormatString(path) { const { base_directory } = path.reduce((acc, key) => acc[key] ?? {}, this.form.resolved); + const schema = getSchema(path, this.info.globalState.schema.source_data); + + const isFile = "file_path" in schema.properties; + const pathType = isFile ? "file" : "directory"; + + const description = isFile ? schema.properties.file_path.description : schema.properties.folder_path.description; + const notify = (message, type) => { if (notification) this.dismiss(notification); return (notification = this.notify(message, type)); @@ -48,14 +55,15 @@ export async function autocompleteFormatString(path) { const propOrder = ["path", "subject_id", "session_id"]; const form = new JSONSchemaForm({ + validateEmptyValues: false, schema: { type: "object", properties: { path: { type: "string", - title: "Example Filesystem Entry", - format: ["file", "directory"], - description: "Provide an example filesystem entry for the selected interface", + title: `Example ${isFile ? "File" : "Folder"}`, + format: pathType, + description: description ?? `Provide an example ${pathType} for the selected interface`, }, subject_id: { type: "string", @@ -73,6 +81,9 @@ export async function autocompleteFormatString(path) { const value = parent[name]; if (name === "path") { + const toUpdate = ["subject_id", "session_id"]; + toUpdate.forEach((key) => form.getFormElement([key]).requestUpdate()); + if (value) { if (fs.lstatSync(value).isSymbolicLink()) return [ @@ -122,7 +133,7 @@ export async function autocompleteFormatString(path) { return new Promise((resolve) => { const button = new Button({ - label: "Create", + label: "Submit", primary: true, onClick: async () => { await form.validate().catch((e) => { @@ -432,7 +443,7 @@ export class GuidedPathExpansionPage extends Page { const form = (this.form = new JSONSchemaForm({ ...structureState, onThrow, - validateEmptyValues: false, + validateEmptyValues: null, controls, diff --git a/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js b/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js index 86e8893d0..7ee16c9bd 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js @@ -86,7 +86,7 @@ export class GuidedNewDatasetPage extends Page { this.form = new JSONSchemaForm({ schema, results: this.state, - // validateEmptyValues: false, + // validateEmptyValues: null, dialogOptions: { properties: ["createDirectory"], }, diff --git a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js index 44a0dfbb0..4fbdced65 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js @@ -87,7 +87,7 @@ export class GuidedSubjectsPage extends Page { const modal = (this.#globalModal = createGlobalFormModal.call(this, { header: "Global Subject Metadata", key: "Subject", - validateEmptyValues: false, + validateEmptyValues: null, schema, formProps: { validateOnChange: (localPath, parent, path) => { diff --git a/src/renderer/src/stories/preview/inspector/InspectorList.js b/src/renderer/src/stories/preview/inspector/InspectorList.js index 5b52676f0..5af09fc02 100644 --- a/src/renderer/src/stories/preview/inspector/InspectorList.js +++ b/src/renderer/src/stories/preview/inspector/InspectorList.js @@ -76,7 +76,8 @@ export class InspectorListItem extends LitElement { border-radius: 10px; overflow: hidden; text-wrap: wrap; - padding: 25px; + padding: 10px; + font-size: 12px; margin: 0 0 1em; } From 1a86f2fe1d84c97016797952ee3037535d508b64 Mon Sep 17 00:00:00 2001 From: Garrett Michael Flynn Date: Sat, 9 Mar 2024 10:51:39 -0800 Subject: [PATCH 3/3] Fix custom schema validation (#639) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- environments/environment-Linux.yml | 6 +++--- environments/environment-MAC-arm64.yml | 3 +-- environments/environment-MAC.yml | 3 +-- environments/environment-Windows.yml | 6 +++--- src/renderer/src/stories/JSONSchemaForm.js | 4 ++-- src/renderer/src/stories/Search.js | 3 ++- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/environments/environment-Linux.yml b/environments/environment-Linux.yml index af3a3d7ee..4ccd56d03 100644 --- a/environments/environment-Linux.yml +++ b/environments/environment-Linux.yml @@ -9,16 +9,16 @@ dependencies: - numcodecs = 0.11.0 # install these from conda-forge so that dependent packages get included in the distributable - jsonschema = 4.18.0 # installs jsonschema-specifications - - pydantic[email] = 1.10.12 # installs email-validator - pip - pip: + - pyinstaller-hooks-contrib == 2024.2 # Fix needed for pydantic v2; otherwise imports pydantic.compiled - chardet == 5.1.0 - configparser == 6.0.0 - flask == 2.3.2 - flask-cors == 4.0.0 - flask_restx == 1.1.0 - - neuroconv @ git+https://github.com/catalystneuro/neuroconv.git@main#neuroconv[full] - - dandi >= 0.58.1 + - neuroconv @ git+https://github.com/catalystneuro/neuroconv.git@try_remove_packaing_bound#neuroconv[full] + - dandi >= 0.60.0 - pytest == 7.4.0 - pytest-cov == 4.1.0 - scikit-learn == 1.4.0 diff --git a/environments/environment-MAC-arm64.yml b/environments/environment-MAC-arm64.yml index 2c80ba4cb..ece5dc94a 100644 --- a/environments/environment-MAC-arm64.yml +++ b/environments/environment-MAC-arm64.yml @@ -13,7 +13,6 @@ dependencies: - pytables = 3.8 # pypi build fails on arm64 so install from conda-forge (used by neuroconv deps) # install these from conda-forge so that dependent packages get included in the distributable - jsonschema = 4.18.0 # installs jsonschema-specifications - - pydantic[email] = 1.10.12 # installs email-validator - pip - pip: - chardet == 5.1.0 @@ -22,7 +21,7 @@ dependencies: - flask-cors == 4.0.0 - flask_restx == 1.1.0 - neuroconv @ git+https://github.com/catalystneuro/neuroconv.git@main#neuroconv[full] - - dandi >= 0.58.1 + - dandi >= 0.60.0 - pytest == 7.4.0 - pytest-cov == 4.1.0 - scikit-learn == 1.4.0 diff --git a/environments/environment-MAC.yml b/environments/environment-MAC.yml index af3a3d7ee..73c55d9f5 100644 --- a/environments/environment-MAC.yml +++ b/environments/environment-MAC.yml @@ -9,7 +9,6 @@ dependencies: - numcodecs = 0.11.0 # install these from conda-forge so that dependent packages get included in the distributable - jsonschema = 4.18.0 # installs jsonschema-specifications - - pydantic[email] = 1.10.12 # installs email-validator - pip - pip: - chardet == 5.1.0 @@ -18,7 +17,7 @@ dependencies: - flask-cors == 4.0.0 - flask_restx == 1.1.0 - neuroconv @ git+https://github.com/catalystneuro/neuroconv.git@main#neuroconv[full] - - dandi >= 0.58.1 + - dandi >= 0.60.0 - pytest == 7.4.0 - pytest-cov == 4.1.0 - scikit-learn == 1.4.0 diff --git a/environments/environment-Windows.yml b/environments/environment-Windows.yml index 731b7bd67..84bfd2167 100644 --- a/environments/environment-Windows.yml +++ b/environments/environment-Windows.yml @@ -9,16 +9,16 @@ dependencies: - pywin32 = 303 - git = 2.20.1 - setuptools = 58.0.4 - - pydantic[email] = 1.10.12 # installs email-validator - pip - pip: + - pyinstaller-hooks-contrib == 2024.2 # Fix needed for pydantic v2; otherwise imports pydantic.compiled - chardet == 5.1.0 - configparser == 6.0.0 - flask == 2.3.2 - flask-cors === 3.0.10 - flask_restx == 1.1.0 - - neuroconv @ git+https://github.com/catalystneuro/neuroconv.git@main#neuroconv[full] - - dandi >= 0.58.1 + - neuroconv @ git+https://github.com/catalystneuro/neuroconv.git@try_remove_packaing_bound#neuroconv[full] + - dandi >= 0.60.0 - pytest == 7.2.2 - pytest-cov == 4.1.0 - scikit-learn == 1.4.0 diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js index d0b3a592d..275736fa3 100644 --- a/src/renderer/src/stories/JSONSchemaForm.js +++ b/src/renderer/src/stories/JSONSchemaForm.js @@ -428,10 +428,10 @@ export class JSONSchemaForm extends LitElement { const isRow = typeof rowName === "number"; - const resolvedValue = e.path.reduce((acc, token) => acc[token], resolved); + const resolvedValue = e.instance; // Get offending value + const schema = e.schema; // Get offending schema // ------------ Exclude Certain Errors ------------ - // Allow for constructing types from object types if (e.message.includes("is not of a type(s)") && "properties" in schema && schema.type === "string") return; diff --git a/src/renderer/src/stories/Search.js b/src/renderer/src/stories/Search.js index 70e601896..300073cf8 100644 --- a/src/renderer/src/stories/Search.js +++ b/src/renderer/src/stories/Search.js @@ -455,7 +455,8 @@ export class Search extends LitElement { }} @blur=${(blurEvent) => { - if (blurEvent.relatedTarget.classList.contains("option")) return; + const relatedTarget = blurEvent.relatedTarget; + if (relatedTarget && relatedTarget.classList.contains("option")) return; this.submit(); }}