Skip to content

Commit 5219484

Browse files
committed
feat: enhance Assign Role Wizard with user validation and navigation improvements
1 parent bcb81a6 commit 5219484

5 files changed

Lines changed: 34 additions & 11 deletions

File tree

src/authz-module/wizard/AssignRoleWizard.test.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ jest.mock('@openedx/paragon', () => {
3737
jest.mock('./SelectUsersAndRoleStep', () => ({
3838
__esModule: true,
3939
default: ({
40-
setUsers, setSelectedRole, invalidUsers, validationError,
40+
users, setUsers, setSelectedRole, invalidUsers, validationError,
4141
}: {
42+
users: string;
4243
setUsers: (v: string) => void;
4344
setSelectedRole: (v: string) => void;
4445
invalidUsers: string[];
@@ -47,6 +48,7 @@ jest.mock('./SelectUsersAndRoleStep', () => ({
4748
<div>
4849
<input
4950
data-testid="users-input"
51+
value={users}
5052
onChange={(e) => setUsers(e.target.value)}
5153
/>
5254
<button type="button" data-testid="select-role" onClick={() => setSelectedRole('library_admin')}>
@@ -250,11 +252,14 @@ describe('AssignRoleWizard', () => {
250252

251253
it('calls assignRole for each scope on Save', async () => {
252254
const user = userEvent.setup();
255+
mockValidateMutateAsync.mockResolvedValue({ invalidUsers: [], validUsers: ['alice'] });
253256
mockAssignMutateAsync.mockResolvedValue({ completed: [], errors: [] });
254257

255258
renderWizard();
256259
await user.type(screen.getByTestId('users-input'), 'alice');
257260
await user.click(screen.getByTestId('select-role'));
261+
await user.click(screen.getByRole('button', { name: /Next/i }));
262+
await waitFor(() => expect(mockValidateMutateAsync).toHaveBeenCalled());
258263
await user.click(screen.getByTestId('toggle-scope'));
259264
await user.click(screen.getByRole('button', { name: /Save/i }));
260265

@@ -268,11 +273,14 @@ describe('AssignRoleWizard', () => {
268273
it('shows success toast and calls onClose after successful save', async () => {
269274
const user = userEvent.setup();
270275
const onClose = jest.fn();
276+
mockValidateMutateAsync.mockResolvedValue({ invalidUsers: [], validUsers: ['alice'] });
271277
mockAssignMutateAsync.mockResolvedValue({ completed: [], errors: [] });
272278

273279
renderWizard({ onClose });
274280
await user.type(screen.getByTestId('users-input'), 'alice');
275281
await user.click(screen.getByTestId('select-role'));
282+
await user.click(screen.getByRole('button', { name: /Next/i }));
283+
await waitFor(() => expect(mockValidateMutateAsync).toHaveBeenCalled());
276284
await user.click(screen.getByTestId('toggle-scope'));
277285
await user.click(screen.getByRole('button', { name: /Save/i }));
278286

@@ -285,11 +293,14 @@ describe('AssignRoleWizard', () => {
285293
it('shows error toast when save fails', async () => {
286294
const user = userEvent.setup();
287295
const saveError = new Error('Server error');
296+
mockValidateMutateAsync.mockResolvedValue({ invalidUsers: [], validUsers: ['alice'] });
288297
mockAssignMutateAsync.mockRejectedValue(saveError);
289298

290299
renderWizard();
291300
await user.type(screen.getByTestId('users-input'), 'alice');
292301
await user.click(screen.getByTestId('select-role'));
302+
await user.click(screen.getByRole('button', { name: /Next/i }));
303+
await waitFor(() => expect(mockValidateMutateAsync).toHaveBeenCalled());
293304
await user.click(screen.getByTestId('toggle-scope'));
294305
await user.click(screen.getByRole('button', { name: /Save/i }));
295306

@@ -298,11 +309,15 @@ describe('AssignRoleWizard', () => {
298309
});
299310
});
300311

301-
it('initialUsers prop pre-fills the users field', () => {
312+
it('initialUsers prop pre-fills the users field', async () => {
313+
const user = userEvent.setup();
302314
renderWizard({ initialUsers: 'prefilled_user' });
303-
// The wizard starts with initialUsers set, but our mock input doesn't show it
304-
// We verify the Next button state — it's still disabled without role
315+
expect(screen.getByTestId('users-input')).toHaveValue('prefilled_user');
316+
// Next is still disabled without a role selected
305317
expect(screen.getByRole('button', { name: /Next/i })).toBeDisabled();
318+
// Selecting a role enables Next since users are already pre-filled
319+
await user.click(screen.getByTestId('select-role'));
320+
expect(screen.getByRole('button', { name: /Next/i })).not.toBeDisabled();
306321
});
307322

308323
it('filters roles based on user permissions (library allowed, course not)', () => {

src/authz-module/wizard/AssignRoleWizard.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const AssignRoleWizard = ({ onClose, scope, initialUsers = '' }: AssignRoleWizar
4545

4646
const [validationError, setValidationError] = useState<string | null>(null);
4747
const [invalidUsers, setInvalidUsers] = useState<string[]>([]);
48+
const [validatedUsers, setValidatedUsers] = useState<string[]>([]);
4849

4950
const validateUsersMutation = useValidateUsers();
5051
const assignRoleMutation = useAssignTeamMembersRole();
@@ -75,13 +76,20 @@ const AssignRoleWizard = ({ onClose, scope, initialUsers = '' }: AssignRoleWizar
7576
return allRolesMetadata.filter((r) => allowedContextTypes.has(r.contextType || ''));
7677
}, [permissionsData]);
7778

79+
useEffect(() => {
80+
if (filteredRoles.length === 0) {
81+
onClose();
82+
}
83+
}, [filteredRoles.length, onClose]);
84+
7885
const handleClose = () => {
7986
setActiveStep(STEPS.SELECT_USERS_AND_ROLE);
8087
setUsers('');
8188
setSelectedRole(null);
8289
setSelectedScopes(new Set());
8390
setValidationError(null);
8491
setInvalidUsers([]);
92+
setValidatedUsers([]);
8593
onClose();
8694
};
8795

@@ -102,6 +110,7 @@ const AssignRoleWizard = ({ onClose, scope, initialUsers = '' }: AssignRoleWizar
102110
if (result.invalidUsers?.length > 0) {
103111
setInvalidUsers(result.invalidUsers);
104112
} else {
113+
setValidatedUsers(usersList);
105114
setActiveStep(STEPS.DEFINE_APPLICATION_SCOPE);
106115
}
107116
} catch {
@@ -118,13 +127,12 @@ const AssignRoleWizard = ({ onClose, scope, initialUsers = '' }: AssignRoleWizar
118127
}, []);
119128

120129
const handleSave = async () => {
121-
if (!selectedRole || selectedScopes.size === 0) { return; }
122-
const usersList = parseUsers(users);
130+
if (!selectedRole || selectedScopes.size === 0 || validatedUsers.length === 0) { return; }
123131

124132
try {
125133
await Promise.all(
126134
Array.from(selectedScopes).map((selectedScope) => assignRoleMutation.mutateAsync({
127-
data: { users: usersList, role: selectedRole, scope: selectedScope },
135+
data: { users: validatedUsers, role: selectedRole, scope: selectedScope },
128136
})),
129137
);
130138
showToast({ message: intl.formatMessage(messages['wizard.save.success']), type: 'success', delay: 5000 });

src/authz-module/wizard/AssignRoleWizardPage.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ describe('AssignRoleWizardPage', () => {
9898
expect(screen.getByRole('heading', { name: 'Assign Role' })).toBeInTheDocument();
9999
});
100100

101-
it('calls navigate(-1) when wizard onClose is triggered', async () => {
101+
it('navigates to team members path when wizard onClose is triggered', async () => {
102102
setupMocks();
103103
const mockNavigate = jest.fn();
104104
const { useNavigate } = jest.requireMock('react-router-dom');
@@ -108,6 +108,6 @@ describe('AssignRoleWizardPage', () => {
108108
renderWrapper(<AssignRoleWizardPage />);
109109
await user.click(screen.getByTestId('wizard-close'));
110110

111-
expect(mockNavigate).toHaveBeenCalledWith(-1);
111+
expect(mockNavigate).toHaveBeenCalledWith('/authz/libraries/lib:123');
112112
});
113113
});

src/authz-module/wizard/AssignRoleWizardPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const AssignRoleWizardPage = () => {
2020
const teamMembersPath = `/authz${ROUTES.LIBRARIES_TEAM_PATH.replace(':libraryId', scope)}`;
2121

2222
const handleCancel = () => {
23-
navigate(-1);
23+
navigate(teamMembersPath);
2424
};
2525

2626
return (

src/authz-module/wizard/SelectUsersAndRoleStep.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ const SelectUsersAndRoleStep = ({
8585
{contextLabels[context] || context}
8686
</h5>
8787
<Form.RadioSet
88-
name="role-selection"
88+
name={`role-selection-${context}`}
8989
value={selectedRole || ''}
9090
onChange={(e) => setSelectedRole(e.target.value)}
9191
className="pl-4"

0 commit comments

Comments
 (0)