From 4b449e8b85b31014186dcef229130872d0beb7d4 Mon Sep 17 00:00:00 2001 From: Andres Aguilera Date: Wed, 16 Sep 2026 10:36:35 -0300 Subject: [PATCH 1/2] feat: enhance node and trigger inspectors with save functionality and improved state management --- .../features/soar/components/FlowCanvas.tsx | 51 +++++--- .../features/soar/components/FlowEditor.tsx | 54 ++++---- .../soar/components/FlowIdentityModal.tsx | 33 +++-- .../soar/components/NodeInspector.tsx | 116 +++++++++++------- .../soar/components/TriggerInspector.tsx | 25 +++- 5 files changed, 178 insertions(+), 101 deletions(-) diff --git a/frontend/src/features/soar/components/FlowCanvas.tsx b/frontend/src/features/soar/components/FlowCanvas.tsx index 4b063300a..b06fb9ed1 100644 --- a/frontend/src/features/soar/components/FlowCanvas.tsx +++ b/frontend/src/features/soar/components/FlowCanvas.tsx @@ -140,7 +140,7 @@ function FlowCanvasInner({ roots, nodes, conditions, readOnly, onChange, onCondi if (c.type === 'select') { if (c.selected) { setSelectedId(c.id) - if (c.id === TRIGGER_ID) setInspectorOpen(true) + setInspectorOpen(true) } else if (!c.selected && selectedId === c.id) setSelectedId(null) } } @@ -298,27 +298,35 @@ function FlowCanvasInner({ roots, nodes, conditions, readOnly, onChange, onCondi const selected = selectedId ? nodes[selectedId] : null - const renameNode = (nextId: string) => { - if (!selectedId || nextId === selectedId || nodes[nextId]) return + const patchNode = (patch: Partial) => { + if (!selectedId || !nodes[selectedId]) return false + onChange({ roots, nodes: { ...nodes, [selectedId]: { ...nodes[selectedId], ...patch } } }) + return true + } + + const saveNode = (nextId: string, patch: Partial) => { + if (!selectedId || !nodes[selectedId]) return false + const trimmedId = nextId.trim() + if (!trimmedId || (trimmedId !== selectedId && nodes[trimmedId])) return false + if (trimmedId === selectedId) { + return patchNode(patch) + } + const nextNodes: Record = {} for (const [id, n] of Object.entries(nodes)) { const copy: FlowNode = { ...n, - onSuccess: n.onSuccess?.map((t) => (t === selectedId ? nextId : t)), - onError: n.onError?.map((t) => (t === selectedId ? nextId : t)), + onSuccess: n.onSuccess?.map((target) => (target === selectedId ? trimmedId : target)), + onError: n.onError?.map((target) => (target === selectedId ? trimmedId : target)), } - nextNodes[id === selectedId ? nextId : id] = copy + nextNodes[id === selectedId ? trimmedId : id] = id === selectedId ? { ...copy, ...patch } : copy } - const nextRoots = roots.map((r) => (r === selectedId ? nextId : r)) - layoutRef.current[nextId] = layoutRef.current[selectedId] + const nextRoots = roots.map((root) => (root === selectedId ? trimmedId : root)) + layoutRef.current[trimmedId] = layoutRef.current[selectedId] delete layoutRef.current[selectedId] onChange({ roots: nextRoots, nodes: nextNodes }) - setSelectedId(nextId) - } - - const patchNode = (patch: Partial) => { - if (!selectedId || !nodes[selectedId]) return - onChange({ roots, nodes: { ...nodes, [selectedId]: { ...nodes[selectedId], ...patch } } }) + setSelectedId(trimmedId) + return true } const deleteNode = () => { @@ -339,7 +347,7 @@ function FlowCanvasInner({ roots, nodes, conditions, readOnly, onChange, onCondi } return ( -
+
{paletteOpen ? (
@@ -385,7 +393,7 @@ function FlowCanvasInner({ roots, nodes, conditions, readOnly, onChange, onCondi } as React.CSSProperties} > - +
@@ -396,6 +404,10 @@ function FlowCanvasInner({ roots, nodes, conditions, readOnly, onChange, onCondi conditions={conditions} readOnly={readOnly} onChange={onConditionsChange} + onClose={() => { + setSelectedId(null) + setInspectorOpen(false) + }} /> ))} + {!readOnly && ( + <> + {!creating && ( + + )} + {creating && ( + + )} + + + )}
- {!readOnly && ( -
- {!creating && ( - - )} - {creating && ( - - )} - -
- )}
) diff --git a/frontend/src/features/soar/components/FlowIdentityModal.tsx b/frontend/src/features/soar/components/FlowIdentityModal.tsx index f49284f67..3b451fba7 100644 --- a/frontend/src/features/soar/components/FlowIdentityModal.tsx +++ b/frontend/src/features/soar/components/FlowIdentityModal.tsx @@ -1,6 +1,7 @@ -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { X } from 'lucide-react' +import { Button } from '@/shared/components/ui/button' import { Input } from '@/shared/components/ui/input' interface Props { @@ -14,10 +15,11 @@ interface Props { // Floating modal for the flow's identity fields — used to be a top-level // SectionCard; now opened from a pencil icon next to the flow title in the -// header. Live-commits on every keystroke via onChange. +// header. Changes are committed only when Save is pressed. // ponytail: no shared Dialog primitive here, just overlay + card + Esc handler. export function FlowIdentityModal({ name, description, maxDepth, readOnly, onChange, onClose }: Props) { const { t } = useTranslation() + const [draft, setDraft] = useState({ name, description, maxDepth }) useEffect(() => { const onKey = (e: KeyboardEvent) => { @@ -27,6 +29,11 @@ export function FlowIdentityModal({ name, description, maxDepth, readOnly, onCha return () => document.removeEventListener('keydown', onKey) }, [onClose]) + const save = () => { + onChange(draft) + onClose() + } + return (
@@ -47,9 +54,9 @@ export function FlowIdentityModal({ name, description, maxDepth, readOnly, onCha {!readOnly && *} onChange({ name: e.target.value })} + onChange={(e) => setDraft((current) => ({ ...current, name: e.target.value }))} placeholder={t('soar.editor.namePlaceholder')} className="text-base font-semibold" autoFocus @@ -63,9 +70,9 @@ export function FlowIdentityModal({ name, description, maxDepth, readOnly, onCha type="number" min={1} max={1000} - value={maxDepth} + value={draft.maxDepth} readOnly={readOnly} - onChange={(e) => onChange({ maxDepth: Number(e.target.value) || 50 })} + onChange={(e) => setDraft((current) => ({ ...current, maxDepth: Number(e.target.value) || 50 }))} className="text-sm" />
@@ -75,15 +82,25 @@ export function FlowIdentityModal({ name, description, maxDepth, readOnly, onCha {t('soar.editor.descriptionLabel')}