mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 12:56:05 +00:00
form: fix popover, improve form types
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.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.",
|
"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": {
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
"oauth4webapi": "^2.11.1",
|
"oauth4webapi": "^2.11.1",
|
||||||
"object-path-immutable": "^4.1.2",
|
"object-path-immutable": "^4.1.2",
|
||||||
"radix-ui": "^1.1.2",
|
"radix-ui": "^1.1.2",
|
||||||
|
"json-schema-to-ts": "^3.1.1",
|
||||||
"swr": "^2.2.5"
|
"swr": "^2.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -75,7 +76,6 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"esbuild-postcss": "^0.0.4",
|
"esbuild-postcss": "^0.0.4",
|
||||||
"jotai": "^2.10.1",
|
"jotai": "^2.10.1",
|
||||||
"json-schema-to-ts": "^3.1.1",
|
|
||||||
"open": "^10.1.0",
|
"open": "^10.1.0",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { IconCopy } from "@tabler/icons-react";
|
|
||||||
import { TbCopy } from "react-icons/tb";
|
import { TbCopy } from "react-icons/tb";
|
||||||
import { JsonView } from "react-json-view-lite";
|
import { JsonView } from "react-json-view-lite";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const ArrayField = ({
|
|||||||
onChange={(e: any) => {
|
onChange={(e: any) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const selected = Array.from(e.target.selectedOptions).map((o) => o.value);
|
const selected = Array.from(e.target.selectedOptions).map((o) => o.value);
|
||||||
setValue(pointer, selected);
|
setValue(ctx.path, selected);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FieldWrapper>
|
</FieldWrapper>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Popover } from "@mantine/core";
|
|
||||||
import { IconBug } from "@tabler/icons-react";
|
import { IconBug } from "@tabler/icons-react";
|
||||||
import type { JsonSchema } from "json-schema-library";
|
import type { JsonSchema } from "json-schema-library";
|
||||||
import { Children, type ReactElement, type ReactNode, cloneElement, isValidElement } from "react";
|
import { Children, type ReactElement, type ReactNode, cloneElement, isValidElement } from "react";
|
||||||
@@ -10,6 +9,7 @@ import {
|
|||||||
useFormError,
|
useFormError,
|
||||||
useFormValue
|
useFormValue
|
||||||
} from "ui/components/form/json-schema-form/Form";
|
} from "ui/components/form/json-schema-form/Form";
|
||||||
|
import { Popover } from "ui/components/overlay/Popover";
|
||||||
import { getLabel } from "./utils";
|
import { getLabel } from "./utils";
|
||||||
|
|
||||||
export type FieldwrapperProps = {
|
export type FieldwrapperProps = {
|
||||||
@@ -62,6 +62,7 @@ export function FieldWrapper({
|
|||||||
{label} {required && <span className="font-medium opacity-30">*</span>}
|
{label} {required && <span className="font-medium opacity-30">*</span>}
|
||||||
</Formy.Label>
|
</Formy.Label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<div className="flex flex-1 flex-col gap-3">
|
<div className="flex flex-1 flex-col gap-3">
|
||||||
{Children.count(children) === 1 && isValidElement(children)
|
{Children.count(children) === 1 && isValidElement(children)
|
||||||
@@ -96,14 +97,15 @@ const FieldDebug = ({
|
|||||||
const errors = useFormError(name, { strict: true });
|
const errors = useFormError(name, { strict: true });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute right-0 top-0">
|
<div className="absolute top-0 right-0">
|
||||||
{/* @todo: use radix */}
|
<Popover
|
||||||
<Popover>
|
overlayProps={{
|
||||||
<Popover.Target>
|
className: "max-w-none"
|
||||||
<IconButton Icon={IconBug} size="xs" className="opacity-30" />
|
}}
|
||||||
</Popover.Target>
|
position="bottom-end"
|
||||||
<Popover.Dropdown>
|
target={({ toggle }) => (
|
||||||
<JsonViewer
|
<JsonViewer
|
||||||
|
className="bg-background pr-3 text-sm"
|
||||||
json={{
|
json={{
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
@@ -112,9 +114,10 @@ const FieldDebug = ({
|
|||||||
errors
|
errors
|
||||||
}}
|
}}
|
||||||
expand={6}
|
expand={6}
|
||||||
className="p-0"
|
|
||||||
/>
|
/>
|
||||||
</Popover.Dropdown>
|
)}
|
||||||
|
>
|
||||||
|
<IconButton Icon={IconBug} size="xs" className="opacity-30" />
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -43,25 +43,10 @@ type FormState<Data = any> = {
|
|||||||
data: Data;
|
data: Data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FormProps<
|
type FormOptions = {
|
||||||
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;
|
debug?: boolean;
|
||||||
keepEmpty?: boolean;
|
keepEmpty?: boolean;
|
||||||
};
|
};
|
||||||
initialValues?: InitialData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FormContext<Data> = {
|
export type FormContext<Data> = {
|
||||||
setData: (data: Data) => void;
|
setData: (data: Data) => void;
|
||||||
@@ -72,7 +57,7 @@ export type FormContext<Data> = {
|
|||||||
submitting: boolean;
|
submitting: boolean;
|
||||||
schema: LibJsonSchema;
|
schema: LibJsonSchema;
|
||||||
lib: Draft2019;
|
lib: Draft2019;
|
||||||
options: FormProps["options"];
|
options: FormOptions;
|
||||||
root: string;
|
root: string;
|
||||||
_formStateAtom: PrimitiveAtom<FormState<Data>>;
|
_formStateAtom: PrimitiveAtom<FormState<Data>>;
|
||||||
};
|
};
|
||||||
@@ -81,8 +66,8 @@ const FormContext = createContext<FormContext<any>>(undefined!);
|
|||||||
FormContext.displayName = "FormContext";
|
FormContext.displayName = "FormContext";
|
||||||
|
|
||||||
export function Form<
|
export function Form<
|
||||||
Schema extends JSONSchema = JSONSchema,
|
const Schema extends JSONSchema,
|
||||||
Data = Schema extends JSONSchema ? FromSchema<Schema> : any
|
const Data = Schema extends JSONSchema ? FromSchema<Schema> : any
|
||||||
>({
|
>({
|
||||||
schema: _schema,
|
schema: _schema,
|
||||||
initialValues: _initialValues,
|
initialValues: _initialValues,
|
||||||
@@ -96,7 +81,18 @@ export function Form<
|
|||||||
ignoreKeys = [],
|
ignoreKeys = [],
|
||||||
options = {},
|
options = {},
|
||||||
...props
|
...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 [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);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useClickOutside } from "@mantine/hooks";
|
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 { twMerge } from "tailwind-merge";
|
||||||
import { useEvent } from "../../hooks/use-event";
|
import { useEvent } from "ui/hooks/use-event";
|
||||||
|
|
||||||
export type PopoverProps = {
|
export type PopoverProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -10,6 +10,7 @@ export type PopoverProps = {
|
|||||||
backdrop?: boolean;
|
backdrop?: boolean;
|
||||||
target: (props: { toggle: () => void }) => ReactElement;
|
target: (props: { toggle: () => void }) => ReactElement;
|
||||||
children: ReactElement<{ onClick: () => void }>;
|
children: ReactElement<{ onClick: () => void }>;
|
||||||
|
overlayProps?: ComponentPropsWithoutRef<"div">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Popover({
|
export function Popover({
|
||||||
@@ -18,20 +19,21 @@ export function Popover({
|
|||||||
defaultOpen = false,
|
defaultOpen = false,
|
||||||
backdrop = false,
|
backdrop = false,
|
||||||
position = "bottom-start",
|
position = "bottom-start",
|
||||||
className,
|
overlayProps,
|
||||||
|
className
|
||||||
}: PopoverProps) {
|
}: PopoverProps) {
|
||||||
const [open, setOpen] = useState(defaultOpen);
|
const [open, setOpen] = useState(defaultOpen);
|
||||||
const clickoutsideRef = useClickOutside(() => setOpen(false));
|
const clickoutsideRef = useClickOutside(() => setOpen(false));
|
||||||
|
|
||||||
const toggle = useEvent((delay: number = 50) =>
|
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 = {
|
const pos = {
|
||||||
"bottom-start": "mt-1 top-[100%]",
|
"bottom-start": "mt-1 top-[100%]",
|
||||||
"bottom-end": "right-0 top-[100%] mt-1",
|
"bottom-end": "right-0 top-[100%] mt-1",
|
||||||
"top-start": "bottom-[100%] mb-1",
|
"top-start": "bottom-[100%] mb-1",
|
||||||
"top-end": "bottom-[100%] right-0 mb-1",
|
"top-end": "bottom-[100%] right-0 mb-1"
|
||||||
}[position];
|
}[position];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -43,9 +45,11 @@ export function Popover({
|
|||||||
{cloneElement(children as any, { onClick: toggle })}
|
{cloneElement(children as any, { onClick: toggle })}
|
||||||
{open && (
|
{open && (
|
||||||
<div
|
<div
|
||||||
|
{...overlayProps}
|
||||||
className={twMerge(
|
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,
|
pos,
|
||||||
|
overlayProps?.className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{target({ toggle })}
|
{target({ toggle })}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export { default as Admin, type BkndAdminProps } from "./Admin";
|
export { default as Admin, type BkndAdminProps } from "./Admin";
|
||||||
export * from "./components/form/json-schema-form";
|
export * from "./components/form/json-schema-form";
|
||||||
|
export { JsonViewer } from "./components/code/JsonViewer";
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const schema2 = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: ["age"]
|
required: ["age"]
|
||||||
};
|
} as const satisfies JSONSchema;
|
||||||
|
|
||||||
export default function JsonSchemaForm3() {
|
export default function JsonSchemaForm3() {
|
||||||
const { schema: _schema, config } = useBknd();
|
const { schema: _schema, config } = useBknd();
|
||||||
@@ -46,7 +46,9 @@ export default function JsonSchemaForm3() {
|
|||||||
return (
|
return (
|
||||||
<Scrollable>
|
<Scrollable>
|
||||||
<div className="flex flex-col p-3">
|
<div className="flex flex-col p-3">
|
||||||
{/*<Form
|
<Form
|
||||||
|
onChange={(data) => console.log("change", data)}
|
||||||
|
onSubmit={(data) => console.log("submit", data)}
|
||||||
schema={{
|
schema={{
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -59,12 +61,14 @@ export default function JsonSchemaForm3() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: ["age"]
|
required: ["age"],
|
||||||
|
additionalProperties: false
|
||||||
}}
|
}}
|
||||||
initialValues={{ name: "Peter", age: 20, deep: { nested: "hello" } }}
|
initialValues={{ name: "Peter", age: 20, deep: { nested: "hello" } }}
|
||||||
className="flex flex-col gap-3"
|
className="flex flex-col gap-3"
|
||||||
validateOn="change"
|
validateOn="change"
|
||||||
/>*/}
|
options={{ debug: true }}
|
||||||
|
/>
|
||||||
|
|
||||||
{/*<Form
|
{/*<Form
|
||||||
schema={{
|
schema={{
|
||||||
@@ -245,12 +249,12 @@ export default function JsonSchemaForm3() {
|
|||||||
</Form>*/}
|
</Form>*/}
|
||||||
|
|
||||||
{/*<CustomMediaForm />*/}
|
{/*<CustomMediaForm />*/}
|
||||||
<Form
|
{/*<Form
|
||||||
schema={schema.media}
|
schema={schema.media}
|
||||||
initialValues={config.media as any}
|
initialValues={config.media as any}
|
||||||
onSubmit={console.log}
|
onSubmit={console.log}
|
||||||
validateOn="change"
|
validateOn="change"
|
||||||
/>
|
/>*/}
|
||||||
|
|
||||||
{/*<Form
|
{/*<Form
|
||||||
schema={removeKeyRecursively(schema.media, "pattern") as any}
|
schema={removeKeyRecursively(schema.media, "pattern") as any}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": ["bun-types", "@cloudflare/workers-types"],
|
"types": ["bun-types", "@cloudflare/workers-types"],
|
||||||
"composite": true,
|
"composite": false,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|||||||
Reference in New Issue
Block a user