Skip to content

Commit

Permalink
Merge branch 'main' into fix-nested-form-cell-error-display
Browse files Browse the repository at this point in the history
  • Loading branch information
CodyCBakerPhD authored Mar 9, 2024
2 parents 7958192 + 1a86f2f commit 71575f6
Show file tree
Hide file tree
Showing 13 changed files with 85 additions and 68 deletions.
6 changes: 3 additions & 3 deletions environments/environment-Linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 1 addition & 2 deletions environments/environment-MAC-arm64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
3 changes: 1 addition & 2 deletions environments/environment-MAC.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
6 changes: 3 additions & 3 deletions environments/environment-Windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
97 changes: 51 additions & 46 deletions src/renderer/src/stories/JSONSchemaForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<br/><small>Type <b>NaN</b> to represent an unknown value.</small>`;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -423,6 +464,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;
Expand Down Expand Up @@ -506,30 +549,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];
Expand All @@ -550,22 +570,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]);
Expand Down Expand Up @@ -653,7 +658,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);
}
}

Expand Down Expand Up @@ -875,7 +880,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",
Expand Down
1 change: 1 addition & 0 deletions src/renderer/src/stories/JSONSchemaInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {}
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/stories/Search.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}}
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/stories/forms/GlobalFormModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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));
Expand All @@ -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",
Expand All @@ -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 [
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -432,7 +443,7 @@ export class GuidedPathExpansionPage extends Page {
const form = (this.form = new JSONSchemaForm({
...structureState,
onThrow,
validateEmptyValues: false,
validateEmptyValues: null,

controls,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export class GuidedNewDatasetPage extends Page {
this.form = new JSONSchemaForm({
schema,
results: this.state,
// validateEmptyValues: false,
// validateEmptyValues: null,
dialogOptions: {
properties: ["createDirectory"],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/stories/preview/inspector/InspectorList.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down

0 comments on commit 71575f6

Please sign in to comment.