From d4a7086c4e92b12303f82ea0c4dc02ef8c19a311 Mon Sep 17 00:00:00 2001 From: Younes Beriane Date: Mon, 5 Oct 2026 15:27:56 +0200 Subject: [PATCH] fix(inbounds): list clients in the detach/attach modals when they mount open (#6736) * fix(inbounds): list clients in the detach modal when it mounts open DetachClientsModal seeded its synced-source state with the open source, so a modal first mounted with open and a source saw no change on its first render and never read the client rows. The table stayed empty until the modal was closed and reopened. Seed the state with null so the first open render loads the rows, as the reset-during-render check already expects. The test mounts the modal already open and fails without this change. Fixes #6733 * fix(inbounds): list clients in the attach modal when it mounts open AttachClientsModal seeds its synced-source state the same way DetachClientsModal did, so a modal first mounted with open and a source saw no change on its first render and never read the client rows. The table stayed empty, and nothing was pre-selected, until the modal was closed and reopened. Seed the state with null here too. The test mounts the attach modal already open and fails without this change. Refs #6733 --------- Co-authored-by: Sanaei --- .../inbounds/clients/AttachClientsModal.tsx | 2 +- .../inbounds/clients/DetachClientsModal.tsx | 2 +- .../src/test/attach-clients-modal.test.tsx | 43 +++++++++++++++++++ .../src/test/detach-clients-modal.test.tsx | 36 ++++++++++++++++ 4 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 frontend/src/test/attach-clients-modal.test.tsx create mode 100644 frontend/src/test/detach-clients-modal.test.tsx diff --git a/frontend/src/pages/inbounds/clients/AttachClientsModal.tsx b/frontend/src/pages/inbounds/clients/AttachClientsModal.tsx index db546a93b..6c0cc893d 100644 --- a/frontend/src/pages/inbounds/clients/AttachClientsModal.tsx +++ b/frontend/src/pages/inbounds/clients/AttachClientsModal.tsx @@ -60,7 +60,7 @@ export default function AttachClientsModal({ // React resets this during render rather than in an effect so the modal's // first open frame already shows cleared fields. const openSource = open ? source : null; - const [syncedSource, setSyncedSource] = useState(openSource); + const [syncedSource, setSyncedSource] = useState(null); if (openSource !== syncedSource) { setSyncedSource(openSource); if (openSource) { diff --git a/frontend/src/pages/inbounds/clients/DetachClientsModal.tsx b/frontend/src/pages/inbounds/clients/DetachClientsModal.tsx index 7dee9625f..bbf3b5e19 100644 --- a/frontend/src/pages/inbounds/clients/DetachClientsModal.tsx +++ b/frontend/src/pages/inbounds/clients/DetachClientsModal.tsx @@ -54,7 +54,7 @@ export default function DetachClientsModal({ // Reset during render, not in an effect, so the first frame is already clean. const openSource = open ? source : null; - const [syncedSource, setSyncedSource] = useState(openSource); + const [syncedSource, setSyncedSource] = useState(null); if (openSource !== syncedSource) { setSyncedSource(openSource); if (openSource) { diff --git a/frontend/src/test/attach-clients-modal.test.tsx b/frontend/src/test/attach-clients-modal.test.tsx new file mode 100644 index 000000000..c7aa18b12 --- /dev/null +++ b/frontend/src/test/attach-clients-modal.test.tsx @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; +import { screen } from '@testing-library/react'; + +import AttachClientsModal from '@/pages/inbounds/clients/AttachClientsModal'; +import { DBInbound } from '@/models/dbinbound'; + +import { renderWithProviders } from './test-utils'; + +function sourceInbound() { + return new DBInbound({ + id: 7, + port: 443, + listen: '', + protocol: 'vless', + remark: 'edge', + enable: true, + settings: JSON.stringify({ + clients: [ + { id: 'uuid-1', email: 'alice@test' }, + { id: 'uuid-2', email: 'bob@test' }, + ], + decryption: 'none', + }), + streamSettings: JSON.stringify({ network: 'tcp', security: 'none' }), + sniffing: '', + }); +} + +describe('AttachClientsModal', () => { + it('lists the source clients, selected, when first mounted already open', async () => { + renderWithProviders( + {}} />, + ); + + expect(await screen.findByText('alice@test')).toBeTruthy(); + expect(screen.getByText('bob@test')).toBeTruthy(); + + const boxes = screen.getAllByRole('checkbox') as HTMLInputElement[]; + const rowBoxes = boxes.slice(1); + expect(rowBoxes).toHaveLength(2); + expect(rowBoxes.every((b) => b.checked)).toBe(true); + }); +}); diff --git a/frontend/src/test/detach-clients-modal.test.tsx b/frontend/src/test/detach-clients-modal.test.tsx new file mode 100644 index 000000000..89d3352bb --- /dev/null +++ b/frontend/src/test/detach-clients-modal.test.tsx @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest'; +import { screen } from '@testing-library/react'; + +import DetachClientsModal from '@/pages/inbounds/clients/DetachClientsModal'; +import { DBInbound } from '@/models/dbinbound'; + +import { renderWithProviders } from './test-utils'; + +function sourceInbound() { + return new DBInbound({ + id: 7, + port: 443, + listen: '', + protocol: 'vless', + remark: 'edge', + enable: true, + settings: JSON.stringify({ + clients: [ + { id: 'uuid-1', email: 'alice@test' }, + { id: 'uuid-2', email: 'bob@test' }, + ], + decryption: 'none', + }), + streamSettings: JSON.stringify({ network: 'tcp', security: 'none' }), + sniffing: '', + }); +} + +describe('DetachClientsModal', () => { + it('lists the attached clients when first mounted already open', async () => { + renderWithProviders( {}} />); + + expect(await screen.findByText('alice@test')).toBeTruthy(); + expect(screen.getByText('bob@test')).toBeTruthy(); + }); +});