diff --git a/schemas/base-metadata.schema.ts b/schemas/base-metadata.schema.ts
index 726ac1df2..7fea3bd08 100644
--- a/schemas/base-metadata.schema.ts
+++ b/schemas/base-metadata.schema.ts
@@ -41,6 +41,12 @@ export const preprocessMetadataSchema = (schema: any = baseMetadataSchema, globa
copy.additionalProperties = false
+
+
+ copy.required = Object.keys(copy.properties) // Require all properties at the top level
+
+ copy.order = [ "NWBFile", "Subject" ]
+
// Add unit to weight
const subjectProps = copy.properties.Subject.properties
subjectProps.weight.unit = 'kg'
@@ -88,6 +94,8 @@ export const preprocessMetadataSchema = (schema: any = baseMetadataSchema, globa
if (ophys) {
+ ophys.required = Object.keys(ophys.properties)
+
const getProp = (name: string) => ophys.properties[name]
if (getProp("TwoPhotonSeries")) {
@@ -143,8 +151,17 @@ export const preprocessMetadataSchema = (schema: any = baseMetadataSchema, globa
// Remove non-global properties
if (global) {
+
Object.entries(copy.properties).forEach(([globalProp, schema]) => {
- instanceSpecificFields[globalProp]?.forEach((prop) => delete schema.properties[prop]);
+
+ const requiredSet = new Set(schema.required)
+
+ instanceSpecificFields[globalProp]?.forEach((prop) => {
+ delete schema.properties[prop]
+ requiredSet.delete(prop)
+ });
+
+ schema.required = Array.from(requiredSet)
});
}
diff --git a/src/renderer/src/stories/Accordion.js b/src/renderer/src/stories/Accordion.js
index d4711e82d..b0bae8602 100644
--- a/src/renderer/src/stories/Accordion.js
+++ b/src/renderer/src/stories/Accordion.js
@@ -11,8 +11,6 @@ import {
import { Chevron } from "./Chevron";
-// import 'fa-icons';
-
const faSize = "1em";
const faColor = "#000000";
@@ -25,13 +23,11 @@ export class Accordion extends LitElement {
:host {
display: block;
- overflow: hidden;
}
.header {
display: flex;
- align-items: end;
- padding: 20px 0px;
+ align-items: center;
white-space: nowrap;
}
@@ -47,14 +43,6 @@ export class Accordion extends LitElement {
margin-right: 10px;
}
- .header > *:nth-child(2) {
- padding-bottom: 2px;
- }
-
- nwb-chevron {
- margin: 10px;
- }
-
.guided--nav-bar-section {
display: flex;
flex-direction: column;
@@ -64,45 +52,39 @@ export class Accordion extends LitElement {
height: 100%;
}
- .guided--nav-bar-section > * {
- padding: 0px 10px;
- }
-
.content {
width: 100%;
}
.guided--nav-bar-dropdown {
position: relative;
- min-height: 40px;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
user-select: none;
- background-color: rgb(240, 240, 240);
- border-bottom: 1px solid gray;
+ background-color: rgb(235, 235, 235);
}
- .guided--nav-bar-dropdown.active {
- border-bottom: none;
+ .guided--nav-bar-section > * {
+ padding: 3px 15px 3px 10px;
}
- .guided--nav-bar-section:last-child > .guided--nav-bar-dropdown {
+ .guided--nav-bar-dropdown.active {
border-bottom: none;
}
.guided--nav-bar-dropdown.error {
- border-bottom: 5px solid hsl(${errorHue}, 100%, 70%) !important;
+ border-bottom: 3px solid hsl(${errorHue}, 100%, 70%) !important;
}
.guided--nav-bar-dropdown.warning {
- border-bottom: 5px solid hsl(${warningHue}, 100%, 70%) !important;
+ border-bottom: 3px solid hsl(${warningHue}, 100%, 70%) !important;
}
.guided--nav-bar-dropdown.valid {
- border-bottom: 5px solid hsl(${successHue}, 100%, 70%) !important;
+ border-bottom: 3px solid hsl(${successHue}, 100%, 70%) !important;
}
.guided--nav-bar-dropdown {
@@ -120,21 +102,9 @@ export class Accordion extends LitElement {
right: 50px;
}
- .guided--nav-bar-dropdown.error::after {
- content: "${errorSymbol}";
- }
-
- .guided--nav-bar-dropdown.warning::after {
- content: "${warningSymbol}";
- }
-
- .guided--nav-bar-dropdown.valid::after {
- content: "${successSymbol}";
- }
-
.guided--nav-bar-dropdown.toggleable:hover {
cursor: pointer;
- background-color: lightgray;
+ background-color: gainsboro;
}
.guided--nav-bar-section-page {
@@ -268,7 +238,7 @@ export class Accordion extends LitElement {
? html`
${this.content}
`
diff --git a/src/renderer/src/stories/BasicTable.js b/src/renderer/src/stories/BasicTable.js
index e880cd146..c56e8329e 100644
--- a/src/renderer/src/stories/BasicTable.js
+++ b/src/renderer/src/stories/BasicTable.js
@@ -243,7 +243,7 @@ export class BasicTable extends LitElement {
else if (value !== "" && thisTypeOf !== type)
result = [{ message: `${col} is expected to be of type ${ogType}, not ${thisTypeOf}`, type: "error" }];
// Otherwise validate using the specified onChange function
- else result = this.validateOnChange([col], parent, value, this.#itemProps[col]);
+ else result = this.validateOnChange(col, parent, value, this.#itemProps[col]);
// Will run synchronously if not a promise result
return promises.resolve(result, () => {
diff --git a/src/renderer/src/stories/Button.js b/src/renderer/src/stories/Button.js
index 066316f26..850dc32dd 100644
--- a/src/renderer/src/stories/Button.js
+++ b/src/renderer/src/stories/Button.js
@@ -52,6 +52,11 @@ export class Button extends LitElement {
background-color: transparent;
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
}
+ .storybook-button--extra-small {
+ font-size: 10px;
+ padding: 7px 12px;
+ }
+
.storybook-button--small {
font-size: 12px;
padding: 10px 16px;
diff --git a/src/renderer/src/stories/Chevron.js b/src/renderer/src/stories/Chevron.js
index caf62e199..9aa31c933 100644
--- a/src/renderer/src/stories/Chevron.js
+++ b/src/renderer/src/stories/Chevron.js
@@ -13,7 +13,7 @@ export class Chevron extends LitElement {
div::before {
border-style: solid;
- border-width: 0.25em 0.25em 0 0;
+ border-width: 0.2em 0.2em 0 0;
content: "";
display: inline-block;
height: 0.45em;
diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js
index abee5c817..36060fd91 100644
--- a/src/renderer/src/stories/JSONSchemaForm.js
+++ b/src/renderer/src/stories/JSONSchemaForm.js
@@ -27,6 +27,7 @@ const provideNaNMessage = `
Type NaN to represent an unknown v
import { Validator } from "jsonschema";
import { successHue, warningHue, errorHue } from "./globals";
+import { Button } from "./Button";
var validator = new Validator();
@@ -302,7 +303,10 @@ export class JSONSchemaForm extends LitElement {
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
+ const reducer = (acc, key) => {
+ const value = acc[key];
+ return value && typeof value === "object" ? value : (acc[key] = {});
+ }; // NOTE: Create nested objects if required to set a new path
const resultParent = path.reduce(reducer, this.results);
const resolvedParent = path.reduce(reducer, this.resolved);
@@ -386,6 +390,10 @@ export class JSONSchemaForm extends LitElement {
const resolvedValue = e.path.reduce((acc, token) => acc[token], resolved);
// ------------ Exclude Certain Errors ------------
+
+ // Ignore required errors if value is empty
+ if (e.name === "required" && !this.validateEmptyValues && !(e.property in e.instance)) return;
+
// Non-Strict Rule
if (schema.strict === false && e.message.includes("is not one of enum values")) return;
@@ -428,7 +436,7 @@ export class JSONSchemaForm extends LitElement {
if (resolvedErrors.length) {
const len = resolvedErrors.length;
if (len === 1) this.throw(resolvedErrors[0].message);
- else this.throw(`${len} JSON Schema errors on this form.`);
+ else this.throw(`${len} JSON Schema errors detected.`);
}
const allErrors = Array.from(flaggedInputs)
@@ -1060,24 +1068,11 @@ export class JSONSchemaForm extends LitElement {
const localPath = [...path, name];
- const enableToggle = document.createElement("input");
- const enableToggleContainer = document.createElement("div");
- Object.assign(enableToggleContainer.style, {
- position: "relative",
- });
- enableToggleContainer.append(enableToggle);
-
// Check properties that will be rendered before creating the accordion
const base = [...this.base, ...localPath];
const explicitlyRequired = schema.required?.includes(name) ?? false;
- Object.assign(enableToggle, {
- type: "checkbox",
- checked: true,
- style: "margin-right: 10px; pointer-events:all;",
- });
-
const headerName = header(info.title ?? name);
const renderableInside = this.#getRenderable(info, required[name], ignore, localPath, true);
@@ -1097,8 +1092,6 @@ export class JSONSchemaForm extends LitElement {
const isDisabled = !!__disabledResolved[name];
- enableToggle.checked = !isDisabled;
-
const nestedResults = __disabled[name] ?? results[name] ?? this.results[name]; // One or the other will exist—depending on global or local disabling
if (renderableInside.length) {
@@ -1151,13 +1144,43 @@ export class JSONSchemaForm extends LitElement {
const oldStates = this.#accordions[headerName];
+ const disableText = "Skip";
+ const enableText = "Enable";
+
+ const disabledPath = [...path, "__disabled"];
+ const interactedPath = [...disabledPath, "__interacted"];
+
+ const enableToggle = new Button({
+ label: isDisabled ? enableText : disableText,
+ size: "extra-small",
+ onClick: (ev) => {
+ ev.stopPropagation();
+
+ const willEnable = enableToggle.label === enableText;
+
+ // Reset parameters on interaction
+ isGlobalEffect = false;
+
+ enableToggle.label = willEnable ? disableText : enableText;
+
+ willEnable ? enable() : disable();
+ this.updateData([...interactedPath, name], true, true);
+
+ this.onUpdate(localPath, this.results[name]);
+ },
+ });
+
+ // const enableToggle = document.createElement("input");
+ const enableToggleContainer = document.createElement("div");
+ Object.assign(enableToggleContainer.style, { position: "relative" });
+ enableToggleContainer.append(enableToggle);
+ Object.assign(enableToggle.style, { marginRight: "10px", pointerEvents: "all" });
+
const accordion = (this.#accordions[headerName] = new Accordion({
name: headerName,
toggleable: hasMany,
subtitle: html`
- ${explicitlyRequired ? "" : enableToggleContainer}${renderableInside.length
- ? `${hasPatternProperties ? "Dynamic" : renderableInside.length} fields`
- : ""}
+ ${explicitlyRequired ? "" : enableToggleContainer}
`,
content: this.#nestedForms[name],
@@ -1169,68 +1192,41 @@ export class JSONSchemaForm extends LitElement {
accordion.id = name; // assign name to accordion id
- // Set enable / disable behavior
- const addDisabled = (name, parentObject) => {
- if (!parentObject.__disabled) parentObject.__disabled = {};
-
- // Do not overwrite cache of disabled values (with globals, for instance)
- if (parentObject.__disabled[name]) {
- if (isGlobalEffect) return;
- }
-
- parentObject.__disabled[name] = parentObject[name] ?? (parentObject[name] = {}); // Track disabled values (or at least something)
- };
-
const disable = () => {
accordion.disabled = true;
- addDisabled(name, this.resolved);
- addDisabled(name, this.results);
- this.resolved[name] = this.results[name] = undefined; // Remove entry from results
- this.checkStatus();
- };
+ const target = this.results;
+ const value = target[name] ?? {};
- const enable = () => {
- accordion.disabled = false;
+ let update = true;
+ if (target.__disabled?.[name] && isGlobalEffect) update = false;
- const { __disabled = {} } = this.results;
- const { __disabled: resolvedDisabled = {} } = this.resolved;
+ // Disabled path is set to actual value
+ if (update) this.updateData([...disabledPath, name], value);
- if (__disabled[name]) this.updateData(localPath, __disabled[name]); // Propagate restored disabled values
- __disabled[name] = undefined; // Clear disabled value
- resolvedDisabled[name] = undefined; // Clear disabled value
+ // Actual data is set to undefined
+ this.updateData(localPath, undefined);
this.checkStatus();
};
- enableToggle.addEventListener("click", (clickEvent) => {
- clickEvent.stopPropagation();
- const { checked } = clickEvent.target;
-
- // Reset parameters on interaction
- isGlobalEffect = false;
- Object.assign(enableToggle.style, {
- accentColor: "unset",
- });
+ const enable = () => {
+ accordion.disabled = false;
const { __disabled = {} } = this.results;
- const { __disabled: resolvedDisabled = {} } = this.resolved;
- if (!__disabled.__interacted) __disabled.__interacted = {};
- if (!resolvedDisabled.__interacted) resolvedDisabled.__interacted = {};
+ // Actual value is restored to the cached value
+ if (__disabled[name]) this.updateData(localPath, __disabled[name]);
- __disabled.__interacted[name] = resolvedDisabled.__interacted[name] = true; // Track that the user has interacted with the form
+ // Cached value is cleared
+ this.updateData([...disabledPath, name], undefined);
- checked ? enable() : disable();
-
- this.onUpdate(localPath, this.results[name]);
- });
+ this.checkStatus();
+ };
if (isGlobalEffect) {
isDisabled ? disable() : enable();
- Object.assign(enableToggle.style, {
- accentColor: "gray",
- });
+ Object.assign(enableToggle.style, { accentColor: "gray" });
}
return accordion;
diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js
index 1d0941e7d..3c61f412b 100644
--- a/src/renderer/src/stories/JSONSchemaInput.js
+++ b/src/renderer/src/stories/JSONSchemaInput.js
@@ -989,17 +989,31 @@ export class JSONSchemaInput extends LitElement {
return search;
}
+ const enumItems = [...schema.enum];
+
+ const noSelection = "No Selection";
+ if (!this.required) enumItems.unshift(noSelection);
+
+ const selectedItem = enumItems.find((item) => this.value === item);
+
return html`