"use client" import { ChevronDown, ChevronRight } from 'lucide-react'; import React, { useState } from 'react'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@djangocfg/ui-core/components'; import { cn } from '@djangocfg/ui-core/lib'; import { ObjectFieldTemplateProps } from '@rjsf/utils'; import type { JsonFormDensity, UiGroup } from '../types'; /** * Static map of literal grid-column classes. * * Tailwind JIT only compiles classes it can see at build time, so a * runtime-built `grid-cols-${n}` string is never emitted. Listing the * full literal classes here keeps `ui:grid` working. Columns collapse * to a single column on small screens for responsive behaviour. */ const GRID_COL_CLASS: Record = { 1: 'grid-cols-1', 2: 'grid-cols-1 sm:grid-cols-2', 3: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3', 4: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-4', }; /** * Object field template for JSON Schema Form * * Supports: * - Collapsible groups via ui:collapsible option * - Grid layout via ui:grid option * - Custom styling via ui:className * * Usage in uiSchema: * ```json * { * "colors": { * "ui:collapsible": true, * "ui:collapsed": false, * "ui:grid": 2 * } * } * ``` */ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { const { title, description, properties, required, uiSchema, } = props; const formContext = (props as { formContext?: { density?: JsonFormDensity } }).formContext; // UI options const isCollapsible = uiSchema?.['ui:collapsible'] === true; const defaultCollapsed = uiSchema?.['ui:collapsed'] === true; const gridCols = uiSchema?.['ui:grid'] as number | undefined; const className = uiSchema?.['ui:className'] as string | undefined; const uiGroups = uiSchema?.['ui:groups'] as readonly UiGroup[] | undefined; const density = (formContext?.density as JsonFormDensity | undefined) ?? 'comfortable'; const compact = density === 'compact'; // Collapsible state const [isOpen, setIsOpen] = useState(!defaultCollapsed); // Check if this is root object (no title usually means root) const isRoot = !title; // Grid class based on columns. `ui:grid` is clamped to the 1-4 range // covered by GRID_COL_CLASS so Tailwind always has a literal class. const colClass = typeof gridCols === 'number' ? GRID_COL_CLASS[Math.min(4, Math.max(1, Math.round(gridCols)))] : undefined; const gridClass = colClass ? cn('grid', compact ? 'gap-2' : 'gap-4', colClass) : compact ? 'space-y-2' : 'space-y-4'; // When `ui:groups` is specified, render listed fields inside collapsible // sub-sections; remaining (ungrouped) fields render flat above the groups. const groupedContent = uiGroups ? renderUiGroups({ properties, uiGroups, gridClass, className, compact }) : null; // Content wrapper const content = groupedContent ?? (
{properties.map((element) => (
{element.content}
))}
); // Root object - no wrapper if (isRoot) { return
{content}
; } // Collapsible group if (isCollapsible) { return (

{title} {required && *}

{description && (

{description}

)}
{content}
); } // Regular group with title return (
{title && (

{title} {required && *}

{description && (

{description}

)}
)} {content}
); } interface RenderUiGroupsOptions { properties: ObjectFieldTemplateProps['properties']; uiGroups: readonly UiGroup[]; gridClass: string; className?: string; compact: boolean; } function renderUiGroups({ properties, uiGroups, gridClass, className, compact, }: RenderUiGroupsOptions) { const propsByName = new Map(properties.map((p) => [p.name, p])); const groupedFieldNames = new Set(uiGroups.flatMap((g) => g.fields)); // Fields not listed in any group render flat above the groups. const ungrouped = properties.filter((p) => !groupedFieldNames.has(p.name)); return (
{ungrouped.length > 0 ? (
{ungrouped.map((element) => (
{element.content}
))}
) : null} {uiGroups.map((group) => { const groupProps = group.fields .map((name) => propsByName.get(name)) .filter((p): p is ObjectFieldTemplateProps['properties'][number] => Boolean(p)); if (groupProps.length === 0) return null; return ( {groupProps.map((element) => (
{element.content}
))}
); })}
); } interface UiGroupSectionProps { group: UiGroup; gridClass: string; compact: boolean; children: React.ReactNode; } function UiGroupSection({ group, gridClass, compact, children }: UiGroupSectionProps) { const [open, setOpen] = useState(group.defaultOpen ?? true); return ( {group.title}
{children}
); }