Skip to content

Commit 8a110d6

Browse files
committed
refactor: manage retry in fail server error
1 parent 9acf631 commit 8a110d6

File tree

6 files changed

+135
-62
lines changed

6 files changed

+135
-62
lines changed

src/authz-module/libraries-manager/LibrariesUserManager.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ describe('LibrariesUserManager', () => {
241241
await user.click(removeButton);
242242

243243
const onSuccessCallback = mockMutate.mock.calls[0][1].onSuccess;
244-
onSuccessCallback();
244+
onSuccessCallback({ errors: [] });
245245

246246
await waitFor(() => {
247247
expect(screen.getByText(/The Admin role has been successfully removed/)).toBeInTheDocument();
@@ -278,7 +278,7 @@ describe('LibrariesUserManager', () => {
278278
await user.click(removeButton);
279279

280280
const onSuccessCallback = mockMutate.mock.calls[0][1].onSuccess;
281-
onSuccessCallback();
281+
onSuccessCallback({ errors: [] });
282282

283283
await waitFor(() => {
284284
expect(screen.getByText(/The user no longer has access to this library/)).toBeInTheDocument();
@@ -322,7 +322,7 @@ describe('LibrariesUserManager', () => {
322322
await user.click(removeButton);
323323

324324
const onSuccessCallback = mockMutate.mock.calls[0][1].onSuccess;
325-
onSuccessCallback();
325+
onSuccessCallback({ errors: [] });
326326

327327
await waitFor(() => {
328328
expect(screen.queryByText('Remove role?')).not.toBeInTheDocument();

src/authz-module/libraries-manager/LibrariesUserManager.tsx

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { useEffect, useMemo, useState } from 'react';
22
import { useNavigate, useParams } from 'react-router-dom';
33
import { useIntl } from '@edx/frontend-platform/i18n';
4-
import { logError } from '@edx/frontend-platform/logging';
54
import { Container, Skeleton } from '@openedx/paragon';
65
import { ROUTES } from '@src/authz-module/constants';
76
import { Role } from 'types';
@@ -47,7 +46,9 @@ const LibrariesUserManager = () => {
4746

4847
const [roleToDelete, setRoleToDelete] = useState<Role | null>(null);
4948
const [showConfirmDeletionModal, setShowConfirmDeletionModal] = useState(false);
50-
const { showToast, Bold, Br } = useToastManager();
49+
const {
50+
showToast, showErrorToast, Bold, Br,
51+
} = useToastManager();
5152

5253
const {
5354
data: teamMember, isLoading: isLoadingTeamMember, isFetching: isFetchingMember,
@@ -91,32 +92,42 @@ const LibrariesUserManager = () => {
9192
scope: libraryId,
9293
};
9394

94-
revokeUserRoles({ data }, {
95-
onSuccess: () => {
96-
const remainingRolesCount = userRoles.length - 1;
97-
showToast({
98-
message: intl.formatMessage(
99-
messages['library.authz.team.remove.user.toast.success.description'],
100-
{
101-
role: roleToDelete.name,
102-
rolesCount: remainingRolesCount,
103-
},
104-
),
105-
type: 'success',
106-
});
107-
108-
handleCloseConfirmDeletionModal();
109-
},
110-
onError: (error) => {
111-
logError(error);
112-
113-
showToast({
114-
type: 'error',
115-
message: intl.formatMessage(messages['library.authz.team.toast.default.error.message'], { Bold, Br }),
116-
});
117-
handleCloseConfirmDeletionModal();
118-
},
119-
});
95+
const runRevokeRole = (variables = { data }) => {
96+
revokeUserRoles(variables, {
97+
onSuccess: (response) => {
98+
const { errors } = response;
99+
100+
if (errors.length) {
101+
showToast({
102+
type: 'error',
103+
message: intl.formatMessage(
104+
messages['library.authz.team.toast.default.error.message'],
105+
{ Bold, Br },
106+
),
107+
});
108+
return;
109+
}
110+
111+
const remainingRolesCount = userRoles.length - 1;
112+
showToast({
113+
message: intl.formatMessage(
114+
messages['library.authz.team.remove.user.toast.success.description'],
115+
{
116+
role: roleToDelete.name,
117+
rolesCount: remainingRolesCount,
118+
},
119+
),
120+
type: 'success',
121+
});
122+
},
123+
onError: (error, retryVariables) => {
124+
showErrorToast(error, () => runRevokeRole(retryVariables));
125+
},
126+
});
127+
};
128+
129+
handleCloseConfirmDeletionModal();
130+
runRevokeRole();
120131
};
121132

122133
return (

src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberTrigger.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { useAssignTeamMembersRole } from '@src/authz-module/data/hooks';
66
import { ToastManagerProvider } from '@src/authz-module/libraries-manager/ToastManagerContext';
77
import AddNewTeamMemberTrigger from './AddNewTeamMemberTrigger';
88

9+
jest.mock('@edx/frontend-platform/logging');
10+
911
const mockMutate = jest.fn();
1012

1113
// Mock the hooks module
@@ -269,6 +271,47 @@ describe('AddNewTeamMemberTrigger', () => {
269271
});
270272
});
271273

274+
it('shows retry toast on API failure and displays another toast when retry fails again', async () => {
275+
const user = userEvent.setup();
276+
277+
const mockError = new Error('Network error');
278+
279+
mockMutate.mockImplementationOnce((_vars, { onError }) => {
280+
onError(mockError, _vars);
281+
});
282+
283+
renderWrapper(
284+
<ToastManagerProvider>
285+
<AddNewTeamMemberTrigger libraryId={mockLibraryId} />
286+
</ToastManagerProvider>,
287+
);
288+
289+
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
290+
await user.click(triggerButton);
291+
292+
const saveButton = screen.getByTestId('save-modal');
293+
await user.click(saveButton);
294+
295+
await waitFor(() => {
296+
expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument();
297+
expect(screen.getByRole('button', { name: /retry/i })).toBeInTheDocument();
298+
});
299+
300+
mockMutate.mockImplementationOnce((_vars, { onError }) => {
301+
onError(new Error('Network error'), _vars);
302+
});
303+
304+
const retryButton = screen.getByRole('button', { name: /retry/i });
305+
await user.click(retryButton);
306+
307+
await waitFor(() => {
308+
expect(screen.getByText(/Something went wrong/)).toBeInTheDocument();
309+
});
310+
311+
// Ensure mutate was called twice (original + retry)
312+
expect(mockMutate).toHaveBeenCalledTimes(2);
313+
});
314+
272315
it('displays loading state when adding team member', async () => {
273316
const user = userEvent.setup();
274317

src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberTrigger.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,8 @@ const AddNewTeamMemberTrigger: FC<AddNewTeamMemberTriggerProps> = ({ libraryId }
121121
scope: libraryId,
122122
};
123123

124-
assignTeamMembersRole(
125-
{ data: payload },
126-
{
124+
const runAssignMembers = (variables = { data: payload }) => {
125+
assignTeamMembersRole(variables, {
127126
onSuccess: (response) => {
128127
const { completed, errors } = response;
129128

@@ -142,11 +141,12 @@ const AddNewTeamMemberTrigger: FC<AddNewTeamMemberTriggerProps> = ({ libraryId }
142141
handleErrors(errors, completed.length);
143142
}
144143
},
145-
onError: (error, variables) => {
146-
showErrorToast(error, () => assignTeamMembersRole(variables));
144+
onError: (error, retryVariables) => {
145+
showErrorToast(error, () => runAssignMembers(retryVariables));
147146
},
148-
},
149-
);
147+
});
148+
};
149+
runAssignMembers();
150150
};
151151

152152
return (

src/authz-module/libraries-manager/components/AssignNewRoleModal/AssignNewRoleTrigger.tsx

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const AssignNewRoleTrigger: FC<AssignNewRoleTriggerProps> = ({
2424
const intl = useIntl();
2525
const [isOpen, open, close] = useToggle(false);
2626
const { roles } = useLibraryAuthZ();
27-
const { showToast } = useToastManager();
27+
const { showToast, showErrorToast } = useToastManager();
2828
const [newRole, setNewRole] = useState<string>('');
2929

3030
const { mutate: assignTeamMembersRole, isPending: isAssignTeamMembersRolePending } = useAssignTeamMembersRole();
@@ -46,16 +46,23 @@ const AssignNewRoleTrigger: FC<AssignNewRoleTriggerProps> = ({
4646
return;
4747
}
4848

49-
assignTeamMembersRole({ data }, {
50-
onSuccess: () => {
51-
showToast({
52-
message: intl.formatMessage(messages['libraries.authz.manage.assign.role.success']),
53-
type: 'success',
54-
});
55-
close();
56-
setNewRole('');
57-
},
58-
});
49+
const runAssignRole = (variables = { data }) => {
50+
assignTeamMembersRole(variables, {
51+
onSuccess: () => {
52+
showToast({
53+
message: intl.formatMessage(messages['libraries.authz.manage.assign.role.success']),
54+
type: 'success',
55+
});
56+
close();
57+
setNewRole('');
58+
},
59+
onError: (error, retryVariables) => {
60+
showErrorToast(error, () => runAssignRole(retryVariables));
61+
},
62+
});
63+
};
64+
65+
runAssignRole();
5966
};
6067

6168
return (

src/authz-module/libraries-manager/components/PublicReadToggle.tsx

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,37 @@ type PublicReadToggleProps = {
1010
canEditToggle: boolean;
1111
};
1212

13+
type UpdateLibraryPublicRead = {
14+
libraryId: string;
15+
updatedData: { allowPublicRead: boolean };
16+
};
17+
1318
const PublicReadToggle = ({ libraryId, canEditToggle }: PublicReadToggleProps) => {
1419
const intl = useIntl();
1520
const { data: library } = useLibrary(libraryId);
1621
const { mutate: updateLibrary, isPending } = useUpdateLibrary();
1722
const { showToast, showErrorToast } = useToastManager();
18-
const onChangeToggle = () => updateLibrary({
19-
libraryId,
20-
updatedData: { allowPublicRead: !library.allowPublicRead },
21-
}, {
22-
onSuccess: () => {
23-
showToast({
24-
message: intl.formatMessage(messages['libraries.authz.public.read.toggle.success']),
25-
type: 'success',
23+
24+
const onChangeToggle = () => {
25+
const runUpdate = (variables: UpdateLibraryPublicRead = {
26+
libraryId,
27+
updatedData: { allowPublicRead: !library.allowPublicRead },
28+
}) => {
29+
updateLibrary(variables, {
30+
onSuccess: () => {
31+
showToast({
32+
message: intl.formatMessage(messages['libraries.authz.public.read.toggle.success']),
33+
type: 'success',
34+
});
35+
},
36+
onError: (error, retryVariables) => {
37+
showErrorToast(error, () => runUpdate(retryVariables as UpdateLibraryPublicRead));
38+
},
2639
});
27-
},
28-
onError: (error, variables) => {
29-
showErrorToast(error, () => updateLibrary(variables));
30-
},
31-
});
40+
};
41+
42+
runUpdate();
43+
};
3244

3345
if (!library.allowPublicRead && !canEditToggle) {
3446
return null;

0 commit comments

Comments
 (0)