Merge pull request #290 from Pouzor/fix/undo-auto-layout-and-yaml-import
fix: allow undo after Auto Layout and YAML import
This commit is contained in:
@@ -53,7 +53,7 @@ import { buildProxmoxClusterEdges } from '@/components/proxmox/clusterEdges'
|
|||||||
const STANDALONE = import.meta.env.VITE_STANDALONE === 'true'
|
const STANDALONE = import.meta.env.VITE_STANDALONE === 'true'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { loadCanvas, markSaved, markUnsaved, selectedNodeId, selectedNodeIds, addNode, updateNode, deleteNode, onConnect, updateEdge, deleteEdge, setProxmoxContainerMode, setNodeZIndex, editingGroupRectId, setEditingGroupRectId, editingTextId, setEditingTextId, nodes, edges, snapshotHistory, undo, redo, addToGroup, addToContainer, floorMap, setFloorMap } = useCanvasStore()
|
const { loadCanvas, applyLayout, markSaved, markUnsaved, selectedNodeId, selectedNodeIds, addNode, updateNode, deleteNode, onConnect, updateEdge, deleteEdge, setProxmoxContainerMode, setNodeZIndex, editingGroupRectId, setEditingGroupRectId, editingTextId, setEditingTextId, nodes, edges, snapshotHistory, undo, redo, addToGroup, addToContainer, floorMap, setFloorMap } = useCanvasStore()
|
||||||
const canvasRef = useRef<HTMLDivElement>(null)
|
const canvasRef = useRef<HTMLDivElement>(null)
|
||||||
const { isAuthenticated } = useAuthStore()
|
const { isAuthenticated } = useAuthStore()
|
||||||
const { activeTheme, setTheme, customStyle, setCustomStyle } = useThemeStore()
|
const { activeTheme, setTheme, customStyle, setCustomStyle } = useThemeStore()
|
||||||
@@ -498,9 +498,11 @@ export default function App() {
|
|||||||
|
|
||||||
const handleAutoLayout = useCallback(() => {
|
const handleAutoLayout = useCallback(() => {
|
||||||
const laid = applyDagreLayout(nodes, edges)
|
const laid = applyDagreLayout(nodes, edges)
|
||||||
loadCanvas(laid, edges)
|
// applyLayout keeps undo history so the user can revert an accidental
|
||||||
|
// Auto Layout (#280); loadCanvas would wipe it.
|
||||||
|
applyLayout(laid, edges)
|
||||||
toast.success('Canvas auto-arranged')
|
toast.success('Canvas auto-arranged')
|
||||||
}, [nodes, edges, loadCanvas])
|
}, [nodes, edges, applyLayout])
|
||||||
|
|
||||||
const handleExportMd = useCallback(async () => {
|
const handleExportMd = useCallback(async () => {
|
||||||
const md = generateMarkdownTable(nodes)
|
const md = generateMarkdownTable(nodes)
|
||||||
@@ -522,14 +524,14 @@ export default function App() {
|
|||||||
const handleImportYaml = useCallback((content: string) => {
|
const handleImportYaml = useCallback((content: string) => {
|
||||||
try {
|
try {
|
||||||
const { nodes: merged, edges: mergedEdges, imported } = parseYamlToCanvas(content, nodes, edges)
|
const { nodes: merged, edges: mergedEdges, imported } = parseYamlToCanvas(content, nodes, edges)
|
||||||
snapshotHistory()
|
// applyLayout keeps undo history so an import can be reverted; loadCanvas
|
||||||
loadCanvas(merged, mergedEdges)
|
// would wipe it (#280).
|
||||||
markUnsaved()
|
applyLayout(merged, mergedEdges)
|
||||||
toast.success(`Imported ${imported} node${imported !== 1 ? 's' : ''}`)
|
toast.success(`Imported ${imported} node${imported !== 1 ? 's' : ''}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(`Import failed: ${err instanceof Error ? err.message : String(err)}`)
|
toast.error(`Import failed: ${err instanceof Error ? err.message : String(err)}`)
|
||||||
}
|
}
|
||||||
}, [nodes, edges, snapshotHistory, loadCanvas, markUnsaved])
|
}, [nodes, edges, applyLayout])
|
||||||
|
|
||||||
// Open the read-only live view of the currently active design in a new tab.
|
// Open the read-only live view of the currently active design in a new tab.
|
||||||
// Standalone has no backend/key — it reads localStorage, so just open /view.
|
// Standalone has no backend/key — it reads localStorage, so just open /view.
|
||||||
|
|||||||
@@ -85,4 +85,31 @@ describe('canvasStore — history', () => {
|
|||||||
addNode(makeNode('n3'))
|
addNode(makeNode('n3'))
|
||||||
expect(useCanvasStore.getState().future).toHaveLength(0)
|
expect(useCanvasStore.getState().future).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// --- Auto Layout keeps history so it can be undone (#280) ---
|
||||||
|
|
||||||
|
it('applyLayout snapshots history and can be undone', () => {
|
||||||
|
const { addNode, applyLayout, undo } = useCanvasStore.getState()
|
||||||
|
addNode(makeNode('n1'))
|
||||||
|
const before = useCanvasStore.getState().nodes
|
||||||
|
// simulate an auto-layout that moves the node
|
||||||
|
applyLayout([{ ...before[0], position: { x: 500, y: 500 } }], [])
|
||||||
|
expect(useCanvasStore.getState().nodes[0].position).toEqual({ x: 500, y: 500 })
|
||||||
|
expect(useCanvasStore.getState().past).toHaveLength(1)
|
||||||
|
undo()
|
||||||
|
expect(useCanvasStore.getState().nodes[0].position).toEqual({ x: 0, y: 0 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applyLayout marks the canvas unsaved and clears future', () => {
|
||||||
|
const { addNode, snapshotHistory, undo, applyLayout } = useCanvasStore.getState()
|
||||||
|
addNode(makeNode('n1'))
|
||||||
|
snapshotHistory()
|
||||||
|
addNode(makeNode('n2'))
|
||||||
|
undo() // creates a future entry
|
||||||
|
expect(useCanvasStore.getState().future).toHaveLength(1)
|
||||||
|
applyLayout(useCanvasStore.getState().nodes, [])
|
||||||
|
const s = useCanvasStore.getState()
|
||||||
|
expect(s.hasUnsavedChanges).toBe(true)
|
||||||
|
expect(s.future).toHaveLength(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -147,6 +147,10 @@ interface CanvasState {
|
|||||||
markSaved: () => void
|
markSaved: () => void
|
||||||
markUnsaved: () => void
|
markUnsaved: () => void
|
||||||
loadCanvas: (nodes: Node<NodeData>[], edges: Edge<EdgeData>[]) => void
|
loadCanvas: (nodes: Node<NodeData>[], edges: Edge<EdgeData>[]) => void
|
||||||
|
/** In-place canvas replacement (e.g. Auto Layout) that KEEPS undo history and
|
||||||
|
* marks the canvas unsaved. Unlike loadCanvas, it does not wipe past/future —
|
||||||
|
* loadCanvas is for switching designs, this is for transforming the current one. */
|
||||||
|
applyLayout: (nodes: Node<NodeData>[], edges: Edge<EdgeData>[]) => void
|
||||||
fitViewPending: boolean
|
fitViewPending: boolean
|
||||||
clearFitViewPending: () => void
|
clearFitViewPending: () => void
|
||||||
notifyScanDeviceFound: () => void
|
notifyScanDeviceFound: () => void
|
||||||
@@ -870,6 +874,22 @@ export const useCanvasStore = create<CanvasState>((set) => ({
|
|||||||
set({ nodes: [...parents, ...children], edges, hasUnsavedChanges: false, selectedNodeId: null, past: [], future: [], fitViewPending: true })
|
set({ nodes: [...parents, ...children], edges, hasUnsavedChanges: false, selectedNodeId: null, past: [], future: [], fitViewPending: true })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
applyLayout: (nodes, edges) =>
|
||||||
|
set((state) => {
|
||||||
|
// React Flow requires parents before children in the array
|
||||||
|
const parents = nodes.filter((n) => !n.parentId)
|
||||||
|
const children = nodes.filter((n) => !!n.parentId)
|
||||||
|
return {
|
||||||
|
nodes: [...parents, ...children],
|
||||||
|
edges,
|
||||||
|
past: [...state.past.slice(-49), { nodes: state.nodes, edges: state.edges }],
|
||||||
|
future: [],
|
||||||
|
hasUnsavedChanges: true,
|
||||||
|
selectedNodeId: null,
|
||||||
|
fitViewPending: true,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
clearFitViewPending: () => set({ fitViewPending: false }),
|
clearFitViewPending: () => set({ fitViewPending: false }),
|
||||||
|
|
||||||
applyTypeNodeStyle: (nodeType, style) =>
|
applyTypeNodeStyle: (nodeType, style) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user