Skip to content

Commit

Permalink
Merge pull request #483 from NeurodataWithoutBorders/global-metadata-…
Browse files Browse the repository at this point in the history
…on-demand

Global metadata on demand
  • Loading branch information
CodyCBakerPhD authored Nov 1, 2023
2 parents 8161005 + a1a5700 commit da7b93f
Show file tree
Hide file tree
Showing 17 changed files with 416 additions and 116 deletions.
27 changes: 27 additions & 0 deletions schemas/base-metadata.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,31 @@ export const preprocessMetadataSchema = (schema: any = 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(preprocessMetadataSchema());
Object.entries(globalSchema.properties).forEach(([globalProp, schema]) => {
instanceSpecificFields[globalProp]?.forEach((prop) => delete schema.properties[prop]);
});

export {
globalSchema
}

export default preprocessMetadataSchema()
2 changes: 0 additions & 2 deletions schemas/json/base_metadata_schema.json
Original file line number Diff line number Diff line change
@@ -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"],
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/stories/BasicTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
102 changes: 75 additions & 27 deletions src/renderer/src/stories/JSONSchemaForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,12 @@ hr {
.required label:after {
content: " *";
color: #ff0033;
}
:host([requirementmode="loose"]) .required label:after {
:host(:not([validateemptyvalues])) .required label:after {
color: gray;
}
Expand Down Expand Up @@ -163,7 +165,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 },
};
}

Expand All @@ -184,6 +187,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();

Expand All @@ -195,6 +200,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;
Expand All @@ -203,7 +210,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;

Expand All @@ -219,6 +225,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;

Expand Down Expand Up @@ -258,6 +265,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];
Expand All @@ -269,12 +282,25 @@ 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);
}
} else resolvedParent[name] = undefined;

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;
}

Expand Down Expand Up @@ -317,7 +343,7 @@ export class JSONSchemaForm extends LitElement {
validate = async (resolved) => {
// Check if any required inputs are missing
const requiredButNotSpecified = await this.#validateRequirements(resolved); // get missing required paths
const isValid = this.requirementMode === "loose" ? true : !requiredButNotSpecified.length;
const isValid = !requiredButNotSpecified.length;

// Print out a detailed error message if any inputs are missing
let message = isValid ? "" : `${requiredButNotSpecified.length} required inputs are not specified properly.`;
Expand Down Expand Up @@ -366,7 +392,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) => {
Expand Down Expand Up @@ -482,7 +511,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]) && this.validateEmptyValues) invalid.push(path);
}
}

Expand All @@ -493,14 +522,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 ?? {});
Expand Down Expand Up @@ -594,21 +624,26 @@ 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, path = [], checkLinks = true) => {
const parent = this.#get(path, this.resolved);

const pathToValidate = [...(this.base ?? []), ...path];

const valid =
!this.validateEmptyValues && !(name in parent)
!this.validateEmptyValues && parent[name] === undefined
? true
: await this.validateOnChange(name, parent, pathToValidate);

const localPath = [...path, name]; // Use basePath to augment the validation
const externalPath = [...this.base, name];

const isRequired = this.#isRequired(localPath);

let warnings = Array.isArray(valid)
? valid.filter((info) => info.type === "warning" && (!isRequired || !info.missing))
: [];
Expand All @@ -635,15 +670,22 @@ 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]) {
// Skip simple required checks in loose mode
if (this.requirementMode !== "loose") {
const schema = this.getSchema(localPath);
if (!errors.length && isRequired && this.isUndefined(parent[name])) {
const schema = this.getSchema(localPath);

// 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,
}); // Throw at least a basic error if the property is required
});
} else {
warnings.push({
message: `${schema.title ?? header(name)} is a suggested property.`,
type: "warning",
missing: true,
});
}
}
}
Expand Down Expand Up @@ -805,6 +847,8 @@ export class JSONSchemaForm extends LitElement {
results: { ...results[name] },
globals: this.globals?.[name],

states: this.states,

mode: this.mode,

onUpdate: (internalPath, value) => {
Expand Down Expand Up @@ -833,15 +877,19 @@ export class JSONSchemaForm extends LitElement {
this.checkAllLoaded();
},
renderTable: (...args) => this.renderTable(...args),
onOverride: (...args) => this.onOverride(...args),
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];

const accordion = new Accordion({
sections: {
[headerName]: {
subtitle: `${this.#getRenderable(info, required[name], localPath, true).length} fields`,
content: this.#nestedForms[name],
},
[headerName]: this.states[headerName],
},
});

Expand Down
8 changes: 7 additions & 1 deletion src/renderer/src/stories/JSONSchemaInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ export class JSONSchemaInput extends LitElement {

const el = this.getElement();
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;
Expand Down Expand Up @@ -311,7 +317,7 @@ export class JSONSchemaInput extends LitElement {
});

return html`
<div class="schema-input" @change=${() => validateOnChange && this.#triggerValidation(name, path)}>
<div class="schema-input list" @change=${() => validateOnChange && this.#triggerValidation(name, path)}>
${list} ${addButton}
</div>
`;
Expand Down
8 changes: 4 additions & 4 deletions src/renderer/src/stories/SimpleTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export class SimpleTable extends LitElement {
constructor({
schema,
data,
template,
globals,
keyColumn,
validateOnChange,
validateEmptyCells,
Expand All @@ -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 ?? "";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
Loading

0 comments on commit da7b93f

Please sign in to comment.