Skip to content

Commit a77878c

Browse files
fix(ulmo): backport changing how the feedback message is displayed for adding team members (#44)
* fix: changing how the feedback message is displayed for adding team members * fix: updating feedback to consider existing roles a successful messages * fix: already has role message update * feat: update not found message
1 parent 3a1cafa commit a77878c

File tree

7 files changed

+270
-70
lines changed

7 files changed

+270
-70
lines changed

src/authz-module/data/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export type PermissionsByRole = {
4040
userCount: number;
4141
};
4242
export interface PutAssignTeamMembersRoleResponse {
43-
completed: { user: string; status: string }[];
43+
completed: { userIdentifier: string; status: string }[];
4444
errors: { userIdentifier: string; error: string }[];
4545
}
4646

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

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,80 @@ describe('ToastManagerContext', () => {
137137
expect(logError).toHaveBeenCalled();
138138
expect(retryFn).toHaveBeenCalled();
139139
});
140+
141+
it('respects custom delay when provided', async () => {
142+
const user = userEvent.setup();
143+
144+
const DelayTestComponent = () => {
145+
const { showToast } = useToastManager();
146+
147+
const handleShowToastWithDelay = () => showToast({
148+
message: 'Custom delay toast',
149+
type: 'success',
150+
delay: 1000, // Custom 1 second delay
151+
});
152+
153+
return (
154+
<button type="button" onClick={handleShowToastWithDelay}>Show Toast With Custom Delay</button>
155+
);
156+
};
157+
158+
renderWrapper(
159+
<ToastManagerProvider>
160+
<DelayTestComponent />
161+
</ToastManagerProvider>,
162+
);
163+
164+
const showButton = screen.getByText('Show Toast With Custom Delay');
165+
await user.click(showButton);
166+
167+
await waitFor(() => {
168+
expect(screen.getByText('Custom delay toast')).toBeInTheDocument();
169+
});
170+
171+
await waitFor(() => {
172+
expect(screen.getByText('Custom delay toast')).toBeInTheDocument();
173+
}, { timeout: 600 });
174+
175+
// Toast should disappear after the custom delay (1000ms)
176+
await waitFor(() => {
177+
expect(screen.queryByText('Custom delay toast')).not.toBeInTheDocument();
178+
}, { timeout: 1200 });
179+
});
180+
181+
it('uses default delay when delay prop is not provided', async () => {
182+
const user = userEvent.setup();
183+
184+
const DefaultDelayTestComponent = () => {
185+
const { showToast } = useToastManager();
186+
187+
const handleShowToastWithoutDelay = () => showToast({
188+
message: 'Default delay toast',
189+
type: 'success',
190+
// No delay prop provided
191+
});
192+
193+
return (
194+
<button type="button" onClick={handleShowToastWithoutDelay}>Show Toast With Default Delay</button>
195+
);
196+
};
197+
198+
renderWrapper(
199+
<ToastManagerProvider>
200+
<DefaultDelayTestComponent />
201+
</ToastManagerProvider>,
202+
);
203+
204+
const showButton = screen.getByText('Show Toast With Default Delay');
205+
await user.click(showButton);
206+
207+
await waitFor(() => {
208+
expect(screen.getByText('Default delay toast')).toBeInTheDocument();
209+
});
210+
211+
// DEFAULT_TOAST_DELAY is 5000ms
212+
await waitFor(() => {
213+
expect(screen.queryByText('Default delay toast')).not.toBeInTheDocument();
214+
}, { timeout: 5050 });
215+
}, 5100);
140216
});

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { logError } from '@edx/frontend-platform/logging';
55
import { useIntl } from '@edx/frontend-platform/i18n';
66
import { Toast } from '@openedx/paragon';
77
import messages from './messages';
8+
import { DEFAULT_TOAST_DELAY } from './constants';
89

910
type ToastType = 'success' | 'error' | 'error-retry';
1011

@@ -19,11 +20,12 @@ export const ERROR_TOAST_MAP: Record<number | string, { type: ToastType; message
1920
DEFAULT: { type: 'error-retry', messageId: 'library.authz.team.toast.default.error.message' },
2021
};
2122

