feat: group node types by category in Custom Style editor
The Custom Style modal listed node types as a flat list. It now groups them under category headers (Hardware, Virtualization, IoT, Zigbee, Z-Wave, Personal, Generic) like the Add/Edit Node modal, and exposes the Z-Wave node types for styling. ha-relevant: yes
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
import { useState, useCallback } from 'react'
|
import { Fragment, useState, useCallback } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import {
|
import {
|
||||||
Globe, Router, Network, Server, Layers, Box, Container, HardDrive,
|
Globe, Router, Network, Server, Layers, Box, Container, HardDrive,
|
||||||
Cpu, Wifi, Camera, Printer, Monitor, Laptop, Smartphone, PlugZap, Anchor, Package, Circle, Flame,
|
Cpu, Wifi, Camera, Printer, Monitor, Laptop, Smartphone, PlugZap, Anchor, Package, Circle, Flame,
|
||||||
Radio, Zap, Lightbulb,
|
Radio, Zap, Lightbulb, RadioTower, Share2,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
@@ -17,13 +17,16 @@ import type {
|
|||||||
} from '@/types'
|
} from '@/types'
|
||||||
import { NODE_TYPE_LABELS, EDGE_TYPE_LABELS } 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[] = [
|
const NODE_TYPE_GROUPS: { label: string; types: NodeType[] }[] = [
|
||||||
'isp', 'router', 'firewall', 'switch', 'server', 'proxmox', 'vm', 'lxc', 'nas',
|
{ label: 'Hardware', types: ['isp', 'router', 'firewall', 'switch', 'server', 'nas', 'ap', 'printer'] },
|
||||||
'iot', 'ap', 'camera', 'printer', 'computer', 'laptop', 'mobile', 'cpl', 'docker_host',
|
{ label: 'Virtualization', types: ['proxmox', 'vm', 'lxc', 'docker_host', 'docker_container'] },
|
||||||
'docker_container', 'zigbee_coordinator', 'zigbee_router', 'zigbee_enddevice',
|
{ label: 'IoT', types: ['iot', 'camera', 'cpl'] },
|
||||||
'generic',
|
{ 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']
|
const EDITABLE_EDGE_TYPES: EdgeType[] = ['ethernet', 'wifi', 'iot', 'vlan', 'virtual', 'cluster', 'fibre', 'electrical']
|
||||||
@@ -34,6 +37,7 @@ const NODE_ICONS: Record<string, LucideIcon> = {
|
|||||||
camera: Camera, printer: Printer, computer: Monitor, laptop: Laptop, mobile: Smartphone, cpl: PlugZap,
|
camera: Camera, printer: Printer, computer: Monitor, laptop: Laptop, mobile: Smartphone, cpl: PlugZap,
|
||||||
docker_host: Anchor, docker_container: Package,
|
docker_host: Anchor, docker_container: Package,
|
||||||
zigbee_coordinator: Radio, zigbee_router: Zap, zigbee_enddevice: Lightbulb,
|
zigbee_coordinator: Radio, zigbee_router: Zap, zigbee_enddevice: Lightbulb,
|
||||||
|
zwave_coordinator: RadioTower, zwave_router: Share2, zwave_enddevice: Lightbulb,
|
||||||
generic: Circle,
|
generic: Circle,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,7 +367,12 @@ export function CustomStyleModal({ open, onClose }: CustomStyleModalProps) {
|
|||||||
|
|
||||||
{/* Type list */}
|
{/* Type list */}
|
||||||
<div className="flex-1 overflow-y-auto py-1">
|
<div className="flex-1 overflow-y-auto py-1">
|
||||||
{tab === 'nodes' && EDITABLE_NODE_TYPES.map((t) => {
|
{tab === 'nodes' && NODE_TYPE_GROUPS.map((group) => (
|
||||||
|
<Fragment key={group.label}>
|
||||||
|
<div className="px-3 pt-2 pb-1 text-[10px] font-semibold uppercase tracking-wider text-[#8b949e]/60">
|
||||||
|
{group.label}
|
||||||
|
</div>
|
||||||
|
{group.types.map((t) => {
|
||||||
const Icon = NODE_ICONS[t] ?? Circle
|
const Icon = NODE_ICONS[t] ?? Circle
|
||||||
const style = draft.nodes[t]
|
const style = draft.nodes[t]
|
||||||
const isSelected = selection?.kind === 'node' && selection.type === t
|
const isSelected = selection?.kind === 'node' && selection.type === t
|
||||||
@@ -391,6 +400,8 @@ export function CustomStyleModal({ open, onClose }: CustomStyleModalProps) {
|
|||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
|
||||||
{tab === 'edges' && EDITABLE_EDGE_TYPES.map((t) => {
|
{tab === 'edges' && EDITABLE_EDGE_TYPES.map((t) => {
|
||||||
const style = draft.edges[t]
|
const style = draft.edges[t]
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ describe('CustomStyleModal', () => {
|
|||||||
expect(screen.getByText(/edge type from the list/i)).toBeDefined()
|
expect(screen.getByText(/edge type from the list/i)).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('groups node types under category headers (incl. Zigbee and Z-Wave)', () => {
|
||||||
|
render(<CustomStyleModal open onClose={vi.fn()} />)
|
||||||
|
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', () => {
|
it('selecting a node type opens the node editor', () => {
|
||||||
render(<CustomStyleModal open onClose={vi.fn()} />)
|
render(<CustomStyleModal open onClose={vi.fn()} />)
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Router' }))
|
fireEvent.click(screen.getByRole('button', { name: 'Router' }))
|
||||||
|
|||||||
Reference in New Issue
Block a user