form: fix switch required, add root form error to media settings

This commit is contained in:
dswbx
2025-02-08 13:27:47 +01:00
parent 97b0ff24c8
commit 3fa682bfe1
8 changed files with 74 additions and 21 deletions

View File

@@ -3,7 +3,7 @@
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"version": "0.7.0-rc.7", "version": "0.7.0-rc.8",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
"homepage": "https://bknd.io", "homepage": "https://bknd.io",
"repository": { "repository": {

View File

@@ -186,7 +186,7 @@ export const Switch = forwardRef<
onChange?: (e: { target: { value: boolean } }) => void; onChange?: (e: { target: { value: boolean } }) => void;
onCheckedChange?: (checked: boolean) => void; onCheckedChange?: (checked: boolean) => void;
} }
>(({ type, ...props }, ref) => { >(({ type, required, ...props }, ref) => {
return ( return (
<RadixSwitch.Root <RadixSwitch.Root
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" 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"

View File

@@ -21,6 +21,7 @@ export type FieldwrapperProps = {
wrapper?: "group" | "fieldset"; wrapper?: "group" | "fieldset";
hidden?: boolean; hidden?: boolean;
children: ReactElement | ReactNode; children: ReactElement | ReactNode;
errorPlacement?: "top" | "bottom";
}; };
export function FieldWrapper({ export function FieldWrapper({
@@ -30,6 +31,7 @@ export function FieldWrapper({
schema, schema,
wrapper, wrapper,
hidden, hidden,
errorPlacement = "bottom",
children children
}: FieldwrapperProps) { }: FieldwrapperProps) {
const errors = useFormError(name, { strict: true }); const errors = useFormError(name, { strict: true });
@@ -38,12 +40,17 @@ export function FieldWrapper({
const description = schema?.description; const description = schema?.description;
const label = typeof _label !== "undefined" ? _label : schema ? getLabel(name, schema) : name; const label = typeof _label !== "undefined" ? _label : schema ? getLabel(name, schema) : name;
const Errors = errors.length > 0 && (
<Formy.ErrorMessage>{errors.map((e) => e.message).join(", ")}</Formy.ErrorMessage>
);
return ( return (
<Formy.Group <Formy.Group
error={errors.length > 0} error={errors.length > 0}
as={wrapper === "fieldset" ? "fieldset" : "div"} as={wrapper === "fieldset" ? "fieldset" : "div"}
className={hidden ? "hidden" : "relative"} className={hidden ? "hidden" : "relative"}
> >
{errorPlacement === "top" && Errors}
<FieldDebug name={name} schema={schema} required={required} /> <FieldDebug name={name} schema={schema} required={required} />
{label && ( {label && (
@@ -73,9 +80,7 @@ export function FieldWrapper({
</div> </div>
</div> </div>
{description && <Formy.Help>{description}</Formy.Help>} {description && <Formy.Help>{description}</Formy.Help>}
{errors.length > 0 && ( {errorPlacement === "bottom" && Errors}
<Formy.ErrorMessage>{errors.map((e) => e.message).join(", ")}</Formy.ErrorMessage>
)}
</Formy.Group> </Formy.Group>
); );
} }

View File

@@ -119,12 +119,12 @@ export function Form<
// @ts-ignore // @ts-ignore
async function handleSubmit(e: FormEvent<HTMLFormElement>) { async function handleSubmit(e: FormEvent<HTMLFormElement>) {
const { data, errors } = validate();
if (onSubmit) { if (onSubmit) {
e.preventDefault(); e.preventDefault();
setFormState((prev) => ({ ...prev, submitting: true })); setFormState((prev) => ({ ...prev, submitting: true }));
try { try {
const { data, errors } = validate();
if (errors.length === 0) { if (errors.length === 0) {
await onSubmit(data as Data); await onSubmit(data as Data);
} else { } else {
@@ -136,6 +136,10 @@ export function Form<
} }
setFormState((prev) => ({ ...prev, submitting: false })); setFormState((prev) => ({ ...prev, submitting: false }));
return false; return false;
} else if (errors.length > 0) {
e.preventDefault();
onInvalidSubmit?.(errors, data);
return false;
} }
} }

View File

@@ -28,6 +28,7 @@ export const ObjectField = ({
name={path} name={path}
schema={{ ...schema, description: undefined }} schema={{ ...schema, description: undefined }}
wrapper="fieldset" wrapper="fieldset"
errorPlacement="top"
{...wrapperProps} {...wrapperProps}
> >
{Object.keys(properties).map((prop) => { {Object.keys(properties).map((prop) => {

View File

@@ -26,16 +26,24 @@ export function coerce(value: any, schema: JsonSchema, opts?: { required?: boole
return value; return value;
} }
const PathFilter = (value: any) => typeof value !== "undefined" && value !== null && value !== "";
export function pathToPointer(path: string) { export function pathToPointer(path: string) {
return "#/" + (path.includes(".") ? path.split(".").join("/") : path); const p = path.includes(".") ? path.split(".") : [path];
return (
"#" +
p
.filter(PathFilter)
.map((part) => "/" + part)
.join("")
);
} }
export function prefixPointer(pointer: string, prefix: string) { export function prefixPointer(pointer: string, prefix: string) {
return pointer.replace("#/", `#/${prefix.length > 0 ? prefix + "/" : ""}`).replace(/\/\//g, "/"); const p = pointer.replace("#", "").split("/");
return "#" + p.map((part, i) => (i === 1 ? prefix : part)).join("/");
} }
const PathFilter = (value: any) => typeof value !== "undefined" && value !== null && value !== "";
export function prefixPath(path: string = "", prefix: string | number = "") { export function prefixPath(path: string = "", prefix: string | number = "") {
const p = path.includes(".") ? path.split(".") : [path]; const p = path.includes(".") ? path.split(".") : [path];
return [prefix, ...p].filter(PathFilter).join("."); return [prefix, ...p].filter(PathFilter).join(".");

View File

@@ -5,6 +5,7 @@ import { twMerge } from "tailwind-merge";
import { useBknd } from "ui/client/BkndProvider"; import { useBknd } from "ui/client/BkndProvider";
import { useBkndMedia } from "ui/client/schema/media/use-bknd-media"; import { useBkndMedia } from "ui/client/schema/media/use-bknd-media";
import { Button } from "ui/components/buttons/Button"; import { Button } from "ui/components/buttons/Button";
import { Alert } from "ui/components/display/Alert";
import { Message } from "ui/components/display/Message"; import { Message } from "ui/components/display/Message";
import * as Formy from "ui/components/form/Formy"; import * as Formy from "ui/components/form/Formy";
import { import {
@@ -14,7 +15,8 @@ import {
FormContextOverride, FormContextOverride,
FormDebug, FormDebug,
ObjectField, ObjectField,
Subscribe Subscribe,
useFormError
} from "ui/components/form/json-schema-form"; } from "ui/components/form/json-schema-form";
import { Media } from "ui/elements"; import { Media } from "ui/elements";
import { useBrowserTitle } from "ui/hooks/use-browser-title"; import { useBrowserTitle } from "ui/hooks/use-browser-title";
@@ -37,7 +39,12 @@ const formConfig = {
}; };
function MediaSettingsInternal() { function MediaSettingsInternal() {
const { config, schema, actions } = useBkndMedia(); const { config, schema: _schema, actions } = useBkndMedia();
const schema = JSON.parse(JSON.stringify(_schema));
schema.if = { properties: { enabled: { const: true } } };
// biome-ignore lint/suspicious/noThenProperty: <explanation>
schema.then = { required: ["adapter"] };
async function onSubmit(data: any) { async function onSubmit(data: any) {
console.log("submit", data); console.log("submit", data);
@@ -71,6 +78,7 @@ function MediaSettingsInternal() {
)} )}
</Subscribe> </Subscribe>
<AppShell.Scrollable> <AppShell.Scrollable>
<RootFormError />
<div className="flex flex-col gap-3 p-3"> <div className="flex flex-col gap-3 p-3">
<Field name="enabled" /> <Field name="enabled" />
<div className="flex flex-col gap-3 relative"> <div className="flex flex-col gap-3 relative">
@@ -92,6 +100,19 @@ function MediaSettingsInternal() {
); );
} }
const RootFormError = () => {
const errors = useFormError("", { strict: true });
if (errors.length === 0) return null;
return (
<Alert.Exception>
{errors.map((error, i) => (
<div key={i}>{error.message}</div>
))}
</Alert.Exception>
);
};
const Icons = [IconBrandAws, IconCloud, IconServer]; const Icons = [IconBrandAws, IconCloud, IconServer];
const AdapterIcon = ({ index }: { index: number }) => { const AdapterIcon = ({ index }: { index: number }) => {

View File

@@ -8,7 +8,8 @@ import {
Form, Form,
FormContextOverride, FormContextOverride,
FormDebug, FormDebug,
ObjectField ObjectField,
useFormError
} 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";
@@ -32,10 +33,14 @@ const schema2 = {
}; };
export default function JsonSchemaForm3() { export default function JsonSchemaForm3() {
const { schema, config } = useBknd(); const { schema: _schema, config } = useBknd();
const schema = JSON.parse(JSON.stringify(_schema));
config.media.storage.body_max_size = 1; config.media.storage.body_max_size = 1;
schema.media.properties.storage.properties.body_max_size.minimum = 0; schema.media.properties.storage.properties.body_max_size.minimum = 0;
schema.media.if = { properties: { enabled: { const: true } } };
// biome-ignore lint/suspicious/noThenProperty: <explanation>
schema.media.then = { required: ["adapter"] };
//schema.media.properties.adapter.anyOf[2].properties.config.properties.path.minLength = 1; //schema.media.properties.adapter.anyOf[2].properties.config.properties.path.minLength = 1;
return ( return (
@@ -243,11 +248,9 @@ export default function JsonSchemaForm3() {
<Form <Form
schema={schema.media} schema={schema.media}
initialValues={config.media as any} initialValues={config.media as any}
/* validateOn="change"*/
onSubmit={console.log} onSubmit={console.log}
> validateOn="change"
<Field name="" /> />
</Form>
{/*<Form {/*<Form
schema={removeKeyRecursively(schema.media, "pattern") as any} schema={removeKeyRecursively(schema.media, "pattern") as any}
@@ -301,18 +304,23 @@ const ss = {
} as const satisfies JSONSchema; } as const satisfies JSONSchema;
function CustomMediaForm() { function CustomMediaForm() {
const { schema, config } = useBknd(); const { schema: _schema, config } = useBknd();
const schema = JSON.parse(JSON.stringify(_schema));
config.media.storage.body_max_size = 1; config.media.storage.body_max_size = 1;
schema.media.properties.storage.properties.body_max_size.minimum = 0; schema.media.properties.storage.properties.body_max_size.minimum = 0;
schema.media.if = { properties: { enabled: { const: true } } };
// biome-ignore lint/suspicious/noThenProperty: <explanation>
schema.media.then = { required: ["adapter"] };
return ( return (
<Form <Form
schema={schema.media} schema={schema.media}
initialValues={config.media as any} /*initialValues={config.media as any}*/
className="flex flex-col gap-3" className="flex flex-col gap-3"
validateOn="change" validateOn="change"
> >
<Test />
<Field name="enabled" /> <Field name="enabled" />
<Field name="basepath" /> <Field name="basepath" />
<Field name="entity_name" /> <Field name="entity_name" />
@@ -320,11 +328,17 @@ function CustomMediaForm() {
<AnyOf.Root path="adapter"> <AnyOf.Root path="adapter">
<CustomMediaFormAdapter /> <CustomMediaFormAdapter />
</AnyOf.Root> </AnyOf.Root>
<FormDebug force /> {/*<FormDebug force />*/}
</Form> </Form>
); );
} }
const Test = () => {
const errors = useFormError("", { strict: true });
return <div>{errors.map((e) => e.message).join("\n")}</div>;
//return <pre>{JSON.stringify(errors, null, 2)}</pre>;
};
function CustomMediaFormAdapter() { function CustomMediaFormAdapter() {
const ctx = AnyOf.useContext(); const ctx = AnyOf.useContext();