From b09ebb5cd2f50d16bc6aa2b850cf2335dffc0254 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Tue, 7 Jul 2026 21:23:28 +0200 Subject: [PATCH] fix: keep approved devices in inventory after bulk approve Bulk/single approve optimistically stripped approved rows from the local list, but scanApi.pending() still returns them (on-canvas, with an 'In N canvas' badge). The list went empty until the modal was reopened. Reload after approve instead of stripping, so approved rows stay visible with a fresh canvas_count. ha-relevant: yes --- .../components/modals/PendingDevicesModal.tsx | 9 ++++++-- .../__tests__/PendingDevicesModal.test.tsx | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/modals/PendingDevicesModal.tsx b/frontend/src/components/modals/PendingDevicesModal.tsx index 5aeadc9..4ad78ba 100644 --- a/frontend/src/components/modals/PendingDevicesModal.tsx +++ b/frontend/src/components/modals/PendingDevicesModal.tsx @@ -287,8 +287,10 @@ export function PendingDevicesModal({ open, onClose, highlightId, initialStatus injectAutoEdges(res.data.edges) const extra = res.data.edges_created > 0 ? ` (+${res.data.edges_created} link${res.data.edges_created !== 1 ? 's' : ''})` : '' toast.success(`Approved ${nodeData.label}${extra}`) - setDevices((prev) => prev.filter((d) => d.id !== device.id)) + // Keep the row (now on-canvas, shown with an "In N canvas" badge); reload + // for a fresh canvas_count rather than dropping it until reopen. setSelected(null) + await load() } catch { toast.error('Failed to approve device') } @@ -353,8 +355,11 @@ export function PendingDevicesModal({ open, onClose, highlightId, initialStatus }) }) injectAutoEdges(res.data.edges) - setDevices((prev) => prev.filter((d) => !ids.includes(d.id))) + // Don't strip approved rows locally: the inventory keeps them with an + // "In N canvas" badge (that's what showOnCanvas toggles). Reload so they + // reappear with a fresh canvas_count instead of vanishing until reopen. setSelectedIds(new Set()) + await load() const linkExtra = res.data.edges_created > 0 ? ` (+${res.data.edges_created} link${res.data.edges_created !== 1 ? 's' : ''})` : '' toast.success(`Approved ${res.data.approved} device${res.data.approved !== 1 ? 's' : ''}${linkExtra}`) } catch { diff --git a/frontend/src/components/modals/__tests__/PendingDevicesModal.test.tsx b/frontend/src/components/modals/__tests__/PendingDevicesModal.test.tsx index 4e7d230..8623855 100644 --- a/frontend/src/components/modals/__tests__/PendingDevicesModal.test.tsx +++ b/frontend/src/components/modals/__tests__/PendingDevicesModal.test.tsx @@ -263,6 +263,27 @@ describe('PendingDevicesModal', () => { await waitFor(() => expect(mockBulkApprove).toHaveBeenCalledWith(['dev-a', 'dev-b'], null)) }) + it('keeps approved devices listed after bulk approve (reloads, not strips)', async () => { + // After approve, pending() still returns the rows (now on-canvas w/ badge). + mockPending + .mockResolvedValueOnce({ data: [DEVICE_IP, DEVICE_ZIGBEE] }) + .mockResolvedValue({ data: [ + { ...DEVICE_IP, canvas_count: 1 }, + { ...DEVICE_ZIGBEE, canvas_count: 1 }, + ] }) + render() + 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(mockBulkApprove).toHaveBeenCalled()) + // Reloaded, so rows remain visible instead of the list going empty. + await waitFor(() => expect(mockPending).toHaveBeenCalledTimes(2)) + expect(screen.getByTestId('pending-card-dev-a')).toBeInTheDocument() + expect(screen.getByTestId('pending-card-dev-b')).toBeInTheDocument() + }) + it('bulk approve carries the scanned MAC onto the canvas node (#168)', async () => { render() await waitFor(() => expect(screen.getByTestId('pending-card-dev-a')).toBeInTheDocument())