'use client' import * as React from 'react' import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@admin/components/ui/accordion' import { Button } from '@admin/components/ui/button' import { Card, CardContent, CardHeader } from '@admin/components/ui/card' import { FormDescription, FormField, FormItem, FormLabel } from '@admin/components/ui/form' import { cloneDefaultValue } from '@admin/utils/form/clone-default-value' import { createListItemKey } from '@admin/utils/form/create-list-item-key' import { cn } from '@admin/utils/shared/cn' import { Plus, X } from 'lucide-react' import { useFormContext, useWatch } from 'react-hook-form' interface NestedObjectListFieldProps { name: string label: string singularLabel: string defaultValue: Record children: (index: number) => React.ReactNode className?: string description?: string disabled?: boolean hideLabel?: boolean maxItems?: number renderTitle?: (index: number) => React.ReactNode validatePaths?: string[] } export function NestedObjectListField({ name, label, singularLabel, defaultValue, children, className, description, disabled, hideLabel, maxItems, renderTitle, validatePaths }: NestedObjectListFieldProps) { const { control, setValue, trigger } = useFormContext() const watchedItems = useWatch({ control, name }) const items = Array.isArray(watchedItems) ? watchedItems : [] const [expanded, setExpanded] = React.useState('') const [itemKeys, setItemKeys] = React.useState([]) const canAddMore = !maxItems || items.length < maxItems React.useEffect(() => { setItemKeys((currentKeys) => { if (currentKeys.length === items.length) return currentKeys if (currentKeys.length > items.length) { return currentKeys.slice(0, items.length) } const missingKeyCount = items.length - currentKeys.length const nextKeys = Array.from({ length: missingKeyCount }, createListItemKey) return [...currentKeys, ...nextKeys] }) }, [items.length]) async function handleAdd() { if (!canAddMore) return if (expanded) { const isExpandedItemValid = await validateExpandedItem() if (!isExpandedItemValid) return } const nextValue = cloneDefaultValue(defaultValue) as Record const nextItems = [...items, nextValue] setValue(name, nextItems, { shouldDirty: true, shouldValidate: false }) setItemKeys((currentKeys) => [...currentKeys, createListItemKey()]) setExpanded(`item-${nextItems.length}`) } const handleRemove = (index: number) => { if (disabled) return const nextItems = items.filter((_, itemIndex) => itemIndex !== index) setValue(name, nextItems, { shouldDirty: true, shouldValidate: false }) const keepOtherKeys = (_: string, itemIndex: number) => itemIndex !== index setItemKeys((currentKeys) => currentKeys.filter(keepOtherKeys)) setExpanded('') } async function validateExpandedItem() { if (!expanded) return true const expandedIndex = Number(expanded.replace('item-', '')) - 1 if (expandedIndex < 0) return true const itemPath = `${name}.${expandedIndex}` const paths = validatePaths && validatePaths.length > 0 ? validatePaths.map((path) => `${itemPath}.${path}`) : [itemPath] return trigger(paths) } async function handleExpandedChange(nextExpanded: string) { if (expanded && nextExpanded && nextExpanded !== expanded) { const isExpandedItemValid = await validateExpandedItem() if (!isExpandedItemValid) return } setExpanded(nextExpanded) } const getTitle = (index: number) => { if (renderTitle) return renderTitle(index) return `${singularLabel} ${index + 1}` } return ( {items.length === 0 ? (

No {label} added yet.

) : ( ( {label} {description ? {description} : null} )} /> handleExpandedChange(nextValue[0] ?? '')} className="border-none" > {items.map((_, index) => (
{getTitle(index)}
{children(index)}
))}
)}
) }