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 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}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user