import React from 'react'; import type { CanvasComponent, DropdownOption, TableColumn, TableRow } from '../../types/project'; import type { ComponentRuntimeState } from './usePreviewRuntime'; import { labelDisplayValue } from './bindingUtils'; import styles from './PreviewComponent.module.css'; // ── Per-type renderers ──────────────────────────────────────────────────────── type LabelRendererProps = { /** Configured/design-time label text from component properties. */ configuredLabel: string; /** Runtime state — a defined value overrides configuredLabel. */ runtimeState?: ComponentRuntimeState; }; function LabelRenderer({ configuredLabel, runtimeState }: LabelRendererProps): React.ReactElement { // A defined runtime value overrides the configured label. // Falsy runtime values (false, 0, "", null) must still override — only // undefined means "no runtime value set". const display = runtimeState?.value !== undefined ? labelDisplayValue(runtimeState.value) : configuredLabel; return {display || '\u00a0'}; } type ButtonRendererProps = { label: string; isLoading: boolean; error?: string; onClick: () => void; }; function ButtonRenderer({ label, isLoading, error, onClick }: ButtonRendererProps): React.ReactElement { return (
{error && (
{error}
)}
); } function TextInputRenderer({ label, placeholder, value, disabled, onChange, }: { label: string; placeholder: string; value: string; disabled: boolean; onChange: (value: string) => void; }): React.ReactElement { return (
{label && } onChange(e.target.value)} />
); } // ── Dropdown renderer ───────────────────────────────────────────────────────── type DropdownRendererProps = { label: string; placeholder: string; options: DropdownOption[]; /** Current selection: runtimeState.value if set, else configured properties.value */ selectedValue: string; disabled: boolean; onChange: (value: string) => void; }; function DropdownRenderer({ label, placeholder, options, selectedValue, disabled, onChange, }: DropdownRendererProps): React.ReactElement { return (
{label && }
); } // ── Table renderer ──────────────────────────────────────────────────────────── /** Converts a cell value to a display string per the step spec. */ function renderTableCellValue(value: unknown): string { if (value === undefined) return ''; if (value === null) return 'null'; if (typeof value === 'string') return value; if (typeof value === 'number' || typeof value === 'boolean') return String(value); try { return JSON.stringify(value); } catch { return '[serialisation error]'; } } type TableRendererProps = { label: string; columns: TableColumn[]; rows: TableRow[]; disabled: boolean; selectedIndex?: number; onRowSelect: (index: number, row: TableRow) => void; }; function TableRenderer({ label, columns, rows, disabled, selectedIndex, onRowSelect, }: TableRendererProps): React.ReactElement { const effectiveCols: TableColumn[] = columns.length > 0 ? columns : rows.length > 0 ? Object.keys(rows[0]).map((k) => ({ key: k, header: k })) : []; return (
{label &&
{label}
} {effectiveCols.length === 0 && rows.length === 0 ? (
No data
) : (
{effectiveCols.map((col) => ( ))} {rows.length === 0 ? ( ) : ( rows.map((row, ri) => ( { if (!disabled) onRowSelect(ri, row); }} > {effectiveCols.map((col) => ( ))} )) )}
{col.header}
No rows
{renderTableCellValue(row[col.key])}
)}
); } type JsonViewerRendererProps = { defaultValue: string; runtimeState?: ComponentRuntimeState; }; function JsonViewerRenderer({ defaultValue, runtimeState }: JsonViewerRendererProps): React.ReactElement { // Loading state if (runtimeState?.loading) { return (
); } // Error state if (runtimeState?.error) { return (
Error
{runtimeState.error}
); } // Runtime value from a completed action call if (runtimeState?.value !== undefined) { const display = typeof runtimeState.value === 'string' ? runtimeState.value : JSON.stringify(runtimeState.value, null, 2); return (
{display}
); } // Design-time defaultValue (no action has fired yet) let display: string; if (defaultValue) { try { display = JSON.stringify(JSON.parse(defaultValue), null, 2); } catch { display = defaultValue; } } else { display = JSON.stringify( { status: 'No data', hint: 'Click a bound button to populate this viewer.' }, null, 2, ); } return (
{display}
); } // ── Main component ──────────────────────────────────────────────────────────── type PreviewComponentProps = { component: CanvasComponent; runtimeState?: ComponentRuntimeState; isLoading: boolean; onButtonClick: (componentId: string) => void; onTextInputChange: (componentId: string, value: string) => void; onDropdownChange: (componentId: string, value: string) => void; onTableRowSelect: (componentId: string, index: number, row: Record) => void; }; function PreviewComponent({ component, runtimeState, isLoading, onButtonClick, onTextInputChange, onDropdownChange, onTableRowSelect, }: PreviewComponentProps): React.ReactElement | null { const { type, position, size, properties } = component; // Respect the visibility flag — hidden components are not rendered if (properties.visible === false) return null; const label = typeof properties.label === 'string' ? properties.label : ''; const placeholder = typeof properties.placeholder === 'string' ? properties.placeholder : ''; const defaultValue = typeof properties.defaultValue === 'string' ? properties.defaultValue : ''; const disabled = properties.disabled === true; return (
{type === 'Label' && ( )} {type === 'Button' && ( onButtonClick(component.id)} /> )} {type === 'TextInput' && ( onTextInputChange(component.id, val)} /> )} {type === 'JsonViewer' && ( )} {type === 'Table' && (() => { const rawColumns = properties.columns; const configuredColumns: TableColumn[] = Array.isArray(rawColumns) ? (rawColumns as TableColumn[]).filter( (c) => c && typeof c.key === 'string' && typeof c.header === 'string', ) : []; const rawRows = properties.rows; const configuredRows: TableRow[] = Array.isArray(rawRows) ? (rawRows as TableRow[]).filter( (r) => r !== null && typeof r === 'object' && !Array.isArray(r), ) : []; // Runtime rows override configured rows when defined (including an empty array). // undefined means "no runtime rows yet" — fall back to configured rows. const effectiveRows: TableRow[] = runtimeState?.rows !== undefined ? runtimeState.rows : configuredRows; return ( onTableRowSelect(component.id, idx, row)} /> ); })()} {type === 'Dropdown' && (() => { // Configured options from design-time properties const rawConfiguredOptions = properties.options; const configuredOptions: DropdownOption[] = Array.isArray(rawConfiguredOptions) ? (rawConfiguredOptions as DropdownOption[]).filter( (o) => o && typeof o.label === 'string' && typeof o.value === 'string', ) : []; // Runtime options override configured options when defined. // An empty runtime array is meaningful — do not fall back. const effectiveOptions: DropdownOption[] = runtimeState?.options !== undefined ? runtimeState.options : configuredOptions; // configured value from properties (string) const configuredValue = typeof properties.value === 'string' ? properties.value : ''; // runtime selection overrides configured value (undefined means no runtime selection yet) const selectedValue = runtimeState?.value !== undefined ? String(runtimeState.value) : configuredValue; return ( onDropdownChange(component.id, val)} /> ); })()}
); } export default PreviewComponent;