Skip to content

Commit 1f9a5b5

Browse files
committed
feat: implement internationalization for Assign Role Wizard and update user role management messages
1 parent df2907c commit 1f9a5b5

7 files changed

Lines changed: 204 additions & 42 deletions

File tree

src/authz-module/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ export const libraryPermissions = [
6767
{ key: CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_COLLECTION, resource: 'library_collection', description: 'Add or remove content from existing collections.' },
6868
{ key: CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY_COLLECTION, resource: 'library_collection', description: 'Delete entire collections from the library.' },
6969

70-
{ key: CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TEAM, resource: 'library_team', description: 'View the list of users who have access to the library.' },
71-
{ key: CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM, resource: 'library_team', description: 'Add, remove, and assign roles to users within the library.' },
70+
{ key: CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TEAM, resource: 'library_team', description: 'Add, remove, and assign roles to users within the library.' },
71+
{ key: CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM, resource: 'library_team', description: 'View the list of users who have access to the library.' },
7272
];
7373

7474
// Course Permission Keys

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,6 @@ jest.mock('./components/TeamTable', () => ({
2929
default: () => <div role="table" aria-label="Team Members Table">Team member list</div>,
3030
}));
3131

32-
jest.mock('./components/AddNewTeamMemberModal', () => ({
33-
__esModule: true,
34-
AddNewTeamMemberTrigger: () => <button type="button">Add Team Member</button>,
35-
}));
36-
3732
jest.mock('../components/RoleCard', () => ({
3833
__esModule: true,
3934
default: ({ title, description, permissionsByResource }: {
@@ -111,8 +106,8 @@ describe('LibrariesTeamManager', () => {
111106
// TeamTable is rendered
112107
expect(screen.getByRole('table', { name: 'Team Members Table' })).toBeInTheDocument();
113108

114-
// AddNewTeamMemberTrigger is rendered
115-
expect(screen.getByRole('button', { name: 'Add Team Member' })).toBeInTheDocument();
109+
// Assign Role button is rendered
110+
expect(screen.getByRole('button', { name: 'Assign Role' })).toBeInTheDocument();
116111
});
117112

118113
it('renders role cards when "Roles" tab is selected', async () => {

src/authz-module/wizard/AssignRoleWizard.tsx

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
useState, useCallback, useEffect, useMemo,
33
} from 'react';
4+
import { useIntl } from '@edx/frontend-platform/i18n';
45
import {
56
Stepper, Button, Container, StatefulButton, Icon,
67
} from '@openedx/paragon';
@@ -13,6 +14,7 @@ import {
1314
} from '../constants';
1415
import { useToastManager } from '../libraries-manager/ToastManagerContext';
1516
import { useValidateUserPermissions } from '../../data/hooks';
17+
import messages from './messages';
1618

1719
const allRolesMetadata = [...courseRolesMetadata, ...libraryRolesMetadata];
1820

@@ -35,6 +37,7 @@ interface AssignRoleWizardProps {
3537
}
3638

3739
const AssignRoleWizard = ({ onClose, scope, initialUsers = '' }: AssignRoleWizardProps) => {
40+
const intl = useIntl();
3841
const [activeStep, setActiveStep] = useState<StepKey>(STEPS.SELECT_USERS_AND_ROLE);
3942
const [users, setUsers] = useState(initialUsers);
4043
const [selectedRole, setSelectedRole] = useState<string | null>(null);
@@ -102,7 +105,7 @@ const AssignRoleWizard = ({ onClose, scope, initialUsers = '' }: AssignRoleWizar
102105
setActiveStep(STEPS.DEFINE_APPLICATION_SCOPE);
103106
}
104107
} catch {
105-
setValidationError('An error occurred while validating users. Please try again.');
108+
setValidationError(intl.formatMessage(messages['wizard.validate.error']));
106109
}
107110
};
108111

@@ -124,9 +127,9 @@ const AssignRoleWizard = ({ onClose, scope, initialUsers = '' }: AssignRoleWizar
124127
data: { users: usersList, role: selectedRole, scope: selectedScope },
125128
})),
126129
);
127-
showToast({ message: 'Role assigned successfully.', type: 'success', delay: 5000 });
130+
showToast({ message: intl.formatMessage(messages['wizard.save.success']), type: 'success', delay: 5000 });
128131
handleClose();
129-
} catch (error: any) {
132+
} catch (error: unknown) {
130133
showErrorToast(error, handleSave);
131134
}
132135
};
@@ -142,7 +145,7 @@ const AssignRoleWizard = ({ onClose, scope, initialUsers = '' }: AssignRoleWizar
142145
<Stepper.Step
143146
onClick={() => setActiveStep(STEPS.SELECT_USERS_AND_ROLE)}
144147
eventKey={STEPS.SELECT_USERS_AND_ROLE}
145-
title="Who and Role"
148+
title={intl.formatMessage(messages['wizard.step.selectUsersAndRole.title'])}
146149
>
147150
<SelectUsersAndRoleStep
148151
users={users}
@@ -158,7 +161,7 @@ const AssignRoleWizard = ({ onClose, scope, initialUsers = '' }: AssignRoleWizar
158161
<Stepper.Step
159162
onClick={() => setActiveStep(STEPS.DEFINE_APPLICATION_SCOPE)}
160163
eventKey={STEPS.DEFINE_APPLICATION_SCOPE}
161-
title="Where it applies"
164+
title={intl.formatMessage(messages['wizard.step.defineScope.title'])}
162165
>
163166
<DefineApplicationScopeStep
164167
selectedRole={selectedRole}
@@ -171,11 +174,14 @@ const AssignRoleWizard = ({ onClose, scope, initialUsers = '' }: AssignRoleWizar
171174
<div className="p-5">
172175
<Stepper.ActionRow eventKey={STEPS.SELECT_USERS_AND_ROLE}>
173176
<Button variant="outline-primary" onClick={handleClose}>
174-
Cancel
177+
{intl.formatMessage(messages['wizard.button.cancel'])}
175178
</Button>
176179
<Stepper.ActionRow.Spacer />
177180
<StatefulButton
178-
labels={{ default: 'Next', pending: 'Validating...' }}
181+
labels={{
182+
default: intl.formatMessage(messages['wizard.button.next']),
183+
pending: intl.formatMessage(messages['wizard.button.next.pending']),
184+
}}
179185
icons={{ pending: <Icon src={SpinnerSimple} /> }}
180186
state={validateUsersMutation.isPending ? 'pending' : 'default'}
181187
onClick={validateUsersAndProceed}
@@ -185,14 +191,17 @@ const AssignRoleWizard = ({ onClose, scope, initialUsers = '' }: AssignRoleWizar
185191

186192
<Stepper.ActionRow eventKey={STEPS.DEFINE_APPLICATION_SCOPE}>
187193
<Button variant="outline-primary" onClick={handleClose}>
188-
Cancel
194+
{intl.formatMessage(messages['wizard.button.cancel'])}
189195
</Button>
190196
<Button variant="tertiary" onClick={() => setActiveStep(STEPS.SELECT_USERS_AND_ROLE)}>
191-
Back
197+
{intl.formatMessage(messages['wizard.button.back'])}
192198
</Button>
193199
<Stepper.ActionRow.Spacer />
194200
<StatefulButton
195-
labels={{ default: 'Save', pending: 'Saving...' }}
201+
labels={{
202+
default: intl.formatMessage(messages['wizard.button.save']),
203+
pending: intl.formatMessage(messages['wizard.button.save.pending']),
204+
}}
196205
icons={{ pending: <Icon src={SpinnerSimple} /> }}
197206
state={assignRoleMutation.isPending ? 'pending' : 'default'}
198207
onClick={handleSave}

src/authz-module/wizard/AssignRoleWizardPage.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import { useNavigate, useSearchParams } from 'react-router-dom';
2+
import { useIntl } from '@edx/frontend-platform/i18n';
23
import AssignRoleWizard from './AssignRoleWizard';
34
import AuthZLayout from '../components/AuthZLayout';
45
import { useLibrary } from '../data/hooks';
56
import { ROUTES } from '../constants';
7+
import messages from './messages';
68

79
const AssignRoleWizardPage = () => {
810
const navigate = useNavigate();
11+
const intl = useIntl();
912
const [searchParams] = useSearchParams();
1013
const scope = searchParams.get('scope') || '';
1114
const initialUsers = searchParams.get('users') || '';
1215

13-
const { data: library, isLoading } = useLibrary(scope);
16+
const { data: library } = useLibrary(scope);
1417

15-
if (isLoading) { return null; }
18+
if (!scope || !library) { return null; }
1619

1720
const teamMembersPath = `/authz${ROUTES.LIBRARIES_TEAM_PATH.replace(':libraryId', scope)}`;
1821

@@ -23,9 +26,9 @@ const AssignRoleWizardPage = () => {
2326
return (
2427
<AuthZLayout
2528
context={{ id: scope, title: library.title, org: library.org }}
26-
navLinks={[{ label: 'Roles and Permissions Management', to: teamMembersPath }]}
27-
activeLabel="Assign Role"
28-
pageTitle="Assign Role"
29+
navLinks={[{ label: intl.formatMessage(messages['wizard.page.breadcrumb']), to: teamMembersPath }]}
30+
activeLabel={intl.formatMessage(messages['wizard.page.title'])}
31+
pageTitle={intl.formatMessage(messages['wizard.page.title'])}
2932
pageSubtitle=""
3033
actions={[]}
3134
>

src/authz-module/wizard/HighlightedUsersInput.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo } from 'react';
1+
import { useMemo, useRef } from 'react';
22

33
// Shared styles between overlay and textarea to keep text positions in sync
44
const INPUT_STYLE: React.CSSProperties = {
@@ -24,6 +24,8 @@ interface HighlightedUsersInputProps {
2424
const HighlightedUsersInput = ({
2525
value, onChange, invalidUsers, placeholder, hasNetworkError = false,
2626
}: HighlightedUsersInputProps) => {
27+
const overlayRef = useRef<HTMLDivElement>(null);
28+
2729
const invalidSet = useMemo(
2830
() => new Set(invalidUsers.map((u) => u.trim())),
2931
[invalidUsers],
@@ -47,11 +49,18 @@ const HighlightedUsersInput = ({
4749
});
4850
}, [value, invalidSet, hasHighlights]);
4951

52+
const handleScroll = (e: React.UIEvent<HTMLTextAreaElement>) => {
53+
if (overlayRef.current) {
54+
overlayRef.current.scrollTop = e.currentTarget.scrollTop;
55+
}
56+
};
57+
5058
return (
5159
<div style={{ position: 'relative' }}>
5260
{/* Highlight layer — sits behind the transparent textarea */}
5361
{hasHighlights && (
5462
<div
63+
ref={overlayRef}
5564
aria-hidden="true"
5665
style={{
5766
...INPUT_STYLE,
@@ -75,6 +84,7 @@ const HighlightedUsersInput = ({
7584
rows={4}
7685
value={value}
7786
onChange={(e) => onChange(e.target.value)}
87+
onScroll={handleScroll}
7888
placeholder={hasHighlights ? undefined : placeholder}
7989
className={`form-control${hasNetworkError ? ' is-invalid' : ''}`}
8090
style={{

src/authz-module/wizard/SelectUsersAndRoleStep.tsx

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import { useIntl } from '@edx/frontend-platform/i18n';
12
import {
23
Form, Stack, OverlayTrigger,
34
Tooltip,
45
} from '@openedx/paragon';
6+
import { getConfig } from '@edx/frontend-platform';
57
import HighlightedUsersInput from './HighlightedUsersInput';
8+
import messages from './messages';
69

710
interface RoleMetadata {
811
role: string;
@@ -24,11 +27,6 @@ interface SelectUsersAndRoleStepProps {
2427

2528
const CONTEXT_ORDER = ['course', 'library'];
2629

27-
const contextLabels: Record<string, string> = {
28-
library: 'Libraries',
29-
course: 'Courses',
30-
};
31-
3230
const SelectUsersAndRoleStep = ({
3331
users,
3432
setUsers,
@@ -38,6 +36,13 @@ const SelectUsersAndRoleStep = ({
3836
validationError,
3937
invalidUsers,
4038
}: SelectUsersAndRoleStepProps) => {
39+
const intl = useIntl();
40+
41+
const contextLabels: Record<string, string> = {
42+
library: intl.formatMessage(messages['wizard.step1.roles.contextLabel.library']),
43+
course: intl.formatMessage(messages['wizard.step1.roles.contextLabel.course']),
44+
};
45+
4146
const rolesByContext = roles.reduce<Record<string, RoleMetadata[]>>((acc, role) => {
4247
const context = role.contextType || 'library';
4348
if (!acc[context]) { acc[context] = []; }
@@ -47,31 +52,33 @@ const SelectUsersAndRoleStep = ({
4752

4853
const orderedContexts = CONTEXT_ORDER.filter((ctx) => rolesByContext[ctx]);
4954

55+
const adminUrl = `${getConfig().LMS_BASE_URL}/admin`;
56+
5057
return (
5158
<div className="select-users-and-role-step">
5259
{/* Users Section */}
53-
<h3 className="mb-2">Users</h3>
60+
<h3 className="mb-2">{intl.formatMessage(messages['wizard.step1.users.heading'])}</h3>
5461
<Form.Group controlId="users-input">
55-
<Form.Label>Add users by username or email</Form.Label>
62+
<Form.Label>{intl.formatMessage(messages['wizard.step1.users.label'])}</Form.Label>
5663
<HighlightedUsersInput
5764
value={users}
5865
onChange={setUsers}
5966
invalidUsers={invalidUsers}
60-
placeholder="Enter one or more email addresses or usernames"
67+
placeholder={intl.formatMessage(messages['wizard.step1.users.placeholder'])}
6168
hasNetworkError={!!validationError || invalidUsers.length > 0}
6269
/>
6370
<Form.Text className={invalidUsers.length > 0 ? 'text-danger' : ''}>
6471
{invalidUsers.length > 0
65-
? 'This email is not associated with an account in this platform.'
66-
: 'The user must already have an account.'}
72+
? intl.formatMessage(messages['wizard.step1.users.invalid'], { count: invalidUsers.length })
73+
: intl.formatMessage(messages['wizard.step1.users.hint'])}
6774
</Form.Text>
6875
{validationError && (
6976
<div className="text-danger small mt-1">{validationError}</div>
7077
)}
7178
</Form.Group>
7279

7380
{/* Roles Section */}
74-
<h3 className="mt-4 mb-2">Roles</h3>
81+
<h3 className="mt-4 mb-2">{intl.formatMessage(messages['wizard.step1.roles.heading'])}</h3>
7582
{orderedContexts.map((context) => (
7683
<div key={context} className="role-group mb-4">
7784
<h5 className="role-group-header mb-3 pl-4">
@@ -102,8 +109,7 @@ const SelectUsersAndRoleStep = ({
102109
placement="right"
103110
overlay={(
104111
<Tooltip variant="light" id={`tooltip-disabled-${role.role}`}>
105-
We are expanding our permissions system. This role is currently
106-
unavailable but will be part of an upcoming update.
112+
{intl.formatMessage(messages['wizard.step1.roles.disabled.tooltip'])}
107113
</Tooltip>
108114
)}
109115
>
@@ -120,11 +126,11 @@ const SelectUsersAndRoleStep = ({
120126

121127
{/* Documentation Link */}
122128
<div className="mt-3">
123-
<p className="mb-1 font-weight-bold small">Can&apos;t find the role you want to assign?</p>
129+
<p className="mb-1 font-weight-bold small">{intl.formatMessage(messages['wizard.step1.docs.heading'])}</p>
124130
<p className="mb-0 small">
125-
Some roles are managed outside this console{' '}
126-
<a href="/admin" target="_blank" rel="noopener noreferrer">
127-
View roles managed in LMS and Django Admin
131+
{intl.formatMessage(messages['wizard.step1.docs.body'])}{' '}
132+
<a href={adminUrl} target="_blank" rel="noopener noreferrer">
133+
{intl.formatMessage(messages['wizard.step1.docs.link'])}
128134
</a>
129135
</p>
130136
</div>

0 commit comments

Comments
 (0)