Merge remote-tracking branch 'origin/main' into feat/autosave-canvas
# Conflicts: # frontend/src/App.tsx
This commit is contained in:
@@ -55,7 +55,7 @@ import { buildProxmoxClusterEdges } from '@/components/proxmox/clusterEdges'
|
||||
const STANDALONE = import.meta.env.VITE_STANDALONE === 'true'
|
||||
|
||||
export default function App() {
|
||||
const { loadCanvas, markSaved, markUnsaved, hasUnsavedChanges, 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, hasUnsavedChanges, 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 { isAuthenticated } = useAuthStore()
|
||||
const { activeTheme, setTheme, customStyle, setCustomStyle } = useThemeStore()
|
||||
@@ -528,9 +528,11 @@ export default function App() {
|
||||
|
||||
const handleAutoLayout = useCallback(() => {
|
||||
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')
|
||||
}, [nodes, edges, loadCanvas])
|
||||
}, [nodes, edges, applyLayout])
|
||||
|
||||
const handleExportMd = useCallback(async () => {
|
||||
const md = generateMarkdownTable(nodes)
|
||||
@@ -552,14 +554,14 @@ export default function App() {
|
||||
const handleImportYaml = useCallback((content: string) => {
|
||||
try {
|
||||
const { nodes: merged, edges: mergedEdges, imported } = parseYamlToCanvas(content, nodes, edges)
|
||||
snapshotHistory()
|
||||
loadCanvas(merged, mergedEdges)
|
||||
markUnsaved()
|
||||
// applyLayout keeps undo history so an import can be reverted; loadCanvas
|
||||
// would wipe it (#280).
|
||||
applyLayout(merged, mergedEdges)
|
||||
toast.success(`Imported ${imported} node${imported !== 1 ? 's' : ''}`)
|
||||
} catch (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.
|
||||
// Standalone has no backend/key — it reads localStorage, so just open /view.
|
||||
|
||||
@@ -85,4 +85,31 @@ describe('canvasStore — history', () => {
|
||||
addNode(makeNode('n3'))
|
||||
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
|
||||
markUnsaved: () => 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
|
||||
clearFitViewPending: () => 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 })
|
||||
},
|
||||
|
||||
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 }),
|
||||
|
||||
applyTypeNodeStyle: (nodeType, style) =>
|
||||
|
||||
Reference in New Issue
Block a user