From 9babca53574b34d3c1abe1cd9d5e0295ef2527a4 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 27 Oct 2023 15:45:41 -0700 Subject: [PATCH 01/14] Add global metadata editor for source data --- src/renderer/src/stories/JSONSchemaForm.js | 1 + .../src/stories/forms/GlobalFormModal.ts | 72 +++++++++++++++++++ .../pages/guided-mode/data/GuidedMetadata.js | 18 ++--- .../guided-mode/data/GuidedSourceData.js | 49 ++++++++++--- 4 files changed, 124 insertions(+), 16 deletions(-) create mode 100644 src/renderer/src/stories/forms/GlobalFormModal.ts diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js index c73e04526..2149ced03 100644 --- a/src/renderer/src/stories/JSONSchemaForm.js +++ b/src/renderer/src/stories/JSONSchemaForm.js @@ -168,6 +168,7 @@ export class JSONSchemaForm extends LitElement { dialogType: { type: String, reflect: false }, dialogOptions: { type: Object, reflect: false }, requirementMode: { type: String, reflect: true }, + globals: { type: Object, reflect: false }, }; } diff --git a/src/renderer/src/stories/forms/GlobalFormModal.ts b/src/renderer/src/stories/forms/GlobalFormModal.ts new file mode 100644 index 000000000..ccc168bb8 --- /dev/null +++ b/src/renderer/src/stories/forms/GlobalFormModal.ts @@ -0,0 +1,72 @@ +import { Modal } from "../Modal" +import { Page } from "../pages/Page.js" +import { Button } from "../Button" +import { JSONSchemaForm } from "../JSONSchemaForm" + +import { onThrow } from "../../errors"; +import { merge } from "../pages/utils.js"; + +export function createGlobalFormModal(this: Page, { + header, + schema, + propsToIgnore = [], + propsToRemove = [], + key +}: { + header: string + schema: any + propsToIgnore?: string[] + propsToRemove?: string[] + key?: string + +}) { + const modal = new Modal({ + header + }) + + const content = document.createElement("div") + + const schemaCopy = structuredClone(schema) + + Object.keys(schemaCopy.properties).forEach(i => { + const iSchemaProps = schemaCopy.properties[i].properties + Object.keys(iSchemaProps).forEach(key => { + if (propsToRemove.includes(key)) delete iSchemaProps[key] + }) + }) + + const form = new JSONSchemaForm({ + requirementMode: "loose", + schema: schemaCopy, + emptyMessage: "No properties to edit globally.", + ignore: propsToIgnore, + onUpdate: () => (this.unsavedUpdates = true), + onThrow, + }) + + content.append(form) + + content.style.padding = "0px 25px 25px 25px" + + const saveButton = new Button({ + label: "Save", + primary: true, + onClick: async () => { + await form.validate() + const result = merge(key ? {[key]: form.results} : form.results, this.info.globalState.project) + await this.save() + this.forms.forEach(({form}) => { + console.log(result) + form.globals = structuredClone(key ? result[key]: result) + }) + modal.open = false + } + }) + + modal.form = form + + modal.footer = saveButton + + modal.append(content) + return modal +} \ No newline at end of file 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 3fe49cc04..0af2bb0df 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js @@ -12,6 +12,15 @@ import { onThrow } from "../../../../errors"; import { merge } from "../../utils.js"; import { NWBFilePreview } from "../../../preview/NWBFilePreview.js"; +const propsToIgnore = [ + "Ophys", // Always ignore ophys metadata (for now) + "Icephys", // Always ignore icephys metadata (for now) + "Behavior", // Always ignore behavior metadata (for now) + new RegExp("ndx-.+"), // Ignore all ndx extensions + "subject_id", + "session_id", +] + const getInfoFromId = (key) => { let [subject, session] = key.split("/"); if (subject.startsWith("sub-")) subject = subject.slice(4); @@ -110,14 +119,7 @@ export class GuidedMetadataPage extends ManagedPage { results, globals: aggregateGlobalMetadata, - ignore: [ - "Ophys", // Always ignore ophys metadata (for now) - "Icephys", // Always ignore icephys metadata (for now) - "Behavior", // Always ignore behavior metadata (for now) - new RegExp("ndx-.+"), // Ignore all ndx extensions - "subject_id", - "session_id", - ], + ignore: propsToIgnore, conditionalRequirements: [ { 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 d23d77956..86f1a0080 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js @@ -8,6 +8,17 @@ import { onThrow } from "../../../../errors"; import { merge } from "../../utils.js"; import getSourceDataSchema from "../../../../../../../schemas/source-data.schema"; +import { createGlobalFormModal } from '../../../forms/GlobalFormModal' + +const propsToIgnore = [ + "verbose", + "es_key", + "exclude_shanks", + "load_sync_channel", + "stream_id", // NOTE: May be desired for other interfaces + "nsx_override", +] + export class GuidedSourceDataPage extends ManagedPage { constructor(...args) { super(...args); @@ -117,14 +128,8 @@ export class GuidedSourceDataPage extends ManagedPage { schema: schemaResolved, results: info.source_data, emptyMessage: "No source data required for this session.", - ignore: [ - "verbose", - "es_key", - "exclude_shanks", - "load_sync_channel", - "stream_id", // NOTE: May be desired for other interfaces - "nsx_override", - ], + ignore: propsToIgnore, + globals: this.info.globalState.project.SourceData, // onlyRequired: true, onUpdate: () => (this.unsavedUpdates = true), onStatusChange: (state) => this.manager.updateState(instanceId, state), @@ -140,6 +145,24 @@ export class GuidedSourceDataPage extends ManagedPage { }; }; + #globalModal = null + + connectedCallback(){ + super.connectedCallback() + const modal = this.#globalModal = createGlobalFormModal.call(this, { + header: "Edit Global Source Data", + propsToRemove: [...propsToIgnore, 'folder_path', 'file_path'], + key: 'SourceData', + schema: this.info.globalState.schema.source_data + }) + document.body.append(modal) + } + + disconnectedCallback(){ + super.disconnectedCallback() + this.#globalModal.remove() + } + render() { this.localState = { results: merge(this.info.globalState.results, {}) }; @@ -155,6 +178,16 @@ export class GuidedSourceDataPage extends ManagedPage { header: "Sessions", // instanceType: 'Session', instances, + controls: [ + { + name: "Edit Global Metadata", + onClick: () => { + this.#globalModal.form.results = merge(this.info.globalState.project?.SourceData, {}) + this.#globalModal.open = true + + }, + }, + ] // onAdded: (path) => { // let details = this.getDetails(path) From d71691f4fe5131605d3729e137896b059f6a23c8 Mon Sep 17 00:00:00 2001 From: Garrett Date: Mon, 30 Oct 2023 09:43:31 -0700 Subject: [PATCH 02/14] Proper global metadata flow for Subject table and Source Data form --- schemas/base-metadata.schema.ts | 27 ++++++++++ src/renderer/src/stories/BasicTable.js | 2 +- src/renderer/src/stories/JSONSchemaForm.js | 51 ++++++++++++++----- src/renderer/src/stories/SimpleTable.js | 8 +-- src/renderer/src/stories/Table.js | 47 ++++++++++++----- .../src/stories/forms/GlobalFormModal.ts | 40 +++++++++------ src/renderer/src/stories/pages/Page.js | 2 +- .../pages/guided-mode/data/GuidedMetadata.js | 5 ++ .../guided-mode/data/GuidedSourceData.js | 7 ++- .../guided-mode/setup/GuidedNewDatasetInfo.js | 31 +++-------- .../pages/guided-mode/setup/GuidedSubjects.js | 45 +++++++++++++++- .../stories/pages/settings/SettingsPage.js | 10 +--- src/renderer/src/stories/pages/utils.js | 13 ++++- 13 files changed, 206 insertions(+), 82 deletions(-) diff --git a/schemas/base-metadata.schema.ts b/schemas/base-metadata.schema.ts index 57abb60db..21e79feea 100644 --- a/schemas/base-metadata.schema.ts +++ b/schemas/base-metadata.schema.ts @@ -4,3 +4,30 @@ baseMetadataSchema.properties.Subject.properties.weight.unit = 'kg' // Add unit export default baseMetadataSchema + + +export const instanceSpecificFields = { + Subject: ["weight", "subject_id", "age", "date_of_birth", "age__reference"], + NWBFile: [ + "session_id", + "session_start_time", + "identifier", + "data_collection", + "notes", + "pharmacolocy", + "session_description", + "slices", + "source_script", + "source_script_file_name", + ], +}; + + +const globalSchema = structuredClone(baseMetadataSchema); +Object.entries(globalSchema.properties).forEach(([globalProp, schema]) => { + instanceSpecificFields[globalProp]?.forEach((prop) => delete schema.properties[prop]); +}); + +export { + globalSchema +} \ No newline at end of file diff --git a/src/renderer/src/stories/BasicTable.js b/src/renderer/src/stories/BasicTable.js index 1cad4503d..5d3396c32 100644 --- a/src/renderer/src/stories/BasicTable.js +++ b/src/renderer/src/stories/BasicTable.js @@ -154,7 +154,7 @@ export class BasicTable extends LitElement { } else value = (hasRow ? this.data[row][col] : undefined) ?? - // this.template[col] ?? + // this.globals[col] ?? this.schema.properties[col].default ?? ""; return value; diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js index 2149ced03..24f14bf75 100644 --- a/src/renderer/src/stories/JSONSchemaForm.js +++ b/src/renderer/src/stories/JSONSchemaForm.js @@ -224,6 +224,7 @@ export class JSONSchemaForm extends LitElement { if (props.onLoaded) this.onLoaded = props.onLoaded; if (props.onUpdate) this.onUpdate = props.onUpdate; if (props.renderTable) this.renderTable = props.renderTable; + if (props.onOverride) this.onOverride = props.onOverride; if (props.onStatusChange) this.onStatusChange = props.onStatusChange; @@ -263,6 +264,12 @@ export class JSONSchemaForm extends LitElement { if (changedProperties === "options") this.requestUpdate(); } + getGlobalValue(path) { + if (typeof path === "string") path = path.split("."); + const resolved = this.#get(path, this.globals); + return resolved; + } + // Track resolved values for the form (data only) updateData(localPath, value) { const path = [...localPath]; @@ -274,12 +281,24 @@ export class JSONSchemaForm extends LitElement { const resolvedParent = path.reduce(reducer, this.resolved); const hasUpdate = resolvedParent[name] !== value; + const globalValue = this.getGlobalValue(localPath); + // NOTE: Forms with nested forms will handle their own state updates - if (!value) { - delete resultParent[name]; - delete resolvedParent[name]; + if (this.isUndefined(value)) { + const globalValue = this.getGlobalValue(localPath); + + // Continue to resolve and re-render... + if (globalValue) { + value = resolvedParent[name] = globalValue; + const input = this.getInput(localPath); + if (input) { + input.updateData(globalValue); + this.onOverride(name, globalValue, path); + } + } + resultParent[name] = undefined; // NOTE: Will be removed when stringified } else { - resultParent[name] = value; + resultParent[name] = (value === globalValue) ? undefined : value; // Retain association with global value resolvedParent[name] = value; } @@ -491,7 +510,7 @@ export class JSONSchemaForm extends LitElement { if (typeof isRequired === "object" && !Array.isArray(isRequired)) invalid.push(...(await this.#validateRequirements(resolved[name], isRequired, path))); - else if (!resolved[name]) invalid.push(path); + else if (this.isUndefined(resolved[name])) invalid.push(path); } } @@ -502,14 +521,15 @@ export class JSONSchemaForm extends LitElement { onInvalid = () => {}; onLoaded = () => {}; onUpdate = () => {}; + onOverride = () => {}; - #deleteExtraneousResults = (results, schema) => { - for (let name in results) { - if (!schema.properties || !(name in schema.properties)) delete results[name]; - else if (results[name] && typeof results[name] === "object" && !Array.isArray(results[name])) - this.#deleteExtraneousResults(results[name], schema.properties[name]); - } - }; + // #deleteExtraneousResults = (results, schema) => { + // for (let name in results) { + // if (!schema.properties || !(name in schema.properties)) delete results[name]; + // else if (results[name] && typeof results[name] === "object" && !Array.isArray(results[name])) + // this.#deleteExtraneousResults(results[name], schema.properties[name]); + // } + // }; #getRenderable = (schema = {}, required, path, recursive = false) => { const entries = Object.entries(schema.properties ?? {}); @@ -603,6 +623,10 @@ export class JSONSchemaForm extends LitElement { return this.shadowRoot.querySelector(`[data-name="${link.name}"]`); }; + isUndefined(value) { + return value === undefined || value === ''; + } + // Assume this is going to return as a Promise—even if the change function isn't returning one triggerValidation = async (name, element, path = [], checkLinks = true) => { const parent = this.#get(path, this.resolved); @@ -644,7 +668,7 @@ export class JSONSchemaForm extends LitElement { } } else { // For non-links, throw a basic requirement error if the property is required - if (!errors.length && isRequired && !parent[name]) { + if (!errors.length && isRequired && this.isUndefined(parent[name])) { const schema = this.getSchema(localPath); errors.push({ message: `${schema.title ?? header(name)} is a required property.`, @@ -837,6 +861,7 @@ export class JSONSchemaForm extends LitElement { this.checkAllLoaded(); }, renderTable: (...args) => this.renderTable(...args), + onOverride: (...args) => this.onOverride(...args), base: [...this.base, ...localPath], }); diff --git a/src/renderer/src/stories/SimpleTable.js b/src/renderer/src/stories/SimpleTable.js index 0f09c62b5..4835f3630 100644 --- a/src/renderer/src/stories/SimpleTable.js +++ b/src/renderer/src/stories/SimpleTable.js @@ -163,7 +163,7 @@ export class SimpleTable extends LitElement { constructor({ schema, data, - template, + globals, keyColumn, validateOnChange, validateEmptyCells, @@ -178,7 +178,7 @@ export class SimpleTable extends LitElement { this.schema = schema ?? {}; this.data = data ?? []; this.keyColumn = keyColumn; - this.template = template ?? {}; + this.globals = globals ?? {}; this.validateEmptyCells = validateEmptyCells ?? true; this.deferLoading = deferLoading ?? false; this.maxHeight = maxHeight ?? ""; @@ -334,7 +334,7 @@ export class SimpleTable extends LitElement { } else value = (hasRow ? this.data[row][col] : undefined) ?? - this.template[col] ?? + this.globals[col] ?? this.schema.properties[col].default ?? ""; return value; @@ -631,7 +631,7 @@ export class SimpleTable extends LitElement { } // Update data on passed object else { - if (value == undefined || value === "") delete target[rowName][header]; + if (value == undefined || value === "") target[rowName][header] = undefined; else target[rowName][header] = value; } diff --git a/src/renderer/src/stories/Table.js b/src/renderer/src/stories/Table.js index eaa0f4948..0949d0493 100644 --- a/src/renderer/src/stories/Table.js +++ b/src/renderer/src/stories/Table.js @@ -60,10 +60,11 @@ export class Table extends LitElement { constructor({ schema, data, - template, + globals, keyColumn, validateOnChange, onUpdate, + onOverride, validateEmptyCells, onStatusChange, contextMenu, @@ -72,11 +73,12 @@ export class Table extends LitElement { this.schema = schema ?? {}; this.data = data ?? []; this.keyColumn = keyColumn; - this.template = template ?? {}; + this.globals = globals ?? {}; this.validateEmptyCells = validateEmptyCells ?? true; this.contextMenu = contextMenu ?? {}; if (onUpdate) this.onUpdate = onUpdate; + if (onOverride) this.onOverride = onOverride; if (validateOnChange) this.validateOnChange = validateOnChange; if (onStatusChange) this.onStatusChange = onStatusChange; @@ -92,6 +94,7 @@ export class Table extends LitElement { static get properties() { return { data: { type: Object, reflect: true }, + globals: { type: Object, reflect: true }, }; } @@ -109,7 +112,7 @@ export class Table extends LitElement { } else value = (hasRow ? this.data[row][col] : undefined) ?? - this.template[col] ?? + this.globals[col] ?? // this.schema.properties[col].default ?? ""; return value; @@ -132,7 +135,7 @@ export class Table extends LitElement { if (!message) { const errors = this.querySelectorAll("[error]"); const len = errors.length; - if (len === 1) message = errors[0].title || "Error found"; + if (len === 1) message = errors[0].getAttribute('data-message') || "Error found"; else if (len) message = `${len} errors exist on this table.`; } @@ -148,6 +151,7 @@ export class Table extends LitElement { status; onStatusChange = () => {}; onUpdate = () => {}; + onOverride = () => {}; updated() { const div = (this.shadowRoot ?? this).querySelector("div"); @@ -326,6 +330,8 @@ export class Table extends LitElement { descriptionEl.innerText = desc; } + if (this.table) this.table.destroy(); + const table = new Handsontable(div, { data, // rowHeaders: rowHeaders.map(v => `sub-${v}`), @@ -371,6 +377,8 @@ export class Table extends LitElement { } } + const isUserUpdate = initialCellsToUpdate <= validated + // Transfer data to object if (header === this.keyColumn) { if (value !== rowName) { @@ -384,13 +392,22 @@ export class Table extends LitElement { // Update data on passed object else { - if (value == undefined || value === "") delete target[rowName][header]; - else target[rowName][header] = value; + const globalValue = this.globals[header]; + + if (value == undefined || value === "") { + if (globalValue) { + value = target[rowName][header] = globalValue; + table.setDataAtCell(row, prop, value); + this.onOverride(header, value, rowName) + } + target[rowName][header] = undefined; + } + else target[rowName][header] = (value === globalValue) ? undefined : value; } validated++; - if (initialCellsToUpdate < validated) this.onUpdate(rowName, header, value); + if (isUserUpdate) this.onUpdate(rowName, header, value); if (typeof isValid === "function") isValid(); // } @@ -437,21 +454,27 @@ export class Table extends LitElement { const cell = this.table.getCell(row, prop); // NOTE: Does not resolve unless the cell is rendered... if (cell) { - let title = ""; + let message = ""; let theme = ""; if (warnings.length) { - (theme = "warning"), (title = warnings.map((o) => o.message).join("\n")); + (theme = "warning"), (message = warnings.map((o) => o.message).join("\n")); } else cell.removeAttribute("warning"); if (errors.length) { - (theme = "error"), (title = errors.map((o) => o.message).join("\n")); // Class switching handled automatically + (theme = "error"), (message = errors.map((o) => o.message).join("\n")); // Class switching handled automatically } else cell.removeAttribute("error"); if (theme) cell.setAttribute(theme, ""); - if (cell._tippy) cell._tippy.destroy(); + if (cell._tippy) { + cell._tippy.destroy(); + cell.removeAttribute('data-message') + } - if (title) tippy(cell, { content: title, theme }); + if (message) { + tippy(cell, { content: message, theme }); + cell.setAttribute('data-message', message) + } } this.#checkStatus(); // Check status after every validation update diff --git a/src/renderer/src/stories/forms/GlobalFormModal.ts b/src/renderer/src/stories/forms/GlobalFormModal.ts index ccc168bb8..f4685220d 100644 --- a/src/renderer/src/stories/forms/GlobalFormModal.ts +++ b/src/renderer/src/stories/forms/GlobalFormModal.ts @@ -5,19 +5,25 @@ import { JSONSchemaForm } from "../JSONSchemaForm" import { onThrow } from "../../errors"; import { merge } from "../pages/utils.js"; +import { save } from "../../progress/index.js"; + export function createGlobalFormModal(this: Page, { header, schema, propsToIgnore = [], propsToRemove = [], - key + key, + hasInstances = false, + validateOnChange }: { header: string schema: any propsToIgnore?: string[] propsToRemove?: string[] - key?: string + key?: string, + hasInstances?: boolean + validateOnChange?: Function }) { const modal = new Modal({ @@ -28,37 +34,39 @@ export function createGlobalFormModal(this: Page, { const schemaCopy = structuredClone(schema) - Object.keys(schemaCopy.properties).forEach(i => { - const iSchemaProps = schemaCopy.properties[i].properties - Object.keys(iSchemaProps).forEach(key => { - if (propsToRemove.includes(key)) delete iSchemaProps[key] - }) - }) + function removeProperties(obj: any, props: string[]) { + props.forEach(prop => delete obj[prop]) + } + + if (hasInstances) Object.keys(schemaCopy.properties).forEach(i => removeProperties(schemaCopy.properties[i].properties, propsToRemove)) + else removeProperties(schemaCopy.properties, propsToRemove) const form = new JSONSchemaForm({ requirementMode: "loose", schema: schemaCopy, emptyMessage: "No properties to edit globally.", ignore: propsToIgnore, - onUpdate: () => (this.unsavedUpdates = true), onThrow, + validateOnChange }) content.append(form) - content.style.padding = "0px 25px 25px 25px" + content.style.padding = "25px" const saveButton = new Button({ - label: "Save", + label: "Update", primary: true, onClick: async () => { await form.validate() const result = merge(key ? {[key]: form.results} : form.results, this.info.globalState.project) - await this.save() - this.forms.forEach(({form}) => { - console.log(result) - form.globals = structuredClone(key ? result[key]: result) - }) + await save(this) + + const forms = hasInstances ? this.forms.map(o => o.form) : this.form ? [ this.form ] : [] + const tables = hasInstances ? this.tables : this.table ? [ this.table ] : [] + forms.forEach(form =>form.globals = structuredClone(key ? result[key]: result)) + tables.forEach(table => table.globals = structuredClone(key ? result[key]: result)) + modal.open = false } }) diff --git a/src/renderer/src/stories/pages/Page.js b/src/renderer/src/stories/pages/Page.js index 2deb6e1c4..7b1e90da6 100644 --- a/src/renderer/src/stories/pages/Page.js +++ b/src/renderer/src/stories/pages/Page.js @@ -2,7 +2,7 @@ import { LitElement, html } from "lit"; import { openProgressSwal, runConversion } from "./guided-mode/options/utils.js"; import { get, save } from "../../progress/index.js"; import { dismissNotification, notify } from "../../dependencies/globals.js"; -import { merge, randomizeElements, mapSessions } from "./utils.js"; +import { randomizeElements, mapSessions } from "./utils.js"; import { ProgressBar } from "../ProgressBar"; import { resolveResults } from "./guided-mode/data/utils.js"; 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 0af2bb0df..f0dc71022 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js @@ -11,6 +11,7 @@ import { SimpleTable } from "../../../SimpleTable.js"; import { onThrow } from "../../../../errors"; import { merge } from "../../utils.js"; import { NWBFilePreview } from "../../../preview/NWBFilePreview.js"; +import { header } from "../../../forms/utils"; const propsToIgnore = [ "Ophys", // Always ignore ophys metadata (for now) @@ -69,6 +70,7 @@ export class GuidedMetadataPage extends ManagedPage { }, }; + createForm = ({ subject, session, info }) => { // const results = createResults({ subject, info }, this.info.globalState); @@ -120,6 +122,9 @@ export class GuidedMetadataPage extends ManagedPage { globals: aggregateGlobalMetadata, ignore: propsToIgnore, + onOverride: (name) => { + this.notify(`${header(name)} has been overriden with a global value.`, 'warning', 3000) + }, conditionalRequirements: [ { 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 86f1a0080..3900501f4 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js @@ -9,6 +9,7 @@ import { merge } from "../../utils.js"; import getSourceDataSchema from "../../../../../../../schemas/source-data.schema"; import { createGlobalFormModal } from '../../../forms/GlobalFormModal' +import { header } from "../../../forms/utils"; const propsToIgnore = [ "verbose", @@ -130,6 +131,9 @@ export class GuidedSourceDataPage extends ManagedPage { emptyMessage: "No source data required for this session.", ignore: propsToIgnore, globals: this.info.globalState.project.SourceData, + onOverride: (name) => { + this.notify(`${header(name)} has been overriden with a global value.`, 'warning', 3000) + }, // onlyRequired: true, onUpdate: () => (this.unsavedUpdates = true), onStatusChange: (state) => this.manager.updateState(instanceId, state), @@ -153,7 +157,8 @@ export class GuidedSourceDataPage extends ManagedPage { header: "Edit Global Source Data", propsToRemove: [...propsToIgnore, 'folder_path', 'file_path'], key: 'SourceData', - schema: this.info.globalState.schema.source_data + schema: this.info.globalState.schema.source_data, + hasInstances: true }) document.body.append(modal) } diff --git a/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js b/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js index 7e44b9b5a..eb3d9f9b6 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js @@ -9,33 +9,15 @@ import projectGlobalSchema from "../../../../../../../schemas/json/project/globa import { merge } from "../../utils.js"; import { schemaToPages } from "../../FormPage.js"; import { onThrow } from "../../../../errors"; -import baseMetadataSchema from "../../../../../../../schemas/base-metadata.schema"; - -const changesAcrossSessions = { - Subject: ["weight", "subject_id", "age", "date_of_birth", "age__reference"], - NWBFile: [ - "session_id", - "session_start_time", - "identifier", - "data_collection", - "notes", - "pharmacolocy", - "session_description", - "slices", - "source_script", - "source_script_file_name", - ], -}; + +import { globalSchema } from "../../../../../../../schemas/base-metadata.schema"; +import { header } from "../../../forms/utils"; + const projectMetadataSchema = merge(projectGlobalSchema, projectGeneralSchema); -Object.entries(baseMetadataSchema.properties).forEach(([globalProp, v]) => { - const info = (projectMetadataSchema.properties[globalProp] = structuredClone(v)); +merge(globalSchema, projectMetadataSchema) - changesAcrossSessions[globalProp]?.forEach((prop) => { - delete info.properties[prop]; - }); -}); export class GuidedNewDatasetPage extends Page { constructor(...args) { @@ -133,6 +115,9 @@ export class GuidedNewDatasetPage extends Page { dialogOptions: { properties: ["createDirectory"], }, + onOverride: (name) => { + this.notify(`${header(name)} has been overriden with a global value.`, 'warning', 3000) + }, validateOnChange, onUpdate: () => (this.unsavedUpdates = true), onThrow, 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 694cf4719..4e74b1fce 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js @@ -6,6 +6,10 @@ import { Table } from "../../../Table.js"; import { updateResultsFromSubjects } from "./utils"; import { merge } from "../../utils.js"; +import { globalSchema } from "../../../../../../../schemas/base-metadata.schema"; +import { Button } from "../../../Button.js"; +import { createGlobalFormModal } from "../../../forms/GlobalFormModal"; +import { header } from "../../../forms/utils"; export class GuidedSubjectsPage extends Page { constructor(...args) { @@ -14,10 +18,22 @@ export class GuidedSubjectsPage extends Page { header = { subtitle: "Enter all metadata known about each experiment subject", + controls: [ + new Button({ + label: "Edit Global Metadata", + onClick: () => { + this.#globalModal.form.results = merge(this.info.globalState.project?.Subject, {}) + this.#globalModal.open = true + } + }) + ] }; // Abort save if subject structure is invalid beforeSave = () => { + + // setUndefinedIfNotDeclared(subjectSchema, this.localState); // Set undefined if not declared in schema + this.info.globalState.subjects = merge(this.localState, this.info.globalState.subjects); // Merge the local and global states const { results, subjects } = this.info.globalState; @@ -51,6 +67,8 @@ export class GuidedSubjectsPage extends Page { this.notify(e.message, "error"); throw e; } + + console.log(this.info.globalState) }; footer = { @@ -63,6 +81,27 @@ export class GuidedSubjectsPage extends Page { super.updated(); // Call if updating data } + #globalModal; + + connectedCallback(){ + super.connectedCallback() + const modal = this.#globalModal = createGlobalFormModal.call(this, { + header: "Edit Global Subject Data", + key: 'Subject', + schema: globalSchema.properties.Subject, + validateOnChange: (key, parent, path) => { + console.log(key, parent[key], path) + return validateOnChange(key, parent, ["Subject", ...path]) + } + }) + document.body.append(modal) + } + + disconnectedCallback(){ + super.disconnectedCallback() + this.#globalModal.remove() + } + render() { const subjects = (this.localState = merge(this.info.globalState.subjects ?? {}, {})); @@ -77,15 +116,19 @@ export class GuidedSubjectsPage extends Page { subjects[subject].sessions = sessions; } + this.table = new Table({ schema: subjectSchema, data: subjects, - template: this.info.globalState.project.Subject, + globals: this.info.globalState.project.Subject, keyColumn: "subject_id", validateEmptyCells: false, contextMenu: { ignore: ["row_below"], }, + onOverride: (name) => { + this.notify(`${header(name)} has been overriden with a global value.`, 'warning', 3000) + }, onUpdate: () => { this.unsavedUpdates = true; }, diff --git a/src/renderer/src/stories/pages/settings/SettingsPage.js b/src/renderer/src/stories/pages/settings/SettingsPage.js index c65617574..ca5138ea3 100644 --- a/src/renderer/src/stories/pages/settings/SettingsPage.js +++ b/src/renderer/src/stories/pages/settings/SettingsPage.js @@ -14,21 +14,13 @@ const schema = { import { Button } from "../../Button.js"; import { global } from "../../../progress/index.js"; -import { merge } from "../utils.js"; +import { merge, setUndefinedIfNotDeclared } from "../utils.js"; import { notyf } from "../../../dependencies/globals.js"; import { header } from "../../forms/utils"; const dandiAPITokenRegex = /^[a-f0-9]{40}$/; -const setUndefinedIfNotDeclared = (schema, resolved) => { - for (let prop in schema.properties) { - const propInfo = schema.properties[prop]; - if (propInfo) setUndefinedIfNotDeclared(propInfo, resolved[prop]); - else if (!(prop in resolved)) resolved[prop] = undefined; - } -}; - export class SettingsPage extends Page { header = { title: "App Settings", diff --git a/src/renderer/src/stories/pages/utils.js b/src/renderer/src/stories/pages/utils.js index 5f7b539aa..76bba6fe2 100644 --- a/src/renderer/src/stories/pages/utils.js +++ b/src/renderer/src/stories/pages/utils.js @@ -18,13 +18,24 @@ const isObject = (o) => { return o && typeof o === "object" && !Array.isArray(o); }; +export const setUndefinedIfNotDeclared = (schemaProps, resolved) => { + if ('properties' in schemaProps) schemaProps = schemaProps.properties; + for (const prop in schemaProps) { + const propInfo = schemaProps[prop]?.properties; + if (propInfo) setUndefinedIfNotDeclared(propInfo, resolved[prop]); + else if (!(prop in resolved)) resolved[prop] = undefined; + } +}; + + export function merge(toMerge = {}, target = {}, mergeOpts = {}) { // Deep merge objects for (const [k, v] of Object.entries(toMerge)) { const targetV = target[k]; if (mergeOpts.arrays && Array.isArray(v) && Array.isArray(targetV)) target[k] = [...targetV, ...v]; // Merge array entries together - else if (isObject(v) || isObject(targetV)) target[k] = merge(v, target[k]); + else if (isObject(v) || isObject(targetV)) target[k] = merge(v, target[k], mergeOpts); + else if (v === undefined) delete target[k]; // Remove matched values else target[k] = v; // Replace primitive values } From c34612f09cf7727a17f1c9f3293ca6dc1a029dd0 Mon Sep 17 00:00:00 2001 From: Garrett Date: Mon, 30 Oct 2023 09:50:30 -0700 Subject: [PATCH 03/14] Add message to triage inspector --- .../pages/guided-mode/options/GuidedInspectorPage.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/stories/pages/guided-mode/options/GuidedInspectorPage.js b/src/renderer/src/stories/pages/guided-mode/options/GuidedInspectorPage.js index 1513eeeb3..7164454c8 100644 --- a/src/renderer/src/stories/pages/guided-mode/options/GuidedInspectorPage.js +++ b/src/renderer/src/stories/pages/guided-mode/options/GuidedInspectorPage.js @@ -15,6 +15,8 @@ import { InstanceManager } from "../../../InstanceManager.js"; import { path as nodePath } from "../../../../electron"; import { getMessageType } from "../../../../validation/index.js"; +import { InfoBox } from '../../../InfoBox' + const filter = (list, toFilter) => { return list.filter((o) => { return Object.entries(toFilter) @@ -36,7 +38,7 @@ export class GuidedInspectorPage extends Page { } header = { - subtitle: `The NWB Inspector has scanned your files for adherence to best practices`, + subtitle: `The NWB Inspector has scanned your files for adherence to best practices.`, controls: () => html`previous page to fix any issues shared across files.` + })} + ${until( (async () => { if (fileArr.length <= 1) { const items = From 0f0030999ecfb3eb8b8c65519c3c53316cc66184 Mon Sep 17 00:00:00 2001 From: Garrett Date: Mon, 30 Oct 2023 11:53:27 -0700 Subject: [PATCH 04/14] Add for File Metadata page --- schemas/json/base_metadata_schema.json | 2 - src/renderer/src/stories/JSONSchemaForm.js | 25 +++++++---- src/renderer/src/stories/JSONSchemaInput.js | 4 +- .../src/stories/forms/GlobalFormModal.ts | 42 +++++++++++++------ .../pages/guided-mode/data/GuidedMetadata.js | 38 +++++++++++++++++ .../guided-mode/data/GuidedSourceData.js | 23 +++++----- 6 files changed, 99 insertions(+), 35 deletions(-) diff --git a/schemas/json/base_metadata_schema.json b/schemas/json/base_metadata_schema.json index 201620788..e5a157e1c 100644 --- a/schemas/json/base_metadata_schema.json +++ b/schemas/json/base_metadata_schema.json @@ -1,8 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "base_metafile.schema.json", - "title": "Base schema for the metafile", - "description": "Base schema for the metafile", "version": "0.1.0", "type": "object", "required": ["NWBFile"], diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js index 24f14bf75..73b4046e2 100644 --- a/src/renderer/src/stories/JSONSchemaForm.js +++ b/src/renderer/src/stories/JSONSchemaForm.js @@ -189,6 +189,8 @@ export class JSONSchemaForm extends LitElement { resolved = {}; // Keep track of actual resolved values—not just what the user provides as results + states = {}; + constructor(props = {}) { super(); @@ -200,6 +202,8 @@ export class JSONSchemaForm extends LitElement { this.results = (props.base ? structuredClone(props.results) : props.results) ?? {}; // Deep clone results in nested forms this.globals = props.globals ?? {}; + this.states = props.states ?? {}; // Accordion and other states + this.ignore = props.ignore ?? []; this.required = props.required ?? {}; this.dialogOptions = props.dialogOptions; @@ -390,7 +394,7 @@ export class JSONSchemaForm extends LitElement { #get = (path, object = this.resolved, omitted = []) => { // path = path.slice(this.base.length); // Correct for base path - return path.reduce((acc, curr) => (acc = acc[curr] ?? acc?.[omitted.find((str) => acc[str])]?.[curr]), object); + return path.reduce((acc, curr) => (acc = acc?.[curr] ?? acc?.[omitted.find((str) => acc[str])]?.[curr]), object); }; #checkRequiredAfterChange = async (localPath) => { @@ -827,12 +831,15 @@ export class JSONSchemaForm extends LitElement { if (this.mode === "accordion" && hasMany) { const headerName = header(name); + this.#nestedForms[name] = new JSONSchemaForm({ identifier: this.identifier, schema: info, results: { ...results[name] }, globals: this.globals?.[name], + states: this.states, + mode: this.mode, onUpdate: (internalPath, value) => { @@ -840,6 +847,7 @@ export class JSONSchemaForm extends LitElement { this.updateData(path, value); }, + requirementMode: this.requirementMode, required: required[name], // Scoped to the sub-schema ignore: this.ignore, dialogOptions: this.dialogOptions, @@ -865,14 +873,13 @@ export class JSONSchemaForm extends LitElement { base: [...this.base, ...localPath], }); - const accordion = new Accordion({ - sections: { - [headerName]: { - subtitle: `${this.#getRenderable(info, required[name], localPath, true).length} fields`, - content: this.#nestedForms[name], - }, - }, - }); + if (!this.states[headerName]) this.states[headerName] = {} + this.states[headerName].subtitle = `${this.#getRenderable(info, required[name], localPath, true).length} fields` + this.states[headerName].content = this.#nestedForms[name] + + const accordion = new Accordion({ sections: { + [headerName]: this.states[headerName] + } }); accordion.id = name; // assign name to accordion id diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js index 687d528e2..453799282 100644 --- a/src/renderer/src/stories/JSONSchemaInput.js +++ b/src/renderer/src/stories/JSONSchemaInput.js @@ -110,9 +110,11 @@ export class JSONSchemaInput extends LitElement { const path = typeof fullPath === "string" ? fullPath.split("-") : [...fullPath]; const name = path.splice(-1)[0]; const el = this.getElement(); + this.#triggerValidation(name, el, path); this.#updateData(fullPath, value); if (el.type === "checkbox") el.checked = value; + else if (el.classList.contains('list')) el.children[0].items = value ? value.map((value) => { return { value } }) : [] else el.value = value; return true; @@ -284,7 +286,7 @@ export class JSONSchemaInput extends LitElement { return html`
validateOnChange && this.#triggerValidation(name, list, path)} > ${list} ${addButton} diff --git a/src/renderer/src/stories/forms/GlobalFormModal.ts b/src/renderer/src/stories/forms/GlobalFormModal.ts index f4685220d..299126a5c 100644 --- a/src/renderer/src/stories/forms/GlobalFormModal.ts +++ b/src/renderer/src/stories/forms/GlobalFormModal.ts @@ -15,7 +15,8 @@ export function createGlobalFormModal(this: Page, { propsToRemove = [], key, hasInstances = false, - validateOnChange + validateOnChange, + mergeFunction = merge }: { header: string schema: any @@ -23,7 +24,8 @@ export function createGlobalFormModal(this: Page, { propsToRemove?: string[] key?: string, hasInstances?: boolean - validateOnChange?: Function + validateOnChange?: Function, + mergeFunction?: Function }) { const modal = new Modal({ @@ -41,8 +43,9 @@ export function createGlobalFormModal(this: Page, { if (hasInstances) Object.keys(schemaCopy.properties).forEach(i => removeProperties(schemaCopy.properties[i].properties, propsToRemove)) else removeProperties(schemaCopy.properties, propsToRemove) - const form = new JSONSchemaForm({ + const globalForm = new JSONSchemaForm({ requirementMode: "loose", + mode: 'accordion', schema: schemaCopy, emptyMessage: "No properties to edit globally.", ignore: propsToIgnore, @@ -50,7 +53,7 @@ export function createGlobalFormModal(this: Page, { validateOnChange }) - content.append(form) + content.append(globalForm) content.style.padding = "25px" @@ -58,20 +61,35 @@ export function createGlobalFormModal(this: Page, { label: "Update", primary: true, onClick: async () => { - await form.validate() - const result = merge(key ? {[key]: form.results} : form.results, this.info.globalState.project) - await save(this) + await globalForm.validate() - const forms = hasInstances ? this.forms.map(o => o.form) : this.form ? [ this.form ] : [] - const tables = hasInstances ? this.tables : this.table ? [ this.table ] : [] - forms.forEach(form =>form.globals = structuredClone(key ? result[key]: result)) - tables.forEach(table => table.globals = structuredClone(key ? result[key]: result)) + const cached: any = {} + + const toPass = { project: key ? {[key]: globalForm.results} : globalForm.results} + + const forms = (hasInstances ? this.forms : this.form ? [ { form: this.form }] : []) ?? [] + const tables = (hasInstances ? this.tables : this.table ? [ this.table ] : []) ?? [] + + forms.forEach(formInfo => { + const { subject, form } = formInfo + const result = cached[subject] ?? (cached[subject] = mergeFunction.call(formInfo, toPass, this.info.globalState)) + form.globals = structuredClone(key ? result[key]: result) + }) + + + tables.forEach(table => { + const subject = null + const result = cached[subject] ?? (cached[subject] = mergeFunction(toPass, this.info.globalState)) + table.globals = structuredClone( key ? result[key]: result) + }) + + await save(this) // Save after all updates are made modal.open = false } }) - modal.form = form + modal.form = globalForm modal.footer = saveButton 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 f0dc71022..055a564bc 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js @@ -13,6 +13,10 @@ import { merge } from "../../utils.js"; import { NWBFilePreview } from "../../../preview/NWBFilePreview.js"; import { header } from "../../../forms/utils"; +import { createGlobalFormModal } from "../../../forms/GlobalFormModal"; +import { Button } from "../../../Button.js"; +import { globalSchema } from "../../../../../../../schemas/base-metadata.schema"; + const propsToIgnore = [ "Ophys", // Always ignore ophys metadata (for now) "Icephys", // Always ignore icephys metadata (for now) @@ -42,6 +46,16 @@ export class GuidedMetadataPage extends ManagedPage { form; header = { + + controls: [ + new Button({ + label: "Edit Global Metadata", + onClick: () => { + this.#globalModal.form.results = merge(this.info.globalState.project, {}) + this.#globalModal.open = true + }, + }) + ], subtitle: "Edit all metadata for this conversion at the session level", }; @@ -71,6 +85,30 @@ export class GuidedMetadataPage extends ManagedPage { }; + #globalModal = null + + connectedCallback(){ + super.connectedCallback() + + const modal = this.#globalModal = createGlobalFormModal.call(this, { + header: "Edit Global Metadata", + propsToRemove: [...propsToIgnore], + schema: globalSchema, // Provide HARDCODED global schema for metadata properties (not automatically abstracting across sessions)... + hasInstances: true, + mergeFunction: function (globalResolved, globals) { + merge(globalResolved, globals) + return resolveGlobalOverrides(this.subject, globals) + } + }) + document.body.append(modal) + } + + disconnectedCallback(){ + super.disconnectedCallback() + this.#globalModal.remove() + } + + createForm = ({ subject, session, info }) => { // const results = createResults({ subject, info }, this.info.globalState); 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 3900501f4..12f39d0a7 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js @@ -10,6 +10,7 @@ import getSourceDataSchema from "../../../../../../../schemas/source-data.schema import { createGlobalFormModal } from '../../../forms/GlobalFormModal' import { header } from "../../../forms/utils"; +import { Button } from "../../../Button.js"; const propsToIgnore = [ "verbose", @@ -30,8 +31,18 @@ export class GuidedSourceDataPage extends ManagedPage { }; header = { + controls: [ + new Button({ + label: "Edit Global Metadata", + onClick: () => { + this.#globalModal.form.results = merge(this.info.globalState.project?.SourceData, {}) + this.#globalModal.open = true + + }, + }) + ], subtitle: - "Specify the file and folder locations on your local system for each interface, as well as any additional details that might be required", + "Specify the file and folder locations on your local system for each interface, as well as any additional details that might be required.", }; footer = { @@ -183,16 +194,6 @@ export class GuidedSourceDataPage extends ManagedPage { header: "Sessions", // instanceType: 'Session', instances, - controls: [ - { - name: "Edit Global Metadata", - onClick: () => { - this.#globalModal.form.results = merge(this.info.globalState.project?.SourceData, {}) - this.#globalModal.open = true - - }, - }, - ] // onAdded: (path) => { // let details = this.getDetails(path) From 2bc8ce88d8b68f7e45fe68cb2a01376e9782a168 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:59:05 +0000 Subject: [PATCH 05/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- schemas/base-metadata.schema.ts | 2 +- src/renderer/src/stories/JSONSchemaForm.js | 26 +++++++----- src/renderer/src/stories/JSONSchemaInput.js | 7 +++- src/renderer/src/stories/Table.js | 13 +++--- .../src/stories/forms/GlobalFormModal.ts | 6 +-- .../pages/guided-mode/data/GuidedMetadata.js | 37 ++++++++--------- .../guided-mode/data/GuidedSourceData.js | 39 +++++++++--------- .../options/GuidedInspectorPage.js | 8 ++-- .../guided-mode/setup/GuidedNewDatasetInfo.js | 6 +-- .../pages/guided-mode/setup/GuidedSubjects.js | 40 +++++++++---------- src/renderer/src/stories/pages/utils.js | 3 +- 11 files changed, 94 insertions(+), 93 deletions(-) diff --git a/schemas/base-metadata.schema.ts b/schemas/base-metadata.schema.ts index 21e79feea..f15110973 100644 --- a/schemas/base-metadata.schema.ts +++ b/schemas/base-metadata.schema.ts @@ -30,4 +30,4 @@ Object.entries(globalSchema.properties).forEach(([globalProp, schema]) => { export { globalSchema -} \ No newline at end of file +} diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js index 73b4046e2..8135e3ec4 100644 --- a/src/renderer/src/stories/JSONSchemaForm.js +++ b/src/renderer/src/stories/JSONSchemaForm.js @@ -302,7 +302,7 @@ export class JSONSchemaForm extends LitElement { } resultParent[name] = undefined; // NOTE: Will be removed when stringified } else { - resultParent[name] = (value === globalValue) ? undefined : value; // Retain association with global value + resultParent[name] = value === globalValue ? undefined : value; // Retain association with global value resolvedParent[name] = value; } @@ -394,7 +394,10 @@ export class JSONSchemaForm extends LitElement { #get = (path, object = this.resolved, omitted = []) => { // path = path.slice(this.base.length); // Correct for base path - return path.reduce((acc, curr) => (acc = acc?.[curr] ?? acc?.[omitted.find((str) => acc[str])]?.[curr]), object); + return path.reduce( + (acc, curr) => (acc = acc?.[curr] ?? acc?.[omitted.find((str) => acc[str])]?.[curr]), + object + ); }; #checkRequiredAfterChange = async (localPath) => { @@ -628,7 +631,7 @@ export class JSONSchemaForm extends LitElement { }; isUndefined(value) { - return value === undefined || value === ''; + return value === undefined || value === ""; } // Assume this is going to return as a Promise—even if the change function isn't returning one @@ -831,7 +834,6 @@ export class JSONSchemaForm extends LitElement { if (this.mode === "accordion" && hasMany) { const headerName = header(name); - this.#nestedForms[name] = new JSONSchemaForm({ identifier: this.identifier, schema: info, @@ -873,13 +875,17 @@ export class JSONSchemaForm extends LitElement { base: [...this.base, ...localPath], }); - if (!this.states[headerName]) this.states[headerName] = {} - this.states[headerName].subtitle = `${this.#getRenderable(info, required[name], localPath, true).length} fields` - this.states[headerName].content = this.#nestedForms[name] + if (!this.states[headerName]) this.states[headerName] = {}; + this.states[headerName].subtitle = `${ + this.#getRenderable(info, required[name], localPath, true).length + } fields`; + this.states[headerName].content = this.#nestedForms[name]; - const accordion = new Accordion({ sections: { - [headerName]: this.states[headerName] - } }); + const accordion = new Accordion({ + sections: { + [headerName]: this.states[headerName], + }, + }); accordion.id = name; // assign name to accordion id diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js index 453799282..ea23df1fb 100644 --- a/src/renderer/src/stories/JSONSchemaInput.js +++ b/src/renderer/src/stories/JSONSchemaInput.js @@ -114,7 +114,12 @@ export class JSONSchemaInput extends LitElement { this.#triggerValidation(name, el, path); this.#updateData(fullPath, value); if (el.type === "checkbox") el.checked = value; - else if (el.classList.contains('list')) el.children[0].items = value ? value.map((value) => { return { value } }) : [] + else if (el.classList.contains("list")) + el.children[0].items = value + ? value.map((value) => { + return { value }; + }) + : []; else el.value = value; return true; diff --git a/src/renderer/src/stories/Table.js b/src/renderer/src/stories/Table.js index 0949d0493..1869492fc 100644 --- a/src/renderer/src/stories/Table.js +++ b/src/renderer/src/stories/Table.js @@ -135,7 +135,7 @@ export class Table extends LitElement { if (!message) { const errors = this.querySelectorAll("[error]"); const len = errors.length; - if (len === 1) message = errors[0].getAttribute('data-message') || "Error found"; + if (len === 1) message = errors[0].getAttribute("data-message") || "Error found"; else if (len) message = `${len} errors exist on this table.`; } @@ -377,7 +377,7 @@ export class Table extends LitElement { } } - const isUserUpdate = initialCellsToUpdate <= validated + const isUserUpdate = initialCellsToUpdate <= validated; // Transfer data to object if (header === this.keyColumn) { @@ -398,11 +398,10 @@ export class Table extends LitElement { if (globalValue) { value = target[rowName][header] = globalValue; table.setDataAtCell(row, prop, value); - this.onOverride(header, value, rowName) + this.onOverride(header, value, rowName); } target[rowName][header] = undefined; - } - else target[rowName][header] = (value === globalValue) ? undefined : value; + } else target[rowName][header] = value === globalValue ? undefined : value; } validated++; @@ -468,12 +467,12 @@ export class Table extends LitElement { if (cell._tippy) { cell._tippy.destroy(); - cell.removeAttribute('data-message') + cell.removeAttribute("data-message"); } if (message) { tippy(cell, { content: message, theme }); - cell.setAttribute('data-message', message) + cell.setAttribute("data-message", message); } } diff --git a/src/renderer/src/stories/forms/GlobalFormModal.ts b/src/renderer/src/stories/forms/GlobalFormModal.ts index 299126a5c..c9b4627fd 100644 --- a/src/renderer/src/stories/forms/GlobalFormModal.ts +++ b/src/renderer/src/stories/forms/GlobalFormModal.ts @@ -69,13 +69,13 @@ export function createGlobalFormModal(this: Page, { const forms = (hasInstances ? this.forms : this.form ? [ { form: this.form }] : []) ?? [] const tables = (hasInstances ? this.tables : this.table ? [ this.table ] : []) ?? [] - + forms.forEach(formInfo => { const { subject, form } = formInfo const result = cached[subject] ?? (cached[subject] = mergeFunction.call(formInfo, toPass, this.info.globalState)) form.globals = structuredClone(key ? result[key]: result) }) - + tables.forEach(table => { const subject = null @@ -95,4 +95,4 @@ export function createGlobalFormModal(this: Page, { modal.append(content) return modal -} \ No newline at end of file +} 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 055a564bc..b0823df37 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js @@ -24,7 +24,7 @@ const propsToIgnore = [ new RegExp("ndx-.+"), // Ignore all ndx extensions "subject_id", "session_id", -] +]; const getInfoFromId = (key) => { let [subject, session] = key.split("/"); @@ -46,15 +46,14 @@ export class GuidedMetadataPage extends ManagedPage { form; header = { - controls: [ new Button({ label: "Edit Global Metadata", onClick: () => { - this.#globalModal.form.results = merge(this.info.globalState.project, {}) - this.#globalModal.open = true + this.#globalModal.form.results = merge(this.info.globalState.project, {}); + this.#globalModal.open = true; }, - }) + }), ], subtitle: "Edit all metadata for this conversion at the session level", }; @@ -84,31 +83,29 @@ export class GuidedMetadataPage extends ManagedPage { }, }; + #globalModal = null; - #globalModal = null - - connectedCallback(){ - super.connectedCallback() + connectedCallback() { + super.connectedCallback(); - const modal = this.#globalModal = createGlobalFormModal.call(this, { + const modal = (this.#globalModal = createGlobalFormModal.call(this, { header: "Edit Global Metadata", propsToRemove: [...propsToIgnore], schema: globalSchema, // Provide HARDCODED global schema for metadata properties (not automatically abstracting across sessions)... hasInstances: true, mergeFunction: function (globalResolved, globals) { - merge(globalResolved, globals) - return resolveGlobalOverrides(this.subject, globals) - } - }) - document.body.append(modal) + merge(globalResolved, globals); + return resolveGlobalOverrides(this.subject, globals); + }, + })); + document.body.append(modal); } - disconnectedCallback(){ - super.disconnectedCallback() - this.#globalModal.remove() + disconnectedCallback() { + super.disconnectedCallback(); + this.#globalModal.remove(); } - createForm = ({ subject, session, info }) => { // const results = createResults({ subject, info }, this.info.globalState); @@ -161,7 +158,7 @@ export class GuidedMetadataPage extends ManagedPage { ignore: propsToIgnore, onOverride: (name) => { - this.notify(`${header(name)} has been overriden with a global value.`, 'warning', 3000) + this.notify(`${header(name)} has been overriden with a global value.`, "warning", 3000); }, conditionalRequirements: [ 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 12f39d0a7..e6a025032 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js @@ -8,7 +8,7 @@ import { onThrow } from "../../../../errors"; import { merge } from "../../utils.js"; import getSourceDataSchema from "../../../../../../../schemas/source-data.schema"; -import { createGlobalFormModal } from '../../../forms/GlobalFormModal' +import { createGlobalFormModal } from "../../../forms/GlobalFormModal"; import { header } from "../../../forms/utils"; import { Button } from "../../../Button.js"; @@ -19,7 +19,7 @@ const propsToIgnore = [ "load_sync_channel", "stream_id", // NOTE: May be desired for other interfaces "nsx_override", -] +]; export class GuidedSourceDataPage extends ManagedPage { constructor(...args) { @@ -35,11 +35,10 @@ export class GuidedSourceDataPage extends ManagedPage { new Button({ label: "Edit Global Metadata", onClick: () => { - this.#globalModal.form.results = merge(this.info.globalState.project?.SourceData, {}) - this.#globalModal.open = true - + this.#globalModal.form.results = merge(this.info.globalState.project?.SourceData, {}); + this.#globalModal.open = true; }, - }) + }), ], subtitle: "Specify the file and folder locations on your local system for each interface, as well as any additional details that might be required.", @@ -143,7 +142,7 @@ export class GuidedSourceDataPage extends ManagedPage { ignore: propsToIgnore, globals: this.info.globalState.project.SourceData, onOverride: (name) => { - this.notify(`${header(name)} has been overriden with a global value.`, 'warning', 3000) + this.notify(`${header(name)} has been overriden with a global value.`, "warning", 3000); }, // onlyRequired: true, onUpdate: () => (this.unsavedUpdates = true), @@ -160,23 +159,23 @@ export class GuidedSourceDataPage extends ManagedPage { }; }; - #globalModal = null - - connectedCallback(){ - super.connectedCallback() - const modal = this.#globalModal = createGlobalFormModal.call(this, { + #globalModal = null; + + connectedCallback() { + super.connectedCallback(); + const modal = (this.#globalModal = createGlobalFormModal.call(this, { header: "Edit Global Source Data", - propsToRemove: [...propsToIgnore, 'folder_path', 'file_path'], - key: 'SourceData', + propsToRemove: [...propsToIgnore, "folder_path", "file_path"], + key: "SourceData", schema: this.info.globalState.schema.source_data, - hasInstances: true - }) - document.body.append(modal) + hasInstances: true, + })); + document.body.append(modal); } - disconnectedCallback(){ - super.disconnectedCallback() - this.#globalModal.remove() + disconnectedCallback() { + super.disconnectedCallback(); + this.#globalModal.remove(); } render() { diff --git a/src/renderer/src/stories/pages/guided-mode/options/GuidedInspectorPage.js b/src/renderer/src/stories/pages/guided-mode/options/GuidedInspectorPage.js index 7164454c8..b31a389cc 100644 --- a/src/renderer/src/stories/pages/guided-mode/options/GuidedInspectorPage.js +++ b/src/renderer/src/stories/pages/guided-mode/options/GuidedInspectorPage.js @@ -15,7 +15,7 @@ import { InstanceManager } from "../../../InstanceManager.js"; import { path as nodePath } from "../../../../electron"; import { getMessageType } from "../../../../validation/index.js"; -import { InfoBox } from '../../../InfoBox' +import { InfoBox } from "../../../InfoBox"; const filter = (list, toFilter) => { return list.filter((o) => { @@ -79,10 +79,10 @@ export class GuidedInspectorPage extends Page { }) ) .flat(); - return html` - ${new InfoBox({ + return html` ${new InfoBox({ header: "How do I fix these suggestions?", - content: html`We suggest editing the Global Metadata on the previous page to fix any issues shared across files.` + content: html`We suggest editing the Global Metadata on the previous page to fix any issues shared + across files.`, })} ${until( (async () => { diff --git a/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js b/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js index eb3d9f9b6..90b66fa54 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js @@ -13,11 +13,9 @@ import { onThrow } from "../../../../errors"; import { globalSchema } from "../../../../../../../schemas/base-metadata.schema"; import { header } from "../../../forms/utils"; - const projectMetadataSchema = merge(projectGlobalSchema, projectGeneralSchema); -merge(globalSchema, projectMetadataSchema) - +merge(globalSchema, projectMetadataSchema); export class GuidedNewDatasetPage extends Page { constructor(...args) { @@ -116,7 +114,7 @@ export class GuidedNewDatasetPage extends Page { properties: ["createDirectory"], }, onOverride: (name) => { - this.notify(`${header(name)} has been overriden with a global value.`, 'warning', 3000) + this.notify(`${header(name)} has been overriden with a global value.`, "warning", 3000); }, validateOnChange, onUpdate: () => (this.unsavedUpdates = true), 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 4e74b1fce..ca8825ce2 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js @@ -22,16 +22,15 @@ export class GuidedSubjectsPage extends Page { new Button({ label: "Edit Global Metadata", onClick: () => { - this.#globalModal.form.results = merge(this.info.globalState.project?.Subject, {}) - this.#globalModal.open = true - } - }) - ] + this.#globalModal.form.results = merge(this.info.globalState.project?.Subject, {}); + this.#globalModal.open = true; + }, + }), + ], }; // Abort save if subject structure is invalid beforeSave = () => { - // setUndefinedIfNotDeclared(subjectSchema, this.localState); // Set undefined if not declared in schema this.info.globalState.subjects = merge(this.localState, this.info.globalState.subjects); // Merge the local and global states @@ -68,7 +67,7 @@ export class GuidedSubjectsPage extends Page { throw e; } - console.log(this.info.globalState) + console.log(this.info.globalState); }; footer = { @@ -83,23 +82,23 @@ export class GuidedSubjectsPage extends Page { #globalModal; - connectedCallback(){ - super.connectedCallback() - const modal = this.#globalModal = createGlobalFormModal.call(this, { + connectedCallback() { + super.connectedCallback(); + const modal = (this.#globalModal = createGlobalFormModal.call(this, { header: "Edit Global Subject Data", - key: 'Subject', + key: "Subject", schema: globalSchema.properties.Subject, validateOnChange: (key, parent, path) => { - console.log(key, parent[key], path) - return validateOnChange(key, parent, ["Subject", ...path]) - } - }) - document.body.append(modal) + console.log(key, parent[key], path); + return validateOnChange(key, parent, ["Subject", ...path]); + }, + })); + document.body.append(modal); } - disconnectedCallback(){ - super.disconnectedCallback() - this.#globalModal.remove() + disconnectedCallback() { + super.disconnectedCallback(); + this.#globalModal.remove(); } render() { @@ -116,7 +115,6 @@ export class GuidedSubjectsPage extends Page { subjects[subject].sessions = sessions; } - this.table = new Table({ schema: subjectSchema, data: subjects, @@ -127,7 +125,7 @@ export class GuidedSubjectsPage extends Page { ignore: ["row_below"], }, onOverride: (name) => { - this.notify(`${header(name)} has been overriden with a global value.`, 'warning', 3000) + this.notify(`${header(name)} has been overriden with a global value.`, "warning", 3000); }, onUpdate: () => { this.unsavedUpdates = true; diff --git a/src/renderer/src/stories/pages/utils.js b/src/renderer/src/stories/pages/utils.js index 76bba6fe2..4d9ac99fd 100644 --- a/src/renderer/src/stories/pages/utils.js +++ b/src/renderer/src/stories/pages/utils.js @@ -19,7 +19,7 @@ const isObject = (o) => { }; export const setUndefinedIfNotDeclared = (schemaProps, resolved) => { - if ('properties' in schemaProps) schemaProps = schemaProps.properties; + if ("properties" in schemaProps) schemaProps = schemaProps.properties; for (const prop in schemaProps) { const propInfo = schemaProps[prop]?.properties; if (propInfo) setUndefinedIfNotDeclared(propInfo, resolved[prop]); @@ -27,7 +27,6 @@ export const setUndefinedIfNotDeclared = (schemaProps, resolved) => { } }; - export function merge(toMerge = {}, target = {}, mergeOpts = {}) { // Deep merge objects for (const [k, v] of Object.entries(toMerge)) { From 1b04bbaba68da400d45cffd591afb6ec1ac35845 Mon Sep 17 00:00:00 2001 From: Garrett Date: Mon, 30 Oct 2023 14:42:54 -0700 Subject: [PATCH 06/14] Fix merge and simplify loose requirements --- src/renderer/src/stories/JSONSchemaForm.js | 13 +++++++------ src/renderer/src/stories/forms/GlobalFormModal.ts | 2 +- .../pages/guided-mode/setup/GuidedNewDatasetInfo.js | 2 +- .../src/stories/pages/settings/SettingsPage.js | 8 -------- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js index 8135e3ec4..fc4605c38 100644 --- a/src/renderer/src/stories/JSONSchemaForm.js +++ b/src/renderer/src/stories/JSONSchemaForm.js @@ -121,10 +121,12 @@ hr { .required label:after { content: " *"; color: #ff0033; + } - :host([requirementmode="loose"]) .required label:after { + :host(:not([validateemptyvalues])) .required label:after { color: gray; + } @@ -167,8 +169,8 @@ export class JSONSchemaForm extends LitElement { required: { type: Object, reflect: false }, dialogType: { type: String, reflect: false }, dialogOptions: { type: Object, reflect: false }, - requirementMode: { type: String, reflect: true }, globals: { type: Object, reflect: false }, + validateEmptyValues: { type: Boolean, reflect: true }, }; } @@ -194,6 +196,7 @@ export class JSONSchemaForm extends LitElement { constructor(props = {}) { super(); + this.#rendered = this.#updateRendered(true); this.identifier = props.identifier; @@ -212,7 +215,6 @@ export class JSONSchemaForm extends LitElement { this.emptyMessage = props.emptyMessage ?? "No properties to render"; - this.requirementMode = props.requirementMode ?? "default"; this.onlyRequired = props.onlyRequired ?? false; this.showLevelOverride = props.showLevelOverride ?? false; @@ -345,7 +347,7 @@ export class JSONSchemaForm extends LitElement { validate = async (resolved) => { // Check if any required inputs are missing const invalidInputs = await this.#validateRequirements(resolved); // get missing required paths - const isValid = this.requirementMode === "loose" ? true : !invalidInputs.length; + const isValid = !invalidInputs.length; // Print out a detailed error message if any inputs are missing let message = isValid ? "" : `${invalidInputs.length} required inputs are not specified properly.`; @@ -517,7 +519,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])) invalid.push(path); + else if (this.isUndefined(resolved[name]) && this.validateEmptyValues) invalid.push(path); } } @@ -849,7 +851,6 @@ export class JSONSchemaForm extends LitElement { this.updateData(path, value); }, - requirementMode: this.requirementMode, required: required[name], // Scoped to the sub-schema ignore: this.ignore, dialogOptions: this.dialogOptions, diff --git a/src/renderer/src/stories/forms/GlobalFormModal.ts b/src/renderer/src/stories/forms/GlobalFormModal.ts index c9b4627fd..620cc93f0 100644 --- a/src/renderer/src/stories/forms/GlobalFormModal.ts +++ b/src/renderer/src/stories/forms/GlobalFormModal.ts @@ -44,7 +44,7 @@ export function createGlobalFormModal(this: Page, { else removeProperties(schemaCopy.properties, propsToRemove) const globalForm = new JSONSchemaForm({ - requirementMode: "loose", + validateEmptyValues: false, mode: 'accordion', schema: schemaCopy, emptyMessage: "No properties to edit globally.", diff --git a/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js b/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js index 90b66fa54..57cf28b2e 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js @@ -92,7 +92,7 @@ export class GuidedNewDatasetPage extends Page { this, schema, ["project"], - { validateEmptyValues: false, requirementMode: "loose" }, + { validateEmptyValues: false }, (info) => { info.title = `${info.label} Global Metadata`; return info; diff --git a/src/renderer/src/stories/pages/settings/SettingsPage.js b/src/renderer/src/stories/pages/settings/SettingsPage.js index 3cc9948ac..03b08ef0b 100644 --- a/src/renderer/src/stories/pages/settings/SettingsPage.js +++ b/src/renderer/src/stories/pages/settings/SettingsPage.js @@ -20,14 +20,6 @@ import { merge, setUndefinedIfNotDeclared } from "../utils.js"; import { notyf } from "../../../dependencies/globals.js"; -const setUndefinedIfNotDeclared = (schemaProps, resolved) => { - for (const prop in schemaProps) { - const propInfo = schemaProps[prop]?.properties; - if (propInfo) setUndefinedIfNotDeclared(propInfo, resolved[prop]); - else if (!(prop in resolved)) resolved[prop] = undefined; - } -}; - export class SettingsPage extends Page { header = { title: "App Settings", From 27a640d69e81fa4b87f0771ea71bef09b9d7d712 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 21:43:11 +0000 Subject: [PATCH 07/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/renderer/src/stories/JSONSchemaForm.js | 1 - .../guided-mode/setup/GuidedNewDatasetInfo.js | 14 ++++---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js index fc4605c38..851a92da5 100644 --- a/src/renderer/src/stories/JSONSchemaForm.js +++ b/src/renderer/src/stories/JSONSchemaForm.js @@ -196,7 +196,6 @@ export class JSONSchemaForm extends LitElement { constructor(props = {}) { super(); - this.#rendered = this.#updateRendered(true); this.identifier = props.identifier; diff --git a/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js b/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js index 57cf28b2e..63c4745bf 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js @@ -88,16 +88,10 @@ export class GuidedNewDatasetPage extends Page { this.state = merge(global.data.output_locations, structuredClone(this.info.globalState.project)); - const pages = schemaToPages.call( - this, - schema, - ["project"], - { validateEmptyValues: false }, - (info) => { - info.title = `${info.label} Global Metadata`; - return info; - } - ); + const pages = schemaToPages.call(this, schema, ["project"], { validateEmptyValues: false }, (info) => { + info.title = `${info.label} Global Metadata`; + return info; + }); pages.forEach((page) => { page.header = { From 429057b4ab7bd9d6c59da949b94bba0bcd60fb65 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 21:52:51 +0000 Subject: [PATCH 08/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/renderer/src/stories/pages/guided-mode/GuidedStart.js | 2 +- src/renderer/src/stories/preview/inspector/InspectorList.js | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/stories/pages/guided-mode/GuidedStart.js b/src/renderer/src/stories/pages/guided-mode/GuidedStart.js index a5b5562e0..62235c912 100644 --- a/src/renderer/src/stories/pages/guided-mode/GuidedStart.js +++ b/src/renderer/src/stories/pages/guided-mode/GuidedStart.js @@ -100,7 +100,7 @@ export class GuidedStartPage extends Page {
${new InfoBox({ header: "Where can I learn more about the conversion process?", - content:` + content: ` Although not required to use the GUIDE, you can learn more about the NWB conversion process in the ${hasObjectType ? `${this.object_type}` : ""} ` : ""} - ${hasMetadata - ? html`${message}` - : html`

${message}

`} + ${hasMetadata ? html`${message}` : html`

${message}

`} ${this.file_path ? html`${this.files && this.files.length > 1 From 69ba80c9eb4ae7f8f1a15a03560eb782a0ec8a59 Mon Sep 17 00:00:00 2001 From: Garrett Date: Mon, 30 Oct 2023 15:05:20 -0700 Subject: [PATCH 09/14] Fix schema used --- .../stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js b/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js index 63c4745bf..6939693bb 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js @@ -15,8 +15,6 @@ import { header } from "../../../forms/utils"; const projectMetadataSchema = merge(projectGlobalSchema, projectGeneralSchema); -merge(globalSchema, projectMetadataSchema); - export class GuidedNewDatasetPage extends Page { constructor(...args) { super(...args); @@ -88,7 +86,7 @@ export class GuidedNewDatasetPage extends Page { this.state = merge(global.data.output_locations, structuredClone(this.info.globalState.project)); - const pages = schemaToPages.call(this, schema, ["project"], { validateEmptyValues: false }, (info) => { + const pages = schemaToPages.call(this, globalSchema, ["project"], { validateEmptyValues: false }, (info) => { info.title = `${info.label} Global Metadata`; return info; }); @@ -103,7 +101,7 @@ export class GuidedNewDatasetPage extends Page { this.form = new JSONSchemaForm({ schema, results: this.state, - validateEmptyValues: false, + // validateEmptyValues: false, dialogOptions: { properties: ["createDirectory"], }, From 26bfa99664b34534ee98f58362bc17034a351d8b Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 31 Oct 2023 11:58:59 -0700 Subject: [PATCH 10/14] Fix popup --- src/renderer/src/stories/JSONSchemaForm.js | 26 ++++++++++++++----- .../src/stories/forms/GlobalFormModal.ts | 7 ++--- src/renderer/src/stories/pages/FormPage.js | 3 +-- .../pages/guided-mode/data/GuidedMetadata.js | 1 + .../pages/guided-mode/setup/GuidedSubjects.js | 1 - 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js index 851a92da5..aac256f5e 100644 --- a/src/renderer/src/stories/JSONSchemaForm.js +++ b/src/renderer/src/stories/JSONSchemaForm.js @@ -300,7 +300,8 @@ export class JSONSchemaForm extends LitElement { input.updateData(globalValue); this.onOverride(name, globalValue, path); } - } + } else resolvedParent[name] = undefined + resultParent[name] = undefined; // NOTE: Will be removed when stringified } else { resultParent[name] = value === globalValue ? undefined : value; // Retain association with global value @@ -642,7 +643,7 @@ export class JSONSchemaForm extends LitElement { const pathToValidate = [...(this.base ?? []), ...path]; const valid = - !this.validateEmptyValues && !(name in parent) + !this.validateEmptyValues && parent[name] === undefined ? true : await this.validateOnChange(name, parent, pathToValidate); @@ -650,6 +651,7 @@ export class JSONSchemaForm extends LitElement { const externalPath = [...this.base, name]; const isRequired = this.#isRequired(localPath); + let warnings = Array.isArray(valid) ? valid.filter((info) => info.type === "warning" && (!isRequired || !info.missing)) : []; @@ -678,11 +680,21 @@ export class JSONSchemaForm extends LitElement { // For non-links, throw a basic requirement error if the property is required if (!errors.length && isRequired && this.isUndefined(parent[name])) { const schema = this.getSchema(localPath); - errors.push({ - message: `${schema.title ?? header(name)} is a required property.`, - type: "error", - missing: true, - }); // Throw at least a basic error if the property is required + + // Throw at least a basic warning if the property is required and missing + if (this.validateEmptyValues) { + errors.push({ + message: `${schema.title ?? header(name)} is a required property.`, + type: "error", + missing: true, + }); + } else { + warnings.push({ + message: `${schema.title ?? header(name)} is a suggested property.`, + type: "warning", + missing: true, + }) + } } } diff --git a/src/renderer/src/stories/forms/GlobalFormModal.ts b/src/renderer/src/stories/forms/GlobalFormModal.ts index 620cc93f0..391785c52 100644 --- a/src/renderer/src/stories/forms/GlobalFormModal.ts +++ b/src/renderer/src/stories/forms/GlobalFormModal.ts @@ -28,13 +28,14 @@ export function createGlobalFormModal(this: Page, { mergeFunction?: Function }) { + const modal = new Modal({ header }) const content = document.createElement("div") - const schemaCopy = structuredClone(schema) + const schemaCopy = structuredClone(schema) // Ensure no mutation function removeProperties(obj: any, props: string[]) { props.forEach(prop => delete obj[prop]) @@ -73,14 +74,14 @@ export function createGlobalFormModal(this: Page, { forms.forEach(formInfo => { const { subject, form } = formInfo const result = cached[subject] ?? (cached[subject] = mergeFunction.call(formInfo, toPass, this.info.globalState)) - form.globals = structuredClone(key ? result[key]: result) + form.globals = structuredClone(key ? result.project[key]: result) }) tables.forEach(table => { const subject = null const result = cached[subject] ?? (cached[subject] = mergeFunction(toPass, this.info.globalState)) - table.globals = structuredClone( key ? result[key]: result) + table.globals = structuredClone( key ? result.project[key]: result) }) await save(this) // Save after all updates are made diff --git a/src/renderer/src/stories/pages/FormPage.js b/src/renderer/src/stories/pages/FormPage.js index de82cceab..5197aa8e6 100644 --- a/src/renderer/src/stories/pages/FormPage.js +++ b/src/renderer/src/stories/pages/FormPage.js @@ -9,6 +9,7 @@ export function schemaToPages(schema, globalStatePath, options, transformationCa return Object.entries(schema.properties) .filter(([_, value]) => value.properties) .map(([key, value]) => { + const optionsCopy = { ...options }; if (optionsCopy.required && optionsCopy.required[key]) @@ -30,8 +31,6 @@ export function schemaToPages(schema, globalStatePath, options, transformationCa }) ); - delete schema.properties[key]; - if (optionsCopy.ignore && optionsCopy.ignore.includes(key)) return null; return page; }) 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 b0823df37..2edbb1036 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js @@ -97,6 +97,7 @@ export class GuidedMetadataPage extends ManagedPage { merge(globalResolved, globals); return resolveGlobalOverrides(this.subject, globals); }, + validateOnChange })); document.body.append(modal); } 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 ca8825ce2..54fa175c2 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js @@ -89,7 +89,6 @@ export class GuidedSubjectsPage extends Page { key: "Subject", schema: globalSchema.properties.Subject, validateOnChange: (key, parent, path) => { - console.log(key, parent[key], path); return validateOnChange(key, parent, ["Subject", ...path]); }, })); From d47c1ff531c3c2111aec068d2d896b352c5a5165 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 31 Oct 2023 19:05:16 +0000 Subject: [PATCH 11/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/renderer/src/stories/JSONSchemaForm.js | 6 +++--- src/renderer/src/stories/JSONSchemaInput.js | 5 +---- src/renderer/src/stories/pages/FormPage.js | 1 - .../src/stories/pages/guided-mode/data/GuidedMetadata.js | 2 +- .../src/stories/pages/guided-mode/setup/GuidedSubjects.js | 1 - 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js index e797b2df4..1e80b594a 100644 --- a/src/renderer/src/stories/JSONSchemaForm.js +++ b/src/renderer/src/stories/JSONSchemaForm.js @@ -296,7 +296,7 @@ export class JSONSchemaForm extends LitElement { input.updateData(globalValue); this.onOverride(name, globalValue, path); } - } else resolvedParent[name] = undefined + } else resolvedParent[name] = undefined; resultParent[name] = undefined; // NOTE: Will be removed when stringified } else { @@ -679,13 +679,13 @@ export class JSONSchemaForm extends LitElement { message: `${schema.title ?? header(name)} is a required property.`, type: "error", missing: true, - }); + }); } else { warnings.push({ message: `${schema.title ?? header(name)} is a suggested property.`, type: "warning", missing: true, - }) + }); } } } diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js index 9d8d23f5e..82cb73e7f 100644 --- a/src/renderer/src/stories/JSONSchemaInput.js +++ b/src/renderer/src/stories/JSONSchemaInput.js @@ -317,10 +317,7 @@ export class JSONSchemaInput extends LitElement { }); return html` -
validateOnChange && this.#triggerValidation(name, path)} - > +
validateOnChange && this.#triggerValidation(name, path)}> ${list} ${addButton}
`; diff --git a/src/renderer/src/stories/pages/FormPage.js b/src/renderer/src/stories/pages/FormPage.js index 5197aa8e6..ad088732c 100644 --- a/src/renderer/src/stories/pages/FormPage.js +++ b/src/renderer/src/stories/pages/FormPage.js @@ -9,7 +9,6 @@ export function schemaToPages(schema, globalStatePath, options, transformationCa return Object.entries(schema.properties) .filter(([_, value]) => value.properties) .map(([key, value]) => { - const optionsCopy = { ...options }; if (optionsCopy.required && optionsCopy.required[key]) 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 2edbb1036..c7fea65e4 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js @@ -97,7 +97,7 @@ export class GuidedMetadataPage extends ManagedPage { merge(globalResolved, globals); return resolveGlobalOverrides(this.subject, globals); }, - validateOnChange + validateOnChange, })); document.body.append(modal); } 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 db50dab8f..1dedc3117 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js @@ -31,7 +31,6 @@ export class GuidedSubjectsPage extends Page { // Abort save if subject structure is invalid beforeSave = () => { - try { this.table.validate(); } catch (e) { From bdfbf17e626da93836819c521dbbca5536591be1 Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 31 Oct 2023 12:46:25 -0700 Subject: [PATCH 12/14] Update metadata.test.ts --- tests/metadata.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/metadata.test.ts b/tests/metadata.test.ts index 5932395d9..ed51c35a9 100644 --- a/tests/metadata.test.ts +++ b/tests/metadata.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from 'vitest' import { createResults } from '../src/renderer/src/stories/pages/guided-mode/data/utils' import { mapSessions } from '../src/renderer/src/stories/pages/utils' -import baseMetadataSchema from '../schemas/base-metadata.schema' +import preprocessMetadataSchema from '../schemas/base-metadata.schema' import { createMockGlobalState } from './utils' @@ -25,7 +25,7 @@ describe('metadata is specified correctly', () => { test('session-specific metadata is merged with project and subject metadata correctly', () => { const globalState = createMockGlobalState() const result = mapSessions(info => createResults(info, globalState), globalState) - const res = v.validate(result[0], baseMetadataSchema) // Check first session with JSON Schema + const res = v.validate(result[0], preprocessMetadataSchema()) // Check first session with JSON Schema expect(res.errors).toEqual([]) }) }) From a06754a2c3a6311d61b5bda1a2b5bfe03f9a918e Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 31 Oct 2023 12:55:41 -0700 Subject: [PATCH 13/14] Update metadata.test.ts --- tests/metadata.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/metadata.test.ts b/tests/metadata.test.ts index ed51c35a9..5932395d9 100644 --- a/tests/metadata.test.ts +++ b/tests/metadata.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from 'vitest' import { createResults } from '../src/renderer/src/stories/pages/guided-mode/data/utils' import { mapSessions } from '../src/renderer/src/stories/pages/utils' -import preprocessMetadataSchema from '../schemas/base-metadata.schema' +import baseMetadataSchema from '../schemas/base-metadata.schema' import { createMockGlobalState } from './utils' @@ -25,7 +25,7 @@ describe('metadata is specified correctly', () => { test('session-specific metadata is merged with project and subject metadata correctly', () => { const globalState = createMockGlobalState() const result = mapSessions(info => createResults(info, globalState), globalState) - const res = v.validate(result[0], preprocessMetadataSchema()) // Check first session with JSON Schema + const res = v.validate(result[0], baseMetadataSchema) // Check first session with JSON Schema expect(res.errors).toEqual([]) }) }) From a1a5700ab40a9e3538b09ee22a927b8470a9efa6 Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 31 Oct 2023 15:50:02 -0700 Subject: [PATCH 14/14] Update Table.js --- src/renderer/src/stories/Table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/stories/Table.js b/src/renderer/src/stories/Table.js index 7c7f512f4..9984a75bf 100644 --- a/src/renderer/src/stories/Table.js +++ b/src/renderer/src/stories/Table.js @@ -119,7 +119,7 @@ export class Table extends LitElement { if (col === this.keyColumn) { if (hasRow) value = row; else return undefined; - } else value = (hasRow ? this.data[row][col] : undefined) ?? this.template[col]; + } else value = (hasRow ? this.data[row][col] : undefined) ?? this.globals[col]; return value; });