mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
form: fix switch required, add root form error to media settings
This commit is contained in:
@@ -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": {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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(".");
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user