diff --git a/schemas/json/dandi/global.json b/schemas/json/dandi/global.json
index 7b72776b5..081eae1f1 100644
--- a/schemas/json/dandi/global.json
+++ b/schemas/json/dandi/global.json
@@ -16,5 +16,6 @@
},
"required": ["main_api_key"]
}
- }
+ },
+ "required": ["api_keys"]
}
diff --git a/src/renderer/src/stories/Accordion.js b/src/renderer/src/stories/Accordion.js
index cae120639..92e6e8c95 100644
--- a/src/renderer/src/stories/Accordion.js
+++ b/src/renderer/src/stories/Accordion.js
@@ -23,6 +23,11 @@ export class Accordion extends LitElement {
box-sizing: border-box;
}
+ :host {
+ display: block;
+ overflow: hidden;
+ }
+
.header {
display: flex;
align-items: end;
@@ -107,10 +112,14 @@ export class Accordion extends LitElement {
.guided--nav-bar-dropdown::after {
font-size: 0.8em;
position: absolute;
- right: 50px;
+ right: 25px;
font-family: ${unsafeCSS(emojiFontFamily)};
}
+ .guided--nav-bar-dropdown.toggleable::after {
+ right: 50px;
+ }
+
.guided--nav-bar-dropdown.error::after {
content: "${errorSymbol}";
}
@@ -123,7 +132,7 @@ export class Accordion extends LitElement {
content: "${successSymbol}";
}
- .guided--nav-bar-dropdown:hover {
+ .guided--nav-bar-dropdown.toggleable:hover {
cursor: pointer;
background-color: lightgray;
}
@@ -143,34 +152,54 @@ export class Accordion extends LitElement {
padding-left: 0px;
overflow-y: auto;
}
+
+ .disabled {
+ opacity: 0.5;
+ pointer-events: none;
+ }
`;
}
static get properties() {
return {
- sections: { type: Object, reflect: false },
+ name: { type: String, reflect: true },
+ open: { type: Boolean, reflect: true },
+ disabled: { type: Boolean, reflect: true },
+ status: { type: String, reflect: true },
};
}
- constructor({ sections = {}, contentPadding } = {}) {
+ constructor({
+ name,
+ subtitle,
+ toggleable = true,
+ content,
+ open = false,
+ status,
+ disabled = false,
+ contentPadding,
+ } = {}) {
super();
- this.sections = sections;
+ this.name = name;
+ this.subtitle = subtitle;
+ this.content = content;
+ this.open = open;
+ this.status = status;
+ this.disabled = disabled;
+ this.toggleable = toggleable;
this.contentPadding = contentPadding;
}
updated() {
- Object.entries(this.sections).map(([sectionName, info]) => {
- const isActive = info.open;
- if (isActive) this.#toggleDropdown(sectionName, true);
- else this.#toggleDropdown(sectionName, false);
- });
+ if (!this.content) return;
+ this.toggle(!!this.open);
}
- setSectionStatus = (sectionName, status) => {
- const el = this.shadowRoot.querySelector("[data-section-name='" + sectionName + "']");
+ setStatus = (status) => {
+ const el = this.shadowRoot.getElementById("dropdown");
el.classList.remove("error", "warning", "valid");
el.classList.add(status);
- this.sections[sectionName].status = status;
+ this.status = status;
};
onClick = () => {}; // Set by the user
@@ -183,60 +212,67 @@ export class Accordion extends LitElement {
}
};
- #toggleDropdown = (sectionName, forcedState) => {
+ toggle = (forcedState) => {
const hasForce = forcedState !== undefined;
- const toggledState = !this.sections[sectionName].open;
+ const toggledState = !this.open;
- let state = hasForce ? forcedState : toggledState;
+ const desiredState = hasForce ? forcedState : toggledState;
+ const state = this.toOpen(desiredState);
//remove hidden from child elements with guided--nav-bar-section-page class
- const section = this.shadowRoot.querySelector("[data-section='" + sectionName + "']");
+ const section = this.shadowRoot.getElementById("section");
section.toggleAttribute("hidden", hasForce ? !state : undefined);
- const dropdown = this.shadowRoot.querySelector("[data-section-name='" + sectionName + "']");
+ const dropdown = this.shadowRoot.getElementById("dropdown");
this.#updateClass("active", dropdown, !state);
//toggle the chevron
const chevron = dropdown.querySelector("nwb-chevron");
- chevron.direction = state ? "bottom" : "right";
+ if (chevron) chevron.direction = state ? "bottom" : "right";
- this.sections[sectionName].open = state;
+ if (desiredState === state) this.open = state; // Update state if not overridden
+ };
+
+ toOpen = (state = this.open) => {
+ if (!this.toggleable) return true; // Force open if not toggleable
+ else if (this.disabled) return false; // Force closed if disabled
+ return state;
};
render() {
+ const isToggleable = this.content && this.toggleable;
+
return html`
-
- ${Object.entries(this.sections)
- .map(([sectionName, info]) => {
- return html`
-
-
this.#toggleDropdown(sectionName, undefined)}
- >
-
- ${new Chevron({
- direction: "right",
- color: faColor,
- size: faSize,
- })}
-
-
- ${info.content}
-
-
- `;
- })
- .flat()}
-
+
+
isToggleable && this.toggle()}
+ >
+
+ ${isToggleable
+ ? new Chevron({
+ direction: "right",
+ color: faColor,
+ size: faSize,
+ })
+ : ""}
+
+ ${this.content
+ ? html`
+ ${this.content}
+
`
+ : ""}
+
`;
}
}
diff --git a/src/renderer/src/stories/InstanceManager.js b/src/renderer/src/stories/InstanceManager.js
index 3336dec28..dad1b7d31 100644
--- a/src/renderer/src/stories/InstanceManager.js
+++ b/src/renderer/src/stories/InstanceManager.js
@@ -124,6 +124,10 @@ export class InstanceManager extends LitElement {
#new-info > input {
margin-right: 10px;
}
+
+ nwb-accordion {
+ margin-bottom: 0.5em;
+ }
`;
}
@@ -161,7 +165,7 @@ export class InstanceManager extends LitElement {
const id = path.slice(0, i + 1).join("/");
const accordion = this.#accordions[id];
target = target[path[i]]; // Progressively check the deeper nested instances
- if (accordion) accordion.setSectionStatus(id, checkStatus(false, false, [...Object.values(target)]));
+ if (accordion) accordion.setStatus(checkStatus(false, false, [...Object.values(target)]));
}
};
@@ -299,11 +303,8 @@ export class InstanceManager extends LitElement {
const list = this.#render(value, [...path, key]);
const accordion = new Accordion({
- sections: {
- [key]: {
- content: list,
- },
- },
+ name: key,
+ content: list,
contentPadding: "10px",
});
diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js
index e3c0b03f2..04bc6d906 100644
--- a/src/renderer/src/stories/JSONSchemaForm.js
+++ b/src/renderer/src/stories/JSONSchemaForm.js
@@ -12,6 +12,10 @@ import { resolveProperties } from "./pages/guided-mode/data/utils";
import { JSONSchemaInput } from "./JSONSchemaInput";
import { InspectorListItem } from "./preview/inspector/InspectorList";
+const isObject = (o) => {
+ return o && typeof o === "object" && !Array.isArray(o);
+};
+
const selfRequiredSymbol = Symbol();
const componentCSS = `
@@ -146,7 +150,11 @@ const componentCSS = `
line-height: 1.4285em;
}
- [disabled] {
+ nwb-accordion {
+ margin-bottom: 0.5em;
+ }
+
+ [disabled]{
opacity: 0.5;
pointer-events: none;
}
@@ -164,7 +172,6 @@ export class JSONSchemaForm extends LitElement {
static get properties() {
return {
- mode: { type: String, reflect: true },
schema: { type: Object, reflect: false },
results: { type: Object, reflect: false },
required: { type: Object, reflect: false },
@@ -192,21 +199,16 @@ export class JSONSchemaForm extends LitElement {
resolved = {}; // Keep track of actual resolved values—not just what the user provides as results
- states = {};
-
constructor(props = {}) {
super();
this.#rendered = this.#updateRendered(true);
this.identifier = props.identifier;
- this.mode = props.mode ?? "default";
this.schema = props.schema ?? {};
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;
@@ -239,15 +241,15 @@ export class JSONSchemaForm extends LitElement {
getTable = (path) => {
if (typeof path === "string") path = path.split(".");
+
if (path.length === 1) return this.tables[path[0]]; // return table if accessible
const copy = [...path];
const tableName = copy.pop();
- if (this.mode === "accordion") return this.getForm(copy).getTable(tableName);
- else return this.shadowRoot.getElementById(path.join("-")).children[1].shadowRoot.children[0]; // Get table from UI container, then JSONSchemaInput
+ return this.getForm(copy).getTable(tableName);
};
-
+ v;
getForm = (path) => {
if (typeof path === "string") path = path.split(".");
const form = this.#nestedForms[path[0]];
@@ -258,9 +260,11 @@ export class JSONSchemaForm extends LitElement {
getInput = (path) => {
if (typeof path === "string") path = path.split(".");
+
const container = this.shadowRoot.querySelector(`#${path.join("-")}`);
- if (!container) return;
- return container.querySelector("jsonschema-input");
+
+ if (!container) return this.getForm(path[0]).getInput(path.slice(1));
+ return container?.querySelector("jsonschema-input");
};
#requirements = {};
@@ -277,7 +281,8 @@ export class JSONSchemaForm extends LitElement {
}
// Track resolved values for the form (data only)
- updateData(localPath, value) {
+ updateData(localPath, value, forceUpdate = false) {
+ if (!value) throw new Error("Cannot update data with undefined value");
const path = [...localPath];
const name = path.pop();
@@ -291,8 +296,6 @@ export class JSONSchemaForm extends LitElement {
// NOTE: Forms with nested forms will handle their own state updates
if (this.isUndefined(value)) {
- const globalValue = this.getGlobalValue(localPath);
-
// Continue to resolve and re-render...
if (globalValue) {
value = resolvedParent[name] = globalValue;
@@ -306,10 +309,11 @@ 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
- resolvedParent[name] = value;
+ resolvedParent[name] =
+ isObject(value) && isObject(resolvedParent) ? merge(value, resolvedParent[name]) : value; // Merge with existing resolved values
}
- if (hasUpdate) this.onUpdate(localPath, value); // Ensure the value has actually changed
+ if (hasUpdate || forceUpdate) this.onUpdate(localPath, value); // Ensure the value has actually changed
}
#addMessage = (name, message, type) => {
@@ -334,11 +338,17 @@ export class JSONSchemaForm extends LitElement {
};
status;
- checkStatus = () =>
+ checkStatus = () => {
checkStatus.call(this, this.#nWarnings, this.#nErrors, [
- ...Object.values(this.#nestedForms),
+ ...Object.entries(this.#nestedForms)
+ .filter(([k, v]) => {
+ const accordion = this.#accordions[k];
+ return !accordion || !accordion.disabled;
+ })
+ .map(([_, v]) => v),
...Object.values(this.tables),
]);
+ };
throw = (message) => {
this.onThrow(message, this.identifier);
@@ -350,6 +360,7 @@ export class JSONSchemaForm extends LitElement {
const requiredButNotSpecified = await this.#validateRequirements(resolved); // get missing required paths
const isValid = !requiredButNotSpecified.length;
+ // Check if all inputs are valid
const flaggedInputs = this.shadowRoot ? this.shadowRoot.querySelectorAll(".invalid") : [];
const allErrors = Array.from(flaggedInputs)
@@ -362,23 +373,40 @@ export class JSONSchemaForm extends LitElement {
return (acc += curr.includes(this.#isARequiredPropertyString) ? 1 : 0);
}, 0);
- console.log(allErrors);
-
// Print out a detailed error message if any inputs are missing
- let message = "";
- if (!isValid && allErrors.length && nMissingRequired === allErrors.length)
- message = `${nMissingRequired} required inputs are not defined.`;
+ let message = isValid
+ ? ""
+ : requiredButNotSpecified.length === 1
+ ? `${requiredButNotSpecified[0]} is not defined`
+ : `${requiredButNotSpecified.length} required inputs are not specified properly`;
+ if (requiredButNotSpecified.length !== nMissingRequired)
+ console.warn("Disagreement about the correct error to throw...");
+
+ // if (!isValid && allErrors.length && nMissingRequired === allErrors.length) message = `${nMissingRequired} required inputs are not defined.`;
// Check if all inputs are valid
if (flaggedInputs.length) {
flaggedInputs[0].focus();
- if (!message) message = `${flaggedInputs.length} invalid form values.`;
- message += ` Please check the highlighted fields.`;
+ if (!message) {
+ console.log(flaggedInputs);
+ if (flaggedInputs.length === 1)
+ message = `${header(flaggedInputs[0].path.join("."))} is not valid`;
+ else message = `${flaggedInputs.length} invalid form values`;
+ }
+ message += `${
+ this.base.length ? ` in the ${this.base.join(".")} section` : ""
+ }. Please check the highlighted fields.`;
}
if (message) this.throw(message);
- for (let key in this.#nestedForms) await this.#nestedForms[key].validate(resolved ? resolved[key] : undefined); // Validate nested forms too
+ // Validate nested forms (skip disabled)
+ for (let name in this.#nestedForms) {
+ const accordion = this.#accordions[name];
+ if (!accordion || !accordion.disabled)
+ await this.#nestedForms[name].validate(resolved ? resolved[name] : undefined); // Validate nested forms too
+ }
+
try {
for (let key in this.tables) await this.tables[key].validate(resolved ? resolved[key] : undefined); // Validate nested tables too
} catch (e) {
@@ -411,6 +439,7 @@ export class JSONSchemaForm extends LitElement {
#get = (path, object = this.resolved, omitted = []) => {
// path = path.slice(this.base.length); // Correct for base path
+ if (!path) throw new Error("Path not specified");
return path.reduce(
(acc, curr) => (acc = acc?.[curr] ?? acc?.[omitted.find((str) => acc[str])]?.[curr]),
object
@@ -523,6 +552,8 @@ export class JSONSchemaForm extends LitElement {
for (let name in requirements) {
let isRequired = requirements[name];
+ if (this.#accordions[name]?.disabled) continue; // Skip disabled accordions
+
// // NOTE: Uncomment to block checking requirements inside optional properties
// if (!requirements[name][selfRequiredSymbol] && !resolved[name]) continue; // Do not continue checking requirements if absent and not required
@@ -530,9 +561,10 @@ export class JSONSchemaForm extends LitElement {
if (isRequired) {
let path = parentPath ? `${parentPath}-${name}` : name;
- if (typeof isRequired === "object" && !Array.isArray(isRequired))
- invalid.push(...(await this.#validateRequirements(resolved[name], isRequired, path)));
- else if (this.isUndefined(resolved[name]) && this.validateEmptyValues) invalid.push(path);
+ // if (typeof isRequired === "object" && !Array.isArray(isRequired))
+ // invalid.push(...(await this.#validateRequirements(resolved[name], isRequired, path)));
+ // else
+ if (this.isUndefined(resolved[name]) && this.validateEmptyValues) invalid.push(path);
}
}
@@ -790,6 +822,8 @@ export class JSONSchemaForm extends LitElement {
}
};
+ #accordions = {};
+
#render = (schema, results, required = {}, path = []) => {
let isLink = Symbol("isLink");
// Filter non-required properties (if specified) and render the sub-schema
@@ -880,85 +914,170 @@ export class JSONSchemaForm extends LitElement {
const localPath = [...path, name];
- if (this.mode === "accordion" && hasMany) {
- const headerName = header(name);
-
- // Check properties that will be rendered before creating the accordion
- const base = [...this.base, ...localPath];
- const renderable = this.#getRenderable(info, required[name], base);
-
- if (renderable.length) {
- 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) => {
- const path = [...localPath, ...internalPath];
- this.updateData(path, value);
- },
-
- required: required[name], // Scoped to the sub-schema
- ignore: this.ignore,
- dialogOptions: this.dialogOptions,
- dialogType: this.dialogType,
- onlyRequired: this.onlyRequired,
- showLevelOverride: this.showLevelOverride,
- deferLoading: this.deferLoading,
- conditionalRequirements: this.conditionalRequirements,
- validateOnChange: (...args) => this.validateOnChange(...args),
- onThrow: (...args) => this.onThrow(...args),
- validateEmptyValues: this.validateEmptyValues,
- onStatusChange: (status) => {
- accordion.setSectionStatus(headerName, status);
- this.checkStatus();
- }, // Forward status changes to the parent form
- onInvalid: (...args) => this.onInvalid(...args),
- onLoaded: () => {
- this.nLoaded++;
- this.checkAllLoaded();
- },
- createTable: (...args) => this.createTable(...args),
- onOverride: (...args) => this.onOverride(...args),
- base,
- });
- }
+ const enableToggle = document.createElement("input");
+ const enableToggleContainer = document.createElement("div");
+ Object.assign(enableToggleContainer.style, {
+ position: "relative",
+ });
+ enableToggleContainer.append(enableToggle);
+
+ // Check properties that will be rendered before creating the accordion
+ const base = [...this.base, ...localPath];
+
+ const explicitlyRequired = schema.required?.includes(name) ?? false;
+
+ Object.assign(enableToggle, {
+ type: "checkbox",
+ checked: true,
+ style: "margin-right: 10px; pointer-events:all;",
+ });
+
+ const headerName = header(name);
+
+ const renderableInside = this.#getRenderable(info, required[name], localPath, true);
+
+ const __disabled = this.results.__disabled ?? (this.results.__disabled = {});
+ const __interacted = __disabled.__interacted ?? (__disabled.__interacted = {});
+
+ const hasInteraction = __interacted[name]; // NOTE: This locks the specific value to what the user has chosen...
- 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 { __disabled: __tempDisabledGlobal = {} } = this.getGlobalValue(localPath.slice(0, -1));
- const accordion = new Accordion({
- sections: {
- [headerName]: this.states[headerName],
+ const __disabledGlobal = structuredClone(__tempDisabledGlobal); // NOTE: Cloning ensures no property transfer
+
+ let isGlobalEffect = !hasInteraction || (!hasInteraction && __disabledGlobal.__interacted?.[name]); // Indicate whether global effect is used
+
+ const __disabledResolved = isGlobalEffect ? __disabledGlobal : __disabled;
+
+ const isDisabled = !!__disabledResolved[name];
+
+ enableToggle.checked = !isDisabled;
+
+ const nestedResults = __disabled[name] ?? results[name] ?? this.results[name]; // One or the other will exist—depending on global or local disabling
+
+ if (renderableInside.length) {
+ this.#nestedForms[name] = new JSONSchemaForm({
+ identifier: this.identifier,
+ schema: info,
+ results: { ...nestedResults },
+ globals: this.globals?.[name],
+
+ onUpdate: (internalPath, value, forceUpdate) => {
+ const path = [...localPath, ...internalPath];
+ this.updateData(path, value, forceUpdate);
},
+
+ required: required[name], // Scoped to the sub-schema
+ ignore: this.ignore,
+ dialogOptions: this.dialogOptions,
+ dialogType: this.dialogType,
+ onlyRequired: this.onlyRequired,
+ showLevelOverride: this.showLevelOverride,
+ deferLoading: this.deferLoading,
+ conditionalRequirements: this.conditionalRequirements,
+ validateOnChange: (...args) => this.validateOnChange(...args),
+ onThrow: (...args) => this.onThrow(...args),
+ validateEmptyValues: this.validateEmptyValues,
+ onStatusChange: (status) => {
+ accordion.setStatus(status);
+ this.checkStatus();
+ }, // Forward status changes to the parent form
+ onInvalid: (...args) => this.onInvalid(...args),
+ onLoaded: () => {
+ this.nLoaded++;
+ this.checkAllLoaded();
+ },
+ createTable: (...args) => this.createTable(...args),
+ onOverride: (...args) => this.onOverride(...args),
+ base,
+ });
+ }
+
+ const oldStates = this.#accordions[headerName];
+
+ const accordion = (this.#accordions[headerName] = new Accordion({
+ name: headerName,
+ toggleable: hasMany,
+ subtitle: html`
+ ${explicitlyRequired ? "" : enableToggleContainer}${renderableInside.length
+ ? `${renderableInside.length} fields`
+ : ""}
+
`,
+ content: this.#nestedForms[name],
+
+ // States
+ open: oldStates?.open ?? !hasMany,
+ disabled: isDisabled,
+ status: oldStates?.status ?? "valid", // Always show a status
+ }));
+
+ accordion.id = name; // assign name to accordion id
+
+ // Set enable / disable behavior
+ const addDisabled = (name, o) => {
+ if (!o.__disabled) o.__disabled = {};
+
+ // Do not overwrite cache of disabled values (with globals, for instance)
+ if (o.__disabled[name]) {
+ if (isGlobalEffect) return;
+ }
+
+ o.__disabled[name] = o[name] ?? (o[name] = {}); // Track disabled values (or at least something)
+ };
+
+ const disable = () => {
+ accordion.disabled = true;
+ addDisabled(name, this.resolved);
+ addDisabled(name, this.results);
+ this.resolved[name] = this.results[name] = undefined; // Remove entry from results
+
+ this.checkStatus();
+ };
+
+ const enable = () => {
+ accordion.disabled = false;
+
+ const { __disabled = {} } = this.results;
+ const { __disabled: resolvedDisabled = {} } = this.resolved;
+
+ if (__disabled[name]) this.updateData(localPath, __disabled[name]); // Propagate restored disabled values
+ __disabled[name] = undefined; // Clear disabled value
+ resolvedDisabled[name] = undefined; // Clear disabled value
+
+ this.checkStatus();
+ };
+
+ enableToggle.addEventListener("click", (e) => {
+ e.stopPropagation();
+ const { checked } = e.target;
+
+ // Reset parameters on interaction
+ isGlobalEffect = false;
+ Object.assign(enableToggle.style, {
+ accentColor: "unset",
});
- accordion.id = name; // assign name to accordion id
+ const { __disabled = {} } = this.results;
+ const { __disabled: resolvedDisabled = {} } = this.resolved;
+
+ if (!__disabled.__interacted) __disabled.__interacted = {};
+ if (!resolvedDisabled.__interacted) resolvedDisabled.__interacted = {};
+
+ __disabled.__interacted[name] = resolvedDisabled.__interacted[name] = true; // Track that the user has interacted with the form
- if (!renderable.length) accordion.setAttribute("disabled", "");
+ checked ? enable() : disable();
- return accordion;
+ this.onUpdate(localPath, this.results[name]);
+ });
+
+ if (isGlobalEffect) {
+ isDisabled ? disable() : enable();
+ Object.assign(enableToggle.style, {
+ accentColor: "gray",
+ });
}
- // Render properties in the sub-schema
- const rendered = this.#render(info, results?.[name], required[name], localPath);
- return hasMany || path.length > 1
- ? html`
-
-
-
- ${rendered}
-
- `
- : rendered;
+ return accordion;
});
return rendered;
diff --git a/src/renderer/src/stories/JSONSchemaForm.stories.js b/src/renderer/src/stories/JSONSchemaForm.stories.js
index a59857ef8..63e71610b 100644
--- a/src/renderer/src/stories/JSONSchemaForm.stories.js
+++ b/src/renderer/src/stories/JSONSchemaForm.stories.js
@@ -3,14 +3,7 @@ import { JSONSchemaForm } from "./JSONSchemaForm";
export default {
title: "Components/JSON Schema Form",
// Set controls
- argTypes: {
- mode: {
- options: ["default", "accordion"],
- control: {
- type: "select",
- },
- },
- },
+ argTypes: {},
};
const Template = (args) => new JSONSchemaForm(args);
@@ -53,7 +46,6 @@ Default.args = {
export const Nested = Template.bind({});
Nested.args = {
- mode: "accordion",
results: {
name: "name",
ignored: true,
diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js
index 784db5813..4ad9a8742 100644
--- a/src/renderer/src/stories/JSONSchemaInput.js
+++ b/src/renderer/src/stories/JSONSchemaInput.js
@@ -135,7 +135,7 @@ export class JSONSchemaInput extends LitElement {
getElement = () => this.shadowRoot.querySelector(".schema-input");
- #activateTimeoutValidation = (name, el, path) => {
+ #activateTimeoutValidation = (name, path) => {
this.#clearTimeoutValidation();
this.#validationTimeout = setTimeout(() => {
this.onValidate ? this.onValidate() : this.form ? this.form.triggerValidation(name, path) : "";
@@ -147,15 +147,15 @@ export class JSONSchemaInput extends LitElement {
};
#validationTimeout = null;
- #updateData = (fullPath, value) => {
- this.onUpdate ? this.onUpdate(value) : this.form ? this.form.updateData(fullPath, value) : "";
+ #updateData = (fullPath, value, forceUpdate) => {
+ this.onUpdate ? this.onUpdate(value) : this.form ? this.form.updateData(fullPath, value, forceUpdate) : "";
const path = [...fullPath];
const name = path.splice(-1)[0];
this.value = value; // Update the latest value
- this.#activateTimeoutValidation(name, this.getElement(), path);
+ this.#activateTimeoutValidation(name, path);
};
#triggerValidation = (name, path) => {
@@ -227,6 +227,8 @@ export class JSONSchemaInput extends LitElement {
schema: itemSchema,
data: this.value,
+ onUpdate: () => this.#updateData(fullPath, tableMetadata.data, true), // Ensure change propagates to all forms
+
// NOTE: This is likely an incorrect declaration of the table validation call
validateOnChange: (key, parent, v) => {
return (
diff --git a/src/renderer/src/stories/SimpleTable.js b/src/renderer/src/stories/SimpleTable.js
index 4835f3630..8d992d7e3 100644
--- a/src/renderer/src/stories/SimpleTable.js
+++ b/src/renderer/src/stories/SimpleTable.js
@@ -635,7 +635,7 @@ export class SimpleTable extends LitElement {
else target[rowName][header] = value;
}
- if (cell.interacted) this.onUpdate(rowName, header, value);
+ if (cell.interacted) this.onUpdate([rowName, header], value);
};
#createCell = (value, info) => {
diff --git a/src/renderer/src/stories/forms/GlobalFormModal.ts b/src/renderer/src/stories/forms/GlobalFormModal.ts
index 391785c52..f6de21e8b 100644
--- a/src/renderer/src/stories/forms/GlobalFormModal.ts
+++ b/src/renderer/src/stories/forms/GlobalFormModal.ts
@@ -46,7 +46,6 @@ export function createGlobalFormModal(this: Page, {
const globalForm = new JSONSchemaForm({
validateEmptyValues: false,
- mode: 'accordion',
schema: schemaCopy,
emptyMessage: "No properties to edit globally.",
ignore: propsToIgnore,
@@ -71,16 +70,20 @@ export function createGlobalFormModal(this: Page, {
const forms = (hasInstances ? this.forms : this.form ? [ { form: this.form }] : []) ?? []
const tables = (hasInstances ? this.tables : this.table ? [ this.table ] : []) ?? []
+ const mergeOpts = {
+ // remove: false
+ }
+
forms.forEach(formInfo => {
const { subject, form } = formInfo
- const result = cached[subject] ?? (cached[subject] = mergeFunction.call(formInfo, toPass, this.info.globalState))
+ const result = cached[subject] ?? (cached[subject] = mergeFunction.call(formInfo, toPass, this.info.globalState, mergeOpts))
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))
+ const result = cached[subject] ?? (cached[subject] = mergeFunction(toPass, this.info.globalState, mergeOpts))
table.globals = structuredClone( key ? result.project[key]: result)
})
diff --git a/src/renderer/src/stories/pages/FormPage.js b/src/renderer/src/stories/pages/FormPage.js
index ecf9ff88d..650a5f2e2 100644
--- a/src/renderer/src/stories/pages/FormPage.js
+++ b/src/renderer/src/stories/pages/FormPage.js
@@ -25,7 +25,10 @@ export function schemaToPages(schema, globalStatePath, options, transformationCa
globalStatePath,
formOptions: {
...optionsCopy,
- schema: { properties: { [key]: value } },
+ schema: {
+ properties: { [key]: value },
+ 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 f8f119616..11ed77495 100644
--- a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js
+++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js
@@ -156,10 +156,11 @@ export class GuidedMetadataPage extends ManagedPage {
resolveResults(subject, session, globalState);
+ console.log(subject, session, results);
+
// Create the form
const form = new JSONSchemaForm({
identifier: instanceId,
- mode: "accordion",
schema: preprocessMetadataSchema(schema),
results,
globals: aggregateGlobalMetadata,
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 20e804242..3b8fe1c1b 100644
--- a/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js
+++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js
@@ -129,7 +129,7 @@ export class GuidedPathExpansionPage extends Page {
const source_data = {};
for (let key in globalState.interfaces) {
- const existing = existingSourceData?.[key]
+ const existing = existingSourceData?.[key];
if (existing) source_data[key] = existing ?? {};
}
@@ -302,7 +302,7 @@ export class GuidedPathExpansionPage extends Page {
if (fs) {
const baseDir = form.getInput([...parentPath, "base_directory"]);
if (name === "format_string_path") {
- if (value && !baseDir.value) {
+ if (value && baseDir && !baseDir.value) {
return [
{
message: html`A base directory must be provided to locate your files.`,
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 80e0e3a8d..b725f134d 100644
--- a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js
+++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js
@@ -5,7 +5,7 @@ import { InstanceManager } from "../../../InstanceManager.js";
import { ManagedPage } from "./ManagedPage.js";
import { baseUrl } from "../../../../globals.js";
import { onThrow } from "../../../../errors";
-import { merge } from "../../utils.js";
+import { merge, sanitize } from "../../utils.js";
import preprocessSourceDataSchema from "../../../../../../../schemas/source-data.schema";
import { createGlobalFormModal } from "../../../forms/GlobalFormModal";
@@ -13,6 +13,7 @@ import { header } from "../../../forms/utils";
import { Button } from "../../../Button.js";
import globalIcon from "../../../assets/global.svg?raw";
+import { run } from "../options/utils.js";
const propsToIgnore = [
"verbose",
@@ -68,7 +69,7 @@ export class GuidedSourceDataPage extends ManagedPage {
backdrop: "rgba(0,0,0, 0.4)",
timerProgressBar: false,
didOpen: () => {
- Swal.showLoading();
+ Swal.showLoading();
},
});
};
@@ -79,7 +80,6 @@ export class GuidedSourceDataPage extends ManagedPage {
await Promise.all(
Object.values(this.forms).map(async ({ subject, session, form }) => {
-
const info = this.info.globalState.results[subject][session];
// NOTE: This clears all user-defined results
@@ -87,7 +87,7 @@ export class GuidedSourceDataPage extends ManagedPage {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
- source_data: form.resolved, // Use resolved values, including global source data
+ source_data: sanitize(structuredClone(form.resolved)), // Use resolved values, including global source data
interfaces: this.info.globalState.interfaces,
}),
})
@@ -117,18 +117,30 @@ export class GuidedSourceDataPage extends ManagedPage {
throw result;
}
+ const { results: metadata, schema } = result;
+
+ // Always delete Ecephys if absent ( NOTE: temporarily manually removing from schema on backend...)
+ const alwaysDelete = ["Ecephys"];
+ alwaysDelete.forEach((k) => {
+ if (!metadata[k]) delete info.metadata[k]; // Delete directly on metadata
+ });
+
+ for (let key in info.metadata) {
+ if (!alwaysDelete.includes(key) && !(key in schema.properties)) metadata[key] = undefined;
+ }
+
// Merge metadata results with the generated info
- merge(result.results, info.metadata);
+ merge(metadata, info.metadata);
// Mirror structure with metadata schema
- const schema = this.info.globalState.schema;
- if (!schema.metadata) schema.metadata = {};
- if (!schema.metadata[subject]) schema.metadata[subject] = {};
- schema.metadata[subject][session] = result.schema;
+ const schemaGlobal = this.info.globalState.schema;
+ if (!schemaGlobal.metadata) schemaGlobal.metadata = {};
+ if (!schemaGlobal.metadata[subject]) schemaGlobal.metadata[subject] = {};
+ schemaGlobal.metadata[subject][session] = schema;
})
);
- await this.save();
+ await this.save(undefined, false); // Just save new raw values
this.to(1);
},
@@ -142,7 +154,6 @@ export class GuidedSourceDataPage extends ManagedPage {
const form = new JSONSchemaForm({
identifier: instanceId,
- mode: "accordion",
schema: preprocessSourceDataSchema(schema),
results: info.source_data,
emptyMessage: "No source data required for this session.",
@@ -152,7 +163,9 @@ export class GuidedSourceDataPage extends ManagedPage {
this.notify(`${header(name)} has been overriden with a global value.`, "warning", 3000);
},
// onlyRequired: true,
- onUpdate: () => (this.unsavedUpdates = true),
+ onUpdate: () => {
+ this.unsavedUpdates = true;
+ },
onStatusChange: (state) => this.manager.updateState(instanceId, state),
onThrow,
});
@@ -173,8 +186,8 @@ export class GuidedSourceDataPage extends ManagedPage {
const modal = (this.#globalModal = createGlobalFormModal.call(this, {
header: "Global Source Data",
propsToRemove: [
- ...propsToIgnore,
- "folder_path",
+ ...propsToIgnore,
+ "folder_path",
"file_path",
// NOTE: Still keeping plural path specifications for now
],
diff --git a/src/renderer/src/stories/pages/guided-mode/data/GuidedStructure.js b/src/renderer/src/stories/pages/guided-mode/data/GuidedStructure.js
index c815215dc..8c92d5428 100644
--- a/src/renderer/src/stories/pages/guided-mode/data/GuidedStructure.js
+++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedStructure.js
@@ -87,10 +87,10 @@ export class GuidedStructurePage extends Page {
this.mapSessions(({ info }) => {
Object.keys(info.source_data).forEach((key) => {
if (!this.info.globalState.interfaces[key]) delete info.source_data[key];
- })
- })
+ });
+ });
}
-
+
await this.save(undefined, false); // Interrim save, in case the schema request fails
await this.getSchema();
};
diff --git a/src/renderer/src/stories/pages/guided-mode/data/utils.js b/src/renderer/src/stories/pages/guided-mode/data/utils.js
index 18deee495..844a4224a 100644
--- a/src/renderer/src/stories/pages/guided-mode/data/utils.js
+++ b/src/renderer/src/stories/pages/guided-mode/data/utils.js
@@ -2,7 +2,7 @@ import { merge } from "../../utils.js";
// Merge project-wide data into metadata
export function populateWithProjectMetadata(info, globalState) {
- const copy = structuredClone(info)
+ const copy = structuredClone(info);
const toMerge = Object.entries(globalState.project).filter(([_, value]) => value && typeof value === "object");
toMerge.forEach(([key, value]) => {
let internalMetadata = copy[key];
@@ -48,7 +48,7 @@ export function resolveProperties(properties = {}, target, globals = {}) {
else if (info.default) target[name] = info.default;
}
- resolveProperties(props, target[name], globals[name]);
+ if (target[name]) resolveProperties(props, target[name], globals[name]);
}
return target;
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 5a9efaae8..db1623de9 100644
--- a/src/renderer/src/stories/pages/guided-mode/options/GuidedInspectorPage.js
+++ b/src/renderer/src/stories/pages/guided-mode/options/GuidedInspectorPage.js
@@ -37,9 +37,9 @@ export class GuidedInspectorPage extends Page {
super(...args);
this.style.height = "100%"; // Fix main section
Object.assign(this.style, {
- display: 'flex',
- flexDirection: 'column'
- })
+ display: "flex",
+ flexDirection: "column",
+ });
}
header = {
@@ -85,85 +85,89 @@ export class GuidedInspectorPage extends Page {
)
.flat();
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.`,
- })}
-
-
-
- ${until(
- (async () => {
- if (fileArr.length <= 1) {
- const items =
- inspector ??
- removeFilePaths(
+ 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.`,
+ })}
+
+
+
+ ${until(
+ (async () => {
+ if (fileArr.length <= 1) {
+ const items =
+ inspector ??
+ removeFilePaths(
+ (this.unsavedUpdates = globalState.preview.inspector =
+ await run(
+ "inspect_file",
+ { nwbfile_path: fileArr[0].info.file, ...opts },
+ { title }
+ ))
+ );
+ return new InspectorList({ items, emptyMessage });
+ }
+
+ const items = await (async () => {
+ const path = getSharedPath(fileArr.map((o) => o.info.file));
+ const report =
+ inspector ??
(this.unsavedUpdates = globalState.preview.inspector =
- await run("inspect_file", { nwbfile_path: fileArr[0].info.file, ...opts }, { title }))
- );
- return new InspectorList({ items, emptyMessage });
- }
-
- const items = await (async () => {
- const path = getSharedPath(fileArr.map((o) => o.info.file));
- const report =
- inspector ??
- (this.unsavedUpdates = globalState.preview.inspector =
- await run("inspect_folder", { path, ...opts }, { title: title + "s" }));
- return truncateFilePaths(report, path);
- })();
-
- const _instances = fileArr.map(({ subject, session, info }) => {
- const file_path = [`sub-${subject}`, `sub-${subject}_ses-${session}`];
- const filtered = removeFilePaths(filter(items, { file_path }));
-
- const display = () => new InspectorList({ items: filtered, emptyMessage });
- display.status = this.getStatus(filtered);
-
- return {
- subject,
- session,
- display,
+ await run("inspect_folder", { path, ...opts }, { title: title + "s" }));
+ return truncateFilePaths(report, path);
+ })();
+
+ const _instances = fileArr.map(({ subject, session, info }) => {
+ const file_path = [`sub-${subject}`, `sub-${subject}_ses-${session}`];
+ const filtered = removeFilePaths(filter(items, { file_path }));
+
+ const display = () => new InspectorList({ items: filtered, emptyMessage });
+ display.status = this.getStatus(filtered);
+
+ return {
+ subject,
+ session,
+ display,
+ };
+ });
+
+ const instances = _instances.reduce((acc, { subject, session, display }) => {
+ const subLabel = `sub-${subject}`;
+ if (!acc[`sub-${subject}`]) acc[subLabel] = {};
+ acc[subLabel][`ses-${session}`] = display;
+ return acc;
+ }, {});
+
+ Object.keys(instances).forEach((subLabel) => {
+ const subItems = filter(items, { file_path: `${subLabel}${nodePath.sep}${subLabel}_ses-` }); // NOTE: This will not run on web-only now
+ const path = getSharedPath(subItems.map((o) => o.file_path));
+ const filtered = truncateFilePaths(subItems, path);
+
+ const display = () => new InspectorList({ items: filtered, emptyMessage });
+ display.status = this.getStatus(filtered);
+
+ instances[subLabel] = {
+ ["All Files"]: display,
+ ...instances[subLabel],
+ };
+ });
+
+ const allDisplay = () => new InspectorList({ items, emptyMessage });
+ allDisplay.status = this.getStatus(items);
+
+ const allInstances = {
+ ["All Files"]: allDisplay,
+ ...instances,
};
- });
-
- const instances = _instances.reduce((acc, { subject, session, display }) => {
- const subLabel = `sub-${subject}`;
- if (!acc[`sub-${subject}`]) acc[subLabel] = {};
- acc[subLabel][`ses-${session}`] = display;
- return acc;
- }, {});
-
- Object.keys(instances).forEach((subLabel) => {
- const subItems = filter(items, { file_path: `${subLabel}${nodePath.sep}${subLabel}_ses-` }); // NOTE: This will not run on web-only now
- const path = getSharedPath(subItems.map((o) => o.file_path));
- const filtered = truncateFilePaths(subItems, path);
-
- const display = () => new InspectorList({ items: filtered, emptyMessage });
- display.status = this.getStatus(filtered);
-
- instances[subLabel] = {
- ["All Files"]: display,
- ...instances[subLabel],
- };
- });
-
- const allDisplay = () => new InspectorList({ items, emptyMessage });
- allDisplay.status = this.getStatus(items);
-
- const allInstances = {
- ["All Files"]: allDisplay,
- ...instances,
- };
- const manager = new InstanceManager({
- instances: allInstances,
- });
+ const manager = new InstanceManager({
+ instances: allInstances,
+ });
- return manager;
- })(),
- ""
- )}`;
+ return manager;
+ })(),
+ ""
+ )}`;
}
}
diff --git a/src/renderer/src/stories/pages/guided-mode/options/utils.js b/src/renderer/src/stories/pages/guided-mode/options/utils.js
index 48031dd06..3eb939744 100644
--- a/src/renderer/src/stories/pages/guided-mode/options/utils.js
+++ b/src/renderer/src/stories/pages/guided-mode/options/utils.js
@@ -1,5 +1,6 @@
import Swal from "sweetalert2";
import { baseUrl } from "../../../../globals.js";
+import { sanitize } from "../../utils.js";
export const openProgressSwal = (options) => {
return new Promise((resolve) => {
@@ -26,6 +27,9 @@ export const run = async (url, payload, options = {}) => {
if (!("base" in options)) options.base = "/neuroconv";
+ // Clear private keys from being passed
+ payload = sanitize(structuredClone(payload));
+
const results = await fetch(`${baseUrl}${options.base || ""}/${url}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
diff --git a/src/renderer/src/stories/pages/settings/SettingsPage.js b/src/renderer/src/stories/pages/settings/SettingsPage.js
index 0b3b47395..749f6368a 100644
--- a/src/renderer/src/stories/pages/settings/SettingsPage.js
+++ b/src/renderer/src/stories/pages/settings/SettingsPage.js
@@ -12,6 +12,7 @@ const schema = {
output_locations: projectGlobalSchema,
DANDI: dandiGlobalSchema,
},
+ required: ["output_locations", "DANDI"],
};
import { Button } from "../../Button.js";
@@ -61,14 +62,12 @@ export class SettingsPage extends Page {
};
render() {
-
this.localState = structuredClone(global.data);
// NOTE: API Keys and Dandiset IDs persist across selected project
this.form = new JSONSchemaForm({
results: this.localState,
schema,
- mode: "accordion",
onUpdate: () => (this.unsavedUpdates = true),
validateOnChange: async (name, parent) => {
const value = parent[name];
diff --git a/src/renderer/src/stories/pages/uploads/UploadsPage.js b/src/renderer/src/stories/pages/uploads/UploadsPage.js
index 03b4e2a9e..0df8220a9 100644
--- a/src/renderer/src/stories/pages/uploads/UploadsPage.js
+++ b/src/renderer/src/stories/pages/uploads/UploadsPage.js
@@ -51,7 +51,7 @@ export async function uploadToDandi(info, type = "project" in info ? "project" :
const staging = isStaging(dandiset_id); // Automatically detect staging IDs
const whichAPIKey = staging ? "staging_api_key" : "main_api_key";
- const DANDI = global.data.DANDI
+ const DANDI = global.data.DANDI;
let api_key = DANDI?.api_keys?.[whichAPIKey];
const errors = await validateDANDIApiKey(api_key, staging);
@@ -66,7 +66,7 @@ export async function uploadToDandi(info, type = "project" in info ? "project" :
const input = new JSONSchemaInput({
path: [whichAPIKey],
- info: dandiGlobalSchema.properties.api_keys.properties[whichAPIKey]
+ info: dandiGlobalSchema.properties.api_keys.properties[whichAPIKey],
});
input.style.padding = "25px";
@@ -92,14 +92,17 @@ export async function uploadToDandi(info, type = "project" in info ? "project" :
const errors = await validateDANDIApiKey(input.value, staging);
if (!errors || !errors.length) {
modal.remove();
-
- merge({
- DANDI: {
- api_keys: {
- [whichAPIKey]: value
- }
- }
- }, global.data)
+
+ merge(
+ {
+ DANDI: {
+ api_keys: {
+ [whichAPIKey]: value,
+ },
+ },
+ },
+ global.data
+ );
global.save();
resolve(value);
@@ -118,7 +121,7 @@ export async function uploadToDandi(info, type = "project" in info ? "project" :
document.body.append(modal);
});
- console.log(api_key)
+ console.log(api_key);
}
const result = await run(
diff --git a/src/renderer/src/stories/pages/utils.js b/src/renderer/src/stories/pages/utils.js
index 59beb7be4..fd1d65e6e 100644
--- a/src/renderer/src/stories/pages/utils.js
+++ b/src/renderer/src/stories/pages/utils.js
@@ -27,12 +27,26 @@ export const setUndefinedIfNotDeclared = (schemaProps, resolved) => {
}
};
+export const isPrivate = (k, v) => k.slice(0, 2) === "__";
+
+export const sanitize = (o, condition = isPrivate) => {
+ if (isObject(o)) {
+ for (const [k, v] of Object.entries(o)) {
+ if (condition(k, v)) delete o[k];
+ else sanitize(v, condition);
+ }
+ }
+
+ return o;
+};
+
export function merge(toMerge = {}, target = {}, mergeOpts = {}) {
// Deep merge objects
for (const [k, v] of Object.entries(toMerge)) {
const targetV = target[k];
// if (isPrivate(k)) continue;
- if (mergeOpts.arrays && Array.isArray(v) && Array.isArray(targetV)) target[k] = [...targetV, ...v]; // Merge array entries together
+ if (mergeOpts.arrays && Array.isArray(v) && Array.isArray(targetV))
+ target[k] = [...targetV, ...v]; // Merge array entries together
else if (v === undefined) {
delete target[k]; // Remove matched values
// if (mergeOpts.remove !== false) delete target[k]; // Remove matched values
@@ -51,7 +65,7 @@ export function merge(toMerge = {}, target = {}, mergeOpts = {}) {
export function mapSessions(callback = (v) => v, globalState) {
return Object.entries(globalState.results)
.map(([subject, sessions]) => {
- return Object.entries(sessions).map(([session, info]) => callback({ subject, session, info }));
+ return Object.entries(sessions).map(([session, info], i) => callback({ subject, session, info }, i));
})
.flat(2);
}
diff --git a/src/renderer/src/stories/table/Cell.ts b/src/renderer/src/stories/table/Cell.ts
index e258e218f..2a25d577e 100644
--- a/src/renderer/src/stories/table/Cell.ts
+++ b/src/renderer/src/stories/table/Cell.ts
@@ -20,6 +20,8 @@ type TableCellProps = {
onValidate?: OnValidateFunction,
}
+const persistentInteraction = Symbol('persistentInteraction')
+
export class TableCell extends LitElement {
declare schema: TableCellProps['schema']
@@ -132,8 +134,9 @@ export class TableCell extends LitElement {
};
setInput(value: any) {
- this.interacted = true
- this.input.set(value) // Ensure all operations are undoable
+ this.interacted = persistentInteraction
+ if (this.input) this.input.set(value) // Ensure all operations are undoable
+ else this.#value = value // Silently set value if not rendered yet
}
#value
@@ -147,7 +150,7 @@ export class TableCell extends LitElement {
#cls: any
- interacted = false
+ interacted: boolean | symbol = false
// input = new TableCellBase({ })
@@ -157,7 +160,7 @@ export class TableCell extends LitElement {
let cls = TableCellBase
- this.interacted = false
+ this.interacted = this.interacted === persistentInteraction
if (this.schema.type === "array") cls = ArrayCell
else if (this.schema.format === "date-time") cls = DateTimeCell
diff --git a/tests/metadata.test.ts b/tests/metadata.test.ts
index 2b53a2fb2..b5dab4e9e 100644
--- a/tests/metadata.test.ts
+++ b/tests/metadata.test.ts
@@ -113,8 +113,7 @@ test('inter-table updates are triggered', async () => {
await form.rendered
// Validate that the results are incorrect
- let errors = false
- await form.validate().catch(e => errors = true)
+ const errors = await form.validate().catch(() => true).catch(e => true)
expect(errors).toBe(true) // Is invalid
// Update the table with the missing electrode group
@@ -123,13 +122,19 @@ test('inter-table updates are triggered', async () => {
const baseRow = table.getRow(0)
row.forEach((cell, i) => {
- if (cell.simpleTableInfo.col === 'name') cell.value = randomStringId // Set name to random string id
- else cell.value = baseRow[i].value // Otherwise carry over info
+ if (cell.simpleTableInfo.col === 'name') cell.setInput(randomStringId) // Set name to random string id
+ else cell.setInput(baseRow[i].value) // Otherwise carry over info
})
+ // Wait a second for new row values to resolve as table data (async)
+ await new Promise((res) => setTimeout(() => res(true), 1000))
+
// Validate that the new structure is correct
- await form.validate().then(res => errors = false).catch(e => errors = true)
- expect(errors).toBe(false) // Is valid
+ const hasErrors = await form.validate().then(() => false).catch((e) => {
+ console.error(e)
+ return true
+ })
+ expect(hasErrors).toBe(false) // Is valid
})
@@ -193,6 +198,7 @@ test('changes are resolved correctly', async () => {
input3.updateData('test')
// Validate that the new structure is correct
- await form.validate(form.results).then(res => errors = false).catch(e => errors = true)
- expect(errors).toBe(false) // Is valid
+ const hasErrors = await form.validate(form.results).then(res => false).catch(e => true)
+
+ expect(hasErrors).toBe(false) // Is valid
})