diff --git a/frontend/src/components/modals/CustomStyleModal.tsx b/frontend/src/components/modals/CustomStyleModal.tsx index b1f6918..8504d81 100644 --- a/frontend/src/components/modals/CustomStyleModal.tsx +++ b/frontend/src/components/modals/CustomStyleModal.tsx @@ -1,9 +1,9 @@ -import { useState, useCallback } from 'react' +import { Fragment, useState, useCallback } from 'react' import { toast } from 'sonner' import { Globe, Router, Network, Server, Layers, Box, Container, HardDrive, Cpu, Wifi, Camera, Printer, Monitor, Laptop, Smartphone, PlugZap, Anchor, Package, Circle, Flame, - Radio, Zap, Lightbulb, + Radio, Zap, Lightbulb, RadioTower, Share2, type LucideIcon, } from 'lucide-react' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' @@ -17,13 +17,16 @@ import type { } from '@/types' import { NODE_TYPE_LABELS, EDGE_TYPE_LABELS } from '@/types' -// ── Node types exposed for custom style (skip groupRect/group) ─────────────── +// ── Node types exposed for custom style, grouped by category (skip groupRect/group) ── -const EDITABLE_NODE_TYPES: NodeType[] = [ - 'isp', 'router', 'firewall', 'switch', 'server', 'proxmox', 'vm', 'lxc', 'nas', - 'iot', 'ap', 'camera', 'printer', 'computer', 'laptop', 'mobile', 'cpl', 'docker_host', - 'docker_container', 'zigbee_coordinator', 'zigbee_router', 'zigbee_enddevice', - 'generic', +const NODE_TYPE_GROUPS: { label: string; types: NodeType[] }[] = [ + { label: 'Hardware', types: ['isp', 'router', 'firewall', 'switch', 'server', 'nas', 'ap', 'printer'] }, + { label: 'Virtualization', types: ['proxmox', 'vm', 'lxc', 'docker_host', 'docker_container'] }, + { label: 'IoT', types: ['iot', 'camera', 'cpl'] }, + { label: 'Zigbee', types: ['zigbee_coordinator', 'zigbee_router', 'zigbee_enddevice'] }, + { label: 'Z-Wave', types: ['zwave_coordinator', 'zwave_router', 'zwave_enddevice'] }, + { label: 'Personal', types: ['computer', 'laptop', 'mobile'] }, + { label: 'Generic', types: ['generic'] }, ] const EDITABLE_EDGE_TYPES: EdgeType[] = ['ethernet', 'wifi', 'iot', 'vlan', 'virtual', 'cluster', 'fibre', 'electrical'] @@ -34,6 +37,7 @@ const NODE_ICONS: Record = { camera: Camera, printer: Printer, computer: Monitor, laptop: Laptop, mobile: Smartphone, cpl: PlugZap, docker_host: Anchor, docker_container: Package, zigbee_coordinator: Radio, zigbee_router: Zap, zigbee_enddevice: Lightbulb, + zwave_coordinator: RadioTower, zwave_router: Share2, zwave_enddevice: Lightbulb, generic: Circle, } @@ -363,34 +367,41 @@ export function CustomStyleModal({ open, onClose }: CustomStyleModalProps) { {/* Type list */}
- {tab === 'nodes' && EDITABLE_NODE_TYPES.map((t) => { - const Icon = NODE_ICONS[t] ?? Circle - const style = draft.nodes[t] - const isSelected = selection?.kind === 'node' && selection.type === t - const swatchColor = style - ? applyOpacity(style.borderColor, style.borderOpacity) - : THEMES.default.colors.nodeAccents[t]?.border ?? '#8b949e' + {tab === 'nodes' && NODE_TYPE_GROUPS.map((group) => ( + +
+ {group.label} +
+ {group.types.map((t) => { + const Icon = NODE_ICONS[t] ?? Circle + const style = draft.nodes[t] + const isSelected = selection?.kind === 'node' && selection.type === t + const swatchColor = style + ? applyOpacity(style.borderColor, style.borderOpacity) + : THEMES.default.colors.nodeAccents[t]?.border ?? '#8b949e' - return ( - - ) - })} + return ( + + ) + })} +
+ ))} {tab === 'edges' && EDITABLE_EDGE_TYPES.map((t) => { const style = draft.edges[t] diff --git a/frontend/src/components/modals/__tests__/CustomStyleModal.test.tsx b/frontend/src/components/modals/__tests__/CustomStyleModal.test.tsx index 0ea98a7..d368a87 100644 --- a/frontend/src/components/modals/__tests__/CustomStyleModal.test.tsx +++ b/frontend/src/components/modals/__tests__/CustomStyleModal.test.tsx @@ -37,6 +37,16 @@ describe('CustomStyleModal', () => { expect(screen.getByText(/edge type from the list/i)).toBeDefined() }) + it('groups node types under category headers (incl. Zigbee and Z-Wave)', () => { + render() + expect(screen.getByText('Hardware')).toBeDefined() + expect(screen.getByText('Zigbee')).toBeDefined() + expect(screen.getByText('Z-Wave')).toBeDefined() + // A Z-Wave node type is selectable from its category. + fireEvent.click(screen.getByRole('button', { name: /Z-Wave Controller/ })) + expect(screen.getByText(/Apply to existing Z-Wave Controller/)).toBeDefined() + }) + it('selecting a node type opens the node editor', () => { render() fireEvent.click(screen.getByRole('button', { name: 'Router' }))