import { useEffect, useState } from 'react'; import { cn } from '../../lib/cn'; import { formatValue } from '../../lib/format'; import type { ZinoClient } from '../../api/client'; import { APP_ID } from '../../api/config'; import { Card } from '../reusable/Card'; import { Spinner } from '../reusable/Spinner'; import { EmptyState } from '../reusable/EmptyState'; export interface DetailViewProps { /** Workflow-bound client (see api/clients.ts). */ client: ZinoClient; /** detailview template uid. */ dvUid?: string; /** Instance to render. */ instanceId: number | string; /** Card header title. */ title?: string; /** Restrict/order visible fields by field_key. Defaults to all. */ fields?: string[]; /** Columns in the definition grid. @default 2 */ columns?: 1 | 2 | 3; } /** * Generic Zino detail view. Fetches `GET /app/{id}/view/detailview/{dvUid}` and * renders the data as a labeled definition grid. */ export function DetailView({ client, dvUid, instanceId, title, fields, columns = 2 }: DetailViewProps) { const [data, setData] = useState | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let live = true; async function run() { setLoading(true); setError(null); try { if (!dvUid) throw new Error("dvUid is required to fetch detail view data."); const r = await client.detailView(dvUid, instanceId); if (live) { setData(r.data || {}); } } catch (e) { if (live) setError((e as { message?: string })?.message ?? 'Failed to load'); } finally { if (live) setLoading(false); } } run(); return () => { live = false; }; }, [client, dvUid, instanceId]); const gridCols = { 1: 'grid-cols-1', 2: 'grid-cols-1 sm:grid-cols-2', 3: 'grid-cols-1 sm:grid-cols-3' }[columns]; if (error) { return ( ); } if (loading) { return (
); } if (!data || Object.keys(data).length === 0) { return ( ); } return (
{Object.entries(data).map(([key, value]) => { if (fields && !fields.includes(key)) return null; const lowerKey = key.toLowerCase(); if (lowerKey.includes('uuid') || /^\d+$/.test(key)) return null; const isGridArray = Array.isArray(value) && value.length > 0 && typeof value[0] === 'object' && value[0] !== null && !('original_name' in value[0]) && !('url' in value[0]); if (isGridArray) { const rows = value as Record[]; // Use all unique keys found across all rows just in case they differ slightly const headers = Array.from(new Set(rows.flatMap(r => Object.keys(r)))); return (
{key.replace(/_/g, ' ')}
{headers.map(h => ( ))} {rows.map((row, idx) => ( {headers.map(h => ( ))} ))}
{h.replace(/_/g, ' ')}
{formatValue(row[h])}
); } const isFileArray = Array.isArray(value) && value.length > 0 && typeof value[0] === 'object' && value[0] !== null && ('original_name' in value[0] || 'uuid' in value[0]); if (isFileArray) { return (
{key.replace(/_/g, ' ')}
{(value as any[]).map((file, idx) => { const previewUrl = `${client.baseUrl}/app/${APP_ID}/view/files/${file.uuid}/preview`; return (
{file.original_name { (e.target as HTMLImageElement).style.display = 'none'; (e.target as HTMLImageElement).parentElement!.innerHTML = `${file.original_name || 'File'}`; }} />
); })}
); } return (
{key.replace(/_/g, ' ')}
{formatValue(value)}
); })}
); }