import { useState, useEffect, useRef } from 'react' import { api } from '../api' const EMPTY_FORM = { name: '', field: '', pattern: '', output_field: '', function_type: 'extract', flags: '', replace_value: '', retain: false, sequence: 0 } function PreviewModal({ rows, onClose }) { const matched = rows.filter(r => r.extracted_value != null).length return (
e.stopPropagation()}>
Pattern results — {matched}/{rows.length} matched
{rows.map((r, i) => ( ))}
Raw value Result
{r.raw_value} {r.extracted_value != null ? (Array.isArray(r.extracted_value) ? r.extracted_value.join(' · ') : String(r.extracted_value)) : '—'}
) } function FormPanel({ form, setForm, editing, error, loading, fields, source, onSubmit, onCancel }) { const [preview, setPreview] = useState([]) const [previewing, setPreviewing] = useState(false) const [modalOpen, setModalOpen] = useState(false) const debounceRef = useRef(null) // Auto-preview when field/pattern/flags change, debounced useEffect(() => { if (!form.field || !form.pattern) { setPreview([]); return } clearTimeout(debounceRef.current) debounceRef.current = setTimeout(async () => { setPreviewing(true) try { const results = await api.previewRule(source, form.field, form.pattern, form.flags, form.function_type, form.replace_value, 50) setPreview(results) } catch { setPreview([]) } finally { setPreviewing(false) } }, 500) return () => clearTimeout(debounceRef.current) }, [form.field, form.pattern, form.flags, form.function_type, form.replace_value, source]) return (

{editing ? 'Edit rule' : 'New rule'}

setForm(f => ({ ...f, name: e.target.value }))} placeholder="e.g. First 20" />
setForm(f => ({ ...f, sequence: parseInt(e.target.value) || 0 }))} />
{fields.length > 0 ? ( ) : ( setForm(f => ({ ...f, field: e.target.value }))} placeholder="e.g. description" /> )}
setForm(f => ({ ...f, output_field: e.target.value }))} placeholder="e.g. merchant" />
setForm(f => ({ ...f, pattern: e.target.value }))} placeholder="e.g. .{1,20}" />
setForm(f => ({ ...f, flags: e.target.value }))} placeholder="e.g. i" />
{form.function_type === 'extract' && ( )} {form.function_type === 'replace' && (
setForm(f => ({ ...f, replace_value: e.target.value }))} placeholder="e.g. leave blank to delete the match" />
)} {/* Live preview */} {(preview.length > 0 || previewing) && (

{previewing ? 'Testing…' : `${preview.filter(r => r.extracted_value != null).length}/${preview.length} matched`}

{!previewing && preview.length > 0 && ( )}
{!previewing && ( {preview.slice(0, 5).map((r, i) => ( ))}
{r.raw_value} {r.extracted_value != null ? (Array.isArray(r.extracted_value) ? r.extracted_value.join(' · ') : String(r.extracted_value)) : '—'}
)}
)} {modalOpen && setModalOpen(false)} />} {error &&

{error}

}
) } export default function Rules({ source }) { const [rules, setRules] = useState([]) const [creating, setCreating] = useState(false) const [editing, setEditing] = useState(null) const [expanded, setExpanded] = useState(null) const [form, setForm] = useState(EMPTY_FORM) const [testResults, setTestResults] = useState({}) const [fields, setFields] = useState([]) const [error, setError] = useState('') const [loading, setLoading] = useState(false) useEffect(() => { if (!source) return api.getRules(source).then(setRules).catch(() => {}) setTestResults({}) api.getFields(source).then(f => setFields(f.map(x => x.key))).catch(() => {}) }, [source]) function startCreate() { setForm(EMPTY_FORM) setEditing(null) setCreating(true) setError('') } function startEdit(rule) { setForm({ name: rule.name, field: rule.field, pattern: rule.pattern, output_field: rule.output_field, function_type: rule.function_type || 'extract', flags: rule.flags || '', replace_value: rule.replace_value || '', retain: rule.retain || false, sequence: rule.sequence, }) setEditing(rule.id) setCreating(false) setError('') } async function handleSubmit(e, ruleId = null) { e.preventDefault() setError('') setLoading(true) const id = ruleId ?? editing try { if (id) { await api.updateRule(id, { ...form, source_name: source }) } else { await api.createRule({ ...form, source_name: source }) } const updated = await api.getRules(source) setRules(updated) setCreating(false) setEditing(null) setExpanded(null) } catch (err) { setError(err.message) } finally { setLoading(false) } } async function handleDelete(id) { if (!confirm('Delete this rule and all its mappings?')) return try { await api.deleteRule(id) setRules(r => r.filter(x => x.id !== id)) setTestResults(t => { const n = { ...t }; delete n[id]; return n }) } catch (err) { alert(err.message) } } async function handleTest(id) { try { const res = await api.testRule(id) setTestResults(t => ({ ...t, [id]: res.results })) } catch (err) { alert(err.message) } } async function handleToggle(rule) { try { await api.updateRule(rule.id, { enabled: !rule.enabled }) setRules(r => r.map(x => x.id === rule.id ? { ...x, enabled: !x.enabled } : x)) } catch (err) { alert(err.message) } } if (!source) return
Select a source first.
return (

Rules — {source}

{creating && ( { setCreating(false); setError('') }} /> )} {rules.length === 0 && !creating && (

No rules yet. Add a regex rule to start extracting values.

)}
{rules.map(rule => { const isExpanded = expanded === rule.id return (
{/* Header — always visible, click to expand/collapse */}
{ if (isExpanded) { setExpanded(null); setEditing(null) } else { setExpanded(rule.id); startEdit(rule) } }} >
{/* Expanded content */} {isExpanded && (
handleSubmit(e, rule.id)} onCancel={() => { setEditing(null); setExpanded(null) }} />
)}
) })}
) }