UI: Init dynamic rendering of module config

This commit is contained in:
Johannes Kirschbauer
2024-10-18 17:15:27 +02:00
parent 45443cf19f
commit dbb9668e74
3 changed files with 203 additions and 1 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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>
); );
}; };