"use client" import { useEffect, useMemo, useState } from "react" import { zodResolver } from "@hookform/resolvers/zod" import type { Resolver } from "react-hook-form" import { useForm } from "react-hook-form" import { z } from "zod/v4" import { ProjectDialog } from "@/components/project-dialog" import { Button } from "@/components/ui/button" import { buildDashboardCrudFormValues, normalizeDashboardCrudSubmitValues, type DashboardCrudFormInputValue, type DashboardCrudFormField, type DashboardCrudFormOption, } from "./dashboard-crud-utils" import { DashboardCrudFieldControl } from "./dashboard-crud-field-control" type DashboardCrudFormDialogProps = { open: boolean saving: boolean item: TItem | null itemId: number | null fields: DashboardCrudFormField[] fetchDetail?: (id: number) => Promise transformSubmitValues?: ( values: Record, context: { mode: "create" | "edit"; item: TItem | null } ) => TPayload labels: { createTitle: string editTitle: string create: string save: string saving: string cancel: string loadingDetail: string required: string invalidNumber: string minValue: (min: number) => string maxValue: (max: number) => string } onOpenChange: (open: boolean) => void onSubmit: (payload: TPayload) => Promise } function createFormSchema( fields: ReadonlyArray>, labels: DashboardCrudFormDialogProps["labels"] ) { const shape: Record = {} fields.forEach((field) => { if (field.type === "section" || field.type === "group") { return } if (field.type === "switch" || field.type === "checkbox") { shape[field.name] = field.required ? z.boolean().refine((value) => value, field.requiredMessage ?? labels.required) : z.boolean() return } if (field.type === "multiSelect") { shape[field.name] = field.required ? z.array(z.string()).min(1, field.requiredMessage ?? labels.required) : z.array(z.string()) return } if (field.type === "custom") { shape[field.name] = z.any() return } let schema = field.trim ? z.string().trim() : z.string() if (field.required) { schema = schema.min(1, field.requiredMessage ?? labels.required) } if (field.pattern) { schema = schema.regex(field.pattern, field.patternMessage ?? labels.required) } if (field.type === "number") { schema = schema.refine((value) => { if (!value.trim()) return !field.required return Number.isFinite(Number(value)) }, labels.invalidNumber) if (field.min !== undefined) { schema = schema.refine((value) => !value.trim() || Number(value) >= field.min!, { message: labels.minValue(field.min), }) } if (field.max !== undefined) { schema = schema.refine((value) => !value.trim() || Number(value) <= field.max!, { message: labels.maxValue(field.max), }) } } if (field.type === "json" && field.validateJson !== false) { schema = schema.refine((value) => { if (!value.trim()) return !field.required try { JSON.parse(value) return true } catch { return false } }, field.patternMessage ?? labels.required) } shape[field.name] = schema }) return z.object(shape) } function normalizeFormLayoutFields(fields: DashboardCrudFormField[]) { return fields.map((field) => ["textarea", "json", "code", "custom", "section", "group"].includes( field.type ?? "" ) ? { ...field, colSpan: field.colSpan ?? 2 } : field ) } export function DashboardCrudFormDialog({ open, saving, item, itemId, fields, fetchDetail, transformSubmitValues, labels, onOpenChange, onSubmit, }: DashboardCrudFormDialogProps) { const layoutFields = useMemo(() => normalizeFormLayoutFields(fields), [fields]) const initialValues = useMemo( () => buildDashboardCrudFormValues(fields, item), [fields, item] ) const schema = useMemo(() => createFormSchema(fields, labels), [fields, labels]) const resolver = useMemo( () => zodResolver(schema as never) as Resolver< Record, undefined, Record >, [schema] ) const [fetchedDetail, setFetchedDetail] = useState<{ id: number item: TItem } | null>(null) const [fieldOptions, setFieldOptions] = useState< Record> >({}) const form = useForm< Record, undefined, Record >({ resolver, defaultValues: initialValues, }) const { control, handleSubmit, register, reset, formState: { errors }, } = form const formId = "dashboard-crud-edit-form" const mode = itemId ? "edit" : "create" const loadingDetail = Boolean( itemId && fetchDetail && fetchedDetail?.id !== itemId ) const detailItem = itemId && fetchDetail && fetchedDetail?.id === itemId ? fetchedDetail.item : item useEffect(() => { let cancelled = false if (!itemId || !fetchDetail) { reset(initialValues) return } void fetchDetail(itemId) .then((detail) => { if (cancelled) return setFetchedDetail({ id: itemId, item: detail }) reset(buildDashboardCrudFormValues(fields, detail)) }) return () => { cancelled = true } }, [fetchDetail, fields, initialValues, item, itemId, reset]) useEffect(() => { if (!open) return let cancelled = false const loaders = fields .filter( (field) => (field.type === "select" || field.type === "multiSelect") && field.loadOptions ) .map(async (field) => { const options = await field.loadOptions!() return [field.name, options] as const }) if (loaders.length === 0) return void Promise.all(loaders).then((entries) => { if (cancelled) return setFieldOptions((current) => ({ ...current, ...Object.fromEntries(entries), })) }) return () => { cancelled = true } }, [fields, open]) async function submit(values: Record) { const normalizedValues = normalizeDashboardCrudSubmitValues(fields, values) const payload = transformSubmitValues ? transformSubmitValues(normalizedValues, { mode, item: detailItem }) : (normalizedValues as TPayload) await onSubmit(payload) } if (!open || fields.length === 0) { return null } return ( } > {loadingDetail ? (
{labels.loadingDetail}
) : (
{layoutFields.map((field) => ( ))} )}
) }