From db5d8a81c1863095edc0389b2150f3dd6a47850c Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 14 Feb 2026 20:48:41 -0500 Subject: [PATCH] feat: procedural editor UX improvements Add URL intake field type, fix variable name editing collapsing fields (index-based keys/updates), auto-generate variable names by field type, add section header as first-class step type, and simplify step editor with "More Options" collapsible for advanced fields. Co-Authored-By: Claude Opus 4.6 --- .../procedural-editor/IntakeFieldEditor.tsx | 4 +- .../procedural-editor/IntakeFormBuilder.tsx | 7 +- .../procedural-editor/StepEditor.tsx | 283 ++++++++++-------- .../components/procedural-editor/StepList.tsx | 84 ++++-- .../components/procedural/IntakeFormModal.tsx | 12 + frontend/src/store/proceduralEditorStore.ts | 85 ++++-- frontend/src/types/tree.ts | 4 +- 7 files changed, 306 insertions(+), 173 deletions(-) diff --git a/frontend/src/components/procedural-editor/IntakeFieldEditor.tsx b/frontend/src/components/procedural-editor/IntakeFieldEditor.tsx index 0a9c94a6..9705702d 100644 --- a/frontend/src/components/procedural-editor/IntakeFieldEditor.tsx +++ b/frontend/src/components/procedural-editor/IntakeFieldEditor.tsx @@ -8,6 +8,7 @@ const FIELD_TYPE_OPTIONS: { value: IntakeFieldType; label: string }[] = [ { value: 'number', label: 'Number' }, { value: 'ip_address', label: 'IP Address' }, { value: 'email', label: 'Email' }, + { value: 'url', label: 'URL' }, { value: 'select', label: 'Select (Dropdown)' }, { value: 'multi_select', label: 'Multi-Select' }, { value: 'checkbox', label: 'Checkbox' }, @@ -16,11 +17,12 @@ const FIELD_TYPE_OPTIONS: { value: IntakeFieldType; label: string }[] = [ interface IntakeFieldEditorProps { field: IntakeFormField + index: number onUpdate: (updates: Partial) => void onRemove: () => void } -export function IntakeFieldEditor({ field, onUpdate, onRemove }: IntakeFieldEditorProps) { +export function IntakeFieldEditor({ field, index: _index, onUpdate, onRemove }: IntakeFieldEditorProps) { const [expanded, setExpanded] = useState(false) const needsOptions = field.field_type === 'select' || field.field_type === 'multi_select' diff --git a/frontend/src/components/procedural-editor/IntakeFormBuilder.tsx b/frontend/src/components/procedural-editor/IntakeFormBuilder.tsx index 0e337179..867f4314 100644 --- a/frontend/src/components/procedural-editor/IntakeFormBuilder.tsx +++ b/frontend/src/components/procedural-editor/IntakeFormBuilder.tsx @@ -36,10 +36,11 @@ export function IntakeFormBuilder() {
{intakeForm.map((field, index) => ( updateField(field.variable_name, updates)} - onRemove={() => removeField(field.variable_name)} + index={index} + onUpdate={(updates) => updateField(index, updates)} + onRemove={() => removeField(index)} /> ))}
diff --git a/frontend/src/components/procedural-editor/StepEditor.tsx b/frontend/src/components/procedural-editor/StepEditor.tsx index 2d958944..c5d80db0 100644 --- a/frontend/src/components/procedural-editor/StepEditor.tsx +++ b/frontend/src/components/procedural-editor/StepEditor.tsx @@ -1,4 +1,5 @@ -import { ChevronUp, AlertTriangle, Clock, ExternalLink, CheckSquare, Terminal, Type } from 'lucide-react' +import { useState } from 'react' +import { ChevronUp, ChevronDown, AlertTriangle, Clock, ExternalLink, CheckSquare, Terminal, Settings2 } from 'lucide-react' import type { ProceduralStep, StepContentType, IntakeFormField } from '@/types' import { cn } from '@/lib/utils' @@ -18,6 +19,35 @@ interface StepEditorProps { } export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVariables }: StepEditorProps) { + const [showMore, setShowMore] = useState(false) + + // Section header steps get a minimal editor + if (step.type === 'section_header') { + return ( +
+
+ Edit Section Header + +
+
+ + onUpdate({ title: e.target.value })} + placeholder="Section title" + className="w-full rounded border border-white/10 bg-black/50 px-3 py-2 text-sm text-white placeholder:text-white/30 focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20" + /> +
+
+ ) + } + return (
{/* Header */} @@ -48,55 +78,18 @@ export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVa />
- {/* Content type + Section header row */} -
-
- -
- {CONTENT_TYPE_OPTIONS.map((opt) => ( - - ))} -
-
- -
- - onUpdate({ estimated_minutes: e.target.value ? parseInt(e.target.value) : undefined })} - placeholder="—" - min={1} - className="w-full rounded border border-white/10 bg-black/50 px-3 py-2 text-sm text-white placeholder:text-white/30 focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20" - /> -
-
- - {/* Section Header */} -
+ {/* Est. Minutes */} +
onUpdate({ section_header: e.target.value || undefined })} - placeholder="e.g. Phase 2: AD Configuration" + type="number" + value={step.estimated_minutes || ''} + onChange={(e) => onUpdate({ estimated_minutes: e.target.value ? parseInt(e.target.value) : undefined })} + placeholder="—" + min={1} className="w-full rounded border border-white/10 bg-black/50 px-3 py-2 text-sm text-white placeholder:text-white/30 focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20" />
@@ -127,23 +120,6 @@ export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVa )}
- {/* Warning text */} - {(step.content_type === 'warning' || step.warning_text) && ( -
- -