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`` 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`