import React, { useCallback, useState } from 'react'; import Palette from './Palette/Palette'; import Canvas from './Canvas/Canvas'; import ProjectToolbar from '../ProjectToolbar/ProjectToolbar'; import { useProject } from '../../context/ProjectContext'; import styles from './VisualEditor.module.css'; import type { ComponentType, DropdownOption, TableColumn, TableRow } from '../../types/project'; // ── Dropdown options row editor ─────────────────────────────────────────────── type DropdownOptionsEditorProps = { componentId: string; options: DropdownOption[]; configuredValue: string; onOptionsChange: (options: DropdownOption[]) => void; onConfiguredValueChange: (value: string) => void; }; function DropdownOptionsEditor({ options, configuredValue, onOptionsChange, onConfiguredValueChange, }: DropdownOptionsEditorProps): React.ReactElement { const [optionsError, setOptionsError] = useState(null); const handleAddOption = () => { onOptionsChange([...options, { label: '', value: '' }]); }; const handleChangeOption = (index: number, field: 'label' | 'value', text: string) => { const updated = options.map((o, i) => i === index ? { ...o, [field]: text } : o, ); onOptionsChange(updated); // Warn on duplicate values (non-blocking) const values = updated.map((o) => o.value).filter(Boolean); const hasDups = values.length !== new Set(values).size; setOptionsError(hasDups ? 'Duplicate option values detected.' : null); }; const handleRemoveOption = (index: number) => { const updated = options.filter((_, i) => i !== index); onOptionsChange(updated); const values = updated.map((o) => o.value).filter(Boolean); const hasDups = values.length !== new Set(values).size; setOptionsError(hasDups ? 'Duplicate option values detected.' : null); }; // Warn if configured value is not in option list (non-blocking) const configuredValueValid = configuredValue === '' || options.some((o) => o.value === configuredValue); return (
Options
{options.map((opt, i) => (
handleChangeOption(i, 'label', e.target.value)} /> handleChangeOption(i, 'value', e.target.value)} />
))} {optionsError && (
{optionsError}
)}
onConfiguredValueChange(e.target.value)} style={!configuredValueValid ? { borderColor: '#f59e0b' } : undefined} title={!configuredValueValid ? 'Value not found in options' : undefined} />
{!configuredValueValid && (
Default value not found in options.
)}
); } // ── Table columns editor ────────────────────────────────────────────────────── type TableColumnsEditorProps = { columns: TableColumn[]; onColumnsChange: (columns: TableColumn[]) => void; }; function TableColumnsEditor({ columns, onColumnsChange }: TableColumnsEditorProps): React.ReactElement { const [colError, setColError] = useState(null); const validate = (cols: TableColumn[]): string | null => { const keys = cols.map((c) => c.key).filter(Boolean); const dupKeys = keys.filter((k, i) => keys.indexOf(k) !== i); if (dupKeys.length > 0) return `Duplicate column keys: ${dupKeys.map((k) => `"${k}"`).join(', ')}.`; const emptyKey = cols.some((c) => !c.key); if (emptyKey) return 'Column key must not be empty.'; return null; }; const handleAdd = () => { const updated = [...columns, { key: '', header: '' }]; onColumnsChange(updated); setColError(validate(updated)); }; const handleChange = (index: number, field: 'key' | 'header', value: string) => { const updated = columns.map((c, i) => i === index ? { ...c, [field]: value } : c); onColumnsChange(updated); setColError(validate(updated)); }; const handleRemove = (index: number) => { const updated = columns.filter((_, i) => i !== index); onColumnsChange(updated); setColError(validate(updated)); }; return (
Columns
{columns.map((col, i) => (
handleChange(i, 'header', e.target.value)} /> handleChange(i, 'key', e.target.value)} title={!col.key ? 'Key must not be empty' : undefined} />
))} {colError && (
{colError}
)}
); } // ── Table rows editor ───────────────────────────────────────────────────────── type TableRowsEditorProps = { rows: TableRow[]; onRowsChange: (rows: TableRow[]) => void; }; function TableRowsEditor({ rows, onRowsChange }: TableRowsEditorProps): React.ReactElement { // Draft text for the JSON textarea; initialise from current rows const [draft, setDraft] = useState(() => JSON.stringify(rows, null, 2)); const [rowsError, setRowsError] = useState(null); // Sync draft when rows prop changes externally (e.g. on component selection) const prevRowsRef = React.useRef(rows); if (rows !== prevRowsRef.current) { prevRowsRef.current = rows; setDraft(JSON.stringify(rows, null, 2)); setRowsError(null); } const handleApply = () => { let parsed: unknown; try { parsed = JSON.parse(draft); } catch (e: unknown) { setRowsError(`Invalid JSON: ${e instanceof Error ? e.message : String(e)}`); return; } if (!Array.isArray(parsed)) { setRowsError('Rows must be a JSON array.'); return; } for (let i = 0; i < parsed.length; i++) { const el = parsed[i]; if (el === null || typeof el !== 'object' || Array.isArray(el)) { setRowsError( `Row at index ${i} must be a non-null object (got ${ el === null ? 'null' : Array.isArray(el) ? 'array' : typeof el }).`, ); return; } } setRowsError(null); onRowsChange(parsed as TableRow[]); }; return (
Rows (JSON array)