UI: Init dynamic rendering of module config
This commit is contained in:
7
pkgs/webview-ui/app/package-lock.json
generated
7
pkgs/webview-ui/app/package-lock.json
generated
@@ -15,6 +15,7 @@
|
|||||||
"@solidjs/router": "^0.14.2",
|
"@solidjs/router": "^0.14.2",
|
||||||
"@tanstack/eslint-plugin-query": "^5.51.12",
|
"@tanstack/eslint-plugin-query": "^5.51.12",
|
||||||
"@tanstack/solid-query": "^5.51.2",
|
"@tanstack/solid-query": "^5.51.2",
|
||||||
|
"@types/json-schema": "^7.0.15",
|
||||||
"material-icons": "^1.13.12",
|
"material-icons": "^1.13.12",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"solid-js": "^1.8.11",
|
"solid-js": "^1.8.11",
|
||||||
@@ -1782,6 +1783,12 @@
|
|||||||
"@types/unist": "^2"
|
"@types/unist": "^2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/json-schema": {
|
||||||
|
"version": "7.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/mdast": {
|
"node_modules/@types/mdast": {
|
||||||
"version": "3.0.15",
|
"version": "3.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz",
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
"@solidjs/router": "^0.14.2",
|
"@solidjs/router": "^0.14.2",
|
||||||
"@tanstack/eslint-plugin-query": "^5.51.12",
|
"@tanstack/eslint-plugin-query": "^5.51.12",
|
||||||
"@tanstack/solid-query": "^5.51.2",
|
"@tanstack/solid-query": "^5.51.2",
|
||||||
|
"@types/json-schema": "^7.0.15",
|
||||||
"material-icons": "^1.13.12",
|
"material-icons": "^1.13.12",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"solid-js": "^1.8.11",
|
"solid-js": "^1.8.11",
|
||||||
|
|||||||
@@ -3,10 +3,19 @@ import { activeURI } from "@/src/App";
|
|||||||
import { BackButton } from "@/src/components/BackButton";
|
import { BackButton } from "@/src/components/BackButton";
|
||||||
import { createModulesQuery } from "@/src/queries";
|
import { createModulesQuery } from "@/src/queries";
|
||||||
import { useParams } from "@solidjs/router";
|
import { useParams } from "@solidjs/router";
|
||||||
import { For, Match, Switch } from "solid-js";
|
import { createEffect, For, Match, Show, Switch } from "solid-js";
|
||||||
import { SolidMarkdown } from "solid-markdown";
|
import { SolidMarkdown } from "solid-markdown";
|
||||||
import toast from "solid-toast";
|
import toast from "solid-toast";
|
||||||
import { ModuleInfo } from "./list";
|
import { ModuleInfo } from "./list";
|
||||||
|
import { createQuery } from "@tanstack/solid-query";
|
||||||
|
import { JSONSchema4 } from "json-schema";
|
||||||
|
import { TextInput } from "@/src/components/TextInput";
|
||||||
|
import {
|
||||||
|
createForm,
|
||||||
|
getValue,
|
||||||
|
setValue,
|
||||||
|
SubmitHandler,
|
||||||
|
} from "@modular-forms/solid";
|
||||||
|
|
||||||
export const ModuleDetails = () => {
|
export const ModuleDetails = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -104,6 +113,191 @@ const Details = (props: DetailsProps) => {
|
|||||||
Add to Clan
|
Add to Clan
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<ModuleForm id={props.id} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ModuleSchemasType = Record<string, Record<string, JSONSchema4>>;
|
||||||
|
|
||||||
|
const Unsupported = (props: { schema: JSONSchema4; what: string }) => (
|
||||||
|
<div>
|
||||||
|
Cannot render {props.what}
|
||||||
|
<pre>
|
||||||
|
<code>{JSON.stringify(props.schema, null, 2)}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
function removeTrailingS(str: string) {
|
||||||
|
// Check if the last character is "s" or "S"
|
||||||
|
if (str.endsWith("s") || str.endsWith("S")) {
|
||||||
|
return str.slice(0, -1); // Remove the last character
|
||||||
|
}
|
||||||
|
return str; // Return unchanged if no trailing "s"
|
||||||
|
}
|
||||||
|
interface SchemaFormProps {
|
||||||
|
title: string;
|
||||||
|
schema: JSONSchema4;
|
||||||
|
path: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModuleForm = (props: { id: string }) => {
|
||||||
|
// TODO: Fetch the synced schema for all the modules at runtime
|
||||||
|
// We use static schema file at build time for now. (Different versions might have different schema at runtime)
|
||||||
|
const schemaQuery = createQuery(() => ({
|
||||||
|
queryKey: [activeURI(), "modules_schema"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const moduleSchema = await import(
|
||||||
|
"../../../api/modules_schemas.json"
|
||||||
|
).then((m) => m.default as ModuleSchemasType);
|
||||||
|
|
||||||
|
return moduleSchema;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log("Schema Query", schemaQuery.data?.[props.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [formStore, { Form, Field }] = createForm();
|
||||||
|
const handleSubmit: SubmitHandler<NonNullable<unknown>> = async (
|
||||||
|
values,
|
||||||
|
event,
|
||||||
|
) => {
|
||||||
|
console.log("Submitted form values", values);
|
||||||
|
};
|
||||||
|
const SchemaForm = (props: SchemaFormProps) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
fallback={<Unsupported what={"schema"} schema={props.schema} />}
|
||||||
|
>
|
||||||
|
<Match when={props.schema.type === "object"}>
|
||||||
|
<Switch
|
||||||
|
fallback={<Unsupported what={"object"} schema={props.schema} />}
|
||||||
|
>
|
||||||
|
<Match
|
||||||
|
when={
|
||||||
|
!props.schema.additionalProperties && props.schema.properties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(properties) => (
|
||||||
|
<For each={Object.entries(properties())}>
|
||||||
|
{([key, value]) => (
|
||||||
|
<SchemaForm
|
||||||
|
title={key}
|
||||||
|
schema={value}
|
||||||
|
path={[...props.path, key]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
)}
|
||||||
|
</Match>
|
||||||
|
<Match
|
||||||
|
when={
|
||||||
|
typeof props.schema.additionalProperties == "object" &&
|
||||||
|
props.schema.additionalProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(additionalProperties) => (
|
||||||
|
<>
|
||||||
|
<div>{props.title}</div>
|
||||||
|
{/* @ts-expect-error: We don't know the field names ahead of time */}
|
||||||
|
<Field name={props.title}>
|
||||||
|
{(f, p) => (
|
||||||
|
<>
|
||||||
|
<Show when={f.value}>
|
||||||
|
<For
|
||||||
|
each={Object.entries(
|
||||||
|
f.value as Record<string, unknown>,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{(v) => (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{removeTrailingS(props.title)}: {v[0]}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<SchemaForm
|
||||||
|
path={[...props.path, v[0]]}
|
||||||
|
schema={additionalProperties()}
|
||||||
|
title={v[0]}
|
||||||
|
/>{" "}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const value = getValue(formStore, props.title);
|
||||||
|
setValue(formStore, props.title, {
|
||||||
|
// @ts-expect-error: TODO: check to be an object
|
||||||
|
...value,
|
||||||
|
foo: {},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</Match>
|
||||||
|
<Match when={props.schema.type === "array"}>
|
||||||
|
TODO: Array field "{props.title}"
|
||||||
|
</Match>
|
||||||
|
<Match when={props.schema.type === "string"}>
|
||||||
|
{/* @ts-expect-error: We dont know the field names ahead of time */}
|
||||||
|
<Field name={props.path.join(".")}>
|
||||||
|
{(field, fieldProps) => (
|
||||||
|
<TextInput
|
||||||
|
formStore={formStore}
|
||||||
|
inputProps={fieldProps}
|
||||||
|
label={props.title}
|
||||||
|
// @ts-expect-error: It is a string, otherwise the json schema would be invalid
|
||||||
|
value={field.value ?? ""}
|
||||||
|
error={field.error}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="ModuleForm">
|
||||||
|
<Switch fallback={"No Schema found"}>
|
||||||
|
<Match when={schemaQuery.isLoading}>Loading...</Match>
|
||||||
|
<Match when={schemaQuery.data?.[props.id]}>
|
||||||
|
{(rolesSchemas) => (
|
||||||
|
<>
|
||||||
|
Configure this module
|
||||||
|
<For each={Object.entries(rolesSchemas())}>
|
||||||
|
{([role, schema]) => (
|
||||||
|
<div class="my-2">
|
||||||
|
<h4 class="text-xl">{role}</h4>
|
||||||
|
<Form onSubmit={handleSubmit}>
|
||||||
|
<SchemaForm title={role} schema={schema} path={[]} />
|
||||||
|
<br />
|
||||||
|
<button class="btn btn-primary">Save</button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user