diff --git a/app/package.json b/app/package.json index 4ae12b1..a2f2ea5 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "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.", "homepage": "https://bknd.io", "repository": { diff --git a/app/src/ui/components/form/Formy/components.tsx b/app/src/ui/components/form/Formy/components.tsx index 34d9c82..3f639e5 100644 --- a/app/src/ui/components/form/Formy/components.tsx +++ b/app/src/ui/components/form/Formy/components.tsx @@ -186,7 +186,7 @@ export const Switch = forwardRef< onChange?: (e: { target: { value: boolean } }) => void; onCheckedChange?: (checked: boolean) => void; } ->(({ type, ...props }, ref) => { +>(({ type, required, ...props }, ref) => { return ( 0 && ( + {errors.map((e) => e.message).join(", ")} + ); + return ( 0} as={wrapper === "fieldset" ? "fieldset" : "div"} className={hidden ? "hidden" : "relative"} > + {errorPlacement === "top" && Errors} {label && ( @@ -73,9 +80,7 @@ export function FieldWrapper({ {description && {description}} - {errors.length > 0 && ( - {errors.map((e) => e.message).join(", ")} - )} + {errorPlacement === "bottom" && Errors} ); } diff --git a/app/src/ui/components/form/json-schema-form/Form.tsx b/app/src/ui/components/form/json-schema-form/Form.tsx index 3e124d6..e7f1a6a 100644 --- a/app/src/ui/components/form/json-schema-form/Form.tsx +++ b/app/src/ui/components/form/json-schema-form/Form.tsx @@ -119,12 +119,12 @@ export function Form< // @ts-ignore async function handleSubmit(e: FormEvent) { + const { data, errors } = validate(); if (onSubmit) { e.preventDefault(); setFormState((prev) => ({ ...prev, submitting: true })); try { - const { data, errors } = validate(); if (errors.length === 0) { await onSubmit(data as Data); } else { @@ -136,6 +136,10 @@ export function Form< } setFormState((prev) => ({ ...prev, submitting: false })); return false; + } else if (errors.length > 0) { + e.preventDefault(); + onInvalidSubmit?.(errors, data); + return false; } } diff --git a/app/src/ui/components/form/json-schema-form/ObjectField.tsx b/app/src/ui/components/form/json-schema-form/ObjectField.tsx index ae6b84b..d6d38b6 100644 --- a/app/src/ui/components/form/json-schema-form/ObjectField.tsx +++ b/app/src/ui/components/form/json-schema-form/ObjectField.tsx @@ -28,6 +28,7 @@ export const ObjectField = ({ name={path} schema={{ ...schema, description: undefined }} wrapper="fieldset" + errorPlacement="top" {...wrapperProps} > {Object.keys(properties).map((prop) => { diff --git a/app/src/ui/components/form/json-schema-form/utils.ts b/app/src/ui/components/form/json-schema-form/utils.ts index 648a704..0d5c33d 100644 --- a/app/src/ui/components/form/json-schema-form/utils.ts +++ b/app/src/ui/components/form/json-schema-form/utils.ts @@ -26,16 +26,24 @@ export function coerce(value: any, schema: JsonSchema, opts?: { required?: boole return value; } +const PathFilter = (value: any) => typeof value !== "undefined" && value !== null && value !== ""; + 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) { - 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 = "") { const p = path.includes(".") ? path.split(".") : [path]; return [prefix, ...p].filter(PathFilter).join("."); diff --git a/app/src/ui/routes/media/media.settings.tsx b/app/src/ui/routes/media/media.settings.tsx index 527c9c8..68b9b04 100644 --- a/app/src/ui/routes/media/media.settings.tsx +++ b/app/src/ui/routes/media/media.settings.tsx @@ -5,6 +5,7 @@ import { twMerge } from "tailwind-merge"; import { useBknd } from "ui/client/BkndProvider"; import { useBkndMedia } from "ui/client/schema/media/use-bknd-media"; import { Button } from "ui/components/buttons/Button"; +import { Alert } from "ui/components/display/Alert"; import { Message } from "ui/components/display/Message"; import * as Formy from "ui/components/form/Formy"; import { @@ -14,7 +15,8 @@ import { FormContextOverride, FormDebug, ObjectField, - Subscribe + Subscribe, + useFormError } from "ui/components/form/json-schema-form"; import { Media } from "ui/elements"; import { useBrowserTitle } from "ui/hooks/use-browser-title"; @@ -37,7 +39,12 @@ const formConfig = { }; 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: + schema.then = { required: ["adapter"] }; async function onSubmit(data: any) { console.log("submit", data); @@ -71,6 +78,7 @@ function MediaSettingsInternal() { )} +
@@ -92,6 +100,19 @@ function MediaSettingsInternal() { ); } +const RootFormError = () => { + const errors = useFormError("", { strict: true }); + if (errors.length === 0) return null; + + return ( + + {errors.map((error, i) => ( +
{error.message}
+ ))} +
+ ); +}; + const Icons = [IconBrandAws, IconCloud, IconServer]; const AdapterIcon = ({ index }: { index: number }) => { diff --git a/app/src/ui/routes/test/tests/json-schema-form3.tsx b/app/src/ui/routes/test/tests/json-schema-form3.tsx index b7330cc..9edd99f 100644 --- a/app/src/ui/routes/test/tests/json-schema-form3.tsx +++ b/app/src/ui/routes/test/tests/json-schema-form3.tsx @@ -8,7 +8,8 @@ import { Form, FormContextOverride, FormDebug, - ObjectField + ObjectField, + useFormError } from "ui/components/form/json-schema-form"; import { Scrollable } from "ui/layouts/AppShell/AppShell"; @@ -32,10 +33,14 @@ const schema2 = { }; 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; schema.media.properties.storage.properties.body_max_size.minimum = 0; + schema.media.if = { properties: { enabled: { const: true } } }; + // biome-ignore lint/suspicious/noThenProperty: + schema.media.then = { required: ["adapter"] }; //schema.media.properties.adapter.anyOf[2].properties.config.properties.path.minLength = 1; return ( @@ -243,11 +248,9 @@ export default function JsonSchemaForm3() {
- - + validateOn="change" + /> {/*
+ schema.media.then = { required: ["adapter"] }; return ( + @@ -320,11 +328,17 @@ function CustomMediaForm() { - + {/**/} ); } +const Test = () => { + const errors = useFormError("", { strict: true }); + return
{errors.map((e) => e.message).join("\n")}
; + //return
{JSON.stringify(errors, null, 2)}
; +}; + function CustomMediaFormAdapter() { const ctx = AnyOf.useContext();