feat: Warnung bei ungespeicherten Workflow-Änderungen
All checks were successful
Deploy Development / deploy (push) Successful in 52s
Build Test / pytest-backend (push) Successful in 4s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 16s

Issue #0: Ungespeicherte Änderungen gehen verloren beim "Zurück"-Klick

Implementiert:
- hasUnsavedChanges State tracking
- Warnung beim "Zurück"-Button (navigate zu /admin/prompts)
- Warnung beim "Neu"-Button (nur wenn unsaved changes)
- Browser beforeunload Event (warnt bei Browser-Back/Refresh)

Tracking für alle Änderungen:
- onNodesChange/onEdgesChange (Node-Bewegung, Löschen via Delete-Taste)
- onConnect (neue Edges)
- handleAddNode (Node hinzufügen)
- handleNodeUpdate (Node-Daten ändern)
- handleDeleteNode (Node löschen via Button)
- workflowName onChange (Titel ändern)

Flag wird cleared:
- Nach erfolgreichem Save (Update/Create)
- Nach erfolgreichem Load
- Bei "Neu" (nach User-Bestätigung)

UX:
- Klare Warnung: "Du hast ungespeicherte Änderungen"
- Kein Datenverlust mehr durch versehentliches Zurück
- Browser warnt auch bei Refresh/Close

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-04-11 15:21:31 +02:00
parent c9357d4c0e
commit 3fa01dd686

View File

@ -77,11 +77,23 @@ export default function WorkflowEditorPage() {
const [placeholderPickerTarget, setPlaceholderPickerTarget] = useState('end') // 'end' | 'inline'
const endNodeTextareaRef = useRef(null)
const inlineTemplateTextareaRef = useRef(null)
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
// Toast & Confirm Dialog
const [toast, setToast] = useState(null)
const [confirmDialog, setConfirmDialog] = useState(null)
// Wrapped handlers to track unsaved changes
const handleNodesChange = useCallback((changes) => {
onNodesChange(changes)
setHasUnsavedChanges(true)
}, [onNodesChange])
const handleEdgesChange = useCallback((changes) => {
onEdgesChange(changes)
setHasUnsavedChanges(true)
}, [onEdgesChange])
// Load available basis prompts for Analysis nodes
useEffect(() => {
async function loadPrompts() {
@ -112,10 +124,26 @@ export default function WorkflowEditorPage() {
setValidationWarnings(warnings)
}, [nodes, edges])
// Warn on browser back/refresh if unsaved changes
useEffect(() => {
const handleBeforeUnload = (e) => {
if (hasUnsavedChanges) {
e.preventDefault()
e.returnValue = '' // Chrome requires returnValue to be set
}
}
window.addEventListener('beforeunload', handleBeforeUnload)
return () => window.removeEventListener('beforeunload', handleBeforeUnload)
}, [hasUnsavedChanges])
// Handlers
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
(params) => {
setEdges((eds) => addEdge(params, eds))
setHasUnsavedChanges(true)
},
[setEdges]
)
@ -144,6 +172,7 @@ export default function WorkflowEditorPage() {
}
}
setNodes((nds) => [...nds, base])
setHasUnsavedChanges(true)
}
const handleNodeUpdate = (nodeId, updates) => {
@ -153,6 +182,7 @@ export default function WorkflowEditorPage() {
console.log('📝 Nodes after update:', updated.find(n => n.id === nodeId))
return updated
})
setHasUnsavedChanges(true)
}
const handleDeleteNode = () => {
@ -161,6 +191,7 @@ export default function WorkflowEditorPage() {
setNodes((nds) => nds.filter((n) => n.id !== selectedNode.id))
setEdges((eds) => eds.filter((e) => e.source !== selectedNode.id && e.target !== selectedNode.id))
setSelectedNodeId(null)
setHasUnsavedChanges(true)
}
const handleSave = async () => {
@ -217,6 +248,9 @@ export default function WorkflowEditorPage() {
console.log('🚀 Navigating to:', `/workflow-editor/${result.id}`)
navigate(`/workflow-editor/${result.id}`)
}
// Clear unsaved changes flag after successful save
setHasUnsavedChanges(false)
} catch (e) {
console.error('❌ handleSave error:', e)
setError(e.message)
@ -275,6 +309,9 @@ export default function WorkflowEditorPage() {
)
nodeIdCounter = maxId + 1
console.log('✅ Workflow loaded successfully, nodes:', loadedNodes.length, 'edges:', loadedEdges.length)
// Clear unsaved changes flag after successful load
setHasUnsavedChanges(false)
} catch (e) {
console.error('❌ loadWorkflow error:', e)
setError(e.message)
@ -296,14 +333,18 @@ export default function WorkflowEditorPage() {
}
const handleNew = () => {
if (confirm('Neuen Workflow erstellen? Ungespeicherte Änderungen gehen verloren.')) {
setNodes([])
setEdges([])
setCurrentPrompt(null)
setWorkflowName('Neuer Workflow')
setSelectedNodeId(null)
navigate('/workflow-editor/new')
if (hasUnsavedChanges) {
if (!confirm('Neuen Workflow erstellen? Ungespeicherte Änderungen gehen verloren.')) {
return
}
}
setNodes([])
setEdges([])
setCurrentPrompt(null)
setWorkflowName('Neuer Workflow')
setSelectedNodeId(null)
setHasUnsavedChanges(false)
navigate('/workflow-editor/new')
}
const handleDelete = async () => {
@ -328,6 +369,17 @@ export default function WorkflowEditorPage() {
setExecutionResult(result)
}
const handleBack = () => {
if (hasUnsavedChanges) {
const confirm = window.confirm(
'Du hast ungespeicherte Änderungen.\n\n' +
'Möchtest du wirklich zurück gehen? Alle Änderungen gehen verloren.'
)
if (!confirm) return
}
navigate('/admin/prompts')
}
const handlePlaceholderSelect = (placeholderString) => {
if (!selectedNode) return
@ -392,14 +444,17 @@ export default function WorkflowEditorPage() {
<div className="workflow-editor">
{/* Toolbar */}
<div className="workflow-toolbar">
<button className="btn-secondary" onClick={() => navigate('/admin/prompts')}>
<button className="btn-secondary" onClick={handleBack}>
Zurück
</button>
<input
type="text"
value={workflowName}
onChange={(e) => setWorkflowName(e.target.value)}
onChange={(e) => {
setWorkflowName(e.target.value)
setHasUnsavedChanges(true)
}}
placeholder="Interner Workflow-Name (Slug-Basis)"
title="Technischer Name in der Datenbank. Den sichtbaren Titel für die KI-Analyse setzt du in der Start-Node."
style={{ flex: 1, padding: '8px', borderRadius: '4px', border: '1px solid var(--border)' }}
@ -498,8 +553,8 @@ export default function WorkflowEditorPage() {
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodesChange={handleNodesChange}
onEdgesChange={handleEdgesChange}
onConnect={onConnect}
onNodeClick={onNodeClick}
/>