Skip to content

Commit

Permalink
Path Expansion Autocomplete Improvements (#632)
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>
  • Loading branch information
garrettmflynn and pre-commit-ci[bot] authored Mar 8, 2024
1 parent 8e0589b commit 1109009
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 54 deletions.
93 changes: 49 additions & 44 deletions src/renderer/src/stories/JSONSchemaForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,47 @@ const encode = (str) => {
}
};

export const get = (path, object, omitted = [], skipped = []) => {
// path = path.slice(this.base.length); // Correct for base path
if (!path) throw new Error("Path not specified");
return path.reduce((acc, curr, i) => {
const tempAcc = acc?.[curr] ?? acc?.[omitted.find((str) => acc[str] && acc[str][curr])]?.[curr];
if (tempAcc) return tempAcc;
else {
const level1 = acc?.[skipped.find((str) => acc[str])];
if (level1) {
// Handle items-like objects
const result = get(path.slice(i), level1, omitted, skipped);
if (result) return result;

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

if (got) return level1[got];
}
}
}, object);
};

export const getSchema = (path, schema, base = []) => {
if (typeof path === "string") path = path.split(".");

// NOTE: Still must correct for the base here
if (base.length) {
const indexOf = path.indexOf(base.slice(-1)[0]);
if (indexOf !== -1) path = path.slice(indexOf + 1);
}

// NOTE: Refs are now pre-resolved
const resolved = 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;
};

const additionalPropPattern = "additional";

const templateNaNMessage = `<br/><small>Type <b>NaN</b> to represent an unknown value.</small>`;
Expand Down Expand Up @@ -235,7 +276,7 @@ export class JSONSchemaForm extends LitElement {

this.groups = props.groups ?? []; // NOTE: We assume properties only belong to one conditional requirement group

this.validateEmptyValues = props.validateEmptyValues ?? true;
this.validateEmptyValues = props.validateEmptyValues === undefined ? true : props.validateEmptyValues; // false = validate when not empty, true = always validate, null = never validate

if (props.onInvalid) this.onInvalid = props.onInvalid;
if (props.sort) this.sort = props.sort;
Expand Down Expand Up @@ -396,7 +437,7 @@ export class JSONSchemaForm extends LitElement {
return;

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

// Non-Strict Rule
if (schema.strict === false && e.message.includes("is not one of enum values")) return;
Expand All @@ -422,6 +463,8 @@ export class JSONSchemaForm extends LitElement {
};

validate = async (resolved = this.resolved) => {
if (this.validateEmptyValues === false) this.validateEmptyValues = true;

// Validate against the entire JSON Schema
const copy = structuredClone(resolved);
delete copy.__disabled;
Expand Down Expand Up @@ -505,30 +548,7 @@ export class JSONSchemaForm extends LitElement {
return true;
};

#get = (path, object = this.resolved, omitted = [], skipped = []) => {
// path = path.slice(this.base.length); // Correct for base path
if (!path) throw new Error("Path not specified");
return path.reduce((acc, curr, i) => {
const tempAcc = acc?.[curr] ?? acc?.[omitted.find((str) => acc[str] && acc[str][curr])]?.[curr];
if (tempAcc) return tempAcc;
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);
if (result && typeof result === "object") return result; // Schema are objects...
});

if (got) return level1[got];
}
}
}, object);
};
#get = (path, object = this.resolved, omitted = [], skipped = []) => get(path, object, omitted, skipped);

#checkRequiredAfterChange = async (localPath) => {
const path = [...localPath];
Expand All @@ -549,22 +569,7 @@ export class JSONSchemaForm extends LitElement {
return this.#schema;
}

getSchema(path, schema = this.schema) {
if (typeof path === "string") path = path.split(".");

// NOTE: Still must correct for the base here
if (this.base.length) {
const base = this.base.slice(-1)[0];
const indexOf = path.indexOf(base);
if (indexOf !== -1) path = path.slice(indexOf + 1);
}

// NOTE: Refs are now pre-resolved
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;
}
getSchema = (path, schema = this.schema) => getSchema(path, schema, this.base);

#renderInteractiveElement = (name, info, required, path = [], value, propertyType) => {
let isRequired = this.#isRequired([...path, name]);
Expand Down Expand Up @@ -652,7 +657,7 @@ export class JSONSchemaForm extends LitElement {
// 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 (this.isUndefined(resolved[name]) && this.validateEmptyValues !== null) invalid.push(path);
}
}

