feat: add Group Rectangle — resizable decorative zones on canvas
- New GroupRectNode: rounded rect with NodeResizer (8 handles), no connection handles, always behind network nodes via negative zIndex - GroupRectModal: label, font preset (Inter/Mono/Serif), 3×3 text position grid, text/border/background color pickers, z-order 1–9 - Sidebar: "Add Rectangle" button (below Add Node), group rects excluded from node count stats - Save/load: size persisted in custom_colors.width/height, no backend schema changes required - elevateNodesOnSelect=false on ReactFlow so selected rects never pop above network nodes - Tests: 3 new store tests + 9 GroupRectModal tests (66 total, all pass)
This commit is contained in:
@@ -25,6 +25,7 @@ describe('canvasStore', () => {
|
||||
edges: [],
|
||||
hasUnsavedChanges: false,
|
||||
selectedNodeId: null,
|
||||
editingGroupRectId: null,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -190,6 +191,40 @@ describe('canvasStore', () => {
|
||||
expect(updatedChild?.extent).toBeUndefined()
|
||||
})
|
||||
|
||||
it('setEditingGroupRectId sets and clears the editing id', () => {
|
||||
useCanvasStore.getState().setEditingGroupRectId('rect-1')
|
||||
expect(useCanvasStore.getState().editingGroupRectId).toBe('rect-1')
|
||||
useCanvasStore.getState().setEditingGroupRectId(null)
|
||||
expect(useCanvasStore.getState().editingGroupRectId).toBeNull()
|
||||
})
|
||||
|
||||
it('setNodeZIndex updates the node zIndex and marks unsaved', () => {
|
||||
useCanvasStore.getState().addNode(makeNode('n1'))
|
||||
useCanvasStore.getState().markSaved()
|
||||
useCanvasStore.getState().setNodeZIndex('n1', -5)
|
||||
const node = useCanvasStore.getState().nodes.find((n) => n.id === 'n1')
|
||||
expect(node?.zIndex).toBe(-5)
|
||||
expect(useCanvasStore.getState().hasUnsavedChanges).toBe(true)
|
||||
})
|
||||
|
||||
it('addNode with groupRect type preserves zIndex and dimensions', () => {
|
||||
const rectNode: Node<NodeData> = {
|
||||
id: 'rect-1',
|
||||
type: 'groupRect',
|
||||
position: { x: 100, y: 100 },
|
||||
data: { label: 'Zone A', type: 'groupRect', status: 'unknown', services: [] },
|
||||
width: 360,
|
||||
height: 240,
|
||||
zIndex: -9,
|
||||
}
|
||||
useCanvasStore.getState().addNode(rectNode)
|
||||
const stored = useCanvasStore.getState().nodes.find((n) => n.id === 'rect-1')
|
||||
expect(stored?.type).toBe('groupRect')
|
||||
expect(stored?.zIndex).toBe(-9)
|
||||
expect(stored?.width).toBe(360)
|
||||
expect(stored?.height).toBe(240)
|
||||
})
|
||||
|
||||
it('loadCanvas sorts parents before children', () => {
|
||||
const parent = makeNode('p1')
|
||||
const child: Node<NodeData> = { ...makeNode('c1', { parent_id: 'p1' }), parentId: 'p1', extent: 'parent' }
|
||||
|
||||
@@ -28,6 +28,9 @@ interface CanvasState {
|
||||
updateEdge: (id: string, data: Partial<EdgeData>) => void
|
||||
deleteEdge: (id: string) => void
|
||||
setProxmoxContainerMode: (proxmoxId: string, enabled: boolean) => void
|
||||
setNodeZIndex: (id: string, zIndex: number) => void
|
||||
editingGroupRectId: string | null
|
||||
setEditingGroupRectId: (id: string | null) => void
|
||||
markSaved: () => void
|
||||
loadCanvas: (nodes: Node<NodeData>[], edges: Edge<EdgeData>[]) => void
|
||||
notifyScanDeviceFound: () => void
|
||||
@@ -38,6 +41,7 @@ export const useCanvasStore = create<CanvasState>((set) => ({
|
||||
edges: [],
|
||||
hasUnsavedChanges: false,
|
||||
selectedNodeId: null,
|
||||
editingGroupRectId: null,
|
||||
scanEventTs: 0,
|
||||
|
||||
onNodesChange: (changes) =>
|
||||
@@ -141,6 +145,14 @@ export const useCanvasStore = create<CanvasState>((set) => ({
|
||||
return { nodes, hasUnsavedChanges: true }
|
||||
}),
|
||||
|
||||
setNodeZIndex: (id, zIndex) =>
|
||||
set((state) => ({
|
||||
nodes: state.nodes.map((n) => n.id === id ? { ...n, zIndex } : n),
|
||||
hasUnsavedChanges: true,
|
||||
})),
|
||||
|
||||
setEditingGroupRectId: (id) => set({ editingGroupRectId: id }),
|
||||
|
||||
markSaved: () => set({ hasUnsavedChanges: false }),
|
||||
|
||||
notifyScanDeviceFound: () => set({ scanEventTs: Date.now() }),
|
||||
|
||||
Reference in New Issue
Block a user