22-
interface AppToast {
23+
export interface AppToast {
2324
id: string;
2425
message: string;
2526
type: ToastType;
2627
onRetry?: () => void;
28+
delay?: number;
2729
}
2830

2931
const Bold = (chunk: string) => <b>{chunk}</b>;
@@ -47,7 +49,7 @@ export const ToastManagerProvider = ({ children }: ToastManagerProviderProps) =>
4749
const [toasts, setToasts] = useState<(AppToast & { visible: boolean })[]>([]);
4850

4951
const showToast = (toast: Omit<AppToast, 'id'>) => {
50-
const id = `toast-notification-${Date.now()}`;
52+
const id = `toast-notification-${Date.now()}-${Math.floor(Math.random() * 1000000)}`;
5153
const newToast = { ...toast, id, visible: true };
5254
setToasts(prev => [...prev, newToast]);
5355
};
@@ -92,6 +94,7 @@ export const ToastManagerProvider = ({ children }: ToastManagerProviderProps) =>
9294
key={toast.id}
9395
show={toast.visible}
9496
onClose={() => discardToast(toast.id)}
97+
delay={toast.delay ?? DEFAULT_TOAST_DELAY}
9598
action={toast.onRetry ? {
9699
onClick: () => {
97100
discardToast(toast.id);

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

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,23 @@ jest.mock('./AddNewTeamMemberModal', () => {
2121
isOpen, close, onSave, isLoading, formValues, handleChangeForm,
2222
}) => (
2323
isOpen ? (
24-
<div data-testid="add-team-member-modal">
25-
<button type="button" onClick={close} data-testid="close-modal">Close</button>
26-
<button type="button" onClick={onSave} data-testid="save-modal">Save</button>
24+
<div data-testid="add-team-member-modal" role="dialog" aria-label="Add New Team Member">
25+
<button type="button" onClick={close} aria-label="Close modal" data-testid="close-modal">Close</button>
26+
<button type="button" onClick={onSave} aria-label="Save team member" data-testid="save-modal">Save</button>
2727
<textarea
2828
name="users"
2929
value={formValues?.users || ''}
3030
onChange={handleChangeForm}
3131
data-testid="users-input"
32+
aria-label="Enter user emails or usernames"
33+
placeholder="Enter emails or usernames"
3234
/>
3335
<select
3436
name="role"
3537
value={formValues?.role || ''}
3638
onChange={handleChangeForm}
3739
data-testid="role-select"
40+
aria-label="Select role"
3841
>
3942
<option value="">Select role</option>
4043
<option value="admin">Admin</option>
@@ -146,7 +149,7 @@ describe('AddNewTeamMemberTrigger', () => {
146149
expect(screen.queryByTestId('add-team-member-modal')).not.toBeInTheDocument();
147150
});
148151

149-
expect(screen.getByText('2 team members added successfully.')).toBeInTheDocument();
152+
expect(screen.getByText(/2 team members added successfully/)).toBeInTheDocument();
150153
});
151154

152155
it('displays mixed success and error toast on partial success', async () => {
@@ -179,6 +182,50 @@ describe('AddNewTeamMemberTrigger', () => {
179182
expect(screen.getByTestId('add-team-member-modal')).toBeInTheDocument();
180183
});
181184

185+
it('filters out successfully added users from error users list', async () => {
186+
const user = userEvent.setup();
187+
188+
const mockPartialResponse = {
189+
completed: [
190+
{ userIdentifier: '[email protected]' },
191+
],
192+
errors: [
193+
{ userIdentifier: '[email protected]', error: 'USER_NOT_FOUND' },
194+
{ userIdentifier: '[email protected]', error: 'USER_NOT_FOUND' },
195+
],
196+
};
197+
198+
(useAssignTeamMembersRole as jest.Mock).mockReturnValue({
199+
mutate: jest.fn((_variables, { onSuccess }) => {
200+
onSuccess(mockPartialResponse);
201+
}),
202+
isPending: false,
203+
});
204+
205+
renderWrapper(<ToastManagerProvider><AddNewTeamMemberTrigger libraryId={mockLibraryId} /></ToastManagerProvider>);
206+
207+
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
208+
await user.click(triggerButton);
209+
210+
const usersInput = screen.getByRole('textbox', { name: /Enter user emails or usernames/i });
211+
const roleSelect = screen.getByRole('combobox', { name: /Select role/i });
212+
const saveButton = screen.getByRole('button', { name: 'Save team member' });
213+
214+
await user.type(usersInput, '[email protected], [email protected], [email protected]');
215+
await user.selectOptions(roleSelect, 'editor');
216+
await user.click(saveButton);
217+
218+
await waitFor(() => {
219+
expect(usersInput).toHaveValue('[email protected], [email protected]');
220+
});
221+
222+
await user.type(usersInput, ', [email protected]');
223+
224+
await waitFor(() => {
225+
expect(usersInput).toHaveValue('[email protected], [email protected], [email protected]');
226+
});
227+
});
228+
182229
it('displays only error toast when all additions fail', async () => {
183230
const user = userEvent.setup();
184231
renderWrapper(<ToastManagerProvider><AddNewTeamMemberTrigger libraryId={mockLibraryId} /></ToastManagerProvider>);
@@ -207,6 +254,33 @@ describe('AddNewTeamMemberTrigger', () => {
207254
expect(screen.getByTestId('add-team-member-modal')).toBeInTheDocument();
208255
});
209256

257+
it('displays different error toast when different errors happen', async () => {
258+
const user = userEvent.setup();
259+
renderWrapper(<ToastManagerProvider><AddNewTeamMemberTrigger libraryId={mockLibraryId} /></ToastManagerProvider>);
260+
261+
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
262+
await user.click(triggerButton);
263+
264+
const saveButton = screen.getByRole('button', { name: 'Save team member' });
265+
await user.click(saveButton);
266+
267+
const [, { onSuccess }] = mockMutate.mock.calls[0];
268+
onSuccess({
269+
completed: [],
270+
errors: [
271+
{ userIdentifier: '[email protected]', error: 'user_not_found' },
272+
{ userIdentifier: '[email protected]', error: 'user_already_has_role' },
273+
],
274+
});
275+
276+
await waitFor(() => {
277+
expect(screen.getByText(/We couldn't find a user for 1 email address or username/)).toBeInTheDocument();
278+
expect(screen.getByText(/The user already has the role/)).toBeInTheDocument();
279+
});
280+
281+
expect(screen.getByRole('dialog', { name: 'Add New Team Member' })).toBeInTheDocument();
282+
});
283+
210284
it('resets form values after successful addition with no errors', async () => {
211285
const user = userEvent.setup();
212286
renderWrapper(<ToastManagerProvider><AddNewTeamMemberTrigger libraryId={mockLibraryId} /></ToastManagerProvider>);
@@ -258,7 +332,7 @@ describe('AddNewTeamMemberTrigger', () => {
258332

259333
// Toast should be visible
260334
await waitFor(() => {
261-
expect(screen.getByText('1 team member added successfully.')).toBeInTheDocument();
335+
expect(screen.getByText(/1 team member added successfully/)).toBeInTheDocument();
262336
});
263337

264338
// Find and close the toast

0 commit comments

Comments
 (0)