diff --git a/schemas/base-metadata.schema.ts b/schemas/base-metadata.schema.ts index 02fd6a61f..c8a16f336 100644 --- a/schemas/base-metadata.schema.ts +++ b/schemas/base-metadata.schema.ts @@ -111,8 +111,8 @@ export const preprocessMetadataSchema = (schema: any = baseMetadataSchema, globa uvProperties.forEach(prop => { electrodeItems[prop] = {} electrodeItems[prop].title = prop.replace('uV', uvMathFormat) - console.log(electrodeItems[prop]) }) + interfaceProps["Electrodes"].items.order = ["channel_name", "group_name", "shank_electrode_number", ...uvProperties]; interfaceProps["ElectrodeColumns"].items.order = ["name", "description", "data_type"]; diff --git a/src/renderer/src/stories/Dashboard.js b/src/renderer/src/stories/Dashboard.js index c14e5c15b..7606fe64c 100644 --- a/src/renderer/src/stories/Dashboard.js +++ b/src/renderer/src/stories/Dashboard.js @@ -234,6 +234,7 @@ export class Dashboard extends LitElement { if (this.#transitionPromise.value) this.#transitionPromise.trigger(page); // This ensures calls to page.to() can be properly awaited until the next page is ready const { skipped } = this.subSidebar.sections[info.section]?.pages?.[info.id] ?? {}; + if (skipped) { if (isStorybook) return; // Do not skip on storybook diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js index 3283bed10..24db49e3a 100644 --- a/src/renderer/src/stories/JSONSchemaForm.js +++ b/src/renderer/src/stories/JSONSchemaForm.js @@ -98,7 +98,7 @@ const componentCSS = ` :host { display: inline-block; - width:100%; + width: 100%; } diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js index a2b388697..d2a018d9c 100644 --- a/src/renderer/src/stories/JSONSchemaInput.js +++ b/src/renderer/src/stories/JSONSchemaInput.js @@ -498,6 +498,31 @@ export class JSONSchemaInput extends LitElement { }; } + // Enforce dynamic required properties + attributeChangedCallback(key, _, latest) { + super.attributeChangedCallback(...arguments); + + const formSchema = this.form.schema; + + if (key === "required") { + const name = this.path.slice(-1)[0]; + + if (latest !== null && !this.conditional) { + const requirements = formSchema.required ?? (formSchema.required = []); + if (!requirements.includes(name)) requirements.push(name); + } + + // Remove requirement from form schema (and force if conditional requirement) + else { + if (formSchema.requirements && formSchema.requirements.includes(name)) { + const set = new Set(formSchema.requirements); + set.remove(name); + formSchema.requirements = Array.from(set); + } + } + } + } + // schema, // parent, // path, @@ -637,7 +662,9 @@ export class JSONSchemaInput extends LitElement { ${ schema.description ? html`
- ${unsafeHTML(capitalize(schema.description))}${schema.description.slice(-1)[0] === "." + ${unsafeHTML(capitalize(schema.description))}${[".", "?", "!"].includes( + schema.description.slice(-1)[0] + ) ? "" : "."}
` 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 e28852fc4..124df6afd 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js @@ -263,6 +263,8 @@ export class GuidedPathExpansionPage extends Page { #initialize = () => (this.localState = merge(this.info.globalState.structure, { results: {} })); workflow = { + subject_id: {}, + session_id: {}, locate_data: { skip: () => { this.#initialize(); @@ -270,37 +272,42 @@ export class GuidedPathExpansionPage extends Page { merge({ structure: this.localState }, globalState); // Merge the actual entries into the structure // Force single subject/session if not keeping existing data - if (!globalState.results) { - const existingMetadata = - globalState.results?.[this.altInfo.subject_id]?.[this.altInfo.session_id]?.metadata; + // if (!globalState.results) { - const existingSourceData = - globalState.results?.[this.altInfo.subject_id]?.[this.altInfo.session_id]?.source_data; + const subject_id = this.workflow.subject_id.value; + const session_id = this.workflow.session_id.value; - const source_data = {}; - for (let key in globalState.interfaces) { - const existing = existingSourceData?.[key]; - if (existing) source_data[key] = existing ?? {}; - } + // Map existing results to new subject information (if available) + const existingResults = Object.values(Object.values(globalState.results ?? {})[0] ?? {})[0] ?? {}; + const existingMetadata = existingResults.metadata; + const existingSourceData = existingResults.source_data; + + const source_data = {}; + for (let key in globalState.interfaces) { + const existing = existingSourceData?.[key]; + if (existing) source_data[key] = existing ?? {}; + } - globalState.results = { - [this.altInfo.subject_id]: { - [this.altInfo.session_id]: { - source_data, - metadata: { - NWBFile: { - session_id: this.altInfo.session_id, - ...(existingMetadata?.NWBFile ?? {}), - }, - Subject: { - subject_id: this.altInfo.subject_id, - ...(existingMetadata?.Subject ?? {}), - }, + globalState.results = { + [subject_id]: { + [session_id]: { + source_data, + metadata: { + NWBFile: { + session_id: session_id, + ...(existingMetadata?.NWBFile ?? {}), + }, + Subject: { + subject_id: subject_id, + ...(existingMetadata?.Subject ?? {}), }, }, }, - }; - } + }, + }; + // } + + this.save({}, false); // Ensure this structure is saved }, }, }; @@ -382,11 +389,6 @@ export class GuidedPathExpansionPage extends Page { }, }; - altInfo = { - subject_id: "001", - session_id: "1", - }; - // altForm = new JSONSchemaForm({ // results: this.altInfo, // schema: { @@ -436,7 +438,7 @@ export class GuidedPathExpansionPage extends Page { const form = (this.form = new JSONSchemaForm({ ...structureState, onThrow, - validateEmptyValues: null, + validateEmptyValues: false, controls, diff --git a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js index 6bd47bc30..2d67ff619 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js @@ -222,10 +222,6 @@ export class GuidedSourceDataPage extends ManagedPage { updated() { const dashboard = document.querySelector("nwb-dashboard"); const page = dashboard.page; - setTimeout(() => { - console.log(page.forms[0].form.accordions["SpikeGLX Recording"]); - }); - console.log(page.forms[0].form.accordions); } render() { 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 11f54307b..060e6a3f4 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js @@ -5,7 +5,6 @@ import { validateOnChange } from "../../../../validation/index.js"; import { Table } from "../../../Table.js"; import { updateResultsFromSubjects } from "./utils"; -import { merge } from "../../utils.js"; import { preprocessMetadataSchema } from "../../../../../../../schemas/base-metadata.schema"; import { Button } from "../../../Button.js"; import { createGlobalFormModal } from "../../../forms/GlobalFormModal"; diff --git a/src/renderer/src/stories/pages/guided-mode/setup/Preform.js b/src/renderer/src/stories/pages/guided-mode/setup/Preform.js index 8ac335db1..a298f026a 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/Preform.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/Preform.js @@ -13,10 +13,36 @@ const questions = { title: "Will this pipeline be run on multiple sessions?", default: false, }, + subject_id: { + type: "string", + description: "Provide an identifier for your subject", + dependencies: { + multiple_sessions: { + condition: [false, undefined], + default: "", + required: true, + attribute: "hidden", + }, + }, + }, + session_id: { + type: "string", + description: "Provide an identifier for your session", + dependencies: { + multiple_sessions: { + condition: [false, undefined], + default: "", + required: true, + attribute: "hidden", + }, + }, + }, locate_data: { type: "boolean", title: "Would you like to locate the source data programmatically?", - dependencies: ["multiple_sessions"], + dependencies: { + multiple_sessions: { default: false }, + }, default: false, }, }; @@ -28,11 +54,19 @@ const questions = { const dependents = Object.entries(questions).reduce((acc, [name, info]) => { acc[name] = []; - if (info.dependencies) { - info.dependencies.forEach((dep) => { - if (!acc[dep]) acc[dep] = []; - acc[dep].push(name); - }); + const deps = info.dependencies; + + if (deps) { + if (Array.isArray(deps)) + deps.forEach((dep) => { + if (!acc[dep]) acc[dep] = []; + acc[dep].push({ name }); + }); + else + Object.entries(deps).forEach(([dep, opts]) => { + if (!acc[dep]) acc[dep] = []; + acc[dep].push({ name, ...opts }); + }); } return acc; }, {}); @@ -80,14 +114,38 @@ export class GuidedPreform extends Page { this.form = new JSONSchemaForm({ schema, results: this.state, + validateEmptyValues: false, // Only show errors after submission validateOnChange: function (name, parent, path, value) { dependents[name].forEach((dependent) => { - const dependencies = questions[dependent].dependencies; - const dependentEl = this.inputs[dependent]; - if (dependencies.every((dep) => parent[dep])) dependentEl.removeAttribute("disabled"); - else { - dependentEl.updateData(false); - dependentEl.setAttribute("disabled", true); + const dependencies = questions[dependent.name].dependencies; + const uniformDeps = Array.isArray(dependencies) + ? dependencies.map((name) => { + return { name }; + }) + : Object.entries(dependencies).map(([name, info]) => { + return { name, ...info }; + }); + + const dependentEl = this.inputs[dependent.name]; + + const attr = dependent.attribute ?? "disabled"; + + let condition = (v) => !!v; + if (!("condition" in dependent)) { + } else if (typeof dependent.condition === "boolean") condition = (v) => v == dependent.condition; + else if (Array.isArray(dependent.condition)) + condition = (v) => dependent.condition.some((condition) => v == condition); + else console.warn("Invalid condition", dependent.condition); + + if (uniformDeps.every(({ name }) => condition(parent[name]))) { + dependentEl.removeAttribute(attr); + if ("required" in dependent) dependentEl.required = dependent.required; + if ("__cached" in dependent) dependentEl.updateData(dependent.__cached); + } else { + if (dependentEl.value !== undefined) dependent.__cached = dependentEl.value; + dependentEl.updateData(dependent.default); + dependentEl.setAttribute(attr, true); + if ("required" in dependent) dependentEl.required = !dependent.required; } }); }, diff --git a/src/renderer/src/stories/pages/settings/SettingsPage.js b/src/renderer/src/stories/pages/settings/SettingsPage.js index 246c89611..d388b2397 100644 --- a/src/renderer/src/stories/pages/settings/SettingsPage.js +++ b/src/renderer/src/stories/pages/settings/SettingsPage.js @@ -37,6 +37,8 @@ function saveNewPipelineFromYaml(name, sourceData, rootFolder) { const subjectId = "mouse1"; const sessions = ["session1"]; + const hasMultipleSessions = sessions.length > 1; + const resolvedSourceData = structuredClone(sourceData); Object.values(resolvedSourceData).forEach((info) => { propertiesToTransform.forEach((property) => { @@ -52,12 +54,22 @@ function saveNewPipelineFromYaml(name, sourceData, rootFolder) { remove(updatedName, true); + const workflowInfo = { + multiple_sessions: hasMultipleSessions, + }; + + if (!workflowInfo.multiple_sessions) { + workflowInfo.subject_id = subjectId; + workflowInfo.session_id = sessions[0]; + } + save({ info: { globalState: { project: { name: updatedName, initialized: true, + workflow: workflowInfo, }, // provide data for all supported interfaces diff --git a/tests/e2e.test.ts b/tests/e2e.test.ts index 82ee7f3f7..9d59f3313 100644 --- a/tests/e2e.test.ts +++ b/tests/e2e.test.ts @@ -215,7 +215,20 @@ describe('E2E Test', () => { test('View the pre-form workflow page', async () => { + await references.page.evaluate(() => { + const dashboard = document.querySelector('nwb-dashboard') + const page = dashboard.page + + const subjectId = page.form.getFormElement(['subject_id']) + subjectId.updateData('subject1') + + const sessionId = page.form.getFormElement(['session_id']) + sessionId.updateData('session1') + }) + await takeScreenshot('workflow-page', 300) + + await toNextPage('structure')