form: fix popover, improve form types

This commit is contained in:
dswbx
2025-02-08 15:43:02 +01:00
parent 3fa682bfe1
commit f7e6928dba
9 changed files with 56 additions and 49 deletions

View File

@@ -3,7 +3,7 @@
"type": "module",
"sideEffects": false,
"bin": "./dist/cli/index.js",
"version": "0.7.0-rc.8",
"version": "0.7.0-rc.11",
"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": {
@@ -55,6 +55,7 @@
"oauth4webapi": "^2.11.1",
"object-path-immutable": "^4.1.2",
"radix-ui": "^1.1.2",
"json-schema-to-ts": "^3.1.1",
"swr": "^2.2.5"
},
"devDependencies": {
@@ -75,7 +76,6 @@
"clsx": "^2.1.1",
"esbuild-postcss": "^0.0.4",
"jotai": "^2.10.1",
"json-schema-to-ts": "^3.1.1",
"open": "^10.1.0",
"openapi-types": "^12.1.3",
"postcss": "^8.4.47",

View File

@@ -1,4 +1,3 @@
import { IconCopy } from "@tabler/icons-react";
import { TbCopy } from "react-icons/tb";
import { JsonView } from "react-json-view-lite";
import { twMerge } from "tailwind-merge";

View File

@@ -31,7 +31,7 @@ export const ArrayField = ({
onChange={(e: any) => {
// @ts-ignore
const selected = Array.from(e.target.selectedOptions).map((o) => o.value);
setValue(pointer, selected);
setValue(ctx.path, selected);
}}
/>
</FieldWrapper>

View File

@@ -1,4 +1,3 @@
import { Popover } from "@mantine/core";
import { IconBug } from "@tabler/icons-react";
import type { JsonSchema } from "json-schema-library";
import { Children, type ReactElement, type ReactNode, cloneElement, isValidElement } from "react";
@@ -10,6 +9,7 @@ import {
useFormError,
useFormValue
} from "ui/components/form/json-schema-form/Form";
import { Popover } from "ui/components/overlay/Popover";
import { getLabel } from "./utils";
export type FieldwrapperProps = {
@@ -62,6 +62,7 @@ export function FieldWrapper({
{label} {required && <span className="font-medium opacity-30">*</span>}
</Formy.Label>
)}
<div className="flex flex-row gap-2">
<div className="flex flex-1 flex-col gap-3">
{Children.count(children) === 1 && isValidElement(children)
@@ -96,14 +97,15 @@ const FieldDebug = ({
const errors = useFormError(name, { strict: true });
return (
<div className="absolute right-0 top-0">
{/* @todo: use radix */}
<Popover>
<Popover.Target>
<IconButton Icon={IconBug} size="xs" className="opacity-30" />
</Popover.Target>
<Popover.Dropdown>
<div className="absolute top-0 right-0">
<Popover
overlayProps={{
className: "max-w-none"
}}
position="bottom-end"
target={({ toggle }) => (
<JsonViewer
className="bg-background pr-3 text-sm"
json={{
name,
value,
@@ -112,9 +114,10 @@ const FieldDebug = ({
errors
}}
expand={6}
className="p-0"
/>
</Popover.Dropdown>
)}
>
<IconButton Icon={IconBug} size="xs" className="opacity-30" />
</Popover>
</div>
);

View File

@@ -43,24 +43,9 @@ type FormState<Data = any> = {
data: Data;
};
export type FormProps<
Schema extends JSONSchema = JSONSchema,
Data = Schema extends JSONSchema ? FromSchema<Schema> : any,
InitialData = Schema extends JSONSchema ? FromSchema<Schema> : any
> = Omit<ComponentPropsWithoutRef<"form">, "onChange" | "onSubmit"> & {
schema: Schema;
validateOn?: "change" | "submit";
initialOpts?: LibTemplateOptions;
ignoreKeys?: string[];
onChange?: (data: Partial<Data>, name: string, value: any) => void;
onSubmit?: (data: Data) => void | Promise<void>;
onInvalidSubmit?: (errors: JsonError[], data: Partial<Data>) => void;
hiddenSubmit?: boolean;
options?: {
debug?: boolean;
keepEmpty?: boolean;
};
initialValues?: InitialData;
type FormOptions = {
debug?: boolean;
keepEmpty?: boolean;
};
export type FormContext<Data> = {
@@ -72,7 +57,7 @@ export type FormContext<Data> = {
submitting: boolean;
schema: LibJsonSchema;
lib: Draft2019;
options: FormProps["options"];
options: FormOptions;
root: string;
_formStateAtom: PrimitiveAtom<FormState<Data>>;
};
@@ -81,8 +66,8 @@ const FormContext = createContext<FormContext<any>>(undefined!);
FormContext.displayName = "FormContext";
export function Form<
Schema extends JSONSchema = JSONSchema,
Data = Schema extends JSONSchema ? FromSchema<Schema> : any
const Schema extends JSONSchema,
const Data = Schema extends JSONSchema ? FromSchema<Schema> : any
>({
schema: _schema,
initialValues: _initialValues,
@@ -96,7 +81,18 @@ export function Form<
ignoreKeys = [],
options = {},
...props
}: FormProps<Schema, Data>) {
}: Omit<ComponentPropsWithoutRef<"form">, "onChange" | "onSubmit"> & {
schema: Schema;
validateOn?: "change" | "submit";
initialOpts?: LibTemplateOptions;
ignoreKeys?: string[];
onChange?: (data: Partial<Data>, name: string, value: any) => void;
onSubmit?: (data: Data) => void | Promise<void>;
onInvalidSubmit?: (errors: JsonError[], data: Partial<Data>) => void;
hiddenSubmit?: boolean;
options?: FormOptions;
initialValues?: Schema extends JSONSchema ? FromSchema<Schema> : never;
}) {
const [schema, initial] = omitSchema(_schema, ignoreKeys, _initialValues);
const lib = useMemo(() => new Draft2019(schema), [JSON.stringify(schema)]);
const initialValues = initial ?? lib.getTemplate(undefined, schema, initialOpts);

View File

@@ -1,7 +1,7 @@
import { useClickOutside } from "@mantine/hooks";
import { type ReactElement, cloneElement, useState } from "react";
import { type ComponentPropsWithoutRef, type ReactElement, cloneElement, useState } from "react";
import { twMerge } from "tailwind-merge";
import { useEvent } from "../../hooks/use-event";
import { useEvent } from "ui/hooks/use-event";
export type PopoverProps = {
className?: string;
@@ -10,6 +10,7 @@ export type PopoverProps = {
backdrop?: boolean;
target: (props: { toggle: () => void }) => ReactElement;
children: ReactElement<{ onClick: () => void }>;
overlayProps?: ComponentPropsWithoutRef<"div">;
};
export function Popover({
@@ -18,20 +19,21 @@ export function Popover({
defaultOpen = false,
backdrop = false,
position = "bottom-start",
className,
overlayProps,
className
}: PopoverProps) {
const [open, setOpen] = useState(defaultOpen);
const clickoutsideRef = useClickOutside(() => setOpen(false));
const toggle = useEvent((delay: number = 50) =>
setTimeout(() => setOpen((prev) => !prev), typeof delay === "number" ? delay : 0),
setTimeout(() => setOpen((prev) => !prev), typeof delay === "number" ? delay : 0)
);
const pos = {
"bottom-start": "mt-1 top-[100%]",
"bottom-end": "right-0 top-[100%] mt-1",
"top-start": "bottom-[100%] mb-1",
"top-end": "bottom-[100%] right-0 mb-1",
"top-end": "bottom-[100%] right-0 mb-1"
}[position];
return (
@@ -43,9 +45,11 @@ export function Popover({
{cloneElement(children as any, { onClick: toggle })}
{open && (
<div
{...overlayProps}
className={twMerge(
"animate-fade-in absolute z-20 flex flex-col bg-background border border-muted px-1 py-1 rounded-lg shadow-lg min-w-full max-w-20 backdrop-blur-sm",
"animate-fade-in absolute z-20 flex flex-col bg-background border border-muted px-1 py-1 rounded-lg shadow-lg backdrop-blur-sm min-w-0 max-w-20",
pos,
overlayProps?.className
)}
>
{target({ toggle })}

View File

@@ -1,2 +1,3 @@
export { default as Admin, type BkndAdminProps } from "./Admin";
export * from "./components/form/json-schema-form";
export { JsonViewer } from "./components/code/JsonViewer";

View File

@@ -30,7 +30,7 @@ const schema2 = {
}
},
required: ["age"]
};
} as const satisfies JSONSchema;
export default function JsonSchemaForm3() {
const { schema: _schema, config } = useBknd();
@@ -46,7 +46,9 @@ export default function JsonSchemaForm3() {
return (
<Scrollable>
<div className="flex flex-col p-3">
{/*<Form
<Form
onChange={(data) => console.log("change", data)}
onSubmit={(data) => console.log("submit", data)}
schema={{
type: "object",
properties: {
@@ -59,12 +61,14 @@ export default function JsonSchemaForm3() {
}
}
},
required: ["age"]
required: ["age"],
additionalProperties: false
}}
initialValues={{ name: "Peter", age: 20, deep: { nested: "hello" } }}
className="flex flex-col gap-3"
validateOn="change"
/>*/}
options={{ debug: true }}
/>
{/*<Form
schema={{
@@ -245,12 +249,12 @@ export default function JsonSchemaForm3() {
</Form>*/}
{/*<CustomMediaForm />*/}
<Form
{/*<Form
schema={schema.media}
initialValues={config.media as any}
onSubmit={console.log}
validateOn="change"
/>
/>*/}
{/*<Form
schema={removeKeyRecursively(schema.media, "pattern") as any}

View File

@@ -1,7 +1,7 @@
{
"compilerOptions": {
"types": ["bun-types", "@cloudflare/workers-types"],
"composite": true,
"composite": false,
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",