mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
improved media settings implementation
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import type { JsonError } from "json-schema-library";
|
||||
import type { JSONSchema } from "json-schema-to-ts";
|
||||
import { type ChangeEvent, type ReactNode, createContext, useContext, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import * as Formy from "ui/components/form/Formy";
|
||||
import { FieldComponent, Field as FormField, type FieldProps as FormFieldProps } from "./Field";
|
||||
import { FormContextOverride, useFieldContext } from "./Form";
|
||||
@@ -19,19 +21,18 @@ export type AnyOfFieldContext = {
|
||||
selected: number | null;
|
||||
select: (index: number | null) => void;
|
||||
options: string[];
|
||||
errors: JsonError[];
|
||||
selectSchema: any;
|
||||
};
|
||||
|
||||
const AnyOfContext = createContext<AnyOfFieldContext>(undefined!);
|
||||
|
||||
export const useAnyOfContext = () => {
|
||||
const ctx = useContext(AnyOfContext);
|
||||
if (!ctx) throw new Error("useAnyOfContext: no context");
|
||||
return ctx;
|
||||
return useContext(AnyOfContext);
|
||||
};
|
||||
|
||||
const Root = ({ path = "", schema: _schema, children }: AnyOfFieldRootProps) => {
|
||||
const { setValue, pointer, lib, value, ...ctx } = useFieldContext(path);
|
||||
const { setValue, pointer, lib, value, errors, ...ctx } = useFieldContext(path);
|
||||
const schema = _schema ?? ctx.schema;
|
||||
if (!schema) return `AnyOfField(${path}): no schema ${pointer}`;
|
||||
const [matchedIndex, schemas = []] = getMultiSchemaMatched(schema, value);
|
||||
@@ -40,6 +41,7 @@ const Root = ({ path = "", schema: _schema, children }: AnyOfFieldRootProps) =>
|
||||
const selectSchema = {
|
||||
enum: options
|
||||
};
|
||||
//console.log("AnyOf:root", { value, matchedIndex, selected, schema });
|
||||
|
||||
const selectedSchema =
|
||||
selected !== null ? (schemas[selected] as Exclude<JSONSchema, boolean>) : undefined;
|
||||
@@ -51,8 +53,19 @@ const Root = ({ path = "", schema: _schema, children }: AnyOfFieldRootProps) =>
|
||||
|
||||
return (
|
||||
<AnyOfContext.Provider
|
||||
value={{ selected, select, options, selectSchema, path, schema, schemas, selectedSchema }}
|
||||
value={{
|
||||
selected,
|
||||
select,
|
||||
options,
|
||||
selectSchema,
|
||||
path,
|
||||
schema,
|
||||
schemas,
|
||||
selectedSchema,
|
||||
errors
|
||||
}}
|
||||
>
|
||||
{/*<pre>{JSON.stringify({ value, selected, errors: errors.length }, null, 2)}</pre>*/}
|
||||
{children}
|
||||
</AnyOfContext.Provider>
|
||||
);
|
||||
@@ -62,7 +75,7 @@ const Select = () => {
|
||||
const { selected, select, path, schema, selectSchema } = useAnyOfContext();
|
||||
|
||||
function handleSelect(e: ChangeEvent<HTMLInputElement>) {
|
||||
console.log("selected", e.target.value);
|
||||
//console.log("selected", e.target.value);
|
||||
const i = e.target.value ? Number(e.target.value) : null;
|
||||
select(i);
|
||||
}
|
||||
@@ -81,11 +94,13 @@ const Select = () => {
|
||||
};
|
||||
|
||||
const Field = ({ name, label, ...props }: Partial<FormFieldProps>) => {
|
||||
const { selected, selectedSchema, path } = useAnyOfContext();
|
||||
const { selected, selectedSchema, path, errors } = useAnyOfContext();
|
||||
if (selected === null) return null;
|
||||
return (
|
||||
<FormContextOverride path={path} schema={selectedSchema} overrideData>
|
||||
<FormField key={`${path}_${selected}`} name={""} label={false} {...props} />
|
||||
<div className={twMerge(errors.length > 0 && "bg-red-500/10")}>
|
||||
<FormField key={`${path}_${selected}`} name={""} label={false} {...props} />
|
||||
</div>
|
||||
</FormContextOverride>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ArrayField } from "./ArrayField";
|
||||
import { FieldWrapper } from "./FieldWrapper";
|
||||
import { useFieldContext } from "./Form";
|
||||
import { ObjectField } from "./ObjectField";
|
||||
import { coerce, isType } from "./utils";
|
||||
import { coerce, isType, isTypeSchema } from "./utils";
|
||||
|
||||
export type FieldProps = {
|
||||
name: string;
|
||||
@@ -18,7 +18,8 @@ export type FieldProps = {
|
||||
export const Field = ({ name, schema: _schema, onChange, label: _label, hidden }: FieldProps) => {
|
||||
const { pointer, value, errors, setValue, required, ...ctx } = useFieldContext(name);
|
||||
const schema = _schema ?? ctx.schema;
|
||||
if (!schema) return `"${name}" (${pointer}) has no schema`;
|
||||
if (!isTypeSchema(schema)) return <Pre>{pointer} has no schema</Pre>;
|
||||
//console.log("field", name, schema);
|
||||
|
||||
if (isType(schema.type, "object")) {
|
||||
return <ObjectField path={name} schema={schema} />;
|
||||
@@ -38,7 +39,7 @@ export const Field = ({ name, schema: _schema, onChange, label: _label, hidden }
|
||||
|
||||
const value = coerce(e.target.value, schema as any, { required });
|
||||
//console.log("handleChange", pointer, e.target.value, { value });
|
||||
if (!value && !required) {
|
||||
if (typeof value === "undefined" && !required) {
|
||||
ctx.deleteValue(pointer);
|
||||
} else {
|
||||
setValue(pointer, value);
|
||||
@@ -58,7 +59,6 @@ export const Field = ({ name, schema: _schema, onChange, label: _label, hidden }
|
||||
<FieldComponent
|
||||
schema={schema}
|
||||
name={pointer}
|
||||
placeholder={pointer}
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
@@ -68,6 +68,10 @@ export const Field = ({ name, schema: _schema, onChange, label: _label, hidden }
|
||||
);
|
||||
};
|
||||
|
||||
export const Pre = ({ children }) => (
|
||||
<pre className="dark:bg-red-950 bg-red-100 rounded-md px-3 py-1.5">{children}</pre>
|
||||
);
|
||||
|
||||
export const FieldComponent = ({
|
||||
schema,
|
||||
...props
|
||||
@@ -82,12 +86,16 @@ export const FieldComponent = ({
|
||||
options = schema.enum.map((v, i) => ({ value: i, label: v }));
|
||||
}
|
||||
|
||||
return <Formy.Select {...(props as any)} options={options} />;
|
||||
return <Formy.Select id={props.name} {...(props as any)} options={options} />;
|
||||
}
|
||||
|
||||
if (isType(schema.type, ["number", "integer"])) {
|
||||
return <Formy.Input type="number" {...props} value={props.value ?? ""} />;
|
||||
return <Formy.Input type="number" id={props.name} {...props} value={props.value ?? ""} />;
|
||||
}
|
||||
|
||||
return <Formy.Input {...props} value={props.value ?? ""} />;
|
||||
if (isType(schema.type, "boolean")) {
|
||||
return <Formy.Switch id={props.name} {...(props as any)} checked={props.value as any} />;
|
||||
}
|
||||
|
||||
return <Formy.Input id={props.name} {...props} value={props.value ?? ""} />;
|
||||
};
|
||||
|
||||
@@ -43,7 +43,7 @@ export function FieldWrapper({
|
||||
as={wrapper === "fieldset" ? "fieldset" : "div"}
|
||||
className={hidden ? "hidden" : "relative"}
|
||||
>
|
||||
<div className="absolute right-0 top-0">
|
||||
{/*<div className="absolute right-0 top-0">
|
||||
<Popover>
|
||||
<Popover.Target>
|
||||
<IconButton Icon={IconBug} size="xs" className="opacity-30" />
|
||||
@@ -56,11 +56,11 @@ export function FieldWrapper({
|
||||
/>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>*/}
|
||||
|
||||
{label && (
|
||||
<Formy.Label as={wrapper === "fieldset" ? "legend" : "label"}>
|
||||
{label} {required ? "*" : ""}
|
||||
<Formy.Label as={wrapper === "fieldset" ? "legend" : "label"} htmlFor={pointer}>
|
||||
{label} {required && <span className="font-medium opacity-30">*</span>}
|
||||
</Formy.Label>
|
||||
)}
|
||||
<div className="flex flex-row gap-2">
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
useState
|
||||
} from "react";
|
||||
import { Field } from "./Field";
|
||||
import { isRequired, normalizePath, prefixPointer } from "./utils";
|
||||
import { isRequired, normalizePath, omitSchema, prefixPointer } from "./utils";
|
||||
|
||||
type JSONSchema = Exclude<$JSONSchema, boolean>;
|
||||
|
||||
@@ -27,7 +27,10 @@ export type FormProps<
|
||||
validateOn?: "change" | "submit";
|
||||
initialValues?: Partial<Data>;
|
||||
initialOpts?: LibTemplateOptions;
|
||||
ignoreKeys?: string[];
|
||||
onChange?: (data: Partial<Data>, name: string, value: any) => void;
|
||||
onSubmit?: (data: Partial<Data>) => void | Promise<void>;
|
||||
onInvalidSubmit?: (errors: JsonError[], data: Partial<Data>) => void;
|
||||
hiddenSubmit?: boolean;
|
||||
};
|
||||
|
||||
@@ -38,6 +41,7 @@ export type FormContext<Data> = {
|
||||
deleteValue: (pointer: string) => void;
|
||||
errors: JsonError[];
|
||||
dirty: boolean;
|
||||
submitting: boolean;
|
||||
schema: JSONSchema;
|
||||
lib: Draft2019;
|
||||
};
|
||||
@@ -48,27 +52,48 @@ export function Form<
|
||||
Schema extends JSONSchema = JSONSchema,
|
||||
Data = Schema extends JSONSchema ? FromSchema<JSONSchema> : any
|
||||
>({
|
||||
schema,
|
||||
schema: _schema,
|
||||
initialValues: _initialValues,
|
||||
initialOpts,
|
||||
children,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onInvalidSubmit,
|
||||
validateOn = "submit",
|
||||
hiddenSubmit = true,
|
||||
ignoreKeys = [],
|
||||
...props
|
||||
}: FormProps<Schema, Data>) {
|
||||
const [schema, initial] = omitSchema(_schema, ignoreKeys, _initialValues);
|
||||
const lib = new Draft2019(schema);
|
||||
const initialValues = _initialValues ?? lib.getTemplate(undefined, schema, initialOpts);
|
||||
const initialValues = initial ?? lib.getTemplate(undefined, schema, initialOpts);
|
||||
const [data, setData] = useState<Partial<Data>>(initialValues);
|
||||
const [dirty, setDirty] = useState<boolean>(false);
|
||||
const formRef = useRef<HTMLFormElement | null>(null);
|
||||
const [errors, setErrors] = useState<JsonError[]>([]);
|
||||
const [submitting, setSubmitting] = useState<boolean>(false);
|
||||
const formRef = useRef<HTMLFormElement | null>(null);
|
||||
|
||||
async function handleChange(e: FormEvent<HTMLFormElement>) {}
|
||||
|
||||
// @ts-ignore
|
||||
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
if (onSubmit) {
|
||||
e.preventDefault();
|
||||
setSubmitting(true);
|
||||
|
||||
try {
|
||||
const { data, errors } = validate();
|
||||
if (errors.length === 0) {
|
||||
await onSubmit(data);
|
||||
} else {
|
||||
console.log("invalid", errors);
|
||||
onInvalidSubmit?.(errors, data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
setSubmitting(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function setValue(pointer: string, value: any) {
|
||||
@@ -99,6 +124,8 @@ export function Form<
|
||||
|
||||
if (validateOn === "change") {
|
||||
validate();
|
||||
} else if (errors.length > 0) {
|
||||
validate();
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
@@ -113,6 +140,7 @@ export function Form<
|
||||
const context = {
|
||||
data: data ?? {},
|
||||
dirty,
|
||||
submitting,
|
||||
setData,
|
||||
setValue,
|
||||
deleteValue,
|
||||
@@ -123,20 +151,16 @@ export function Form<
|
||||
//console.log("context", context);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form {...props} ref={formRef} onChange={handleChange} onSubmit={handleSubmit}>
|
||||
<FormContext.Provider value={context}>
|
||||
{children ? children : <Field name="" />}
|
||||
</FormContext.Provider>
|
||||
{hiddenSubmit && (
|
||||
<button style={{ visibility: "hidden" }} type="submit">
|
||||
Submit
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
<pre>{JSON.stringify(errors, null, 2)}</pre>
|
||||
</>
|
||||
<form {...props} ref={formRef} onSubmit={handleSubmit}>
|
||||
<FormContext.Provider value={context}>
|
||||
{children ? children : <Field name="" />}
|
||||
</FormContext.Provider>
|
||||
{hiddenSubmit && (
|
||||
<button style={{ visibility: "hidden" }} type="submit">
|
||||
Submit
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { JsonError } from "json-schema-library";
|
||||
import type { JSONSchema } from "json-schema-to-ts";
|
||||
import { AnyOfField } from "./AnyOfField";
|
||||
import { Field } from "./Field";
|
||||
@@ -17,7 +18,7 @@ export const ObjectField = ({
|
||||
label: _label,
|
||||
wrapperProps = {}
|
||||
}: ObjectFieldProps) => {
|
||||
const { errors, ...ctx } = useFieldContext(path);
|
||||
const ctx = useFieldContext(path);
|
||||
const schema = _schema ?? ctx.schema;
|
||||
if (!schema) return "ObjectField: no schema";
|
||||
const properties = schema.properties ?? {};
|
||||
@@ -25,8 +26,8 @@ export const ObjectField = ({
|
||||
return (
|
||||
<FieldWrapper
|
||||
pointer={path}
|
||||
errors={errors}
|
||||
schema={schema}
|
||||
errors={ctx.errors}
|
||||
schema={{ ...schema, description: undefined }}
|
||||
wrapper="fieldset"
|
||||
{...wrapperProps}
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { autoFormatString } from "core/utils";
|
||||
import { Draft2019, type JsonSchema } from "json-schema-library";
|
||||
import type { JSONSchema } from "json-schema-to-ts";
|
||||
import type { JSONSchemaType } from "json-schema-to-ts/lib/types/definitions/jsonSchema";
|
||||
import { set } from "lodash-es";
|
||||
import { omit, set } from "lodash-es";
|
||||
import type { FormEvent } from "react";
|
||||
|
||||
export function getFormTarget(e: FormEvent<HTMLFormElement>) {
|
||||
@@ -94,7 +94,7 @@ export function coerce(
|
||||
case "number":
|
||||
return Number(value);
|
||||
case "boolean":
|
||||
return ["true", "1", 1, "on"].includes(value);
|
||||
return ["true", "1", 1, "on", true].includes(value);
|
||||
case "null":
|
||||
return null;
|
||||
}
|
||||
@@ -154,7 +154,7 @@ export function isRequired(pointer: string, schema: JSONSchema, data?: any) {
|
||||
return !!required;
|
||||
}
|
||||
|
||||
type TType = JSONSchemaType | JSONSchemaType[] | readonly JSONSchemaType[] | undefined;
|
||||
type TType = JSONSchemaType | JSONSchemaType[] | readonly JSONSchemaType[] | string | undefined;
|
||||
export function isType(_type: TType, _compare: TType) {
|
||||
if (!_type || !_compare) return false;
|
||||
const type = Array.isArray(_type) ? _type : [_type];
|
||||
@@ -200,3 +200,26 @@ export function removeKeyRecursively<Given extends object>(obj: Given, keyToRemo
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function omitSchema<Given extends JSONSchema>(_schema: Given, keys: string[], _data?: any) {
|
||||
if (typeof _schema !== "object" || !("properties" in _schema) || keys.length === 0)
|
||||
return [_schema, _data];
|
||||
const schema = JSON.parse(JSON.stringify(_schema));
|
||||
const data = _data ? JSON.parse(JSON.stringify(_data)) : undefined;
|
||||
|
||||
const updated = {
|
||||
...schema,
|
||||
properties: omit(schema.properties, keys)
|
||||
};
|
||||
if (updated.required) {
|
||||
updated.required = updated.required.filter((key) => !keys.includes(key as any));
|
||||
}
|
||||
|
||||
const reducedConfig = omit(data, keys) as any;
|
||||
|
||||
return [updated, reducedConfig];
|
||||
}
|
||||
|
||||
export function isTypeSchema(schema?: JSONSchema): schema is Exclude<JSONSchema, boolean> {
|
||||
return typeof schema === "object" && "type" in schema && !isType(schema.type, "error");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user