Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/components/builder/hooks/use-builder-clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ type ClipboardPayload = {
connections: Connection[]
}

type SaveToHistory = (snapshot?: Workflow) => void
type SaveToHistory = () => void

type MutateWorkflow = (id: string) => Promise<void>

export function useBuilderClipboard(options: {
workflowId: string | null
selectedNodeIds: string[]
workflow: Workflow | null | undefined
saveToHistory: SaveToHistory
mutateWorkflow: (id: string) => void
mutateWorkflow: MutateWorkflow
safeFetch: SafeFetch
toast: ToastFn
}) {
Expand Down Expand Up @@ -66,16 +68,15 @@ export function useBuilderClipboard(options: {
toast({ title: "Nothing to paste", variant: "destructive" })
return
}
const previous = workflow
const response = await safeFetch(`/api/workflows/${workflowId}/paste`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nodes: clipboard.nodes, connections: clipboard.connections }),
})
if (response.ok) {
const result = await response.json()
saveToHistory(previous)
mutateWorkflow(workflowId)
saveToHistory()
await mutateWorkflow(workflowId)
toast({ title: `Pasted ${result.nodeIds?.length ?? 0} node(s)` })
} else {
toast({ title: "Nothing to paste", variant: "destructive" })
Expand All @@ -85,7 +86,6 @@ export function useBuilderClipboard(options: {
const duplicateNodeIds = useCallback(
async (nodeIds: string[], successTitle: string) => {
if (!nodeIds.length || !workflowId || !workflow) return
const previous = workflow
const copied = await copyNodeIds(nodeIds)
if (!copied || !clipboardRef.current) return
const pasteRes = await safeFetch(`/api/workflows/${workflowId}/paste`, {
Expand All @@ -97,8 +97,8 @@ export function useBuilderClipboard(options: {
}),
})
if (pasteRes.ok) {
saveToHistory(previous)
mutateWorkflow(workflowId)
saveToHistory()
await mutateWorkflow(workflowId)
toast({ title: successTitle })
}
},
Expand Down
76 changes: 41 additions & 35 deletions src/components/builder/hooks/use-builder-graph-mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@ type ToastFn = (props: {
variant?: "default" | "destructive"
}) => void

type SaveToHistory = (snapshot?: Workflow) => void
type SaveToHistory = () => void

type MutateWorkflow = (id: string) => Promise<void>

export function useBuilderGraphMutations(options: {
workflowId: string | null
workflow: Workflow | null | undefined
edges: Edge[]
saveToHistory: SaveToHistory
mutateWorkflow: (id: string) => void
mutateWorkflow: MutateWorkflow
safeFetch: SafeFetch
toast: ToastFn
screenToFlowPosition: (position: XYPosition) => XYPosition
Expand Down Expand Up @@ -62,33 +64,33 @@ export function useBuilderGraphMutations(options: {
const handleNodeDeleteById = useCallback(
async (nodeId: string) => {
if (!workflowId || !workflow) return
const previous = workflow
const response = await safeFetch(`/api/workflows/${workflowId}/nodes/${nodeId}`, {
method: "DELETE",
})
if (response.ok) {
saveToHistory(previous)
saveToHistory()
await mutateWorkflow(workflowId)
return
}
mutateWorkflow(workflowId)
void mutateWorkflow(workflowId)
},
[workflowId, workflow, saveToHistory, mutateWorkflow, safeFetch],
)

const handleAssignToFrame = useCallback(
async (nodeId: string, frameId: string) => {
if (!workflowId || !workflow) return
const previous = workflow
const response = await safeFetch(`/api/workflows/${workflowId}/nodes/${nodeId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ parentId: frameId }),
})
if (!response.ok) {
mutateWorkflow(workflowId)
void mutateWorkflow(workflowId)
return
}
saveToHistory(previous)
mutateWorkflow(workflowId)
saveToHistory()
await mutateWorkflow(workflowId)
toast({ title: "Node added to frame" })
},
[workflowId, workflow, saveToHistory, mutateWorkflow, toast, safeFetch],
Expand All @@ -97,18 +99,17 @@ export function useBuilderGraphMutations(options: {
const handleRemoveFromFrame = useCallback(
async (nodeId: string) => {
if (!workflowId || !workflow) return
const previous = workflow
const response = await safeFetch(`/api/workflows/${workflowId}/nodes/${nodeId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ parentId: null }),
})
if (!response.ok) {
mutateWorkflow(workflowId)
void mutateWorkflow(workflowId)
return
}
saveToHistory(previous)
mutateWorkflow(workflowId)
saveToHistory()
await mutateWorkflow(workflowId)
toast({ title: "Node removed from frame" })
},
[workflowId, workflow, saveToHistory, mutateWorkflow, toast, safeFetch],
Expand All @@ -119,16 +120,17 @@ export function useBuilderGraphMutations(options: {
if (!workflowId || !workflow) return
const node = workflow.nodes.find((n) => n.id === nodeId)
if (!node) return
const previous = workflow
const response = await safeFetch(`/api/workflows/${workflowId}/nodes/${nodeId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data: { ...node.data, label: newLabel } }),
})
if (response.ok) {
saveToHistory(previous)
saveToHistory()
await mutateWorkflow(workflowId)
return
}
mutateWorkflow(workflowId)
void mutateWorkflow(workflowId)
},
[workflowId, workflow, saveToHistory, mutateWorkflow, safeFetch],
)
Expand All @@ -145,17 +147,18 @@ export function useBuilderGraphMutations(options: {
onEdgesChange(changes)
const removeChanges = changes.filter((c) => c.type === "remove") as { id: string }[]
if (removeChanges.length > 0 && workflowId && workflow) {
const previous = workflow
const updatedEdges = edges.filter((e) => !removeChanges.some((r) => r.id === e.id))
void safeFetch(`/api/workflows/${workflowId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ connections: reactFlowEdgesToConnections(updatedEdges) }),
}).then((response) => {
}).then(async (response) => {
if (response.ok) {
saveToHistory(previous)
saveToHistory()
await mutateWorkflow(workflowId)
return
}
mutateWorkflow(workflowId)
void mutateWorkflow(workflowId)
})
}
},
Expand All @@ -165,7 +168,6 @@ export function useBuilderGraphMutations(options: {
const handleConnect = useCallback(
async (connection: ReactFlowConnection) => {
if (!workflowId || !workflow || !connection.source || !connection.target) return
const previous = workflow
const response = await safeFetch(`/api/workflows/${workflowId}/connections`, {
method: "POST",
headers: { "Content-Type": "application/json" },
Expand All @@ -177,17 +179,18 @@ export function useBuilderGraphMutations(options: {
}),
})
if (response.ok) {
saveToHistory(previous)
saveToHistory()
await mutateWorkflow(workflowId)
return
}
mutateWorkflow(workflowId)
void mutateWorkflow(workflowId)
},
[workflowId, workflow, saveToHistory, mutateWorkflow, safeFetch],
)

const handleNodeDragStop = useCallback(
async (_: React.MouseEvent, node: Node<WorkflowNodeData, NodeType>) => {
if (!workflowId || !workflow) return
const previous = workflow
const snappedPosition = {
x: Math.round(node.position.x / GRID_SIZE) * GRID_SIZE,
y: Math.round(node.position.y / GRID_SIZE) * GRID_SIZE,
Expand All @@ -198,17 +201,18 @@ export function useBuilderGraphMutations(options: {
body: JSON.stringify({ position: snappedPosition }),
})
if (response.ok) {
saveToHistory(previous)
saveToHistory()
await mutateWorkflow(workflowId)
return
}
mutateWorkflow(workflowId)
void mutateWorkflow(workflowId)
},
[workflowId, workflow, saveToHistory, mutateWorkflow, safeFetch],
)

const handleAddNode = useCallback(
async (type: NodeType, position?: Position) => {
if (!workflowId || !workflow) return
const previous = workflow

let posX: number
let posY: number
Expand Down Expand Up @@ -263,9 +267,11 @@ export function useBuilderGraphMutations(options: {
body: JSON.stringify(nodePayload),
})
if (response.ok) {
saveToHistory(previous)
saveToHistory()
await mutateWorkflow(workflowId)
return
}
mutateWorkflow(workflowId)
void mutateWorkflow(workflowId)
},
[workflowId, workflow, saveToHistory, mutateWorkflow, screenToFlowPosition, safeFetch],
)
Expand All @@ -280,12 +286,11 @@ export function useBuilderGraphMutations(options: {
clearTimeout(layoutTransitionTimeoutRef.current)
layoutTransitionTimeoutRef.current = null
}
const previous = workflow
setIsLayoutTransitioning(true)
const response = await safeFetch(`/api/workflows/${workflowId}/auto-layout`, { method: "POST" })
if (response.ok) {
saveToHistory(previous)
mutateWorkflow(workflowId)
saveToHistory()
await mutateWorkflow(workflowId)
toast({ title: "Layout applied successfully" })
layoutTransitionTimeoutRef.current = setTimeout(() => {
layoutTransitionTimeoutRef.current = null
Expand All @@ -301,16 +306,17 @@ export function useBuilderGraphMutations(options: {
async (nodeIds: string | string[] | null) => {
const ids = Array.isArray(nodeIds) ? nodeIds : nodeIds ? [nodeIds] : []
if (!ids.length || !workflowId || !workflow) return
const previous = workflow
const results = await Promise.all(
ids.map((nodeId) =>
safeFetch(`/api/workflows/${workflowId}/nodes/${nodeId}`, { method: "DELETE" }),
),
)
if (results.every((response) => response.ok)) {
saveToHistory(previous)
saveToHistory()
await mutateWorkflow(workflowId)
return
}
mutateWorkflow(workflowId)
void mutateWorkflow(workflowId)
},
[workflowId, workflow, saveToHistory, mutateWorkflow, safeFetch],
)
Expand Down
42 changes: 26 additions & 16 deletions src/components/builder/hooks/use-builder-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,19 +79,30 @@ export function useBuilderHistory(options: {
const canUndo = canUndoBit === "1"
const canRedo = canRedoBit === "1"

const mutateWorkflow = useCallback((id: string) => {
mutate(`/api/workflows/${id}`)
const mutateWorkflow = useCallback(async (id: string) => {
const fresh = await mutate<Workflow>(
`/api/workflows/${id}`,
async () => {
const response = await fetch(`/api/workflows/${id}`)
if (!response.ok) {
throw new Error(await response.text())
}
return (await response.json()) as Workflow
},
{ revalidate: true },
)
if (fresh) {
lastPersistedRef.current = fresh
lastSyncedUpdatedAtRef.current = workflowUpdatedAtMs(fresh)
}
}, [])

const saveToHistory = useCallback(
(snapshot?: Workflow) => {
const toSave = snapshot ?? workflow
if (toSave && workflowId) {
getHistoryManager(workflowId).saveState(toSave)
}
},
[workflow, workflowId],
)
const saveToHistory = useCallback(() => {
const toSave = lastPersistedRef.current ?? workflow
if (toSave && workflowId) {
getHistoryManager(workflowId).saveState(toSave)
}
}, [workflow, workflowId])

const applyHistoryTransition = useCallback(
async (direction: "undo" | "redo") => {
Expand Down Expand Up @@ -158,7 +169,6 @@ export function useBuilderHistory(options: {
if (transitionInFlightRef.current) return
transitionInFlightRef.current = true
setIsHistoryTransitioning(true)
const previous = workflow
try {
const response = await safeFetch(`/api/workflows/${workflowId}`, {
method: "PATCH",
Expand All @@ -169,13 +179,13 @@ export function useBuilderHistory(options: {
}),
})
if (!response.ok) return
getHistoryManager(workflowId).saveState(previous)
saveToHistory()
lastPersistedRef.current = {
...previous,
...workflow,
nodes: version.nodes,
connections: version.connections,
}
mutateWorkflow(workflowId)
void mutateWorkflow(workflowId)
toast({
title: "Version restored",
description: `Restored ${version.name}`,
Expand All @@ -187,7 +197,7 @@ export function useBuilderHistory(options: {
setIsHistoryTransitioning(false)
}
},
[workflowId, workflow, mutateWorkflow, safeFetch, toast],
[workflowId, workflow, mutateWorkflow, safeFetch, toast, saveToHistory],
)

return {
Expand Down
8 changes: 7 additions & 1 deletion src/lib/history-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,14 @@ export class HistoryManager {
}

saveState(workflow: Workflow) {
const snapshot = JSON.parse(JSON.stringify(workflow)) as Workflow
const top = this.undoStack[this.undoStack.length - 1]
if (top && JSON.stringify(top.workflow) === JSON.stringify(snapshot)) {
return
}

this.undoStack.push({
workflow: JSON.parse(JSON.stringify(workflow)),
workflow: snapshot,
timestamp: Date.now(),
})

Expand Down
Loading
Loading