Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Global metadata on demand #483

Merged
merged 20 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9babca5
Add global metadata editor for source data
garrettmflynn Oct 27, 2023
d71691f
Proper global metadata flow for Subject table and Source Data form
garrettmflynn Oct 30, 2023
c34612f
Add message to triage inspector
garrettmflynn Oct 30, 2023
0f00309
Add for File Metadata page
garrettmflynn Oct 30, 2023
2bc8ce8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 30, 2023
2f89e7b
Merge branch 'main' into global-metadata-on-demand
garrettmflynn Oct 30, 2023
1b04bba
Fix merge and simplify loose requirements
garrettmflynn Oct 30, 2023
27a640d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 30, 2023
68d2992
Merge branch 'main' into global-metadata-on-demand
garrettmflynn Oct 30, 2023
429057b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 30, 2023
69ba80c
Fix schema used
garrettmflynn Oct 30, 2023
9ee1420
Merge branch 'global-metadata-on-demand' of https://github.com/Neurod…
garrettmflynn Oct 30, 2023
54bdc07
Merge branch 'main' into global-metadata-on-demand
CodyCBakerPhD Oct 31, 2023
26bfa99
Fix popup
garrettmflynn Oct 31, 2023
ba2c279
Merge branch 'main' into global-metadata-on-demand
garrettmflynn Oct 31, 2023
d47c1ff
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 31, 2023
2cb04b6
Merge branch 'main' into global-metadata-on-demand
garrettmflynn Oct 31, 2023
bdfbf17
Update metadata.test.ts
garrettmflynn Oct 31, 2023
a06754a
Update metadata.test.ts
garrettmflynn Oct 31, 2023
a1a5700
Update Table.js
garrettmflynn Oct 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading