From c464816b48ca553cf2f16dd8b67233ee109f5022 Mon Sep 17 00:00:00 2001 From: "@daniel-lxs" <57051444+daniel-lxs@users.noreply.github.com> Date: Fri, 16 Oct 2026 05:34:35 +0000 Subject: [PATCH 1/2] fix: recover Users settings reads without discarding cached content --- .../settings/UsersSettings.client.test.tsx | 92 +++++++++++++++++-- .../src/components/settings/UsersSettings.tsx | 30 ++++-- 2 files changed, 109 insertions(+), 13 deletions(-) diff --git a/apps/web/src/components/settings/UsersSettings.client.test.tsx b/apps/web/src/components/settings/UsersSettings.client.test.tsx index 2972d11214..cff3bb1970 100644 --- a/apps/web/src/components/settings/UsersSettings.client.test.tsx +++ b/apps/web/src/components/settings/UsersSettings.client.test.tsx @@ -71,6 +71,7 @@ function createDeferred() { const { mockAuthorizedUser, mockSettingsState, + mockGetSettings, mockCreateInvite, mockRevokeInvite, mockUpdateUserRole, @@ -104,6 +105,7 @@ const { } as LicenseSummary, }, }, + mockGetSettings: vi.fn(), mockCreateInvite: vi.fn( async (input: { label?: string; maxUses?: number }) => { mockSettingsState.current = { @@ -195,7 +197,7 @@ vi.mock('@/trpc/client', () => ({ queryKey: () => ['access-policy'], queryOptions: () => ({ queryKey: ['access-policy'], - queryFn: async () => mockSettingsState.current, + queryFn: () => mockGetSettings(), }), }, accountLinkHelp: { @@ -266,16 +268,21 @@ function renderUsersSettings() { defaultOptions: { queries: { retry: false } }, }); - return render( - - - , - ); + return { + queryClient, + ...render( + + + , + ), + }; } describe('UsersSettings', () => { beforeEach(() => { vi.clearAllMocks(); + mockGetSettings.mockReset(); + mockGetSettings.mockImplementation(async () => mockSettingsState.current); mockClipboardWriteText.mockResolvedValue(undefined); mockAuthorizedUser.current = { userId: 'user-1', cloudEnabled: false }; mockAccountLinkHelpState.current = { helpText: null }; @@ -289,6 +296,79 @@ describe('UsersSettings', () => { }; }); + it('recovers an initial read failure through Retry and prevents duplicate retries', async () => { + const retry = createDeferred(); + mockGetSettings.mockRejectedValueOnce(new Error('Read unavailable')); + mockGetSettings.mockImplementationOnce(() => retry.promise); + renderUsersSettings(); + + const button = await screen.findByRole('button', { name: 'Retry' }); + expect(screen.getByText('Failed to load user settings.')).toBeVisible(); + fireEvent.click(button); + await waitFor(() => + expect( + screen.queryByRole('button', { name: 'Retry' }), + ).not.toBeInTheDocument(), + ); + fireEvent.click(button); + expect(mockGetSettings).toHaveBeenCalledTimes(2); + + await act(async () => retry.resolve(mockSettingsState.current)); + expect( + await screen.findByRole('button', { name: 'Create invite' }), + ).toBeEnabled(); + expect( + screen.queryByText('Failed to load user settings.'), + ).not.toBeInTheDocument(); + }); + + it('keeps the invite draft and cached settings usable after a background read failure', async () => { + const { queryClient } = renderUsersSettings(); + const label = await screen.findByLabelText(/Label/); + fireEvent.change(label, { target: { value: 'Unsaved invite' } }); + mockGetSettings.mockRejectedValueOnce(new Error('Read unavailable')); + await act(async () => { + await queryClient.refetchQueries({ queryKey: ['access-policy'] }); + }); + + expect(label).toBeVisible(); + expect(label).toHaveValue('Unsaved invite'); + expect(screen.getByRole('button', { name: 'Create invite' })).toBeEnabled(); + expect( + screen.queryByText('Failed to load user settings.'), + ).not.toBeInTheDocument(); + }); + + it('recovers the one-time invite link through Retry after post-create refresh fails', async () => { + const { queryClient } = renderUsersSettings(); + await screen.findByRole('button', { name: 'Create invite' }); + mockGetSettings.mockRejectedValueOnce(new Error('Read unavailable')); + fireEvent.click(screen.getByRole('button', { name: 'Create invite' })); + await waitFor(() => expect(mockGetSettings).toHaveBeenCalledTimes(2)); + await waitFor(() => expect(toast.success).toHaveBeenCalled()); + + expect( + screen.queryByText('Failed to load user settings.'), + ).not.toBeInTheDocument(); + expect(screen.getByText('Failed to refresh user settings.')).toBeVisible(); + fireEvent.click(screen.getByRole('button', { name: 'Retry' })); + expect( + await screen.findByText('https://roomote.example.com/invite/new-token'), + ).toBeVisible(); + expect( + screen.queryByText('Failed to refresh user settings.'), + ).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Create invite' })).toBeEnabled(); + + mockGetSettings.mockRejectedValueOnce(new Error('Read unavailable')); + await act(async () => { + await queryClient.refetchQueries({ queryKey: ['access-policy'] }); + }); + expect( + screen.getByText('https://roomote.example.com/invite/new-token'), + ).toBeVisible(); + }); + it('saves deployment-specific account linking help', async () => { renderUsersSettings(); diff --git a/apps/web/src/components/settings/UsersSettings.tsx b/apps/web/src/components/settings/UsersSettings.tsx index 4a0d234027..572bf45bad 100644 --- a/apps/web/src/components/settings/UsersSettings.tsx +++ b/apps/web/src/components/settings/UsersSettings.tsx @@ -234,13 +234,28 @@ export function UsersSettings() { return ; } - if (settingsQuery.isError || !settingsQuery.data) { - return ( -
- -

Failed to load user settings.

-
- ); + const loadError = ( +
+ +

+ {settingsQuery.data + ? 'Failed to refresh user settings.' + : 'Failed to load user settings.'} +

+ +
+ ); + + if (!settingsQuery.data) { + return loadError; } const { @@ -327,6 +342,7 @@ export function UsersSettings() { return (
+ {settingsQuery.isError ? loadError : null} {cloudEnabled ? null : (
From 50b773603824ed5919b6305264cda02ca7c9a202 Mon Sep 17 00:00:00 2001 From: "@daniel-lxs" <57051444+daniel-lxs@users.noreply.github.com> Date: Fri, 16 Oct 2026 06:50:44 +0000 Subject: [PATCH 2/2] fix: recover the independent account linking help read --- .../settings/AccountLinkHelpSection.tsx | 66 ++++++++++++------- .../settings/UsersSettings.client.test.tsx | 66 ++++++++++++++++++- 2 files changed, 108 insertions(+), 24 deletions(-) diff --git a/apps/web/src/components/settings/AccountLinkHelpSection.tsx b/apps/web/src/components/settings/AccountLinkHelpSection.tsx index 4844c82aa9..bf6e904dae 100644 --- a/apps/web/src/components/settings/AccountLinkHelpSection.tsx +++ b/apps/web/src/components/settings/AccountLinkHelpSection.tsx @@ -75,6 +75,26 @@ export function AccountLinkHelpSection() { ); + const loadError = ( +
+ +

+ {settingsQuery.data + ? 'Failed to refresh account linking help.' + : 'Failed to load account linking help.'} +

+ +
+ ); + return (

@@ -84,30 +104,30 @@ export function AccountLinkHelpSection() {

{settingsQuery.isPending ? ( - ) : settingsQuery.isError ? ( -
- -

Failed to load account linking help.

-
) : ( -
- -