Expand Down Expand Up @@ -874,7 +879,7 @@ export class JSONSchemaForm extends LitElement {
type: "error",
missing: true,
});
} else {
} else if (this.validateEmptyValues === null) {
warnings.push({
message: `${schema.title ?? header(name)} is a suggested property.`,
type: "warning",
Expand Down
1 change: 1 addition & 0 deletions src/renderer/src/stories/JSONSchemaInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ export class JSONSchemaInput extends LitElement {
constructor(props) {
super();
Object.assign(this, props);
if (props.validateEmptyValue === false) this.validateEmptyValue = true; // False is treated as required but not triggered if empty
}

// onUpdate = () => {}
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/stories/forms/GlobalFormModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export function createFormModal ({
else removeProperties(schemaCopy.properties, propsToRemove)

const globalForm = new JSONSchemaForm({
validateEmptyValues: false,
validateEmptyValues: null,
schema: schemaCopy,
emptyMessage: "No properties to edit globally.",
onThrow,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { html } from "lit";
import { Page } from "../../Page.js";

// For Multi-Select Form
import { JSONSchemaForm } from "../../../JSONSchemaForm.js";
import { JSONSchemaForm, getSchema } from "../../../JSONSchemaForm.js";
import { OptionalSection } from "../../../OptionalSection.js";
import { run } from "../options/utils.js";
import { onThrow } from "../../../../errors";
Expand All @@ -25,6 +25,13 @@ export async function autocompleteFormatString(path) {

const { base_directory } = path.reduce((acc, key) => acc[key] ?? {}, this.form.resolved);

const schema = getSchema(path, this.info.globalState.schema.source_data);

const isFile = "file_path" in schema.properties;
const pathType = isFile ? "file" : "directory";

const description = isFile ? schema.properties.file_path.description : schema.properties.folder_path.description;

const notify = (message, type) => {
if (notification) this.dismiss(notification);
return (notification = this.notify(message, type));
Expand All @@ -48,14 +55,15 @@ export async function autocompleteFormatString(path) {

const propOrder = ["path", "subject_id", "session_id"];
const form = new JSONSchemaForm({
validateEmptyValues: false,
schema: {
type: "object",
properties: {
path: {
type: "string",
title: "Example Filesystem Entry",
format: ["file", "directory"],
description: "Provide an example filesystem entry for the selected interface",
title: `Example ${isFile ? "File" : "Folder"}`,
format: pathType,
description: description ?? `Provide an example ${pathType} for the selected interface`,
},
subject_id: {
type: "string",
Expand All @@ -73,6 +81,9 @@ export async function autocompleteFormatString(path) {
const value = parent[name];

if (name === "path") {
const toUpdate = ["subject_id", "session_id"];
toUpdate.forEach((key) => form.getFormElement([key]).requestUpdate());

if (value) {
if (fs.lstatSync(value).isSymbolicLink())
return [
Expand Down Expand Up @@ -122,7 +133,7 @@ export async function autocompleteFormatString(path) {

return new Promise((resolve) => {
const button = new Button({
label: "Create",
label: "Submit",
primary: true,
onClick: async () => {
await form.validate().catch((e) => {
Expand Down Expand Up @@ -432,7 +443,7 @@ export class GuidedPathExpansionPage extends Page {
const form = (this.form = new JSONSchemaForm({
...structureState,
onThrow,
validateEmptyValues: false,
validateEmptyValues: null,

controls,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export class GuidedNewDatasetPage extends Page {
this.form = new JSONSchemaForm({
schema,
results: this.state,
// validateEmptyValues: false,
// validateEmptyValues: null,
dialogOptions: {
properties: ["createDirectory"],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export class GuidedSubjectsPage extends Page {
const modal = (this.#globalModal = createGlobalFormModal.call(this, {
header: "Global Subject Metadata",
key: "Subject",
validateEmptyValues: false,
validateEmptyValues: null,
schema,
formProps: {
validateOnChange: (localPath, parent, path) => {
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/stories/preview/inspector/InspectorList.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ export class InspectorListItem extends LitElement {
border-radius: 10px;
overflow: hidden;
text-wrap: wrap;
padding: 25px;
padding: 10px;
font-size: 12px;
margin: 0 0 1em;
}
Expand Down

0 comments on commit 1109009

Please sign in to comment.