fix: Phase 5 - Workflow Editor UX Fixes (Round 3)
All checks were successful
Deploy Development / deploy (push) Successful in 53s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s

Frontend Fixes:
- AdminPromptsPage: Edit button navigates to workflow-editor for workflow type prompts
- WorkflowEditorPage: Fixed save navigation (alert before navigate)
- WorkflowEditorPage: selectedNode derived from selectedNodeId (eliminates stale state)
- FallbackConfig: Show node labels instead of IDs in fallback edge dropdown
- WorkflowCanvas: Enable edge deletion with deletable: true
- WorkflowEditorPage: Hide sidebar when config panel is open

Bugs Fixed:
- C1: Save error "Method Not Allowed" after success
- C2: Edit button in admin doesn't open workflow editor
- H1: Prompt selection not displayed when re-editing node
- H2: Fallback edge dropdown shows node_1/node_2 instead of names
- H3: Cannot delete edges
- M1: Sidebar takes space when config panel open

Technical Changes:
- Replaced useState(selectedNode) with useState(selectedNodeId) + derived selectedNode
- Removed sync useEffect (no longer needed with derived state)
- Added nodes prop to FallbackConfig for label lookup
- Swapped alert/navigate order to prevent navigation errors

Testing: Manual testing required (see manual test cases)
This commit is contained in:
Lars 2026-04-04 21:16:15 +02:00
parent 7d22b052dd
commit 2f70a39052
4 changed files with 30 additions and 27 deletions

View File

