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 ed318d187..275736fa3 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;
@@ -387,16 +428,16 @@ 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;
// 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 1719dae89..512224813 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/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();
}}
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 0c4c616a5..2609918c0 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) => {
@@ -424,7 +435,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 04cbdfce1..162de5d32 100644
--- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js
+++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js
@@ -96,7 +96,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;
}