Merge remote-tracking branch 'origin/main' into feat/electrical

This commit is contained in:
Pouzor
2026-06-02 16:10:46 +02:00
13 changed files with 553 additions and 101 deletions
+14 -14
View File
@@ -1,12 +1,12 @@
{
"name": "frontend",
"version": "2.2.0",
"version": "2.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "frontend",
"version": "2.2.0",
"version": "2.3.0",
"dependencies": {
"@base-ui/react": "^1.2.0",
"@dagrejs/dagre": "^2.0.4",
@@ -3237,9 +3237,9 @@
}
},
"node_modules/@ts-morph/common/node_modules/brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
@@ -3651,9 +3651,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4250,9 +4250,9 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8040,9 +8040,9 @@
}
},
"node_modules/qs": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
"version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "2.2.0",
"version": "2.3.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -10,6 +10,7 @@ import { toast } from 'sonner'
import { PendingDeviceModal, type PendingDevice } from '@/components/modals/PendingDeviceModal'
import type { NodeType, ServiceInfo } from '@/types'
import { buildZigbeeProperties, isZigbeeType } from '@/utils/zigbeeProperties'
import { buildMacProperty } from '@/utils/macProperty'
interface PendingDevicesModalProps {
open: boolean
@@ -255,11 +256,12 @@ export function PendingDevicesModal({ open, onClose, highlightId, initialStatus
const fallbackLabel = deviceLabel(device)
const type = (device.suggested_type ?? 'generic') as NodeType
const zigbee = isZigbeeType(type)
const properties = zigbee ? buildZigbeeProperties(device) : []
const properties = zigbee ? buildZigbeeProperties(device) : buildMacProperty(device.mac)
const nodeData = {
label: fallbackLabel,
type,
ip: device.ip ?? undefined,
mac: device.mac ?? undefined,
hostname: device.hostname ?? undefined,
status: zigbee ? 'online' : 'unknown',
services: (device.services ?? []) as ServiceInfo[],
@@ -325,10 +327,11 @@ export function PendingDevicesModal({ open, onClose, highlightId, initialStatus
label: deviceLabel(d),
type,
ip: d.ip ?? undefined,
mac: d.mac ?? undefined,
hostname: d.hostname ?? undefined,
status: zigbee ? ('online' as const) : ('unknown' as const),
services: (d.services ?? []) as ServiceInfo[],
properties: zigbee ? buildZigbeeProperties(d) : [],
properties: zigbee ? buildZigbeeProperties(d) : buildMacProperty(d.mac),
},
})
})
@@ -13,6 +13,7 @@ const mockApprove = vi.fn()
const mockHide = vi.fn()
const mockPending = vi.fn()
const mockHidden = vi.fn()
const mockAddNode = vi.fn()
vi.mock('@/api/client', () => ({
scanApi: {
@@ -69,7 +70,7 @@ const DEVICE_ZIGBEE = {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useCanvasStore).mockReturnValue({
addNode: vi.fn(),
addNode: mockAddNode,
scanEventTs: 0,
} as unknown as ReturnType<typeof useCanvasStore>)
// setState is used by injectAutoEdges
@@ -174,6 +175,34 @@ describe('PendingDevicesModal', () => {
await waitFor(() => expect(mockBulkApprove).toHaveBeenCalledWith(['dev-a', 'dev-b']))
})
it('bulk approve carries the scanned MAC onto the canvas node (#168)', async () => {
render(<PendingDevicesModal {...baseProps} />)
await waitFor(() => expect(screen.getByTestId('pending-card-dev-a')).toBeInTheDocument())
fireEvent.click(screen.getByRole('button', { name: 'Select mode' }))
fireEvent.click(screen.getByTestId('pending-card-dev-a'))
fireEvent.click(screen.getByTestId('pending-card-dev-b'))
fireEvent.click(screen.getByRole('button', { name: /Approve \(2\)/ }))
await waitFor(() => expect(mockAddNode).toHaveBeenCalledTimes(2))
// dev-a is an IP device with a MAC → node carries mac + a MAC property row.
const ipNode = mockAddNode.mock.calls
.map((c) => c[0])
.find((n) => n.id === 'n1')
expect(ipNode.data.mac).toBe('aa:bb:cc:dd:ee:01')
expect(ipNode.data.properties).toContainEqual({
key: 'MAC',
value: 'aa:bb:cc:dd:ee:01',
icon: null,
visible: false,
})
// dev-b is zigbee with no MAC → no MAC property row.
const zbNode = mockAddNode.mock.calls
.map((c) => c[0])
.find((n) => n.id === 'n2')
expect(zbNode.data.properties.some((p: { key: string }) => p.key === 'MAC')).toBe(false)
})
it('bulk hide calls API with selected ids', async () => {
render(<PendingDevicesModal {...baseProps} />)
await waitFor(() => expect(screen.getByTestId('pending-card-dev-a')).toBeInTheDocument())
@@ -0,0 +1,16 @@
import { describe, it, expect } from 'vitest'
import { buildMacProperty } from '../macProperty'
describe('buildMacProperty', () => {
it('returns a hidden MAC property row for a MAC', () => {
expect(buildMacProperty('aa:bb:cc:dd:ee:ff')).toEqual([
{ key: 'MAC', value: 'aa:bb:cc:dd:ee:ff', icon: null, visible: false },
])
})
it('returns an empty array when MAC is null/undefined/empty', () => {
expect(buildMacProperty(null)).toEqual([])
expect(buildMacProperty(undefined)).toEqual([])
expect(buildMacProperty('')).toEqual([])
})
})
+9
View File
@@ -0,0 +1,9 @@
import type { NodeProperty } from '@/types'
/** Build the MAC address property row shown in the right panel.
* Hidden by default — the user opts in to showing it on the canvas card.
* Matches backend `build_mac_property`. Returns an empty array when no MAC. */
export function buildMacProperty(mac?: string | null): NodeProperty[] {
if (!mac) return []
return [{ key: 'MAC', value: mac, icon: null, visible: false }]
}