@ -41,7 +41,8 @@ export function WorkflowCanvas({
maxZoom={2} maxZoom={2}
defaultEdgeOptions={{ defaultEdgeOptions={{
animated: false, animated: false,
style: { strokeWidth: 2 } style: { strokeWidth: 2 },
deletable: true
}} }}
> >
<Background <Background

View File

@ -4,15 +4,22 @@
* Props: * Props:
* - node: React Flow Node object * - node: React Flow Node object
* - edges: Array of React Flow edges * - edges: Array of React Flow edges
* - nodes: Array of React Flow nodes (for label lookup)
* - onChange: (nodeId, updates) => void * - onChange: (nodeId, updates) => void
*/ */
export function FallbackConfig({ node, edges, onChange }) { export function FallbackConfig({ node, edges, nodes, onChange }) {
const fallbackStrategy = node.data.fallback_strategy || 'conservative_skip' const fallbackStrategy = node.data.fallback_strategy || 'conservative_skip'
const fallbackEdge = node.data.fallback_edge || null const fallbackEdge = node.data.fallback_edge || null
// Outgoing Edges von diesem Node // Outgoing Edges von diesem Node
const outgoingEdges = edges.filter(e => e.source === node.id) const outgoingEdges = edges.filter(e => e.source === node.id)
// Helper: Get node label by ID
const getNodeLabel = (nodeId) => {
const targetNode = nodes.find(n => n.id === nodeId)
return targetNode?.data?.label || nodeId
}
const handleStrategyChange = (e) => { const handleStrategyChange = (e) => {
const strategy = e.target.value const strategy = e.target.value
onChange(node.id, { fallback_strategy: strategy }) onChange(node.id, { fallback_strategy: strategy })
@ -53,7 +60,7 @@ export function FallbackConfig({ node, edges, onChange }) {
<option value="">-- Kante wählen --</option> <option value="">-- Kante wählen --</option>
{outgoingEdges.map(e => ( {outgoingEdges.map(e => (
<option key={e.id} value={e.id}> <option key={e.id} value={e.id}>
{e.data?.label || `Edge → ${e.target}`} {e.data?.label || `Edge → ${getNodeLabel(e.target)}`}
</option> </option>
))} ))}
</select> </select>

View File

@ -530,7 +530,13 @@ export default function AdminPromptsPage() {
justifyContent: 'flex-end' justifyContent: 'flex-end'
}}> }}>
<button <button
onClick={() => setEditingPrompt(prompt)} onClick={() => {
if (prompt.type === 'workflow') {
navigate(`/workflow-editor/${prompt.id}`)
} else {
setEditingPrompt(prompt)
}
}}
style={{ style={{
background: 'none', background: 'none',
border: 'none', border: 'none',

View File

@ -34,7 +34,8 @@ export default function WorkflowEditorPage() {
// State // State
const [nodes, setNodes, onNodesChange] = useNodesState([]) const [nodes, setNodes, onNodesChange] = useNodesState([])
const [edges, setEdges, onEdgesChange] = useEdgesState([]) const [edges, setEdges, onEdgesChange] = useEdgesState([])
const [selectedNode, setSelectedNode] = useState(null) const [selectedNodeId, setSelectedNodeId] = useState(null)
const selectedNode = selectedNodeId ? nodes.find(n => n.id === selectedNodeId) : null
const [currentPrompt, setCurrentPrompt] = useState(null) const [currentPrompt, setCurrentPrompt] = useState(null)
const [workflowName, setWorkflowName] = useState('Neuer Workflow') const [workflowName, setWorkflowName] = useState('Neuer Workflow')
const [workflowDescription, setWorkflowDescription] = useState('') const [workflowDescription, setWorkflowDescription] = useState('')
@ -73,16 +74,6 @@ export default function WorkflowEditorPage() {
setValidationWarnings(warnings) setValidationWarnings(warnings)
}, [nodes, edges]) }, [nodes, edges])
// Keep selectedNode in sync with nodes array (wichtig für Config Panel!)
useEffect(() => {
if (selectedNode) {
const updatedNode = nodes.find(n => n.id === selectedNode.id)
if (updatedNode && updatedNode !== selectedNode) {
setSelectedNode(updatedNode)
}
}
}, [nodes])
// Handlers // Handlers
const onConnect = useCallback( const onConnect = useCallback(
@ -91,7 +82,7 @@ export default function WorkflowEditorPage() {
) )
const onNodeClick = useCallback((event, node) => { const onNodeClick = useCallback((event, node) => {
setSelectedNode(node) setSelectedNodeId(node.id)
}, []) }, [])
const handleAddNode = (nodeType) => { const handleAddNode = (nodeType) => {
@ -118,7 +109,7 @@ export default function WorkflowEditorPage() {
setNodes((nds) => nds.filter((n) => n.id !== selectedNode.id)) setNodes((nds) => nds.filter((n) => n.id !== selectedNode.id))
setEdges((eds) => eds.filter((e) => e.source !== selectedNode.id && e.target !== selectedNode.id)) setEdges((eds) => eds.filter((e) => e.source !== selectedNode.id && e.target !== selectedNode.id))
setSelectedNode(null) setSelectedNodeId(null)
} }
const handleSave = async () => { const handleSave = async () => {
@ -157,8 +148,8 @@ export default function WorkflowEditorPage() {
graph_data graph_data
}) })
setCurrentPrompt({ id: result.id, name: workflowName }) setCurrentPrompt({ id: result.id, name: workflowName })
navigate(`/workflow-editor/${result.id}`)
alert('Workflow erstellt!') alert('Workflow erstellt!')
navigate(`/workflow-editor/${result.id}`)
} }
} catch (e) { } catch (e) {
setError(e.message) setError(e.message)
@ -219,7 +210,7 @@ export default function WorkflowEditorPage() {
setCurrentPrompt(null) setCurrentPrompt(null)
setWorkflowName('Neuer Workflow') setWorkflowName('Neuer Workflow')
setWorkflowDescription('') setWorkflowDescription('')
setSelectedNode(null) setSelectedNodeId(null)
navigate('/workflow-editor/new') navigate('/workflow-editor/new')
} }
} }
@ -296,7 +287,7 @@ export default function WorkflowEditorPage() {
{/* Main Content */} {/* Main Content */}
<div className="workflow-content"> <div className="workflow-content">
{/* Sidebar */} {/* Sidebar */}
<div className="workflow-sidebar"> <div className="workflow-sidebar" style={{ display: selectedNode ? 'none' : 'block' }}>
<div className="sidebar-section"> <div className="sidebar-section">
<h3>Workflow-Knoten</h3> <h3>Workflow-Knoten</h3>
<div className="node-palette"> <div className="node-palette">
@ -357,7 +348,7 @@ export default function WorkflowEditorPage() {
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<h2 style={{ margin: 0 }}>Node-Konfiguration</h2> <h2 style={{ margin: 0 }}>Node-Konfiguration</h2>
<button <button
onClick={() => setSelectedNode(null)} onClick={() => setSelectedNodeId(null)}
style={{ style={{
background: 'none', background: 'none',
border: 'none', border: 'none',
@ -435,7 +426,7 @@ export default function WorkflowEditorPage() {
</div> </div>
<QuestionAugmentationPanel node={selectedNode} onChange={handleNodeUpdate} /> <QuestionAugmentationPanel node={selectedNode} onChange={handleNodeUpdate} />
<FallbackConfig node={selectedNode} edges={edges} onChange={handleNodeUpdate} /> <FallbackConfig node={selectedNode} edges={edges} nodes={nodes} onChange={handleNodeUpdate} />
</> </>
)} )}
@ -447,7 +438,7 @@ export default function WorkflowEditorPage() {
edges={edges} edges={edges}
onChange={handleNodeUpdate} onChange={handleNodeUpdate}
/> />
<FallbackConfig node={selectedNode} edges={edges} onChange={handleNodeUpdate} /> <FallbackConfig node={selectedNode} edges={edges} nodes={nodes} onChange={handleNodeUpdate} />
</> </>
)} )}
@ -464,8 +455,7 @@ export default function WorkflowEditorPage() {
{validationErrors.map((err, i) => ( {validationErrors.map((err, i) => (
<div key={i} className="validation-error" onClick={() => { <div key={i} className="validation-error" onClick={() => {
if (err.nodeId) { if (err.nodeId) {
const node = nodes.find(n => n.id === err.nodeId) setSelectedNodeId(err.nodeId)
if (node) setSelectedNode(node)
} }
}}> }}>
{err.message} {err.message}
@ -475,8 +465,7 @@ export default function WorkflowEditorPage() {
{validationWarnings.map((warn, i) => ( {validationWarnings.map((warn, i) => (
<div key={i} className="validation-warning" onClick={() => { <div key={i} className="validation-warning" onClick={() => {
if (warn.nodeId) { if (warn.nodeId) {
const node = nodes.find(n => n.id === warn.nodeId) setSelectedNodeId(warn.nodeId)
if (node) setSelectedNode(node)
} }
}}> }}>
{warn.message} {warn.message}