import { Tabs, TextInput, Textarea, Tooltip, Switch } from "@mantine/core"; import { objectCleanEmpty, omitKeys, ucFirstAllSnakeToPascalWithSpaces, s, stringIdentifier, } from "bknd/utils"; import { type TAppDataEntityFields, fieldsSchemaObject as originalFieldsSchemaObject, } from "data/data-schema"; import { omit } from "lodash-es"; import { forwardRef, memo, useEffect, useImperativeHandle } from "react"; import { type FieldArrayWithId, type UseFormReturn, useFieldArray, useForm } from "react-hook-form"; import { TbGripVertical, TbSettings, TbTrash } from "react-icons/tb"; import { twMerge } from "tailwind-merge"; import { Button } from "ui/components/buttons/Button"; import { IconButton } from "ui/components/buttons/IconButton"; import { JsonViewer } from "ui/components/code/JsonViewer"; import { MantineSwitch } from "ui/components/form/hook-form-mantine/MantineSwitch"; import { JsonSchemaForm } from "ui/components/form/json-schema"; import { type SortableItemProps, SortableList } from "ui/components/list/SortableList"; import { Popover } from "ui/components/overlay/Popover"; import { type TFieldSpec, fieldSpecs } from "ui/modules/data/components/fields-specs"; import { dataFieldsUiSchema } from "../../settings/routes/data.settings"; import { useRoutePathState } from "ui/hooks/use-route-path-state"; import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelect"; import type { TPrimaryFieldFormat } from "data/fields/PrimaryField"; import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; import ErrorBoundary from "ui/components/display/ErrorBoundary"; import { SchemaEditable } from "ui/client/bknd"; const fieldsSchemaObject = originalFieldsSchemaObject; const fieldsSchema = s.anyOf(Object.values(fieldsSchemaObject)); const fieldSchema = s.strictObject({ name: stringIdentifier, new: s.boolean({ const: true }).optional(), field: fieldsSchema, }); type TFieldSchema = s.Static; const schema = s.strictObject({ fields: s.array(fieldSchema), }); type TFieldsFormSchema = s.Static; const fieldTypes = Object.keys(fieldsSchemaObject); const defaultType = fieldTypes[0]; const commonProps = ["label", "description", "required", "fillable", "hidden", "virtual"]; function specificFieldSchema(type: keyof typeof fieldsSchemaObject) { //console.log("specificFieldSchema", type); return s.object( omitKeys(fieldsSchemaObject[type]?.properties.config.properties, commonProps as any), ); } export type EntityFieldsFormProps = { fields: TAppDataEntityFields; onChange?: (formData: TAppDataEntityFields) => void; sortable?: boolean; additionalFieldTypes?: (TFieldSpec & { onClick: () => void })[]; routePattern?: string; defaultPrimaryFormat?: TPrimaryFieldFormat; isNew?: boolean; readonly?: boolean; }; export type EntityFieldsFormRef = { getValues: () => TFieldsFormSchema; getData: () => TAppDataEntityFields; isValid: () => boolean; getErrors: () => any; reset: () => void; }; export const EntityFieldsForm = forwardRef( function EntityFieldsForm( { fields: _fields, sortable, additionalFieldTypes, routePattern, isNew, readonly, ...props }, ref, ) { const entityFields = Object.entries(_fields).map(([name, field]) => ({ name, field, })); const { control, formState: { isValid, errors }, getValues, watch, register, setValue, setError, reset, } = useForm({ mode: "all", resolver: standardSchemaResolver(schema), defaultValues: { fields: entityFields, } as TFieldsFormSchema, }); const { fields, append, remove, move } = useFieldArray({ control, name: "fields", }); function toCleanValues(formData: TFieldsFormSchema): TAppDataEntityFields { return Object.fromEntries( formData.fields.map((field) => [field.name, objectCleanEmpty(field.field)]), ); } useEffect(() => { if (props?.onChange) { watch((data: any) => { props?.onChange?.(toCleanValues(data)); }); } }, []); useImperativeHandle(ref, () => ({ reset, getValues: () => getValues(), getData: () => { return toCleanValues(getValues()); }, isValid: () => isValid, getErrors: () => errors, })); function handleAppend(_type: keyof typeof fieldsSchemaObject) { append({ name: "", new: true, field: { type: _type, config: fieldsSchemaObject[_type]?.properties.config.template() as any, }, }); } const formProps = { watch, register, setValue, getValues, control, setError, }; return ( <>
{sortable ? ( item.id} disableIndices={[0]} renderItem={({ dnd, ...props }, index) => ( )} /> ) : (
{fields.map((field, index) => ( ))}
)} ( { handleAppend(type as any); }} /> )} >
); }, ); const SelectType = ({ onSelect, additionalFieldTypes = [], onSelected, }: { onSelect: (type: string) => void; additionalFieldTypes?: (TFieldSpec & { onClick?: () => void })[]; onSelected?: () => void; }) => { const types: (TFieldSpec & { onClick?: () => void })[] = fieldSpecs.filter( (s) => s.addable !== false, ); if (additionalFieldTypes) { types.push(...additionalFieldTypes); } return (
{types.map((type) => ( ))}
); }; const Debug = ({ watch, errors }) => { return (
{Object.entries(errors).map(([key, value]) => (

{/* @ts-ignore */} {key}: {value.message}

))}
{JSON.stringify(watch(), null, 2)}
); }; const EntityFieldMemo = memo(EntityField, (prev, next) => { return prev.field.id !== next.field.id; }); function EntityField({ field, index, form: { watch, register, setValue, getValues, control, setError }, remove, errors, dnd, routePattern, primary, readonly, }: { field: FieldArrayWithId; index: number; form: Pick< UseFormReturn, "watch" | "register" | "setValue" | "getValues" | "control" | "setError" >; remove: (index: number) => void; errors: any; dnd?: SortableItemProps; routePattern?: string; primary?: { defaultFormat?: TPrimaryFieldFormat; editable?: boolean; }; readonly?: boolean; }) { const prefix = `fields.${index}.field` as const; const type = field.field.type; const name = watch(`fields.${index}.name`); const { active, toggle } = useRoutePathState(routePattern ?? "", name); const fieldSpec = fieldSpecs.find((s) => s.type === type)!; const specificData = omit(field.field.config, commonProps); const disabled = fieldSpec.disabled || []; const hidden = fieldSpec.hidden || []; const dragDisabled = index === 0; const hasErrors = !!errors?.fields?.[index]; const is_primary = type === "primary"; function handleDelete(index: number) { return () => { if (name.length === 0) { remove(index); toggle(); } else if (window.confirm(`Sure to delete "${name}"?`)) { remove(index); toggle(); } }; } //console.log("register", register(`${prefix}.config.required`)); const dndProps = dnd ? { ...dnd.provided.draggableProps, ref: dnd.provided.innerRef } : {}; return (
{dnd ? (
) : null}
{field.new ? ( ) : (
{name} {field.field.config?.label && ( {field.field.config?.label} )}
)}
{is_primary ? ( <> ) : ( <> Required )}
toggle()} variant={active ? "primary" : "ghost"} />
{active && (
{/*
{JSON.stringify(field, null, 2)}
*/} General {ucFirstAllSnakeToPascalWithSpaces(type)} Visiblity
Code