"use client"; import { useState, useMemo } from "react"; // --- Types --- type Field = { id: string; name: string; type: string; enum_values?: string[]; enum_colors?: string[]; enum_multiple?: boolean; related_object_id?: string; relationship_type?: string; related_object_name?: string; sort_order?: number; }; type ReverseRelation = { fieldName: string; sourceObjectName: string; sourceObjectId: string; displayField: string; entries: Record>; }; type ObjectTableProps = { objectName: string; fields: Field[]; entries: Record[]; members?: Array<{ id: string; name: string }>; relationLabels?: Record>; reverseRelations?: ReverseRelation[]; onNavigateToObject?: (objectName: string) => void; onEntryClick?: (entryId: string) => void; }; // --- Helpers --- function parseRelationValue(value: string | null | undefined): string[] { if (!value) {return [];} const trimmed = value.trim(); if (!trimmed) {return [];} if (trimmed.startsWith("[")) { try { const parsed = JSON.parse(trimmed); if (Array.isArray(parsed)) {return parsed.map(String).filter(Boolean);} } catch { // not valid JSON } } return [trimmed]; } // --- Sort helpers --- type SortState = { column: string; direction: "asc" | "desc"; } | null; function SortIcon({ active, direction }: { active: boolean; direction: "asc" | "desc" }) { return ( {direction === "asc" ? ( ) : ( )} ); } // --- Cell Renderers --- function EnumBadge({ value, enumValues, enumColors, }: { value: string; enumValues?: string[]; enumColors?: string[]; }) { const idx = enumValues?.indexOf(value) ?? -1; const color = idx >= 0 && enumColors ? enumColors[idx] : "#94a3b8"; return ( {value} ); } function BooleanCell({ value }: { value: unknown }) { const isTrue = value === true || value === "true" || value === "1" || value === "yes"; return ( {isTrue ? "Yes" : "No"} ); } function UserCell({ value, members, }: { value: unknown; members?: Array<{ id: string; name: string }>; }) { const memberId = String(value); const member = members?.find((m) => m.id === memberId); return ( {(member?.name ?? memberId).charAt(0).toUpperCase()} {member?.name ?? memberId} ); } /** Inline link icon (small arrow) for relation chips. */ function LinkIcon() { return ( ); } /** A single relation chip showing a display label with an optional link icon. */ function RelationChip({ label, objectName, onNavigate, }: { label: string; objectName?: string; onNavigate?: (objectName: string) => void; }) { const handleClick = objectName && onNavigate ? (e: React.MouseEvent) => { e.stopPropagation(); onNavigate(objectName); } : undefined; return ( {label} ); } /** Render a relation field cell with resolved display labels. */ function RelationCell({ value, field, relationLabels, onNavigate, }: { value: unknown; field: Field; relationLabels?: Record>; onNavigate?: (objectName: string) => void; }) { const fieldLabels = relationLabels?.[field.name]; const ids = parseRelationValue(String(value)); if (ids.length === 0) { return ( -- ); } return ( {ids.map((id) => ( ))} ); } /** Render a reverse relation cell (incoming links from another object). */ function ReverseRelationCell({ links, sourceObjectName, onNavigate, }: { links: Array<{ id: string; label: string }>; sourceObjectName: string; onNavigate?: (objectName: string) => void; }) { if (!links || links.length === 0) { return ( -- ); } const displayLinks = links.slice(0, 5); const overflow = links.length - displayLinks.length; return ( {displayLinks.map((link) => ( ))} {overflow > 0 && ( +{overflow} more )} ); } function CellValue({ value, field, members, relationLabels, onNavigate, }: { value: unknown; field: Field; members?: Array<{ id: string; name: string }>; relationLabels?: Record>; onNavigate?: (objectName: string) => void; }) { if (value === null || value === undefined || value === "") { return ( -- ); } switch (field.type) { case "enum": return ( ); case "boolean": return ; case "user": return ; case "relation": return ( ); case "email": return ( {String(value)} ); case "date": return {String(value)}; case "number": return {String(value)}; default: return {String(value)}; } } // --- Table Component --- export function ObjectTable({ objectName, fields, entries, members, relationLabels, reverseRelations, onNavigateToObject, onEntryClick, }: ObjectTableProps) { const [sort, setSort] = useState(null); const handleSort = (column: string) => { setSort((prev) => { if (prev?.column === column) { return prev.direction === "asc" ? { column, direction: "desc" } : null; } return { column, direction: "asc" }; }); }; const sortedEntries = useMemo(() => { if (!sort) {return entries;} return [...entries].toSorted((a, b) => { const aVal = String(a[sort.column] ?? ""); const bVal = String(b[sort.column] ?? ""); const cmp = aVal.localeCompare(bVal, undefined, { numeric: true }); return sort.direction === "asc" ? cmp : -cmp; }); }, [entries, sort]); // Filter out reverse relations with no actual data const activeReverseRelations = useMemo(() => { if (!reverseRelations) {return [];} return reverseRelations.filter( (rr) => Object.keys(rr.entries).length > 0, ); }, [reverseRelations]); if (entries.length === 0) { return (

No entries in {objectName}

); } return (
{/* Regular field columns */} {fields.map((field) => ( ))} {/* Reverse relation columns */} {activeReverseRelations.map((rr) => ( ))} {sortedEntries.map((entry, idx) => ( { const eid = String(entry.entry_id ?? ""); if (eid && onEntryClick) {onEntryClick(eid);} }} onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.background = "var(--color-surface-hover)"; }} onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.background = idx % 2 === 0 ? "transparent" : "var(--color-surface)"; }} > {/* Regular field cells */} {fields.map((field) => ( ))} {/* Reverse relation cells */} {activeReverseRelations.map((rr) => { const entryId = String(entry.entry_id ?? ""); const links = rr.entries[entryId] ?? []; return ( ); })} ))}
handleSort(field.name)} > {field.name} {field.type === "relation" && field.related_object_name && ( ({field.related_object_name}) )} {rr.sourceObjectName} via {rr.fieldName}
); }