diff --git a/guideGlobalMetadata.json b/guideGlobalMetadata.json
index e6012572d..cd27361e2 100644
--- a/guideGlobalMetadata.json
+++ b/guideGlobalMetadata.json
@@ -21,6 +21,7 @@
"TiffImagingInterface",
"MiniscopeImagingInterface",
"SbxImagingInterface",
+ "CaimanSegmentationInterface",
"MCSRawRecordingInterface",
"MEArecRecordingInterface"
]
diff --git a/pyflask/manageNeuroconv/manage_neuroconv.py b/pyflask/manageNeuroconv/manage_neuroconv.py
index df6aa4a3d..af15b1ab0 100644
--- a/pyflask/manageNeuroconv/manage_neuroconv.py
+++ b/pyflask/manageNeuroconv/manage_neuroconv.py
@@ -88,7 +88,9 @@ def coerce_schema_compliance_recursive(obj, schema):
coerce_schema_compliance_recursive(value, prop_schema)
elif isinstance(obj, list):
for item in obj:
- coerce_schema_compliance_recursive(item, schema.get("items", {}))
+ coerce_schema_compliance_recursive(
+ item, schema.get("items", schema if "properties" else {})
+ ) # NEUROCONV PATCH
return obj
diff --git a/schemas/json/generated/CaimanSegmentationInterface.json b/schemas/json/generated/CaimanSegmentationInterface.json
new file mode 100644
index 000000000..db580d9a5
--- /dev/null
+++ b/schemas/json/generated/CaimanSegmentationInterface.json
@@ -0,0 +1,29 @@
+{
+ "required": [],
+ "properties": {
+ "CaimanSegmentationInterface": {
+ "required": [
+ "file_path"
+ ],
+ "properties": {
+ "file_path": {
+ "format": "file",
+ "type": "string"
+ },
+ "verbose": {
+ "type": "boolean",
+ "default": true
+ }
+ },
+ "type": "object",
+ "additionalProperties": false
+ }
+ },
+ "type": "object",
+ "additionalProperties": false,
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "source.schema.json",
+ "title": "Source data schema",
+ "description": "Schema for the source data, files and directories",
+ "version": "0.1.0"
+}
diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js
index 0753e421b..334a03ed6 100644
--- a/src/renderer/src/stories/JSONSchemaForm.js
+++ b/src/renderer/src/stories/JSONSchemaForm.js
@@ -12,6 +12,8 @@ import { resolveProperties } from "./pages/guided-mode/data/utils";
import { JSONSchemaInput } from "./JSONSchemaInput";
import { InspectorListItem } from "./preview/inspector/InspectorList";
+const selfRequiredSymbol = Symbol();
+
const componentCSS = `
* {
@@ -153,7 +155,7 @@ export class JSONSchemaForm extends LitElement {
};
}
- #base = [];
+ base = [];
#nestedForms = {};
tables = {};
#nErrors = 0;
@@ -205,7 +207,7 @@ export class JSONSchemaForm extends LitElement {
if (props.onStatusChange) this.onStatusChange = props.onStatusChange;
- if (props.base) this.#base = props.base;
+ if (props.base) this.base = props.base;
}
getTable = (path) => {
@@ -242,8 +244,8 @@ export class JSONSchemaForm extends LitElement {
}
// Track resolved values for the form (data only)
- updateData(fullPath, value) {
- const path = [...fullPath];
+ updateData(localPath, value) {
+ const path = [...localPath];
const name = path.pop();
const reducer = (acc, key) => (key in acc ? acc[key] : (acc[key] = {})); // NOTE: Create nested objects if required to set a new path
@@ -261,7 +263,7 @@ export class JSONSchemaForm extends LitElement {
resolvedParent[name] = value;
}
- if (hasUpdate) this.onUpdate(fullPath, value); // Ensure the value has actually changed
+ if (hasUpdate) this.onUpdate(localPath, value); // Ensure the value has actually changed
}
#addMessage = (name, message, type) => {
@@ -271,9 +273,9 @@ export class JSONSchemaForm extends LitElement {
container.appendChild(item);
};
- #clearMessages = (fullPath, type) => {
- if (Array.isArray(fullPath)) fullPath = fullPath.join("-"); // Convert array to string
- const container = this.shadowRoot.querySelector(`#${fullPath} .${type}`);
+ #clearMessages = (localPath, type) => {
+ if (Array.isArray(localPath)) localPath = localPath.join("-"); // Convert array to string
+ const container = this.shadowRoot.querySelector(`#${localPath} .${type}`);
if (container) {
const nChildren = container.children.length;
@@ -348,15 +350,15 @@ export class JSONSchemaForm extends LitElement {
};
#get = (path, object = this.resolved) => {
- // path = path.slice(this.#base.length); // Correct for base path
+ // path = path.slice(this.base.length); // Correct for base path
return path.reduce((acc, curr) => (acc = acc[curr]), object);
};
- #checkRequiredAfterChange = async (fullPath) => {
- const path = [...fullPath];
+ #checkRequiredAfterChange = async (localPath) => {
+ const path = [...localPath];
const name = path.pop();
const element = this.shadowRoot
- .querySelector(`#${fullPath.join("-")}`)
+ .querySelector(`#${localPath.join("-")}`)
.querySelector("jsonschema-input")
.getElement();
const isValid = await this.triggerValidation(name, element, path, false);
@@ -367,13 +369,13 @@ export class JSONSchemaForm extends LitElement {
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];
+ if (this.base.length) {
+ const base = this.base.slice(-1)[0];
const indexOf = path.indexOf(base);
if (indexOf !== -1) path = path.slice(indexOf + 1);
}
- const resolved = path.reduce((acc, curr) => (acc = acc[curr]), schema);
+ const resolved = this.#get(path, schema);
if (resolved["$ref"]) return this.getSchema(resolved["$ref"].split("/").slice(1)); // NOTE: This assumes reference to the root of the schema
return resolved;
@@ -382,8 +384,8 @@ export class JSONSchemaForm extends LitElement {
#renderInteractiveElement = (name, info, required, path = []) => {
let isRequired = required[name];
- const fullPath = [...path, name];
- const externalPath = [...this.#base, ...fullPath];
+ const localPath = [...path, name];
+ const externalPath = [...this.base, ...localPath];
const resolved = this.#get(path, this.resolved);
const value = resolved[name];
@@ -392,11 +394,11 @@ export class JSONSchemaForm extends LitElement {
if (isConditional && !isRequired)
isRequired = required[name] = async () => {
- const isRequiredAfterChange = await this.#checkRequiredAfterChange(fullPath);
+ const isRequiredAfterChange = await this.#checkRequiredAfterChange(localPath);
if (isRequiredAfterChange) {
return true;
} else {
- const linkResults = await this.#applyToLinkedProperties(this.#checkRequiredAfterChange, fullPath); // Check links
+ const linkResults = await this.#applyToLinkedProperties(this.#checkRequiredAfterChange, localPath); // Check links
if (linkResults.includes(true)) return true;
// Handle updates when no longer required
else return false;
@@ -405,7 +407,7 @@ export class JSONSchemaForm extends LitElement {
const interactiveInput = new JSONSchemaInput({
info,
- path: fullPath,
+ path: localPath,
value,
form: this,
required: isRequired,
@@ -425,7 +427,7 @@ export class JSONSchemaForm extends LitElement {
return html`
= path.length) return isRenderable(key, value);
if (required[key]) return isRenderable(key, value);
- if (this.#getLink([...this.#base, ...path, key])) return isRenderable(key, value);
+ if (this.#getLink([...this.base, ...path, key])) return isRenderable(key, value);
if (!this.onlyRequired) return isRenderable(key, value);
return false;
})
@@ -549,7 +555,7 @@ export class JSONSchemaForm extends LitElement {
#isLinkResolved = async (pathArr) => {
return (
await this.#applyToLinkedProperties((link) => {
- const isRequired = this.#isRequired(link.slice((this.#base ?? []).length));
+ const isRequired = this.#isRequired(link.slice((this.base ?? []).length));
if (typeof isRequired === "function") return !isRequired.call(this.resolved);
else return !isRequired;
}, pathArr)
@@ -558,8 +564,11 @@ export class JSONSchemaForm extends LitElement {
#isRequired = (path) => {
if (typeof path === "string") path = path.split("-");
- // path = path.slice(this.#base.length); // Remove base path
- return path.reduce((obj, key) => obj && obj[key], this.#requirements);
+ // path = path.slice(this.base.length); // Remove base path
+ const res = path.reduce((obj, key) => obj && obj[key], this.#requirements);
+
+ if (typeof res === "object") res = res[selfRequiredSymbol];
+ return res;
};
#getLinkElement = (externalPath) => {
@@ -572,15 +581,17 @@ export class JSONSchemaForm extends LitElement {
triggerValidation = async (name, element, path = [], checkLinks = true) => {
const parent = this.#get(path, this.resolved);
+ const pathToValidate = [...(this.base ?? []), ...path];
+
const valid =
!this.validateEmptyValues && !(name in parent)
? true
- : await this.validateOnChange(name, parent, [...(this.#base ?? []), ...path]);
+ : await this.validateOnChange(name, parent, pathToValidate);
- const fullPath = [...path, name]; // Use basePath to augment the validation
- const externalPath = [...this.#base, name];
+ const localPath = [...path, name]; // Use basePath to augment the validation
+ const externalPath = [...this.base, name];
- const isRequired = this.#isRequired(fullPath);
+ const isRequired = this.#isRequired(localPath);
let warnings = Array.isArray(valid)
? valid.filter((info) => info.type === "warning" && (!isRequired || !info.missing))
: [];
@@ -599,7 +610,7 @@ export class JSONSchemaForm extends LitElement {
// Clear old errors and warnings on linked properties
this.#applyToLinkedProperties((path) => {
- const internalPath = path.slice((this.#base ?? []).length);
+ const internalPath = path.slice((this.base ?? []).length);
this.#clearMessages(internalPath, "errors");
this.#clearMessages(internalPath, "warnings");
}, externalPath);
@@ -613,9 +624,9 @@ export class JSONSchemaForm extends LitElement {
}
// Clear old errors and warnings
- this.#clearMessages(fullPath, "errors");
- this.#clearMessages(fullPath, "warnings");
- this.#clearMessages(fullPath, "info");
+ this.#clearMessages(localPath, "errors");
+ this.#clearMessages(localPath, "warnings");
+ this.#clearMessages(localPath, "info");
const isFunction = typeof valid === "function";
const isValid =
@@ -632,8 +643,8 @@ export class JSONSchemaForm extends LitElement {
this.checkStatus();
// Show aggregated errors and warnings (if any)
- warnings.forEach((info) => this.#addMessage(fullPath, info, "warnings"));
- info.forEach((info) => this.#addMessage(fullPath, info, "info"));
+ warnings.forEach((info) => this.#addMessage(localPath, info, "warnings"));
+ info.forEach((info) => this.#addMessage(localPath, info, "info"));
if (isValid && errors.length === 0) {
element.classList.remove("invalid");
@@ -643,7 +654,7 @@ export class JSONSchemaForm extends LitElement {
await this.#applyToLinkedProperties((path, element) => {
element.classList.remove("required", "conditional"); // Links manage their own error and validity states, but only one needs to be valid
- }, fullPath);
+ }, localPath);
if (isFunction) valid(); // Run if returned value is a function
@@ -661,7 +672,7 @@ export class JSONSchemaForm extends LitElement {
[...path, name]
);
- errors.forEach((info) => this.#addMessage(fullPath, info, "errors"));
+ errors.forEach((info) => this.#addMessage(localPath, info, "errors"));
// element.title = errors.map((info) => info.message).join("\n"); // Set all errors to show on hover
return false;
@@ -679,7 +690,7 @@ export class JSONSchemaForm extends LitElement {
if (renderable.length === 0) return html`
No properties to render
`;
let renderableWithLinks = renderable.reduce((acc, [name, info]) => {
- const externalPath = [...this.#base, ...path, name];
+ const externalPath = [...this.base, ...path, name];
const link = this.#getLink(externalPath); // Use the base path to find a link
if (link) {
@@ -740,7 +751,7 @@ export class JSONSchemaForm extends LitElement {
// Render linked properties
if (entry[isLink]) {
const linkedProperties = info.properties.map((path) => {
- const pathCopy = [...path].slice((this.#base ?? []).length);
+ const pathCopy = [...path].slice((this.base ?? []).length);
const name = pathCopy.pop();
return this.#renderInteractiveElement(name, schema.properties[name], required, pathCopy);
});
@@ -756,7 +767,7 @@ export class JSONSchemaForm extends LitElement {
const hasMany = renderable.length > 1; // How many siblings?
- const fullPath = [...path, name];
+ const localPath = [...path, name];
if (this.mode === "accordion" && hasMany) {
const headerName = header(name);
@@ -767,8 +778,10 @@ export class JSONSchemaForm extends LitElement {
results: { ...results[name] },
globals: this.globals?.[name],
+ mode: this.mode,
+
onUpdate: (internalPath, value) => {
- const path = [...fullPath, ...internalPath];
+ const path = [...localPath, ...internalPath];
this.updateData(path, value);
},
@@ -793,13 +806,13 @@ export class JSONSchemaForm extends LitElement {
this.checkAllLoaded();
},
renderTable: (...args) => this.renderTable(...args),
- base: fullPath,
+ base: [...this.base, ...localPath],
});
const accordion = new Accordion({
sections: {
[headerName]: {
- subtitle: `${this.#getRenderable(info, required[name], fullPath, true).length} fields`,
+ subtitle: `${this.#getRenderable(info, required[name], localPath, true).length} fields`,
content: this.#nestedForms[name],
},
},
@@ -811,7 +824,7 @@ export class JSONSchemaForm extends LitElement {
}
// Render properties in the sub-schema
- const rendered = this.#render(info, results?.[name], required[name], fullPath);
+ const rendered = this.#render(info, results?.[name], required[name], localPath);
return hasMany || path.length > 1
? html`
@@ -834,7 +847,9 @@ export class JSONSchemaForm extends LitElement {
Object.entries(schema.properties).forEach(([key, value]) => {
if (value.properties) {
let nextAccumulator = acc[key];
- if (!nextAccumulator || typeof nextAccumulator !== "object") nextAccumulator = acc[key] = {};
+ const isNotObject = typeof nextAccumulator !== "object";
+ if (!nextAccumulator || isNotObject)
+ nextAccumulator = acc[key] = { [selfRequiredSymbol]: !!nextAccumulator };
this.#registerRequirements(value, requirements[key], nextAccumulator);
}
});
diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js
index 67e2920a5..d20c54ecd 100644
--- a/src/renderer/src/stories/JSONSchemaInput.js
+++ b/src/renderer/src/stories/JSONSchemaInput.js
@@ -200,7 +200,7 @@ export class JSONSchemaInput extends LitElement {
(this.onValidate
? this.onValidate()
: this.form
- ? this.form.validateOnChange(key, parent, fullPath, v)
+ ? this.form.validateOnChange(key, parent, [...this.form.base, ...fullPath], v)
: "")
);
},
diff --git a/src/renderer/src/stories/pages/guided-mode/SourceData.stories.js b/src/renderer/src/stories/pages/guided-mode/SourceData.stories.js
index 55c0b736a..e668929eb 100644
--- a/src/renderer/src/stories/pages/guided-mode/SourceData.stories.js
+++ b/src/renderer/src/stories/pages/guided-mode/SourceData.stories.js
@@ -20,6 +20,7 @@ import ScanImageImagingInterfaceSchema from "../../../../../../schemas/json/gene
import TiffImagingInterfaceSchema from "../../../../../../schemas/json/generated/TiffImagingInterface.json";
import MiniscopeImagingInterfaceSchema from "../../../../../../schemas/json/generated/MiniscopeImagingInterface.json";
import SbxImagingInterfaceSchema from "../../../../../../schemas/json/generated/SbxImagingInterface.json";
+import CaimanSegmentationInterfaceSchema from "../../../../../../schemas/json/generated/CaimanSegmentationInterface.json";
import MCSRawRecordingInterfaceSchema from "../../../../../../schemas/json/generated/MCSRawRecordingInterface.json";
import MEArecRecordingInterfaceSchema from "../../../../../../schemas/json/generated/MEArecRecordingInterface.json";
@@ -75,6 +76,8 @@ globalStateCopy.schema.source_data.properties.MiniscopeImagingInterface =
MiniscopeImagingInterfaceSchema.properties.MiniscopeImagingInterface;
globalStateCopy.schema.source_data.properties.SbxImagingInterface =
SbxImagingInterfaceSchema.properties.SbxImagingInterface;
+globalStateCopy.schema.source_data.properties.CaimanSegmentationInterface =
+ CaimanSegmentationInterfaceSchema.properties.CaimanSegmentationInterface;
globalStateCopy.schema.source_data.properties.MCSRawRecordingInterface =
MCSRawRecordingInterfaceSchema.properties.MCSRawRecordingInterface;
globalStateCopy.schema.source_data.properties.MEArecRecordingInterface =
@@ -218,6 +221,12 @@ SbxImagingInterfaceGlobalCopy.interfaces.interface = SbxImagingInterface;
SbxImagingInterfaceGlobalCopy.schema.source_data = SbxImagingInterfaceSchema;
SbxImagingInterface.args = { activePage, globalState: SbxImagingInterfaceGlobalCopy };
+export const CaimanSegmentationInterface = PageTemplate.bind({});
+const CaimanSegmentationInterfaceGlobalCopy = JSON.parse(JSON.stringify(globalState));
+CaimanSegmentationInterfaceGlobalCopy.interfaces.interface = CaimanSegmentationInterface;
+CaimanSegmentationInterfaceGlobalCopy.schema.source_data = CaimanSegmentationInterfaceSchema;
+CaimanSegmentationInterface.args = { activePage, globalState: CaimanSegmentationInterfaceGlobalCopy };
+
export const MCSRawRecordingInterface = PageTemplate.bind({});
const MCSRawRecordingInterfaceGlobalCopy = JSON.parse(JSON.stringify(globalState));
MCSRawRecordingInterfaceGlobalCopy.interfaces.interface = MCSRawRecordingInterface;
diff --git a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js
index d3de48412..e265e15d5 100644
--- a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js
+++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js
@@ -128,7 +128,7 @@ export class GuidedMetadataPage extends ManagedPage {
this.#checkAllLoaded();
},
- onUpdate: (...args) => {
+ onUpdate: () => {
this.unsavedUpdates = true;
},
diff --git a/src/renderer/src/stories/pages/guided-mode/data/utils.js b/src/renderer/src/stories/pages/guided-mode/data/utils.js
index e5ab73f9d..17a06aa0e 100644
--- a/src/renderer/src/stories/pages/guided-mode/data/utils.js
+++ b/src/renderer/src/stories/pages/guided-mode/data/utils.js
@@ -29,10 +29,18 @@ export function resolveProperties(properties = {}, target, globals = {}) {
for (let name in properties) {
const info = properties[name];
+
+ // NEUROCONV PATCH: Correct for incorrect array schema
+ if (info.properties && info.type === "array") {
+ info.items = { type: "object", properties: info.properties, required: info.required };
+ delete info.properties;
+ }
+
const props = info.properties;
if (!(name in target)) {
if (props) target[name] = {}; // Regisiter new interfaces in results
+ // if (info.type === "array") target[name] = []; // Auto-populate arrays (NOTE: Breaks PyNWB when adding to TwoPhotonSeries field...)
// Apply global or default value if empty
if (name in globals) target[name] = globals[name];