feat: Warnung bei ungespeicherten Workflow-Änderungen
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:
parent
c9357d4c0e
commit
3fa01dd686
|
|
@ -77,11 +77,23 @@ export default function WorkflowEditorPage() {
|
||||||
const [placeholderPickerTarget, setPlaceholderPickerTarget] = useState('end') // 'end' | 'inline'
|
const [placeholderPickerTarget, setPlaceholderPickerTarget] = useState('end') // 'end' | 'inline'
|
||||||
const endNodeTextareaRef = useRef(null)
|
const endNodeTextareaRef = useRef(null)
|
||||||
const inlineTemplateTextareaRef = useRef(null)
|
const inlineTemplateTextareaRef = useRef(null)
|
||||||
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
|
||||||
|
|
||||||
// Toast & Confirm Dialog
|
// Toast & Confirm Dialog
|
||||||
const [toast, setToast] = useState(null)
|
const [toast, setToast] = useState(null)
|
||||||
const [confirmDialog, setConfirmDialog] = 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
|
// Load available basis prompts for Analysis nodes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function loadPrompts() {
|
async function loadPrompts() {
|
||||||
|
|
@ -112,10 +124,26 @@ export default function WorkflowEditorPage() {
|
||||||
setValidationWarnings(warnings)
|
setValidationWarnings(warnings)
|
||||||
}, [nodes, edges])
|
}, [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 ──────────────────────────────────────────────────────────────
|
// ── Handlers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const onConnect = useCallback(
|
const onConnect = useCallback(
|
||||||
(params) => setEdges((eds) => addEdge(params, eds)),
|
(params) => {
|
||||||
|
setEdges((eds) => addEdge(params, eds))
|
||||||
|
setHasUnsavedChanges(true)
|
||||||
|
},
|
||||||
[setEdges]
|
[setEdges]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -144,6 +172,7 @@ export default function WorkflowEditorPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setNodes((nds) => [...nds, base])
|
setNodes((nds) => [...nds, base])
|
||||||
|
setHasUnsavedChanges(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNodeUpdate = (nodeId, updates) => {
|
const handleNodeUpdate = (nodeId, updates) => {
|
||||||
|
|
@ -153,6 +182,7 @@ export default function WorkflowEditorPage() {
|
||||||
console.log('📝 Nodes after update:', updated.find(n => n.id === nodeId))
|
console.log('📝 Nodes after update:', updated.find(n => n.id === nodeId))
|
||||||
return updated
|
return updated
|
||||||
})
|
})
|
||||||
|
setHasUnsavedChanges(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteNode = () => {
|
const handleDeleteNode = () => {
|
||||||
|
|
@ -161,6 +191,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))
|
||||||
setSelectedNodeId(null)
|
setSelectedNodeId(null)
|
||||||
|
setHasUnsavedChanges(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
|
|
@ -217,6 +248,9 @@ export default function WorkflowEditorPage() {
|
||||||
console.log('🚀 Navigating to:', `/workflow-editor/${result.id}`)
|
console.log('🚀 Navigating to:', `/workflow-editor/${result.id}`)
|
||||||
navigate(`/workflow-editor/${result.id}`)
|
navigate(`/workflow-editor/${result.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear unsaved changes flag after successful save
|
||||||
|
setHasUnsavedChanges(false)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('❌ handleSave error:', e)
|
console.error('❌ handleSave error:', e)
|
||||||
setError(e.message)
|
setError(e.message)
|
||||||
|
|
@ -275,6 +309,9 @@ export default function WorkflowEditorPage() {
|
||||||
)
|
)
|
||||||
nodeIdCounter = maxId + 1
|
nodeIdCounter = maxId + 1
|
||||||
console.log('✅ Workflow loaded successfully, nodes:', loadedNodes.length, 'edges:', loadedEdges.length)
|
console.log('✅ Workflow loaded successfully, nodes:', loadedNodes.length, 'edges:', loadedEdges.length)
|
||||||
|
|
||||||
|
// Clear unsaved changes flag after successful load
|
||||||
|
setHasUnsavedChanges(false)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('❌ loadWorkflow error:', e)
|
console.error('❌ loadWorkflow error:', e)
|
||||||
setError(e.message)
|
setError(e.message)
|
||||||
|
|
@ -296,14 +333,18 @@ export default function WorkflowEditorPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNew = () => {
|
const handleNew = () => {
|
||||||
if (confirm('Neuen Workflow erstellen? Ungespeicherte Änderungen gehen verloren.')) {
|
if (hasUnsavedChanges) {
|
||||||
setNodes([])
|
if (!confirm('Neuen Workflow erstellen? Ungespeicherte Änderungen gehen verloren.')) {
|
||||||
setEdges([])
|
return
|
||||||
setCurrentPrompt(null)
|
}
|
||||||
setWorkflowName('Neuer Workflow')
|
|
||||||
setSelectedNodeId(null)
|
|
||||||
navigate('/workflow-editor/new')
|
|
||||||
}
|
}
|
||||||
|
setNodes([])
|
||||||
|
setEdges([])
|
||||||
|
setCurrentPrompt(null)
|
||||||
|
setWorkflowName('Neuer Workflow')
|
||||||
|
setSelectedNodeId(null)
|
||||||
|
setHasUnsavedChanges(false)
|
||||||
|
navigate('/workflow-editor/new')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
|
|
@ -328,6 +369,17 @@ export default function WorkflowEditorPage() {
|
||||||
setExecutionResult(result)
|
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) => {
|
const handlePlaceholderSelect = (placeholderString) => {
|
||||||
if (!selectedNode) return
|
if (!selectedNode) return
|
||||||
|
|
||||||
|
|
@ -392,14 +444,17 @@ export default function WorkflowEditorPage() {
|
||||||
<div className="workflow-editor">
|
<div className="workflow-editor">
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div className="workflow-toolbar">
|
<div className="workflow-toolbar">
|
||||||
<button className="btn-secondary" onClick={() => navigate('/admin/prompts')}>
|
<button className="btn-secondary" onClick={handleBack}>
|
||||||
← Zurück
|
← Zurück
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={workflowName}
|
value={workflowName}
|
||||||
onChange={(e) => setWorkflowName(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setWorkflowName(e.target.value)
|
||||||
|
setHasUnsavedChanges(true)
|
||||||
|
}}
|
||||||
placeholder="Interner Workflow-Name (Slug-Basis)"
|
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."
|
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)' }}
|
style={{ flex: 1, padding: '8px', borderRadius: '4px', border: '1px solid var(--border)' }}
|
||||||
|
|
@ -498,8 +553,8 @@ export default function WorkflowEditorPage() {
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={handleNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={handleEdgesChange}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user