Skip to content

Commit 2fde793

Browse files
garrettmflynnpre-commit-ci[bot]CodyCBakerPhD
authored
Pop-Up Form Fixes + Test (#600)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Cody Baker <[email protected]>
1 parent d5f5c8b commit 2fde793

File tree

5 files changed

+244
-113
lines changed

5 files changed

+244
-113
lines changed

schemas/json/base_metadata_schema.json

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"required": ["session_start_time"],
1212
"properties": {
1313
"keywords": {
14+
"title": "Keyword",
1415
"description": "Terms to search over",
1516
"type": "array",
1617
"items": {
@@ -30,20 +31,24 @@
3031
"description": "Name of person/people who performed experiment",
3132
"type": "array",
3233
"items": {
34+
"title": "Experimenter",
3335
"type": "string",
3436
"format": "{last_name}, {first_name} {middle_name_or_initial}",
3537
"properties": {
3638
"first_name": {
37-
"pattern": "^[A-Z][a-z,'-]+$",
38-
"type": "string"
39+
"pattern": "^[\\p{L}\\s\\-\\.']+$",
40+
"type": "string",
41+
"flags": "u"
3942
},
4043
"last_name": {
41-
"pattern": "^[A-Z][a-z,'-]+$",
42-
"type": "string"
44+
"pattern": "^[\\p{L}\\s\\-\\.']+$",
45+
"type": "string",
46+
"flags": "u"
4347
},
4448
"middle_name_or_initial": {
45-
"pattern": "^[A-Z][a-z.'-]*$",
46-
"type": "string"
49+
"pattern": "^[\\p{L}\\s\\-\\.']+$",
50+
"type": "string",
51+
"flags": "u"
4752
}
4853
},
4954
"required": [
@@ -89,6 +94,7 @@
8994
"type": "array",
9095
"description": "Provide a DOI for each publication.",
9196
"items": {
97+
"title": "Related Publication",
9298
"type": "string",
9399
"format": "{doi}",
94100
"properties": {

src/renderer/src/stories/JSONSchemaForm.js

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export class JSONSchemaForm extends LitElement {
190190
}
191191

192192
base = [];
193-
#nestedForms = {};
193+
forms = {};
194194
inputs = [];
195195

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

271-
const form = this.#nestedForms[name]; // Check forms
271+
const form = this.forms[name]; // Check forms
272272
if (!form) {
273273
const table = this.tables[name]; // Check tables
274274
if (table && tables) return table; // Skip table cells
@@ -363,7 +363,7 @@ export class JSONSchemaForm extends LitElement {
363363
status;
364364
checkStatus = () => {
365365
checkStatus.call(this, this.#nWarnings, this.#nErrors, [
366-
...Object.entries(this.#nestedForms)
366+
...Object.entries(this.forms)
367367
.filter(([k, v]) => {
368368
const accordion = this.#accordions[k];
369369
return !accordion || !accordion.disabled;
@@ -382,7 +382,7 @@ export class JSONSchemaForm extends LitElement {
382382
return validator
383383
.validate(resolved, schema)
384384
.errors.map((e) => {
385-
const propName = e.path.slice(-1)[0] ?? name ?? e.property;
385+
const propName = e.path.slice(-1)[0] ?? name ?? (e.property === "instance" ? "Form" : e.property);
386386
const rowName = e.path.slice(-2)[0];
387387

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

392392
// ------------ Exclude Certain Errors ------------
393393

394+
// Allow for constructing types from object types
395+
if (e.message.includes("is not of a type(s)") && "properties" in schema && schema.type === "string")
396+
return;
397+
394398
// Ignore required errors if value is empty
395399
if (e.name === "required" && !this.validateEmptyValues && !(e.property in e.instance)) return;
396400

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

480484
// Validate nested forms (skip disabled)
481-
for (let name in this.#nestedForms) {
485+
for (let name in this.forms) {
482486
const accordion = this.#accordions[name];
483487
if (!accordion || !accordion.disabled)
484-
await this.#nestedForms[name].validate(resolved ? resolved[name] : undefined); // Validate nested forms too
488+
await this.forms[name].validate(resolved ? resolved[name] : undefined); // Validate nested forms too
485489
}
486490

487491
for (let key in this.tables) {
@@ -510,10 +514,16 @@ export class JSONSchemaForm extends LitElement {
510514
else {
511515
const level1 = acc?.[skipped.find((str) => acc[str])];
512516
if (level1) {
517+
// Handle items-like objects
518+
const result = this.#get(path.slice(i), level1, omitted, skipped);
519+
if (result) return result;
520+
521+
// Handle pattern properties objects
513522
const got = Object.keys(level1).find((key) => {
514523
const result = this.#get(path.slice(i + 1), level1[key], omitted, skipped);
515-
return result;
524+
if (result && typeof result === "object") return result; // Schema are objects...
516525
});
526+
517527
if (got) return level1[got];
518528
}
519529
}
@@ -550,7 +560,7 @@ export class JSONSchemaForm extends LitElement {
550560
}
551561

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

556566
return resolved;
@@ -596,16 +606,6 @@ export class JSONSchemaForm extends LitElement {
596606
});
597607

598608
this.inputs[localPath.join("-")] = interactiveInput;
599-
// this.validateEmptyValues ? undefined : (el) => (el.value ?? el.checked) !== ""
600-
601-
// const possibleInputs = Array.from(this.shadowRoot.querySelectorAll("jsonschema-input")).map(input => input.children)
602-
// const inputs = possibleInputs.filter(el => el instanceof HTMLElement);
603-
// const fileInputs = Array.from(this.shadowRoot.querySelectorAll("filesystem-selector") ?? []);
604-
// const allInputs = [...inputs, ...fileInputs];
605-
// const filtered = filter ? allInputs.filter(filter) : allInputs;
606-
// filtered.forEach((input) => input.dispatchEvent(new Event("change")));
607-
608-
// console.log(interactiveInput)
609609

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

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

887887
// Validate Regex Pattern automatically
888888
else if (schema.pattern) {
889-
const regex = new RegExp(schema.pattern);
889+
const regex = new RegExp(schema.pattern, schema.flags);
890890
if (!regex.test(parent[name])) {
891891
errors.push({
892-
message: `${schema.title ?? header(name)} does not match the required pattern (${
893-
schema.pattern
894-
}).`,
892+
message: `${schema.title ?? header(name)} does not match the required pattern (${regex}).`,
895893
type: "error",
896894
});
897895
}
@@ -1105,7 +1103,7 @@ export class JSONSchemaForm extends LitElement {
11051103
const ignore = getIgnore(this.ignore, name);
11061104

11071105
const ogContext = this;
1108-
const nested = (this.#nestedForms[name] = new JSONSchemaForm({
1106+
const nested = (this.forms[name] = new JSONSchemaForm({
11091107
identifier: this.identifier,
11101108
schema: info,
11111109
results: { ...nestedResults },
@@ -1189,7 +1187,7 @@ export class JSONSchemaForm extends LitElement {
11891187
subtitle: html`<div style="display:flex; align-items: center;">
11901188
${explicitlyRequired ? "" : enableToggleContainer}
11911189
</div>`,
1192-
content: this.#nestedForms[name],
1190+
content: this.forms[name],
11931191

11941192
// States
11951193
open: oldStates?.open ?? !hasMany,
@@ -1329,9 +1327,7 @@ export class JSONSchemaForm extends LitElement {
13291327
// Check if everything is internally rendered
13301328
get rendered() {
13311329
const isRendered = resolve(this.#rendered, () =>
1332-
Promise.all(
1333-
[...Object.values(this.#nestedForms), ...Object.values(this.tables)].map(({ rendered }) => rendered)
1334-
)
1330+
Promise.all([...Object.values(this.forms), ...Object.values(this.tables)].map(({ rendered }) => rendered))
13351331
);
13361332
return isRendered;
13371333
}

src/renderer/src/stories/JSONSchemaInput.js

Lines changed: 44 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,7 @@ export class JSONSchemaInput extends LitElement {
573573
return this.onValidate
574574
? this.onValidate()
575575
: this.form?.triggerValidation
576-
? this.form.triggerValidation(name, path, this)
576+
? this.form.triggerValidation(name, path, undefined, this)
577577
: "";
578578
};
579579

@@ -639,14 +639,13 @@ export class JSONSchemaInput extends LitElement {
639639
new Button({
640640
label: "Edit",
641641
size: "small",
642-
onClick: () => {
642+
onClick: () =>
643643
this.#createModal({
644644
key,
645645
schema: isAdditionalProperties(this.pattern) ? undefined : schema,
646646
results: value,
647647
list: list ?? this.#list,
648-
});
649-
},
648+
}),
650649
}),
651650
],
652651
};
@@ -659,28 +658,25 @@ export class JSONSchemaInput extends LitElement {
659658
})
660659
: [];
661660
}
662-
663-
return items;
664661
}
665662

666-
#schemaElement;
667663
#modal;
668664

669-
async #createModal({ key, schema = {}, results, list } = {}) {
670-
const createNewObject = !results;
665+
#createModal({ key, schema = {}, results, list, label } = {}) {
666+
const schemaCopy = structuredClone(schema);
667+
668+
const createNewObject = !results && (schemaCopy.type === "object" || schemaCopy.properties);
671669

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

676674
const allowPatternProperties = isPatternProperties(this.pattern);
677675
const allowAdditionalProperties = isAdditionalProperties(this.pattern);
678-
const creatNewPatternProperty = allowPatternProperties && createNewObject;
679-
680-
const schemaCopy = structuredClone(schema);
676+
const createNewPatternProperty = allowPatternProperties && createNewObject;
681677

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

698-
const updateTarget = results ?? {};
694+
const isObject = schemaCopy.type === "object" || schemaCopy.properties; // NOTE: For formatted strings, this is not an object
695+
696+
// NOTE: Will be replaced by single instances
697+
let updateTarget = results ?? (isObject ? {} : undefined);
699698

700-
submitButton.addEventListener("click", async () => {
701-
if (this.#schemaElement instanceof JSONSchemaForm) await this.#schemaElement.validate();
699+
submitButton.onClick = async () => {
700+
await nestedModalElement.validate();
702701

703702
let value = updateTarget;
704703

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

715714
// Add to the list
716-
if (createNewObject) {
717-
if (creatNewPatternProperty) {
718-
const key = value.__;
719-
delete value.__;
720-
list.add({ key, value });
721-
} else list.add({ key, value });
722-
} else list.requestUpdate();
715+
if (createNewPatternProperty) {
716+
const key = value.__;
717+
delete value.__;
718+
list.add({ key, value });
719+
} else list.add({ key, value });
723720

724721
this.#modal.toggle(false);
725-
});
722+
};
726723

727724
this.#modal = new Modal({
728-
header: key ? header(key) : "Property Definition",
725+
header: label ? `${header(label)} Editor` : key ? header(key) : `Property Editor`,
729726
footer: submitButton,
730727
showCloseButton: createNewObject,
731728
});
732729

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

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

738-
this.#schemaElement = isObject
735+
const nestedModalElement = isObject
739736
? new JSONSchemaForm({
740737
schema: schemaCopy,
741738
results: updateTarget,
@@ -748,26 +745,34 @@ export class JSONSchemaInput extends LitElement {
748745
renderTable: this.renderTable,
749746
onThrow: this.#onThrow,
750747
})
751-
: new JSONSchemaInput({
752-
schema: schemaCopy,
753-
validateOnChange: allowAdditionalProperties,
754-
path: this.path,
755-
form: this.form,
756-
value: updateTarget,
757-
renderTable: this.renderTable,
758-
onUpdate: (value) => {
748+
: new JSONSchemaForm({
749+
schema: {
750+
properties: {
751+
[tempPropertyKey]: {
752+
...schemaCopy,
753+
title: inputTitle,
754+
},
755+
},
756+
required: [tempPropertyKey],
757+
},
758+
results: updateTarget,
759+
onUpdate: (_, value) => {
759760
if (createNewObject) updateTarget[key] = value;
760-
else this.#updateData(key, value); // NOTE: Untested
761+
else updateTarget = value;
761762
},
763+
// renderTable: this.renderTable,
764+
// onThrow: this.#onThrow,
762765
});
763766

764-
div.append(this.#schemaElement);
767+
div.append(nestedModalElement);
765768

766769
this.#modal.append(div);
767770

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

770773
setTimeout(() => this.#modal.toggle(true));
774+
775+
return this.#modal;
771776
}
772777

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

940-
addButton.addEventListener("click", () => {
941-
this.#createModal({ list, schema: allowPatternProperties ? schema : itemSchema });
942-
});
945+
addButton.onClick = () =>
946+
this.#createModal({ label: name, list, schema: allowPatternProperties ? schema : itemSchema });
943947

944948
return html`
945949
<div class="schema-input list" @change=${() => validateOnChange && this.#triggerValidation(name, path)}>

src/renderer/src/validation/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ export async function validateOnChange(name, parent, path, value) {
3131
// let overridden = false;
3232
let lastWildcard;
3333
toIterate.reduce((acc, key) => {
34-
if (acc && "*" in acc) {
34+
// Disable the value is a hardcoded list of functions + a wildcard has already been specified
35+
if (acc && lastWildcard && Array.isArray(acc[key] ?? {})) overridden = true;
36+
else if (acc && "*" in acc) {
3537
if (acc["*"] === false && lastWildcard)
3638
overridden = true; // Disable if false and a wildcard has already been specified
3739
// Otherwise set the last wildcard

0 commit comments

Comments
 (0)