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())