mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
form rerenders optimized
This commit is contained in:
@@ -189,7 +189,7 @@ export const Switch = forwardRef<
|
|||||||
>(({ type, ...props }, ref) => {
|
>(({ type, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<RadixSwitch.Root
|
<RadixSwitch.Root
|
||||||
className="relative h-7 w-12 p-[2px] cursor-pointer rounded-full bg-muted/50 border border-muted outline-none data-[state=checked]:bg-primary appearance-none transition-colors hover:bg-muted/80"
|
className="relative h-7 w-12 p-[2px] cursor-pointer rounded-full bg-muted border border-primary/10 outline-none data-[state=checked]:bg-primary/75 appearance-none transition-colors hover:bg-muted/80"
|
||||||
onCheckedChange={(bool) => {
|
onCheckedChange={(bool) => {
|
||||||
props.onChange?.({ target: { value: bool } });
|
props.onChange?.({ target: { value: bool } });
|
||||||
}}
|
}}
|
||||||
@@ -229,7 +229,7 @@ export const Select = forwardRef<
|
|||||||
<>
|
<>
|
||||||
{!props.required && <option value="" />}
|
{!props.required && <option value="" />}
|
||||||
{options
|
{options
|
||||||
.map((o, i) => {
|
.map((o) => {
|
||||||
if (typeof o !== "object") {
|
if (typeof o !== "object") {
|
||||||
return { value: o, label: String(o) };
|
return { value: o, label: String(o) };
|
||||||
}
|
}
|
||||||
@@ -246,7 +246,10 @@ export const Select = forwardRef<
|
|||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
{!props.multiple && (
|
{!props.multiple && (
|
||||||
<TbChevronDown className="absolute right-3 top-0 bottom-0 h-full opacity-70" size={18} />
|
<TbChevronDown
|
||||||
|
className="absolute right-3 top-0 bottom-0 h-full opacity-70 pointer-events-none"
|
||||||
|
size={18}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
import type { JsonError } from "json-schema-library";
|
import { atom, useAtom } from "jotai";
|
||||||
import type { JSONSchema } from "json-schema-to-ts";
|
import type { JsonError, JsonSchema } from "json-schema-library";
|
||||||
import { type ChangeEvent, type ReactNode, createContext, useContext, useState } from "react";
|
import { type ChangeEvent, type ReactNode, createContext, useContext, useMemo } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import * as Formy from "ui/components/form/Formy";
|
import * as Formy from "ui/components/form/Formy";
|
||||||
|
import { useEvent } from "ui/hooks/use-event";
|
||||||
import { FieldComponent, Field as FormField, type FieldProps as FormFieldProps } from "./Field";
|
import { FieldComponent, Field as FormField, type FieldProps as FormFieldProps } from "./Field";
|
||||||
import { FormContextOverride, useFieldContext } from "./Form";
|
import { FormContextOverride, useDerivedFieldContext } from "./Form";
|
||||||
import { getLabel, getMultiSchemaMatched } from "./utils";
|
import { getLabel, getMultiSchemaMatched } from "./utils";
|
||||||
|
|
||||||
export type AnyOfFieldRootProps = {
|
export type AnyOfFieldRootProps = {
|
||||||
path?: string;
|
path?: string;
|
||||||
schema?: Exclude<JSONSchema, boolean>;
|
schema?: JsonSchema;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AnyOfFieldContext = {
|
export type AnyOfFieldContext = {
|
||||||
path: string;
|
path: string;
|
||||||
schema: Exclude<JSONSchema, boolean>;
|
schema: JsonSchema;
|
||||||
schemas?: JSONSchema[];
|
schemas?: JsonSchema[];
|
||||||
selectedSchema?: Exclude<JSONSchema, boolean>;
|
selectedSchema?: JsonSchema;
|
||||||
selected: number | null;
|
selected: number | null;
|
||||||
select: (index: number | null) => void;
|
select: (index: number | null) => void;
|
||||||
options: string[];
|
options: string[];
|
||||||
errors: JsonError[];
|
errors: JsonError[];
|
||||||
selectSchema: JSONSchema;
|
selectSchema: JsonSchema;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AnyOfContext = createContext<AnyOfFieldContext>(undefined!);
|
const AnyOfContext = createContext<AnyOfFieldContext>(undefined!);
|
||||||
@@ -31,61 +32,80 @@ export const useAnyOfContext = () => {
|
|||||||
return useContext(AnyOfContext);
|
return useContext(AnyOfContext);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectedAtom = atom<number | null>(null);
|
||||||
|
|
||||||
const Root = ({ path = "", schema: _schema, children }: AnyOfFieldRootProps) => {
|
const Root = ({ path = "", schema: _schema, children }: AnyOfFieldRootProps) => {
|
||||||
const { setValue, pointer, lib, value, errors, ...ctx } = useFieldContext(path);
|
const {
|
||||||
const schema = _schema ?? ctx.schema;
|
setValue,
|
||||||
|
lib,
|
||||||
|
pointer,
|
||||||
|
errors,
|
||||||
|
value: { matchedIndex, schemas },
|
||||||
|
schema
|
||||||
|
} = useDerivedFieldContext(path, _schema, (ctx) => {
|
||||||
|
const [matchedIndex, schemas = []] = getMultiSchemaMatched(ctx.schema, ctx.value);
|
||||||
|
return { matchedIndex, schemas };
|
||||||
|
});
|
||||||
if (!schema) return `AnyOfField(${path}): no schema ${pointer}`;
|
if (!schema) return `AnyOfField(${path}): no schema ${pointer}`;
|
||||||
const [matchedIndex, schemas = []] = getMultiSchemaMatched(schema, value);
|
const [_selected, setSelected] = useAtom(selectedAtom);
|
||||||
const [selected, setSelected] = useState<number | null>(matchedIndex > -1 ? matchedIndex : null);
|
const selected = _selected !== null ? _selected : matchedIndex > -1 ? matchedIndex : null;
|
||||||
const options = schemas.map((s, i) => s.title ?? `Option ${i + 1}`);
|
|
||||||
const selectSchema = {
|
|
||||||
type: "string",
|
|
||||||
enum: options
|
|
||||||
} satisfies JSONSchema;
|
|
||||||
//console.log("AnyOf:root", { value, matchedIndex, selected, schema });
|
|
||||||
|
|
||||||
const selectedSchema =
|
const select = useEvent((index: number | null) => {
|
||||||
selected !== null ? (schemas[selected] as Exclude<JSONSchema, boolean>) : undefined;
|
|
||||||
|
|
||||||
function select(index: number | null) {
|
|
||||||
setValue(pointer, index !== null ? lib.getTemplate(undefined, schemas[index]) : undefined);
|
setValue(pointer, index !== null ? lib.getTemplate(undefined, schemas[index]) : undefined);
|
||||||
setSelected(index);
|
setSelected(index);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
const context = useMemo(() => {
|
||||||
|
const options = schemas.map((s, i) => s.title ?? `Option ${i + 1}`);
|
||||||
|
const selectSchema = {
|
||||||
|
type: "string",
|
||||||
|
enum: options
|
||||||
|
} satisfies JsonSchema;
|
||||||
|
|
||||||
|
const selectedSchema = selected !== null ? (schemas[selected] as JsonSchema) : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
options,
|
||||||
|
selectSchema,
|
||||||
|
selectedSchema,
|
||||||
|
schema,
|
||||||
|
schemas,
|
||||||
|
selected
|
||||||
|
};
|
||||||
|
}, [selected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnyOfContext.Provider
|
<AnyOfContext.Provider
|
||||||
|
key={selected}
|
||||||
value={{
|
value={{
|
||||||
selected,
|
...context,
|
||||||
select,
|
select,
|
||||||
options,
|
|
||||||
selectSchema,
|
|
||||||
path,
|
path,
|
||||||
schema,
|
|
||||||
schemas,
|
|
||||||
selectedSchema,
|
|
||||||
errors
|
errors
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/*<pre>{JSON.stringify({ value, selected, errors: errors.length }, null, 2)}</pre>*/}
|
|
||||||
{children}
|
{children}
|
||||||
</AnyOfContext.Provider>
|
</AnyOfContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Select = () => {
|
const Select = () => {
|
||||||
const { selected, select, path, schema, selectSchema } = useAnyOfContext();
|
const { selected, select, path, schema, options, selectSchema } = useAnyOfContext();
|
||||||
|
|
||||||
function handleSelect(e: ChangeEvent<HTMLInputElement>) {
|
function handleSelect(e: ChangeEvent<HTMLInputElement>) {
|
||||||
//console.log("selected", e.target.value);
|
|
||||||
const i = e.target.value ? Number(e.target.value) : null;
|
const i = e.target.value ? Number(e.target.value) : null;
|
||||||
select(i);
|
select(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Formy.Label>{getLabel(path, schema)}</Formy.Label>
|
<Formy.Label>
|
||||||
|
{getLabel(path, schema)} {selected}
|
||||||
|
</Formy.Label>
|
||||||
<FieldComponent
|
<FieldComponent
|
||||||
schema={selectSchema as any}
|
schema={selectSchema as any}
|
||||||
|
/* @ts-ignore */
|
||||||
|
options={options.map((label, value) => ({ label, value }))}
|
||||||
onChange={handleSelect}
|
onChange={handleSelect}
|
||||||
value={selected ?? undefined}
|
value={selected ?? undefined}
|
||||||
className="h-8 py-1"
|
className="h-8 py-1"
|
||||||
@@ -94,11 +114,11 @@ const Select = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Field = ({ name, label, ...props }: Partial<FormFieldProps>) => {
|
const Field = ({ name, label, schema, ...props }: Partial<FormFieldProps>) => {
|
||||||
const { selected, selectedSchema, path, errors } = useAnyOfContext();
|
const { selected, selectedSchema, path, errors } = useAnyOfContext();
|
||||||
if (selected === null) return null;
|
if (selected === null) return null;
|
||||||
return (
|
return (
|
||||||
<FormContextOverride path={path} schema={selectedSchema} overrideData>
|
<FormContextOverride prefix={path} schema={selectedSchema}>
|
||||||
<div className={twMerge(errors.length > 0 && "bg-red-500/10")}>
|
<div className={twMerge(errors.length > 0 && "bg-red-500/10")}>
|
||||||
<FormField key={`${path}_${selected}`} name={""} label={false} {...props} />
|
<FormField key={`${path}_${selected}`} name={""} label={false} {...props} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,49 +1,33 @@
|
|||||||
import { IconLibraryPlus, IconTrash } from "@tabler/icons-react";
|
import { IconLibraryPlus, IconTrash } from "@tabler/icons-react";
|
||||||
import type { JSONSchema } from "json-schema-to-ts";
|
import type { JsonSchema } from "json-schema-library";
|
||||||
|
import { isEqual } from "lodash-es";
|
||||||
|
import { memo, useMemo } from "react";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import { IconButton } from "ui/components/buttons/IconButton";
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
import * as Formy from "ui/components/form/Formy";
|
|
||||||
import { Dropdown } from "ui/components/overlay/Dropdown";
|
import { Dropdown } from "ui/components/overlay/Dropdown";
|
||||||
|
import { useEvent } from "ui/hooks/use-event";
|
||||||
import { FieldComponent } from "./Field";
|
import { FieldComponent } from "./Field";
|
||||||
import { FieldWrapper } from "./FieldWrapper";
|
import { FieldWrapper } from "./FieldWrapper";
|
||||||
import { useFieldContext } from "./Form";
|
import { useDerivedFieldContext, useFormContext, useFormValue } from "./Form";
|
||||||
import { coerce, getMultiSchema, getMultiSchemaMatched } from "./utils";
|
import { coerce, getMultiSchema, getMultiSchemaMatched } from "./utils";
|
||||||
|
|
||||||
export const ArrayField = ({
|
export const ArrayField = ({
|
||||||
path = "",
|
path = "",
|
||||||
schema: _schema
|
schema: _schema
|
||||||
}: { path?: string; schema?: Exclude<JSONSchema, boolean> }) => {
|
}: { path?: string; schema?: JsonSchema }) => {
|
||||||
const { setValue, value, pointer, required, ...ctx } = useFieldContext(path);
|
const { setValue, pointer, required, ...ctx } = useDerivedFieldContext(path, _schema);
|
||||||
const schema = _schema ?? ctx.schema;
|
const schema = _schema ?? ctx.schema;
|
||||||
if (!schema || typeof schema === "undefined") return `ArrayField(${path}): no schema ${pointer}`;
|
if (!schema || typeof schema === "undefined") return `ArrayField(${path}): no schema ${pointer}`;
|
||||||
|
|
||||||
const itemsMultiSchema = getMultiSchema(schema.items);
|
|
||||||
|
|
||||||
function handleAdd(template?: any) {
|
|
||||||
const currentIndex = value?.length ?? 0;
|
|
||||||
const newPointer = `${path}/${currentIndex}`.replace(/\/+/g, "/");
|
|
||||||
setValue(newPointer, template ?? ctx.lib.getTemplate(undefined, schema!.items));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleUpdate(pointer: string, value: any) {
|
|
||||||
setValue(pointer, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDelete(pointer: string) {
|
|
||||||
return () => {
|
|
||||||
ctx.deleteValue(pointer);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// if unique items with enum
|
// if unique items with enum
|
||||||
if (schema.uniqueItems && typeof schema.items === "object" && "enum" in schema.items) {
|
if (schema.uniqueItems && typeof schema.items === "object" && "enum" in schema.items) {
|
||||||
return (
|
return (
|
||||||
<FieldWrapper pointer={path} schema={schema} wrapper="fieldset">
|
<FieldWrapper name={path} schema={schema} wrapper="fieldset">
|
||||||
<FieldComponent
|
<FieldComponent
|
||||||
required
|
required
|
||||||
|
name={path}
|
||||||
schema={schema.items}
|
schema={schema.items}
|
||||||
multiple
|
multiple
|
||||||
value={value}
|
|
||||||
className="h-auto"
|
className="h-auto"
|
||||||
onChange={(e: any) => {
|
onChange={(e: any) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -56,48 +40,101 @@ export const ArrayField = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldWrapper pointer={path} schema={schema} wrapper="fieldset">
|
<FieldWrapper name={path} schema={schema} wrapper="fieldset">
|
||||||
{value?.map((v, index: number) => {
|
<ArrayIterator name={path}>
|
||||||
const pointer = `${path}/${index}`.replace(/\/+/g, "/");
|
{({ value }) =>
|
||||||
let subschema = schema.items;
|
value?.map((v, index: number) => (
|
||||||
if (itemsMultiSchema) {
|
<ArrayItem key={index} path={path} index={index} schema={schema} />
|
||||||
const [, , _subschema] = getMultiSchemaMatched(schema.items, v);
|
))
|
||||||
subschema = _subschema;
|
|
||||||
}
|
}
|
||||||
|
</ArrayIterator>
|
||||||
return (
|
|
||||||
<div key={pointer} className="flex flex-row gap-2">
|
|
||||||
<FieldComponent
|
|
||||||
name={pointer}
|
|
||||||
schema={subschema!}
|
|
||||||
value={v}
|
|
||||||
onChange={(e) => {
|
|
||||||
handleUpdate(pointer, coerce(e.target.value, subschema!));
|
|
||||||
}}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<IconButton Icon={IconTrash} onClick={handleDelete(pointer)} size="sm" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
{itemsMultiSchema ? (
|
<ArrayAdd path={path} schema={schema} />
|
||||||
<Dropdown
|
|
||||||
dropdownWrapperProps={{
|
|
||||||
className: "min-w-0"
|
|
||||||
}}
|
|
||||||
items={itemsMultiSchema.map((s, i) => ({
|
|
||||||
label: s!.title ?? `Option ${i + 1}`,
|
|
||||||
onClick: () => handleAdd(ctx.lib.getTemplate(undefined, s!))
|
|
||||||
}))}
|
|
||||||
onClickItem={console.log}
|
|
||||||
>
|
|
||||||
<Button IconLeft={IconLibraryPlus}>Add</Button>
|
|
||||||
</Dropdown>
|
|
||||||
) : (
|
|
||||||
<Button onClick={() => handleAdd()}>Add</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</FieldWrapper>
|
</FieldWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ArrayItem = memo(({ path, index, schema }: any) => {
|
||||||
|
const { value, ...ctx } = useDerivedFieldContext(path, schema, (ctx) => {
|
||||||
|
return ctx.value?.[index];
|
||||||
|
});
|
||||||
|
const pointer = [path, index].join(".");
|
||||||
|
let subschema = schema.items;
|
||||||
|
const itemsMultiSchema = getMultiSchema(schema.items);
|
||||||
|
if (itemsMultiSchema) {
|
||||||
|
const [, , _subschema] = getMultiSchemaMatched(schema.items, value);
|
||||||
|
subschema = _subschema;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdate = useEvent((pointer: string, value: any) => {
|
||||||
|
ctx.setValue(pointer, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDelete = useEvent((pointer: string) => {
|
||||||
|
ctx.deleteValue(pointer);
|
||||||
|
});
|
||||||
|
|
||||||
|
const DeleteButton = useMemo(
|
||||||
|
() => <IconButton Icon={IconTrash} onClick={() => handleDelete(pointer)} size="sm" />,
|
||||||
|
[pointer]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={pointer} className="flex flex-row gap-2">
|
||||||
|
<FieldComponent
|
||||||
|
name={pointer}
|
||||||
|
schema={subschema!}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
handleUpdate(pointer, coerce(e.target.value, subschema!));
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
{DeleteButton}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, isEqual);
|
||||||
|
|
||||||
|
const ArrayIterator = memo(
|
||||||
|
({ name, children }: any) => {
|
||||||
|
return children(useFormValue(name));
|
||||||
|
},
|
||||||
|
(prev, next) => prev.value.length === next.value.length
|
||||||
|
);
|
||||||
|
|
||||||
|
const ArrayAdd = ({ schema, path }: { schema: JsonSchema; path: string }) => {
|
||||||
|
const {
|
||||||
|
setValue,
|
||||||
|
value: { currentIndex },
|
||||||
|
...ctx
|
||||||
|
} = useDerivedFieldContext(path, schema, (ctx) => {
|
||||||
|
return { currentIndex: ctx.value?.length ?? 0 };
|
||||||
|
});
|
||||||
|
const itemsMultiSchema = getMultiSchema(schema.items);
|
||||||
|
|
||||||
|
function handleAdd(template?: any) {
|
||||||
|
//const currentIndex = value?.length ?? 0;
|
||||||
|
const newPointer = `${path}/${currentIndex}`.replace(/\/+/g, "/");
|
||||||
|
setValue(newPointer, template ?? ctx.lib.getTemplate(undefined, schema!.items));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemsMultiSchema) {
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
dropdownWrapperProps={{
|
||||||
|
className: "min-w-0"
|
||||||
|
}}
|
||||||
|
items={itemsMultiSchema.map((s, i) => ({
|
||||||
|
label: s!.title ?? `Option ${i + 1}`,
|
||||||
|
onClick: () => handleAdd(ctx.lib.getTemplate(undefined, s!))
|
||||||
|
}))}
|
||||||
|
onClickItem={console.log}
|
||||||
|
>
|
||||||
|
<Button IconLeft={IconLibraryPlus}>Add</Button>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Button onClick={() => handleAdd()}>Add</Button>;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,26 +1,30 @@
|
|||||||
import type { JSONSchema } from "json-schema-to-ts";
|
import type { JsonSchema } from "json-schema-library";
|
||||||
import type { ChangeEvent, ComponentPropsWithoutRef } from "react";
|
import type { ChangeEvent, ComponentPropsWithoutRef } from "react";
|
||||||
import * as Formy from "ui/components/form/Formy";
|
import * as Formy from "ui/components/form/Formy";
|
||||||
import { useEvent } from "ui/hooks/use-event";
|
import { useEvent } from "ui/hooks/use-event";
|
||||||
import { ArrayField } from "./ArrayField";
|
import { ArrayField } from "./ArrayField";
|
||||||
import { FieldWrapper } from "./FieldWrapper";
|
import { FieldWrapper } from "./FieldWrapper";
|
||||||
import { useFieldContext } from "./Form";
|
import { useDerivedFieldContext, useFormValue } from "./Form";
|
||||||
import { ObjectField } from "./ObjectField";
|
import { ObjectField } from "./ObjectField";
|
||||||
import { coerce, enumToOptions, isType, isTypeSchema } from "./utils";
|
import { coerce, isType, isTypeSchema } from "./utils";
|
||||||
|
|
||||||
export type FieldProps = {
|
export type FieldProps = {
|
||||||
name: string;
|
name: string;
|
||||||
schema?: Exclude<JSONSchema, boolean>;
|
schema?: JsonSchema;
|
||||||
onChange?: (e: ChangeEvent<any>) => void;
|
onChange?: (e: ChangeEvent<any>) => void;
|
||||||
label?: string | false;
|
label?: string | false;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Field = ({ name, schema: _schema, onChange, label: _label, hidden }: FieldProps) => {
|
export const Field = ({ name, schema: _schema, onChange, label: _label, hidden }: FieldProps) => {
|
||||||
const { pointer, value, errors, setValue, required, ...ctx } = useFieldContext(name);
|
const { pointer, setValue, required, ...ctx } = useDerivedFieldContext(name, _schema);
|
||||||
const schema = _schema ?? ctx.schema;
|
const schema = _schema ?? ctx.schema;
|
||||||
if (!isTypeSchema(schema)) return <Pre>{pointer} has no schema</Pre>;
|
if (!isTypeSchema(schema))
|
||||||
//console.log("field", name, schema);
|
return (
|
||||||
|
<Pre>
|
||||||
|
[Field] {pointer} has no schema ({JSON.stringify(schema)})
|
||||||
|
</Pre>
|
||||||
|
);
|
||||||
|
|
||||||
if (isType(schema.type, "object")) {
|
if (isType(schema.type, "object")) {
|
||||||
return <ObjectField path={name} schema={schema} />;
|
return <ObjectField path={name} schema={schema} />;
|
||||||
@@ -31,39 +35,23 @@ export const Field = ({ name, schema: _schema, onChange, label: _label, hidden }
|
|||||||
}
|
}
|
||||||
|
|
||||||
const disabled = schema.readOnly ?? "const" in schema ?? false;
|
const disabled = schema.readOnly ?? "const" in schema ?? false;
|
||||||
//console.log("field", name, disabled, schema, ctx.schema, _schema);
|
|
||||||
|
|
||||||
const handleChange = useEvent((e: ChangeEvent<HTMLInputElement>) => {
|
const handleChange = useEvent((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
// don't remove for now, causes issues in anyOf
|
|
||||||
/*const value = coerce(e.target.value, schema as any);
|
|
||||||
setValue(pointer, value as any);*/
|
|
||||||
|
|
||||||
const value = coerce(e.target.value, schema as any, { required });
|
const value = coerce(e.target.value, schema as any, { required });
|
||||||
//console.log("handleChange", pointer, e.target.value, { value }, ctx.options);
|
|
||||||
if (typeof value === "undefined" && !required && ctx.options?.keepEmpty !== true) {
|
if (typeof value === "undefined" && !required && ctx.options?.keepEmpty !== true) {
|
||||||
ctx.deleteValue(pointer);
|
ctx.deleteValue(pointer);
|
||||||
} else {
|
} else {
|
||||||
//console.log("setValue", pointer, value);
|
|
||||||
setValue(pointer, value);
|
setValue(pointer, value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldWrapper
|
<FieldWrapper name={name} label={_label} required={required} schema={schema} hidden={hidden}>
|
||||||
pointer={pointer}
|
|
||||||
label={_label}
|
|
||||||
required={required}
|
|
||||||
errors={errors}
|
|
||||||
schema={schema}
|
|
||||||
debug={{ value }}
|
|
||||||
hidden={hidden}
|
|
||||||
>
|
|
||||||
<FieldComponent
|
<FieldComponent
|
||||||
schema={schema}
|
schema={schema}
|
||||||
name={pointer}
|
name={name}
|
||||||
required={required}
|
required={required}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={value}
|
|
||||||
onChange={onChange ?? handleChange}
|
onChange={onChange ?? handleChange}
|
||||||
/>
|
/>
|
||||||
</FieldWrapper>
|
</FieldWrapper>
|
||||||
@@ -71,19 +59,25 @@ export const Field = ({ name, schema: _schema, onChange, label: _label, hidden }
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Pre = ({ children }) => (
|
export const Pre = ({ children }) => (
|
||||||
<pre className="dark:bg-red-950 bg-red-100 rounded-md px-3 py-1.5">{children}</pre>
|
<pre className="dark:bg-red-950 bg-red-100 rounded-md px-3 py-1.5 text-wrap whitespace-break-spaces break-all">
|
||||||
|
{children}
|
||||||
|
</pre>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FieldComponent = ({
|
export const FieldComponent = ({
|
||||||
schema,
|
schema,
|
||||||
...props
|
..._props
|
||||||
}: { schema: JSONSchema } & ComponentPropsWithoutRef<"input">) => {
|
}: { schema: JsonSchema } & ComponentPropsWithoutRef<"input">) => {
|
||||||
|
const { value } = useFormValue(_props.name!);
|
||||||
if (!isTypeSchema(schema)) return null;
|
if (!isTypeSchema(schema)) return null;
|
||||||
|
const props = {
|
||||||
|
..._props,
|
||||||
|
// allow override
|
||||||
|
value: typeof _props.value !== "undefined" ? _props.value : value
|
||||||
|
};
|
||||||
|
|
||||||
if (schema.enum) {
|
if (schema.enum) {
|
||||||
return (
|
return <Formy.Select id={props.name} options={schema.enum} {...(props as any)} />;
|
||||||
<Formy.Select id={props.name} {...(props as any)} options={enumToOptions(schema.enum)} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isType(schema.type, ["number", "integer"])) {
|
if (isType(schema.type, ["number", "integer"])) {
|
||||||
@@ -91,7 +85,7 @@ export const FieldComponent = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isType(schema.type, "boolean")) {
|
if (isType(schema.type, "boolean")) {
|
||||||
return <Formy.Switch id={props.name} {...(props as any)} checked={props.value as any} />;
|
return <Formy.Switch id={props.name} {...(props as any)} checked={value as any} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Formy.Input id={props.name} {...props} value={props.value ?? ""} />;
|
return <Formy.Input id={props.name} {...props} value={props.value ?? ""} />;
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import { Popover } from "@mantine/core";
|
import { Popover } from "@mantine/core";
|
||||||
import { IconBug } from "@tabler/icons-react";
|
import { IconBug } from "@tabler/icons-react";
|
||||||
import type { JsonError } from "json-schema-library";
|
import type { JsonSchema } from "json-schema-library";
|
||||||
import type { JSONSchema } from "json-schema-to-ts";
|
|
||||||
import { Children, type ReactElement, type ReactNode, cloneElement, isValidElement } from "react";
|
import { Children, type ReactElement, type ReactNode, cloneElement, isValidElement } from "react";
|
||||||
import { IconButton } from "ui/components/buttons/IconButton";
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
import { JsonViewer } from "ui/components/code/JsonViewer";
|
import { JsonViewer } from "ui/components/code/JsonViewer";
|
||||||
import * as Formy from "ui/components/form/Formy";
|
import * as Formy from "ui/components/form/Formy";
|
||||||
|
import { useFormError } from "ui/components/form/json-schema-form/Form";
|
||||||
import { getLabel } from "./utils";
|
import { getLabel } from "./utils";
|
||||||
|
|
||||||
export type FieldwrapperProps = {
|
export type FieldwrapperProps = {
|
||||||
pointer: string;
|
name: string;
|
||||||
label?: string | false;
|
label?: string | false;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
errors?: JsonError[];
|
schema?: JsonSchema;
|
||||||
schema?: Exclude<JSONSchema, boolean>;
|
|
||||||
debug?: object | boolean;
|
debug?: object | boolean;
|
||||||
wrapper?: "group" | "fieldset";
|
wrapper?: "group" | "fieldset";
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
@@ -21,21 +20,20 @@ export type FieldwrapperProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function FieldWrapper({
|
export function FieldWrapper({
|
||||||
pointer,
|
name,
|
||||||
label: _label,
|
label: _label,
|
||||||
required,
|
required,
|
||||||
errors = [],
|
|
||||||
schema,
|
schema,
|
||||||
debug,
|
debug,
|
||||||
wrapper,
|
wrapper,
|
||||||
hidden,
|
hidden,
|
||||||
children
|
children
|
||||||
}: FieldwrapperProps) {
|
}: FieldwrapperProps) {
|
||||||
|
const errors = useFormError(name, { strict: true });
|
||||||
const examples = schema?.examples || [];
|
const examples = schema?.examples || [];
|
||||||
const examplesId = `${pointer}-examples`;
|
const examplesId = `${name}-examples`;
|
||||||
const description = schema?.description;
|
const description = schema?.description;
|
||||||
const label =
|
const label = typeof _label !== "undefined" ? _label : schema ? getLabel(name, schema) : name;
|
||||||
typeof _label !== "undefined" ? _label : schema ? getLabel(pointer, schema) : pointer;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formy.Group
|
<Formy.Group
|
||||||
@@ -54,7 +52,7 @@ export function FieldWrapper({
|
|||||||
<JsonViewer
|
<JsonViewer
|
||||||
json={{
|
json={{
|
||||||
...(typeof debug === "object" ? debug : {}),
|
...(typeof debug === "object" ? debug : {}),
|
||||||
pointer,
|
name,
|
||||||
required,
|
required,
|
||||||
schema,
|
schema,
|
||||||
errors
|
errors
|
||||||
@@ -70,7 +68,7 @@ export function FieldWrapper({
|
|||||||
{label && (
|
{label && (
|
||||||
<Formy.Label
|
<Formy.Label
|
||||||
as={wrapper === "fieldset" ? "legend" : "label"}
|
as={wrapper === "fieldset" ? "legend" : "label"}
|
||||||
htmlFor={pointer}
|
htmlFor={name}
|
||||||
className="self-start"
|
className="self-start"
|
||||||
>
|
>
|
||||||
{label} {required && <span className="font-medium opacity-30">*</span>}
|
{label} {required && <span className="font-medium opacity-30">*</span>}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
|
import {
|
||||||
|
type PrimitiveAtom,
|
||||||
|
atom,
|
||||||
|
getDefaultStore,
|
||||||
|
useAtom,
|
||||||
|
useAtomValue,
|
||||||
|
useSetAtom
|
||||||
|
} from "jotai";
|
||||||
import { selectAtom } from "jotai/utils";
|
import { selectAtom } from "jotai/utils";
|
||||||
import { Draft2019, type JsonError } from "json-schema-library";
|
import { Draft2019, type JsonError, type JsonSchema as LibJsonSchema } from "json-schema-library";
|
||||||
import type { TemplateOptions as LibTemplateOptions } from "json-schema-library/dist/lib/getTemplate";
|
import type { TemplateOptions as LibTemplateOptions } from "json-schema-library/dist/lib/getTemplate";
|
||||||
import type { JsonSchema as LibJsonSchema } from "json-schema-library/dist/lib/types";
|
|
||||||
import type { JSONSchema as $JSONSchema, FromSchema } from "json-schema-to-ts";
|
import type { JSONSchema as $JSONSchema, FromSchema } from "json-schema-to-ts";
|
||||||
import { get, isEqual } from "lodash-es";
|
import { get, isEqual } from "lodash-es";
|
||||||
import * as immutable from "object-path-immutable";
|
import * as immutable from "object-path-immutable";
|
||||||
@@ -15,13 +21,19 @@ import {
|
|||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef
|
||||||
useState
|
|
||||||
} from "react";
|
} from "react";
|
||||||
import { JsonViewer } from "ui/components/code/JsonViewer";
|
import { JsonViewer } from "ui/components/code/JsonViewer";
|
||||||
import { useEvent } from "ui/hooks/use-event";
|
import { useEvent } from "ui/hooks/use-event";
|
||||||
import { Field } from "./Field";
|
import { Field } from "./Field";
|
||||||
import { isRequired, normalizePath, omitSchema, prefixPointer } from "./utils";
|
import {
|
||||||
|
isRequired,
|
||||||
|
normalizePath,
|
||||||
|
omitSchema,
|
||||||
|
pathToPointer,
|
||||||
|
prefixPath,
|
||||||
|
prefixPointer
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
type JSONSchema = Exclude<$JSONSchema, boolean>;
|
type JSONSchema = Exclude<$JSONSchema, boolean>;
|
||||||
type FormState<Data = any> = {
|
type FormState<Data = any> = {
|
||||||
@@ -31,13 +43,6 @@ type FormState<Data = any> = {
|
|||||||
data: Data;
|
data: Data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formStateAtom = atom<FormState>({
|
|
||||||
dirty: false,
|
|
||||||
submitting: false,
|
|
||||||
errors: [] as JsonError[],
|
|
||||||
data: {} as any
|
|
||||||
});
|
|
||||||
|
|
||||||
export type FormProps<
|
export type FormProps<
|
||||||
Schema extends JSONSchema = JSONSchema,
|
Schema extends JSONSchema = JSONSchema,
|
||||||
Data = Schema extends JSONSchema ? FromSchema<JSONSchema> : any
|
Data = Schema extends JSONSchema ? FromSchema<JSONSchema> : any
|
||||||
@@ -68,9 +73,12 @@ export type FormContext<Data> = {
|
|||||||
schema: JSONSchema;
|
schema: JSONSchema;
|
||||||
lib: Draft2019;
|
lib: Draft2019;
|
||||||
options: FormProps["options"];
|
options: FormProps["options"];
|
||||||
|
root: string;
|
||||||
|
_formStateAtom: PrimitiveAtom<FormState<Data>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FormContext = createContext<FormContext<any>>(undefined!);
|
const FormContext = createContext<FormContext<any>>(undefined!);
|
||||||
|
FormContext.displayName = "FormContext";
|
||||||
|
|
||||||
export function Form<
|
export function Form<
|
||||||
Schema extends JSONSchema = JSONSchema,
|
Schema extends JSONSchema = JSONSchema,
|
||||||
@@ -92,20 +100,28 @@ export function Form<
|
|||||||
const [schema, initial] = omitSchema(_schema, ignoreKeys, _initialValues);
|
const [schema, initial] = omitSchema(_schema, ignoreKeys, _initialValues);
|
||||||
const lib = useMemo(() => new Draft2019(schema), [JSON.stringify(schema)]);
|
const lib = useMemo(() => new Draft2019(schema), [JSON.stringify(schema)]);
|
||||||
const initialValues = initial ?? lib.getTemplate(undefined, schema, initialOpts);
|
const initialValues = initial ?? lib.getTemplate(undefined, schema, initialOpts);
|
||||||
const [formState, setFormState] = useAtom<FormState<Data>>(formStateAtom);
|
const _formStateAtom = useMemo(() => {
|
||||||
|
return atom<FormState<Data>>({
|
||||||
|
dirty: false,
|
||||||
|
submitting: false,
|
||||||
|
errors: [] as JsonError[],
|
||||||
|
data: initialValues
|
||||||
|
});
|
||||||
|
}, [initialValues]);
|
||||||
|
const setFormState = useSetAtom(_formStateAtom);
|
||||||
const formRef = useRef<HTMLFormElement | null>(null);
|
const formRef = useRef<HTMLFormElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("setting data");
|
if (initialValues) {
|
||||||
setFormState((prev) => ({ ...prev, data: initialValues }));
|
validate();
|
||||||
}, []);
|
}
|
||||||
|
}, [initialValues]);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
||||||
if (onSubmit) {
|
if (onSubmit) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setFormState((prev) => ({ ...prev, submitting: true }));
|
setFormState((prev) => ({ ...prev, submitting: true }));
|
||||||
//setSubmitting(true);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data, errors } = validate();
|
const { data, errors } = validate();
|
||||||
@@ -119,29 +135,20 @@ export function Form<
|
|||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
setFormState((prev) => ({ ...prev, submitting: false }));
|
setFormState((prev) => ({ ...prev, submitting: false }));
|
||||||
|
|
||||||
//setSubmitting(false);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setValue = useEvent((pointer: string, value: any) => {
|
const setValue = useEvent((pointer: string, value: any) => {
|
||||||
const normalized = normalizePath(pointer);
|
const normalized = normalizePath(pointer);
|
||||||
//console.log("setValue", { pointer, normalized, value });
|
|
||||||
const key = normalized.substring(2).replace(/\//g, ".");
|
const key = normalized.substring(2).replace(/\//g, ".");
|
||||||
setFormState((state) => {
|
setFormState((state) => {
|
||||||
const prev = state.data;
|
const prev = state.data;
|
||||||
const changed = immutable.set(prev, key, value);
|
const changed = immutable.set(prev, key, value);
|
||||||
onChange?.(changed, key, value);
|
onChange?.(changed, key, value);
|
||||||
//console.log("changed", prev, changed, { key, value });
|
|
||||||
return { ...state, data: changed };
|
return { ...state, data: changed };
|
||||||
});
|
});
|
||||||
/*setData((prev) => {
|
check();
|
||||||
const changed = immutable.set(prev, key, value);
|
|
||||||
onChange?.(changed, key, value);
|
|
||||||
//console.log("changed", prev, changed, { key, value });
|
|
||||||
return changed;
|
|
||||||
});*/
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteValue = useEvent((pointer: string) => {
|
const deleteValue = useEvent((pointer: string) => {
|
||||||
@@ -151,56 +158,49 @@ export function Form<
|
|||||||
const prev = state.data;
|
const prev = state.data;
|
||||||
const changed = immutable.del(prev, key);
|
const changed = immutable.del(prev, key);
|
||||||
onChange?.(changed, key, undefined);
|
onChange?.(changed, key, undefined);
|
||||||
//console.log("changed", prev, changed, { key, value });
|
|
||||||
return { ...state, data: changed };
|
return { ...state, data: changed };
|
||||||
});
|
});
|
||||||
/*setData((prev) => {
|
check();
|
||||||
const changed = immutable.del(prev, key);
|
|
||||||
onChange?.(changed, key, undefined);
|
|
||||||
//console.log("changed", prev, changed, { key });
|
|
||||||
return changed;
|
|
||||||
});*/
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const getCurrentState = useEvent(() => getDefaultStore().get(_formStateAtom));
|
||||||
//setDirty(!isEqual(initialValues, data));
|
|
||||||
//setFormState((prev => ({ ...prev, dirty: !isEqual(initialValues, data) })));
|
const check = useEvent(() => {
|
||||||
|
const state = getCurrentState();
|
||||||
|
setFormState((prev) => ({ ...prev, dirty: !isEqual(initialValues, state.data) }));
|
||||||
|
|
||||||
if (validateOn === "change") {
|
if (validateOn === "change") {
|
||||||
validate();
|
validate();
|
||||||
} else if (formState?.errors?.length > 0) {
|
} else if (state?.errors?.length > 0) {
|
||||||
validate();
|
validate();
|
||||||
}
|
}
|
||||||
}, [formState?.data]);
|
});
|
||||||
|
|
||||||
function validate(_data?: Partial<Data>) {
|
const validate = useEvent((_data?: Partial<Data>) => {
|
||||||
const actual = _data ?? formState?.data;
|
const actual = _data ?? getCurrentState()?.data;
|
||||||
const errors = lib.validate(actual, schema);
|
const errors = lib.validate(actual, schema);
|
||||||
//console.log("errors", errors);
|
|
||||||
setFormState((prev) => ({ ...prev, errors }));
|
setFormState((prev) => ({ ...prev, errors }));
|
||||||
//setErrors(errors);
|
|
||||||
return { data: actual, errors };
|
return { data: actual, errors };
|
||||||
}
|
});
|
||||||
|
|
||||||
const context = useMemo(
|
const context = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
_formStateAtom,
|
||||||
setValue,
|
setValue,
|
||||||
deleteValue,
|
deleteValue,
|
||||||
schema,
|
schema,
|
||||||
lib,
|
lib,
|
||||||
options
|
options,
|
||||||
|
root: ""
|
||||||
}),
|
}),
|
||||||
[]
|
[schema, initialValues]
|
||||||
) as any;
|
) as any;
|
||||||
//console.log("context", context);
|
|
||||||
|
|
||||||
const Component = useMemo(() => {
|
|
||||||
return children ? children : <Field name="" />;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form {...props} ref={formRef} onSubmit={handleSubmit}>
|
<form {...props} ref={formRef} onSubmit={handleSubmit}>
|
||||||
<FormContext.Provider value={context}>{Component}</FormContext.Provider>
|
<FormContext.Provider value={context}>
|
||||||
|
{children ? children : <Field name="" />}
|
||||||
|
</FormContext.Provider>
|
||||||
{hiddenSubmit && (
|
{hiddenSubmit && (
|
||||||
<button style={{ visibility: "hidden" }} type="submit">
|
<button style={{ visibility: "hidden" }} type="submit">
|
||||||
Submit
|
Submit
|
||||||
@@ -217,25 +217,21 @@ export function useFormContext() {
|
|||||||
export function FormContextOverride({
|
export function FormContextOverride({
|
||||||
children,
|
children,
|
||||||
overrideData,
|
overrideData,
|
||||||
path,
|
prefix,
|
||||||
...overrides
|
...overrides
|
||||||
}: Partial<FormContext<any>> & { children: ReactNode; path?: string; overrideData?: boolean }) {
|
}: Partial<FormContext<any>> & { children: ReactNode; prefix?: string; overrideData?: boolean }) {
|
||||||
const ctx = useFormContext();
|
const ctx = useFormContext();
|
||||||
const additional: Partial<FormContext<any>> = {};
|
const additional: Partial<FormContext<any>> = {};
|
||||||
|
|
||||||
// this makes a local schema down the three
|
// this makes a local schema down the three
|
||||||
// especially useful for AnyOf, since it doesn't need to fully validate (e.g. pattern)
|
// especially useful for AnyOf, since it doesn't need to fully validate (e.g. pattern)
|
||||||
if (overrideData && path) {
|
if (prefix) {
|
||||||
const pointer = normalizePath(path);
|
additional.root = prefix;
|
||||||
const value =
|
|
||||||
pointer === "#/" ? ctx.data : get(ctx.data, pointer.substring(2).replace(/\//g, "."));
|
|
||||||
|
|
||||||
additional.data = value;
|
|
||||||
additional.setValue = (pointer: string, value: any) => {
|
additional.setValue = (pointer: string, value: any) => {
|
||||||
ctx.setValue(prefixPointer(pointer, path), value);
|
ctx.setValue(prefixPointer(pointer, prefix), value);
|
||||||
};
|
};
|
||||||
additional.deleteValue = (pointer: string) => {
|
additional.deleteValue = (pointer: string) => {
|
||||||
ctx.deleteValue(prefixPointer(pointer, path));
|
ctx.deleteValue(prefixPointer(pointer, prefix));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,88 +245,128 @@ export function FormContextOverride({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useFormValue(name: string) {
|
export function useFormValue(name: string) {
|
||||||
const pointer = normalizePath(name);
|
const { _formStateAtom, root } = useFormContext();
|
||||||
const isRootPointer = pointer === "#/";
|
|
||||||
const selected = selectAtom(
|
const selected = selectAtom(
|
||||||
formStateAtom,
|
_formStateAtom,
|
||||||
useCallback(
|
useCallback(
|
||||||
(state) => {
|
(state) => {
|
||||||
const data = state.data;
|
const prefixedName = prefixPath(name, root);
|
||||||
console.log("data", data);
|
const pointer = pathToPointer(prefixedName);
|
||||||
return isRootPointer ? data : get(data, pointer.substring(2).replace(/\//g, "."));
|
return {
|
||||||
|
value: get(state.data, prefixedName),
|
||||||
|
errors: state.errors.filter((error) => error.data.pointer.startsWith(pointer))
|
||||||
|
};
|
||||||
},
|
},
|
||||||
[pointer]
|
[name]
|
||||||
),
|
),
|
||||||
isEqual
|
isEqual
|
||||||
);
|
);
|
||||||
return useAtom(selected)[0];
|
return useAtom(selected)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFieldContext(name: string) {
|
export function useFormError(name: string, opt?: { strict?: boolean }) {
|
||||||
const { lib, schema, errors: formErrors = [], ...rest } = useFormContext();
|
const { _formStateAtom, root } = useFormContext();
|
||||||
const pointer = normalizePath(name);
|
const selected = selectAtom(
|
||||||
const isRootPointer = pointer === "#/";
|
_formStateAtom,
|
||||||
//console.log("pointer", pointer);
|
useCallback(
|
||||||
const data = {};
|
(state) => {
|
||||||
|
const prefixedName = prefixPath(name, root);
|
||||||
const value = useFormValue(name);
|
const pointer = pathToPointer(prefixedName);
|
||||||
console.log("value", pointer, value);
|
return state.errors.filter((error) => {
|
||||||
//const value = isRootPointer ? data : get(data, pointer.substring(2).replace(/\//g, "."));
|
return opt?.strict
|
||||||
const errors = useMemo(
|
? error.data.pointer === pointer
|
||||||
() => formErrors.filter((error) => error.data.pointer.startsWith(pointer)),
|
: error.data.pointer.startsWith(pointer);
|
||||||
[name]
|
});
|
||||||
|
},
|
||||||
|
[name]
|
||||||
|
),
|
||||||
|
isEqual
|
||||||
);
|
);
|
||||||
const fieldSchema = useMemo(
|
return useAtom(selected)[0];
|
||||||
() => (isRootPointer ? (schema as LibJsonSchema) : lib.getSchema({ pointer, data, schema })),
|
}
|
||||||
[name]
|
|
||||||
|
export function useFormStateSelector<Data = any, Reduced = Data>(
|
||||||
|
selector: (state: FormState<Data>) => Reduced
|
||||||
|
): Reduced {
|
||||||
|
const { _formStateAtom } = useFormContext();
|
||||||
|
const selected = selectAtom(_formStateAtom, useCallback(selector, []), isEqual);
|
||||||
|
return useAtom(selected)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectorFn<Ctx = any, Refined = any> = (state: Ctx) => Refined;
|
||||||
|
|
||||||
|
export function useDerivedFieldContext<Data = any, Reduced = undefined>(
|
||||||
|
path,
|
||||||
|
_schema?: LibJsonSchema,
|
||||||
|
deriveFn?: SelectorFn<
|
||||||
|
FormContext<Data> & {
|
||||||
|
pointer: string;
|
||||||
|
required: boolean;
|
||||||
|
errors: JsonError[];
|
||||||
|
value: any;
|
||||||
|
},
|
||||||
|
Reduced
|
||||||
|
>
|
||||||
|
): FormContext<Data> & { value: Reduced; pointer: string; required: boolean; errors: JsonError[] } {
|
||||||
|
const { _formStateAtom, root, lib, ...ctx } = useFormContext();
|
||||||
|
const schema = _schema ?? ctx.schema;
|
||||||
|
const selected = selectAtom(
|
||||||
|
_formStateAtom,
|
||||||
|
useCallback(
|
||||||
|
(state) => {
|
||||||
|
const pointer = pathToPointer(path);
|
||||||
|
const prefixedName = prefixPath(path, root);
|
||||||
|
const prefixedPointer = pathToPointer(prefixedName);
|
||||||
|
const value = get(state.data, prefixedName);
|
||||||
|
const errors = state.errors.filter((error) =>
|
||||||
|
error.data.pointer.startsWith(prefixedPointer)
|
||||||
|
);
|
||||||
|
const fieldSchema =
|
||||||
|
pointer === "#/"
|
||||||
|
? (schema as LibJsonSchema)
|
||||||
|
: lib.getSchema({ pointer, data: value, schema });
|
||||||
|
const required = isRequired(prefixedPointer, schema, state.data);
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
...ctx,
|
||||||
|
root,
|
||||||
|
schema: fieldSchema as LibJsonSchema,
|
||||||
|
pointer,
|
||||||
|
required,
|
||||||
|
errors
|
||||||
|
};
|
||||||
|
const derived = deriveFn?.({ ...context, _formStateAtom, lib, value });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...context,
|
||||||
|
value: derived
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[path, schema ?? {}, root]
|
||||||
|
),
|
||||||
|
isEqual
|
||||||
);
|
);
|
||||||
const required = false; // isRequired(pointer, schema, data);
|
return {
|
||||||
const options = useMemo(() => ({}), []);
|
...useAtomValue(selected),
|
||||||
|
_formStateAtom,
|
||||||
return useMemo(
|
lib
|
||||||
() => ({
|
} as any;
|
||||||
...rest,
|
|
||||||
dirty: false,
|
|
||||||
submitting: false,
|
|
||||||
options,
|
|
||||||
lib,
|
|
||||||
value,
|
|
||||||
errors,
|
|
||||||
schema: fieldSchema,
|
|
||||||
pointer,
|
|
||||||
required
|
|
||||||
}),
|
|
||||||
[JSON.stringify([value])]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
useFieldContext.displayName = "useFieldContext";
|
|
||||||
|
|
||||||
export function Subscribe({ children }: { children: (ctx: FormContext<any>) => ReactNode }) {
|
|
||||||
const ctx = useFormContext();
|
|
||||||
return children(ctx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormDebug() {
|
export function Subscribe<Data = any, Refined = Data>({
|
||||||
const { options, data, dirty, errors, submitting } = useFormContext();
|
children,
|
||||||
if (options?.debug !== true) return null;
|
selector
|
||||||
|
}: {
|
||||||
return <JsonViewer json={{ dirty, submitting, data, errors }} expand={99} />;
|
children: (state: Refined) => ReactNode;
|
||||||
|
selector?: SelectorFn<FormState<Data>, Refined>;
|
||||||
|
}) {
|
||||||
|
return children(useFormStateSelector(selector ?? ((state) => state as unknown as Refined)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function useFieldContext2(name: string) {
|
export function FormDebug({ force = false }: { force?: boolean }) {
|
||||||
const ctx = useRef(useFormContext());
|
const { options } = useFormContext();
|
||||||
const pointer = normalizePath(name);
|
if (options?.debug !== true && force !== true) return null;
|
||||||
const isRootPointer = pointer === "#/";
|
const ctx = useFormStateSelector((s) => s);
|
||||||
//console.log("pointer", pointer);
|
|
||||||
const data = {};
|
|
||||||
const options = useMemo(() => ({}), []);
|
|
||||||
const required = false;
|
|
||||||
|
|
||||||
const value = useFormValue(name);
|
return <JsonViewer json={ctx} expand={99} />;
|
||||||
return { value, options, dirty: false, submitting: false, required, pointer };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FormDebug2({ name }: any) {
|
|
||||||
const { ...ctx } = useFieldContext2(name);
|
|
||||||
return <pre>{JSON.stringify({ ctx })}</pre>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { JsonError } from "json-schema-library";
|
|
||||||
import type { JSONSchema } from "json-schema-to-ts";
|
import type { JSONSchema } from "json-schema-to-ts";
|
||||||
|
import { isTypeSchema } from "ui/components/form/json-schema-form/utils";
|
||||||
import { AnyOfField } from "./AnyOfField";
|
import { AnyOfField } from "./AnyOfField";
|
||||||
import { Field } from "./Field";
|
import { Field } from "./Field";
|
||||||
import { FieldWrapper, type FieldwrapperProps } from "./FieldWrapper";
|
import { FieldWrapper, type FieldwrapperProps } from "./FieldWrapper";
|
||||||
import { useFieldContext } from "./Form";
|
import { useDerivedFieldContext } from "./Form";
|
||||||
|
|
||||||
export type ObjectFieldProps = {
|
export type ObjectFieldProps = {
|
||||||
path?: string;
|
path?: string;
|
||||||
@@ -18,29 +18,28 @@ export const ObjectField = ({
|
|||||||
label: _label,
|
label: _label,
|
||||||
wrapperProps = {}
|
wrapperProps = {}
|
||||||
}: ObjectFieldProps) => {
|
}: ObjectFieldProps) => {
|
||||||
const ctx = useFieldContext(path);
|
const ctx = useDerivedFieldContext(path, _schema);
|
||||||
const schema = _schema ?? ctx.schema;
|
const schema = _schema ?? ctx.schema;
|
||||||
if (!schema) return "ObjectField: no schema";
|
if (!isTypeSchema(schema)) return `ObjectField "${path}": no schema`;
|
||||||
const properties = schema.properties ?? {};
|
const properties = schema.properties ?? {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldWrapper
|
<FieldWrapper
|
||||||
pointer={path}
|
name={path}
|
||||||
errors={ctx.errors}
|
|
||||||
schema={{ ...schema, description: undefined }}
|
schema={{ ...schema, description: undefined }}
|
||||||
wrapper="fieldset"
|
wrapper="fieldset"
|
||||||
{...wrapperProps}
|
{...wrapperProps}
|
||||||
>
|
>
|
||||||
{Object.keys(properties).map((prop) => {
|
{Object.keys(properties).map((prop) => {
|
||||||
const schema = properties[prop];
|
const schema = properties[prop];
|
||||||
const pointer = `${path}/${prop}`.replace(/\/+/g, "/");
|
const name = [path, prop].filter(Boolean).join(".");
|
||||||
if (!schema) return;
|
if (typeof schema === "undefined" || typeof schema === "boolean") return;
|
||||||
|
|
||||||
if (schema.anyOf || schema.oneOf) {
|
if (schema.anyOf || schema.oneOf) {
|
||||||
return <AnyOfField key={pointer} path={pointer} />;
|
return <AnyOfField key={name} path={name} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Field key={pointer} name={pointer} />;
|
return <Field key={name} name={name} />;
|
||||||
})}
|
})}
|
||||||
</FieldWrapper>
|
</FieldWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function flatten(obj: any, parentKey = "", result: any = {}): any {
|
|||||||
// @todo: make sure it's in the right order
|
// @todo: make sure it's in the right order
|
||||||
export function unflatten(
|
export function unflatten(
|
||||||
obj: Record<string, string>,
|
obj: Record<string, string>,
|
||||||
schema: JSONSchema,
|
schema: JsonSchema,
|
||||||
selections?: Record<string, number | undefined>
|
selections?: Record<string, number | undefined>
|
||||||
) {
|
) {
|
||||||
const result = {};
|
const result = {};
|
||||||
@@ -78,11 +78,7 @@ export function unflatten(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function coerce(
|
export function coerce(value: any, schema: JsonSchema, opts?: { required?: boolean }) {
|
||||||
value: any,
|
|
||||||
schema: Exclude<JSONSchema, boolean>,
|
|
||||||
opts?: { required?: boolean }
|
|
||||||
) {
|
|
||||||
if (!value && typeof opts?.required === "boolean" && !opts.required) {
|
if (!value && typeof opts?.required === "boolean" && !opts.required) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -121,16 +117,25 @@ export function normalizePath(path: string) {
|
|||||||
: `#/${path.replace(/#?\/?/, "").replace(/\./g, "/").replace(/\[/g, "/").replace(/\]/g, "")}`;
|
: `#/${path.replace(/#?\/?/, "").replace(/\./g, "/").replace(/\[/g, "/").replace(/\]/g, "")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function pathToPointer(path: string) {
|
||||||
|
return "#/" + (path.includes(".") ? path.split(".").join("/") : path);
|
||||||
|
}
|
||||||
|
|
||||||
export function prefixPointer(pointer: string, prefix: string) {
|
export function prefixPointer(pointer: string, prefix: string) {
|
||||||
return pointer.replace("#/", `#/${prefix}/`).replace(/\/\//g, "/");
|
return pointer.replace("#/", `#/${prefix.length > 0 ? prefix + "/" : ""}`).replace(/\/\//g, "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prefixPath(path: string = "", prefix: string = "") {
|
||||||
|
const p = path.includes(".") ? path.split(".") : [path];
|
||||||
|
return [prefix, ...p].filter(Boolean).join(".");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getParentPointer(pointer: string) {
|
export function getParentPointer(pointer: string) {
|
||||||
return pointer.substring(0, pointer.lastIndexOf("/"));
|
return pointer.substring(0, pointer.lastIndexOf("/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRequired(pointer: string, schema: JSONSchema, data?: any) {
|
export function isRequired(pointer: string, schema: JsonSchema, data?: any) {
|
||||||
if (pointer === "#/") {
|
if (pointer === "#/" || !schema) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const lib = new Draft2019(schema as any);
|
const lib = new Draft2019(schema as any);
|
||||||
@@ -144,13 +149,6 @@ export function isRequired(pointer: string, schema: JSONSchema, data?: any) {
|
|||||||
const parentSchema = lib.getSchema({ pointer: parentPointer, data });
|
const parentSchema = lib.getSchema({ pointer: parentPointer, data });
|
||||||
const required = parentSchema?.required?.includes(pointer.split("/").pop()!);
|
const required = parentSchema?.required?.includes(pointer.split("/").pop()!);
|
||||||
|
|
||||||
/*console.log("isRequired", {
|
|
||||||
pointer,
|
|
||||||
parentPointer,
|
|
||||||
parent: parentSchema ? JSON.parse(JSON.stringify(parentSchema)) : null,
|
|
||||||
required
|
|
||||||
});*/
|
|
||||||
|
|
||||||
return !!required;
|
return !!required;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,13 +160,14 @@ export function isType(_type: TType, _compare: TType) {
|
|||||||
return compare.some((t) => type.includes(t));
|
return compare.some((t) => type.includes(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLabel(name: string, schema: JSONSchema) {
|
export function getLabel(name: string, schema: JsonSchema) {
|
||||||
if (typeof schema === "object" && "title" in schema) return schema.title;
|
if (typeof schema === "object" && "title" in schema) return schema.title;
|
||||||
const label = name.includes("/") ? (name.split("/").pop() ?? "") : name;
|
if (!name) return "";
|
||||||
|
const label = name.includes(".") ? (name.split(".").pop() ?? "") : name;
|
||||||
return autoFormatString(label);
|
return autoFormatString(label);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMultiSchema(schema: JSONSchema): Exclude<JSONSchema, boolean>[] | undefined {
|
export function getMultiSchema(schema: JsonSchema): JsonSchema[] | undefined {
|
||||||
if (!schema || typeof schema !== "object") return;
|
if (!schema || typeof schema !== "object") return;
|
||||||
return (schema.anyOf ?? schema.oneOf) as any;
|
return (schema.anyOf ?? schema.oneOf) as any;
|
||||||
}
|
}
|
||||||
@@ -176,8 +175,9 @@ export function getMultiSchema(schema: JSONSchema): Exclude<JSONSchema, boolean>
|
|||||||
export function getMultiSchemaMatched(
|
export function getMultiSchemaMatched(
|
||||||
schema: JsonSchema,
|
schema: JsonSchema,
|
||||||
data: any
|
data: any
|
||||||
): [number, Exclude<JSONSchema, boolean>[], Exclude<JSONSchema, boolean> | undefined] {
|
): [number, JsonSchema[], JsonSchema | undefined] {
|
||||||
const multiSchema = getMultiSchema(schema);
|
const multiSchema = getMultiSchema(schema);
|
||||||
|
//console.log("getMultiSchemaMatched", schema, data, multiSchema);
|
||||||
if (!multiSchema) return [-1, [], undefined];
|
if (!multiSchema) return [-1, [], undefined];
|
||||||
const index = multiSchema.findIndex((subschema) => {
|
const index = multiSchema.findIndex((subschema) => {
|
||||||
const lib = new Draft2019(subschema as any);
|
const lib = new Draft2019(subschema as any);
|
||||||
@@ -220,7 +220,7 @@ export function omitSchema<Given extends JSONSchema>(_schema: Given, keys: strin
|
|||||||
return [updated, reducedConfig];
|
return [updated, reducedConfig];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isTypeSchema(schema?: JSONSchema): schema is Exclude<JSONSchema, boolean> {
|
export function isTypeSchema(schema?: JsonSchema): schema is JsonSchema {
|
||||||
return typeof schema === "object" && "type" in schema && !isType(schema.type, "error");
|
return typeof schema === "object" && "type" in schema && !isType(schema.type, "error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
import type { IconType } from "react-icons";
|
import type { IconType } from "react-icons";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { IconButton } from "ui/components/buttons/IconButton";
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
|
import { useEvent } from "ui/hooks/use-event";
|
||||||
import { AppShellProvider, useAppShell } from "ui/layouts/AppShell/use-appshell";
|
import { AppShellProvider, useAppShell } from "ui/layouts/AppShell/use-appshell";
|
||||||
import { useEvent } from "../../hooks/use-event";
|
|
||||||
|
|
||||||
export function Root({ children }) {
|
export function Root({ children }) {
|
||||||
return (
|
return (
|
||||||
@@ -74,8 +74,15 @@ export function Content({ children, center }: { children: React.ReactNode; cente
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Main({ children }) {
|
export function Main({ children }) {
|
||||||
|
const { sidebar } = useAppShell();
|
||||||
return (
|
return (
|
||||||
<div data-shell="main" className="flex flex-col flex-grow w-1 flex-shrink-0">
|
<div
|
||||||
|
data-shell="main"
|
||||||
|
className={twMerge(
|
||||||
|
"flex flex-col flex-grow w-1 flex-shrink-1",
|
||||||
|
sidebar.open && "max-w-[calc(100%-350px)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -298,7 +305,7 @@ export function Scrollable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea.Root style={{ height: `calc(100dvh - ${offset}px` }} ref={scrollRef}>
|
<ScrollArea.Root style={{ height: `calc(100dvh - ${offset}px` }} ref={scrollRef}>
|
||||||
<ScrollArea.Viewport className="w-full h-full ">{children}</ScrollArea.Viewport>
|
<ScrollArea.Viewport className="w-full h-full">{children}</ScrollArea.Viewport>
|
||||||
<ScrollArea.Scrollbar
|
<ScrollArea.Scrollbar
|
||||||
forceMount
|
forceMount
|
||||||
className="flex select-none touch-none bg-transparent w-0.5"
|
className="flex select-none touch-none bg-transparent w-0.5"
|
||||||
|
|||||||
@@ -73,18 +73,15 @@ export function HeaderNavigation() {
|
|||||||
<>
|
<>
|
||||||
<nav className="hidden md:flex flex-row gap-2.5 pl-0 p-2.5 items-center">
|
<nav className="hidden md:flex flex-row gap-2.5 pl-0 p-2.5 items-center">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<Tooltip
|
<NavLink
|
||||||
key={item.label}
|
key={item.href}
|
||||||
label={item.tooltip}
|
as={Link}
|
||||||
disabled={typeof item.tooltip === "undefined"}
|
href={item.href}
|
||||||
position="bottom"
|
Icon={item.Icon}
|
||||||
|
disabled={item.disabled}
|
||||||
>
|
>
|
||||||
<div>
|
{item.label}
|
||||||
<NavLink as={Link} href={item.href} Icon={item.Icon} disabled={item.disabled}>
|
</NavLink>
|
||||||
{item.label}
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
<nav className="flex md:hidden flex-row items-center">
|
<nav className="flex md:hidden flex-row items-center">
|
||||||
|
|||||||
@@ -48,14 +48,20 @@ function MediaSettingsInternal() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form schema={schema} initialValues={config as any} onSubmit={onSubmit} {...formConfig}>
|
<Form schema={schema} initialValues={config as any} onSubmit={onSubmit} {...formConfig}>
|
||||||
<Subscribe>
|
<Subscribe
|
||||||
|
selector={(state) => ({
|
||||||
|
dirty: state.dirty,
|
||||||
|
errors: state.errors.length > 0,
|
||||||
|
submitting: state.submitting
|
||||||
|
})}
|
||||||
|
>
|
||||||
{({ dirty, errors, submitting }) => (
|
{({ dirty, errors, submitting }) => (
|
||||||
<AppShell.SectionHeader
|
<AppShell.SectionHeader
|
||||||
right={
|
right={
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!dirty || errors.length > 0 || submitting}
|
disabled={!dirty || errors || submitting}
|
||||||
>
|
>
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
@@ -132,7 +138,7 @@ function Adapters() {
|
|||||||
<Formy.Label as="legend" className="font-mono px-2">
|
<Formy.Label as="legend" className="font-mono px-2">
|
||||||
{autoFormatString(ctx.selectedSchema!.title!)}
|
{autoFormatString(ctx.selectedSchema!.title!)}
|
||||||
</Formy.Label>
|
</Formy.Label>
|
||||||
<FormContextOverride schema={ctx.selectedSchema} path={ctx.path} overrideData>
|
<FormContextOverride schema={ctx.selectedSchema} prefix={ctx.path}>
|
||||||
<Field name="type" hidden />
|
<Field name="type" hidden />
|
||||||
<ObjectField path="config" wrapperProps={{ label: false, wrapper: "group" }} />
|
<ObjectField path="config" wrapperProps={{ label: false, wrapper: "group" }} />
|
||||||
</FormContextOverride>
|
</FormContextOverride>
|
||||||
@@ -143,9 +149,9 @@ function Adapters() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Overlay = () => (
|
const Overlay = () => (
|
||||||
<Subscribe>
|
<Subscribe selector={(state) => ({ enabled: state.data.enabled })}>
|
||||||
{({ data }) =>
|
{({ enabled }) =>
|
||||||
!data.enabled && (
|
!enabled && (
|
||||||
<div className="absolute w-full h-full z-50 bg-background opacity-70 pointer-events-none" />
|
<div className="absolute w-full h-full z-50 bg-background opacity-70 pointer-events-none" />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ function TestRoot({ children }) {
|
|||||||
</div>
|
</div>
|
||||||
</AppShell.Scrollable>
|
</AppShell.Scrollable>
|
||||||
</AppShell.Sidebar>
|
</AppShell.Sidebar>
|
||||||
<main className="flex flex-col flex-grow">{children}</main>
|
<AppShell.Main>{children}</AppShell.Main>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { useBknd } from "ui/client/bknd";
|
|||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import {
|
import {
|
||||||
AnyOf,
|
AnyOf,
|
||||||
|
AnyOfField,
|
||||||
Field,
|
Field,
|
||||||
Form,
|
Form,
|
||||||
FormContextOverride,
|
FormContextOverride,
|
||||||
FormDebug,
|
FormDebug,
|
||||||
FormDebug2,
|
|
||||||
ObjectField
|
ObjectField
|
||||||
} from "ui/components/form/json-schema-form";
|
} from "ui/components/form/json-schema-form";
|
||||||
import { Scrollable } from "ui/layouts/AppShell/AppShell";
|
import { Scrollable } from "ui/layouts/AppShell/AppShell";
|
||||||
@@ -33,16 +33,108 @@ const schema2 = {
|
|||||||
export default function JsonSchemaForm3() {
|
export default function JsonSchemaForm3() {
|
||||||
const { schema, config } = useBknd();
|
const { schema, config } = useBknd();
|
||||||
|
|
||||||
|
config.media.storage.body_max_size = 1;
|
||||||
|
schema.media.properties.storage.properties.body_max_size.minimum = 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Scrollable>
|
<Scrollable>
|
||||||
<div className="flex flex-col p-3">
|
<div className="flex flex-col p-3">
|
||||||
<Form schema={schema2} className="flex flex-col gap-3">
|
{/*<Form
|
||||||
|
schema={{
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
name: { type: "string", default: "Peter", maxLength: 3 },
|
||||||
|
age: { type: "number" },
|
||||||
|
deep: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
nested: { type: "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["age"]
|
||||||
|
}}
|
||||||
|
initialValues={{ name: "Peter", age: 20, deep: { nested: "hello" } }}
|
||||||
|
className="flex flex-col gap-3"
|
||||||
|
validateOn="change"
|
||||||
|
/>*/}
|
||||||
|
|
||||||
|
{/*<Form
|
||||||
|
schema={{
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
name: { type: "string", default: "Peter", minLength: 3 },
|
||||||
|
age: { type: "number" },
|
||||||
|
deep: {
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
nested: { type: "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
nested2: { type: "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["age"]
|
||||||
|
}}
|
||||||
|
className="flex flex-col gap-3"
|
||||||
|
validateOn="change"
|
||||||
|
>
|
||||||
|
<Field name="" />
|
||||||
|
<Subscribe2>
|
||||||
|
{(state) => (
|
||||||
|
<pre className="text-wrap whitespace-break-spaces break-all">
|
||||||
|
{JSON.stringify(state, null, 2)}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</Subscribe2>
|
||||||
|
</Form>*/}
|
||||||
|
|
||||||
|
{/*<Form
|
||||||
|
schema={{
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
name: { type: "string", default: "Peter", maxLength: 3 },
|
||||||
|
age: { type: "number" },
|
||||||
|
gender: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["male", "female", "uni"]
|
||||||
|
},
|
||||||
|
deep: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
nested: { type: "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["age"]
|
||||||
|
}}
|
||||||
|
className="flex flex-col gap-3"
|
||||||
|
validateOn="change"
|
||||||
|
>
|
||||||
<div>random thing</div>
|
<div>random thing</div>
|
||||||
<Field name="name" />
|
<Field name="name" />
|
||||||
<Field name="age" />
|
<Field name="age" />
|
||||||
<FormDebug />
|
<FormDebug />
|
||||||
<FormDebug2 name="name" />
|
<FormDebug2 name="name" />
|
||||||
</Form>
|
<hr />
|
||||||
|
<Subscribe2
|
||||||
|
selector={(state) => ({ dirty: state.dirty, submitting: state.submitting })}
|
||||||
|
>
|
||||||
|
{(state) => (
|
||||||
|
<pre className="text-wrap whitespace-break-spaces break-all">
|
||||||
|
{JSON.stringify(state)}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</Subscribe2>
|
||||||
|
</Form>*/}
|
||||||
|
|
||||||
{/*<Form
|
{/*<Form
|
||||||
schema={{
|
schema={{
|
||||||
@@ -114,7 +206,7 @@ export default function JsonSchemaForm3() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
initialValues={{ tags: [0, 1] }}
|
initialValues={{ tags: [0, 1], method: ["GET"] }}
|
||||||
options={{ debug: true }}
|
options={{ debug: true }}
|
||||||
>
|
>
|
||||||
<Field name="" />
|
<Field name="" />
|
||||||
@@ -140,10 +232,15 @@ export default function JsonSchemaForm3() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
initialValues={{ tags: [0, 1] }}
|
initialValues={{ tags: [0, 1] }}
|
||||||
/>*/}
|
>
|
||||||
|
<Field name="" />
|
||||||
|
<FormDebug force />
|
||||||
|
</Form>*/}
|
||||||
|
|
||||||
{/*<CustomMediaForm />*/}
|
<CustomMediaForm />
|
||||||
{/*<Form schema={schema.media} initialValues={config.media} />*/}
|
{/*<Form schema={schema.media} initialValues={config.media} validateOn="change">
|
||||||
|
<Field name="" />
|
||||||
|
</Form>*/}
|
||||||
|
|
||||||
{/*<Form
|
{/*<Form
|
||||||
schema={removeKeyRecursively(schema.media, "pattern") as any}
|
schema={removeKeyRecursively(schema.media, "pattern") as any}
|
||||||
@@ -166,8 +263,16 @@ export default function JsonSchemaForm3() {
|
|||||||
function CustomMediaForm() {
|
function CustomMediaForm() {
|
||||||
const { schema, config } = useBknd();
|
const { schema, config } = useBknd();
|
||||||
|
|
||||||
|
config.media.storage.body_max_size = 1;
|
||||||
|
schema.media.properties.storage.properties.body_max_size.minimum = 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form schema={schema.media} initialValues={config.media} className="flex flex-col gap-3">
|
<Form
|
||||||
|
schema={schema.media}
|
||||||
|
initialValues={config.media}
|
||||||
|
className="flex flex-col gap-3"
|
||||||
|
validateOn="change"
|
||||||
|
>
|
||||||
<Field name="enabled" />
|
<Field name="enabled" />
|
||||||
<Field name="basepath" />
|
<Field name="basepath" />
|
||||||
<Field name="entity_name" />
|
<Field name="entity_name" />
|
||||||
@@ -175,6 +280,7 @@ function CustomMediaForm() {
|
|||||||
<AnyOf.Root path="adapter">
|
<AnyOf.Root path="adapter">
|
||||||
<CustomMediaFormAdapter />
|
<CustomMediaFormAdapter />
|
||||||
</AnyOf.Root>
|
</AnyOf.Root>
|
||||||
|
<FormDebug force />
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -197,7 +303,7 @@ function CustomMediaFormAdapter() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ctx.selected !== null && (
|
{ctx.selected !== null && (
|
||||||
<FormContextOverride schema={ctx.selectedSchema} path={ctx.path} overrideData>
|
<FormContextOverride schema={ctx.selectedSchema} prefix={ctx.path}>
|
||||||
<Field name="type" hidden />
|
<Field name="type" hidden />
|
||||||
<ObjectField path="config" wrapperProps={{ label: false, wrapper: "group" }} />
|
<ObjectField path="config" wrapperProps={{ label: false, wrapper: "group" }} />
|
||||||
</FormContextOverride>
|
</FormContextOverride>
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ body {
|
|||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*div[data-radix-scroll-area-viewport] > div:first-child {}*/
|
div[data-radix-scroll-area-viewport] > div:first-child {
|
||||||
|
min-width: auto !important;
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* hide calendar icon on inputs */
|
/* hide calendar icon on inputs */
|
||||||
input[type="datetime-local"]::-webkit-calendar-picker-indicator,
|
input[type="datetime-local"]::-webkit-calendar-picker-indicator,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type BkndContextProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const BkndContextContext = createContext<BkndGlobalContextProps>({} as any);
|
const BkndContextContext = createContext<BkndGlobalContextProps>({} as any);
|
||||||
|
BkndContextContext.displayName = "BkndContext";
|
||||||
|
|
||||||
export const BkndContext = ({
|
export const BkndContext = ({
|
||||||
children,
|
children,
|
||||||
|
|||||||
Reference in New Issue
Block a user