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
| Raw value |
Result |
{rows.map((r, i) => (
| {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'}
)
}
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) }}
/>
)}
)
})}
)
}