Skip to content

Commit

Permalink
Pop-Up Form Fixes + Test (#600)
Browse files Browse the repository at this point in the history
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Cody Baker <[email protected]>
  • Loading branch information
3 people authored Feb 7, 2024
1 parent d5f5c8b commit 2fde793
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 113 deletions.
18 changes: 12 additions & 6 deletions schemas/json/base_metadata_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"required": ["session_start_time"],
"properties": {
"keywords": {
"title": "Keyword",
"description": "Terms to search over",
"type": "array",
"items": {
Expand All @@ -30,20 +31,24 @@
"description": "Name of person/people who performed experiment",
"type": "array",
"items": {
"title": "Experimenter",
"type": "string",
"format": "{last_name}, {first_name} {middle_name_or_initial}",
"properties": {
"first_name": {
"pattern": "^[A-Z][a-z,'-]+$",
"type": "string"
"pattern": "^[\\p{L}\\s\\-\\.']+$",
"type": "string",
"flags": "u"
},
"last_name": {
"pattern": "^[A-Z][a-z,'-]+$",
"type": "string"
"pattern": "^[\\p{L}\\s\\-\\.']+$",
"type": "string",
"flags": "u"
},
"middle_name_or_initial": {
"pattern": "^[A-Z][a-z.'-]*$",
"type": "string"
"pattern": "^[\\p{L}\\s\\-\\.']+$",
"type": "string",
"flags": "u"
}
},
"required": [
Expand Down Expand Up @@ -89,6 +94,7 @@
"type": "array",
"description": "Provide a DOI for each publication.",
"items": {
"title": "Related Publication",
"type": "string",
"format": "{doi}",
"properties": {
Expand Down
52 changes: 24 additions & 28 deletions src/renderer/src/stories/JSONSchemaForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export class JSONSchemaForm extends LitElement {
}

base = [];
#nestedForms = {};
forms = {};
inputs = [];

tables = {};
Expand Down Expand Up @@ -268,7 +268,7 @@ export class JSONSchemaForm extends LitElement {
const name = path[0];
const updatedPath = path.slice(1);

const form = this.#nestedForms[name]; // Check forms
const form = this.forms[name]; // Check forms
if (!form) {
const table = this.tables[name]; // Check tables
if (table && tables) return table; // Skip table cells
Expand Down Expand Up @@ -363,7 +363,7 @@ export class JSONSchemaForm extends LitElement {
status;
checkStatus = () => {
checkStatus.call(this, this.#nWarnings, this.#nErrors, [
...Object.entries(this.#nestedForms)
...Object.entries(this.forms)
.filter(([k, v]) => {
const accordion = this.#accordions[k];
return !accordion || !accordion.disabled;
Expand All @@ -382,7 +382,7 @@ export class JSONSchemaForm extends LitElement {
return validator
.validate(resolved, schema)
.errors.map((e) => {
const propName = e.path.slice(-1)[0] ?? name ?? e.property;
const propName = e.path.slice(-1)[0] ?? name ?? (e.property === "instance" ? "Form" : e.property);
const rowName = e.path.slice(-2)[0];

const isRow = typeof rowName === "number";
Expand All @@ -391,6 +391,10 @@ export class JSONSchemaForm extends LitElement {

// ------------ Exclude Certain Errors ------------

// Allow for constructing types from object types
if (e.message.includes("is not of a type(s)") && "properties" in schema && schema.type === "string")
return;

// Ignore required errors if value is empty
if (e.name === "required" && !this.validateEmptyValues && !(e.property in e.instance)) return;

Expand Down Expand Up @@ -478,10 +482,10 @@ export class JSONSchemaForm extends LitElement {
if (message) this.throw(message);

// Validate nested forms (skip disabled)
for (let name in this.#nestedForms) {
for (let name in this.forms) {
const accordion = this.#accordions[name];
if (!accordion || !accordion.disabled)
await this.#nestedForms[name].validate(resolved ? resolved[name] : undefined); // Validate nested forms too
await this.forms[name].validate(resolved ? resolved[name] : undefined); // Validate nested forms too
}

for (let key in this.tables) {
Expand Down Expand Up @@ -510,10 +514,16 @@ export class JSONSchemaForm extends LitElement {
else {
const level1 = acc?.[skipped.find((str) => acc[str])];
if (level1) {
// Handle items-like objects
const result = this.#get(path.slice(i), level1, omitted, skipped);
if (result) return result;

// Handle pattern properties objects
const got = Object.keys(level1).find((key) => {
const result = this.#get(path.slice(i + 1), level1[key], omitted, skipped);
return result;
if (result && typeof result === "object") return result; // Schema are objects...
});

if (got) return level1[got];
}
}
Expand Down Expand Up @@ -550,7 +560,7 @@ export class JSONSchemaForm extends LitElement {
}

// NOTE: Refs are now pre-resolved
const resolved = this.#get(path, schema, ["properties", "patternProperties"], ["patternProperties"]);
const resolved = this.#get(path, schema, ["properties", "patternProperties"], ["patternProperties", "items"]);
// if (resolved?.["$ref"]) return this.getSchema(resolved["$ref"].split("/").slice(1)); // NOTE: This assumes reference to the root of the schema

return resolved;
Expand Down Expand Up @@ -596,16 +606,6 @@ export class JSONSchemaForm extends LitElement {
});

this.inputs[localPath.join("-")] = interactiveInput;
// this.validateEmptyValues ? undefined : (el) => (el.value ?? el.checked) !== ""

// const possibleInputs = Array.from(this.shadowRoot.querySelectorAll("jsonschema-input")).map(input => input.children)
// const inputs = possibleInputs.filter(el => el instanceof HTMLElement);
// const fileInputs = Array.from(this.shadowRoot.querySelectorAll("filesystem-selector") ?? []);
// const allInputs = [...inputs, ...fileInputs];
// const filtered = filter ? allInputs.filter(filter) : allInputs;
// filtered.forEach((input) => input.dispatchEvent(new Event("change")));

// console.log(interactiveInput)

return html`
<div id=${encode(localPath.join("-"))} class="form-section">
Expand All @@ -625,7 +625,7 @@ export class JSONSchemaForm extends LitElement {
nLoaded = 0;

checkAllLoaded = () => {
const expected = [...Object.keys(this.#nestedForms), ...Object.keys(this.tables)].length;
const expected = [...Object.keys(this.forms), ...Object.keys(this.tables)].length;
if (this.nLoaded === expected) {
this.#loaded = true;
this.onLoaded();
Expand Down Expand Up @@ -886,12 +886,10 @@ export class JSONSchemaForm extends LitElement {

// Validate Regex Pattern automatically
else if (schema.pattern) {
const regex = new RegExp(schema.pattern);
const regex = new RegExp(schema.pattern, schema.flags);
if (!regex.test(parent[name])) {
errors.push({
message: `${schema.title ?? header(name)} does not match the required pattern (${
schema.pattern
}).`,
message: `${schema.title ?? header(name)} does not match the required pattern (${regex}).`,
type: "error",
});
}
Expand Down Expand Up @@ -1105,7 +1103,7 @@ export class JSONSchemaForm extends LitElement {
const ignore = getIgnore(this.ignore, name);

const ogContext = this;
const nested = (this.#nestedForms[name] = new JSONSchemaForm({
const nested = (this.forms[name] = new JSONSchemaForm({
identifier: this.identifier,
schema: info,
results: { ...nestedResults },
Expand Down Expand Up @@ -1189,7 +1187,7 @@ export class JSONSchemaForm extends LitElement {
subtitle: html`<div style="display:flex; align-items: center;">
${explicitlyRequired ? "" : enableToggleContainer}
</div>`,
content: this.#nestedForms[name],
content: this.forms[name],

// States
open: oldStates?.open ?? !hasMany,
Expand Down Expand Up @@ -1329,9 +1327,7 @@ export class JSONSchemaForm extends LitElement {
// Check if everything is internally rendered
get rendered() {
const isRendered = resolve(this.#rendered, () =>
Promise.all(
[...Object.values(this.#nestedForms), ...Object.values(this.tables)].map(({ rendered }) => rendered)
)
Promise.all([...Object.values(this.forms), ...Object.values(this.tables)].map(({ rendered }) => rendered))
);
return isRendered;
}
Expand Down
84 changes: 44 additions & 40 deletions src/renderer/src/stories/JSONSchemaInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ export class JSONSchemaInput extends LitElement {
return this.onValidate
? this.onValidate()
: this.form?.triggerValidation
? this.form.triggerValidation(name, path, this)
? this.form.triggerValidation(name, path, undefined, this)
: "";
};

Expand Down Expand Up @@ -639,14 +639,13 @@ export class JSONSchemaInput extends LitElement {
new Button({
label: "Edit",
size: "small",
onClick: () => {
onClick: () =>
this.#createModal({
key,
schema: isAdditionalProperties(this.pattern) ? undefined : schema,
results: value,
list: list ?? this.#list,
});
},
}),
}),
],
};
Expand All @@ -659,28 +658,25 @@ export class JSONSchemaInput extends LitElement {
})
: [];
}

return items;
}

#schemaElement;
#modal;

async #createModal({ key, schema = {}, results, list } = {}) {
const createNewObject = !results;
#createModal({ key, schema = {}, results, list, label } = {}) {
const schemaCopy = structuredClone(schema);

const createNewObject = !results && (schemaCopy.type === "object" || schemaCopy.properties);

// const schemaProperties = Object.keys(schema.properties ?? {});
// const additionalProperties = Object.keys(results).filter((key) => !schemaProperties.includes(key));
// // const additionalElement = html`<label class="guided--form-label">Additional Properties</label><small>Cannot edit additional properties (${additionalProperties}) at this time</small>`

const allowPatternProperties = isPatternProperties(this.pattern);
const allowAdditionalProperties = isAdditionalProperties(this.pattern);
const creatNewPatternProperty = allowPatternProperties && createNewObject;

const schemaCopy = structuredClone(schema);
const createNewPatternProperty = allowPatternProperties && createNewObject;

// Add a property name entry to the schema
if (creatNewPatternProperty) {
if (createNewPatternProperty) {
schemaCopy.properties = {
__: { title: "Property Name", type: "string", pattern: this.pattern },
...schemaCopy.properties,
Expand All @@ -695,10 +691,13 @@ export class JSONSchemaInput extends LitElement {
primary: true,
});

const updateTarget = results ?? {};
const isObject = schemaCopy.type === "object" || schemaCopy.properties; // NOTE: For formatted strings, this is not an object

// NOTE: Will be replaced by single instances
let updateTarget = results ?? (isObject ? {} : undefined);

submitButton.addEventListener("click", async () => {
if (this.#schemaElement instanceof JSONSchemaForm) await this.#schemaElement.validate();
submitButton.onClick = async () => {
await nestedModalElement.validate();

let value = updateTarget;

Expand All @@ -713,29 +712,27 @@ export class JSONSchemaInput extends LitElement {
return this.#modal.toggle(false);

// Add to the list
if (createNewObject) {
if (creatNewPatternProperty) {
const key = value.__;
delete value.__;
list.add({ key, value });
} else list.add({ key, value });
} else list.requestUpdate();
if (createNewPatternProperty) {
const key = value.__;
delete value.__;
list.add({ key, value });
} else list.add({ key, value });

this.#modal.toggle(false);
});
};

this.#modal = new Modal({
header: key ? header(key) : "Property Definition",
header: label ? `${header(label)} Editor` : key ? header(key) : `Property Editor`,
footer: submitButton,
showCloseButton: createNewObject,
});

const div = document.createElement("div");
div.style.padding = "25px";

const isObject = schemaCopy.type === "object" || schemaCopy.properties; // NOTE: For formatted strings, this is not an object
const inputTitle = header(schemaCopy.title ?? label ?? "Value");

this.#schemaElement = isObject
const nestedModalElement = isObject
? new JSONSchemaForm({
schema: schemaCopy,
results: updateTarget,
Expand All @@ -748,26 +745,34 @@ export class JSONSchemaInput extends LitElement {
renderTable: this.renderTable,
onThrow: this.#onThrow,
})
: new JSONSchemaInput({
schema: schemaCopy,
validateOnChange: allowAdditionalProperties,
path: this.path,
form: this.form,
value: updateTarget,
renderTable: this.renderTable,
onUpdate: (value) => {
: new JSONSchemaForm({
schema: {
properties: {
[tempPropertyKey]: {
...schemaCopy,
title: inputTitle,
},
},
required: [tempPropertyKey],
},
results: updateTarget,
onUpdate: (_, value) => {
if (createNewObject) updateTarget[key] = value;
else this.#updateData(key, value); // NOTE: Untested
else updateTarget = value;
},
// renderTable: this.renderTable,
// onThrow: this.#onThrow,
});

div.append(this.#schemaElement);
div.append(nestedModalElement);

this.#modal.append(div);

document.body.append(this.#modal);

setTimeout(() => this.#modal.toggle(true));

return this.#modal;
}

#getType = (value = this.value) => (Array.isArray(value) ? "array" : typeof value);
Expand Down Expand Up @@ -937,9 +942,8 @@ export class JSONSchemaInput extends LitElement {
submessage: "They don't have a predictable structure.",
});

addButton.addEventListener("click", () => {
this.#createModal({ list, schema: allowPatternProperties ? schema : itemSchema });
});
addButton.onClick = () =>
this.#createModal({ label: name, list, schema: allowPatternProperties ? schema : itemSchema });

return html`
<div class="schema-input list" @change=${() => validateOnChange && this.#triggerValidation(name, path)}>
Expand Down
4 changes: 3 additions & 1 deletion src/renderer/src/validation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ export async function validateOnChange(name, parent, path, value) {
// let overridden = false;
let lastWildcard;
toIterate.reduce((acc, key) => {
if (acc && "*" in acc) {
// Disable the value is a hardcoded list of functions + a wildcard has already been specified
if (acc && lastWildcard && Array.isArray(acc[key] ?? {})) overridden = true;
else if (acc && "*" in acc) {
if (acc["*"] === false && lastWildcard)
overridden = true; // Disable if false and a wildcard has already been specified
// Otherwise set the last wildcard
Expand Down
Loading

0 comments on commit 2fde793

Please sign in to comment.