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