Skip to content

Commit 6d3e4a8

Browse files
committed
feat: implement add new team member functionality with modal and actions
1 parent 09a0479 commit 6d3e4a8

6 files changed

Lines changed: 270 additions & 4 deletions

File tree

src/authz-module/components/AuthZTitle.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ReactNode } from 'react';
1+
import { ComponentType, isValidElement, ReactNode } from 'react';
22
import {
33
Breadcrumb, Col, Container, Row, Button, Badge,
44
} from '@openedx/paragon';
@@ -10,6 +10,7 @@ interface BreadcrumbLink {
1010

1111
interface Action {
1212
label: string;
13+
icon?: ComponentType;
1314
onClick: () => void;
1415
}
1516

@@ -18,7 +19,7 @@ export interface AuthZTitleProps {
1819
pageTitle: string;
1920
pageSubtitle: string | ReactNode;
2021
navLinks?: BreadcrumbLink[];
21-
actions?: Action[];
22+
actions?: (Action | ReactNode)[];
2223
}
2324

2425
const AuthZTitle = ({
@@ -39,7 +40,22 @@ const AuthZTitle = ({
3940
<Col xs={12} md={4}>
4041
<div className="d-flex justify-content-md-end">
4142
{
42-
actions.map(({ label, onClick }) => <Button key={`authz-header-action-${label}`} onClick={onClick}>{label}</Button>)
43+
actions.map((action) => {
44+
if (isValidElement(action)) {
45+
return action;
46+
}
47+
48+
const { label, icon, onClick } = action as Action;
49+
return (
50+
<Button
51+
key={`authz-header-action-${label}`}
52+
iconBefore={icon}
53+
onClick={onClick}
54+
>
55+
{label}
56+
</Button>
57+
);
58+
})
4359
}
4460
</div>
4561
</Col>

src/authz-module/index.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,11 @@
88
background-color: var(--pgn-color-light-200);
99
}
1010
}
11+
12+
.toast-container {
13+
// Ensure toast appears above modal
14+
z-index: 1000;
15+
// Move toast to the right
16+
left: auto;
17+
right: var(--pgn-spacing-toast-container-gutter-lg);
18+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import AuthZLayout from '../components/AuthZLayout';
66
import { LibraryAuthZProvider, useLibraryAuthZ } from './context';
77

88
import messages from './messages';
9+
import AddNewTeamMemberTrigger from './components/AddNewTeamMemberTrigger';
910

1011
const LibrariesAuthZTeamView = () => {
1112
const intl = useIntl();
@@ -21,7 +22,9 @@ const LibrariesAuthZTeamView = () => {
2122
activeLabel={pageTitle}
2223
pageTitle={pageTitle}
2324
pageSubtitle={libraryId}
24-
actions={[]}
25+
actions={[
26+
<AddNewTeamMemberTrigger libraryId={libraryId} />,
27+
]}
2528
>
2629
<Tabs
2730
variant="tabs"
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { FC } from 'react';
2+
import { useIntl } from '@edx/frontend-platform/i18n';
3+
import {
4+
ActionRow, Button, Form, ModalDialog,
5+
Stack,
6+
} from '@openedx/paragon';
7+
import { useLibraryAuthZ } from 'authz-module/libraries-manager/context';
8+
import messages from './messages';
9+
10+
interface AddNewTeamMemberModalProps {
11+
isOpen: boolean;
12+
isLoading: boolean;
13+
formValues: {
14+
users: string;
15+
role: string;
16+
};
17+
close: () => void;
18+
onSave: () => void;
19+
handleChangeForm: (e: React.ChangeEvent<HTMLTextAreaElement | HTMLSelectElement>) => void;
20+
}
21+
22+
const AddNewTeamMemberModal: FC<AddNewTeamMemberModalProps> = ({
23+
isOpen, isLoading, formValues, close, onSave, handleChangeForm,
24+
}) => {
25+
const intl = useIntl();
26+
const { roles } = useLibraryAuthZ();
27+
return (
28+
<ModalDialog
29+
title={intl.formatMessage(messages['libraries.authz.manage.add.member.title'])}
30+
isOpen={isOpen}
31+
onClose={isLoading ? () => {} : close}
32+
size="lg"
33+
variant="dark"
34+
hasCloseButton
35+
isOverflowVisible={false}
36+
zIndex={5}
37+
>
38+
<ModalDialog.Header className="bg-primary-500 text-light-100">
39+
<ModalDialog.Title>
40+
{intl.formatMessage(messages['libraries.authz.manage.add.member.title'])}
41+
</ModalDialog.Title>
42+
</ModalDialog.Header>
43+
44+
<ModalDialog.Body className="my-4">
45+
<Stack gap={3}>
46+
<p>
47+
{intl.formatMessage(messages['libraries.authz.manage.add.member.description'])}
48+
</p>
49+
50+
<Form.Group controlId="users_list">
51+
<Form.Label>{intl.formatMessage(messages['libraries.authz.manage.add.member.users.label'])}</Form.Label>
52+
<Form.Control
53+
as="textarea"
54+
name="users"
55+
rows="3"
56+
value={formValues.users}
57+
onChange={(e) => handleChangeForm(e)}
58+
/>
59+
</Form.Group>
60+
61+
<Form.Group controlId="role_options">
62+
<Form.Label>{intl.formatMessage(messages['libraries.authz.manage.add.member.roles.label'])}</Form.Label>
63+
<Form.Control as="select" name="role" value={formValues.role} onChange={(e) => handleChangeForm(e)}>
64+
<option value="" disabled>Select a role</option>
65+
{roles.map(({ role }) => <option key={role}>{role}</option>)}
66+
</Form.Control>
67+
</Form.Group>
68+
</Stack>
69+
</ModalDialog.Body>
70+
71+
<ModalDialog.Footer>
72+
<ActionRow>
73+
<ModalDialog.CloseButton variant="tertiary" disabled={isLoading}>
74+
{intl.formatMessage(messages['libraries.authz.manage.add.member.cancel.button'])}
75+
</ModalDialog.CloseButton>
76+
<Button
77+
className="px-4"
78+
onClick={() => onSave()}
79+
disabled={!formValues.users || !formValues.role || isLoading}
80+
>
81+
{isLoading
82+
? intl.formatMessage(messages['libraries.authz.manage.add.member.saving.button'])
83+
: intl.formatMessage(messages['libraries.authz.manage.add.member.save.button'])}
84+
</Button>
85+
</ActionRow>
86+
</ModalDialog.Footer>
87+
</ModalDialog>
88+
);
89+
};
90+
91+
export default AddNewTeamMemberModal;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import React, { FC, useState } from 'react';
2+
import { useIntl } from '@edx/frontend-platform/i18n';
3+
import { Button, Toast, useToggle } from '@openedx/paragon';
4+
import { Plus } from '@openedx/paragon/icons';
5+
6+
import { useAddTeamMember } from 'authz-module/data/hooks';
7+
import AddNewTeamMemberModal from './AddNewTeamMemberModal';
8+
import messages from './messages';
9+
10+
interface AddNewTeamMemberTriggerProps {
11+
libraryId: string;
12+
}
13+
14+
const DEFAULT_FORM_VALUES = {
15+
users: '',
16+
role: '',
17+
};
18+
19+
const AddNewTeamMemberTrigger: FC<AddNewTeamMemberTriggerProps> = ({
20+
libraryId,
21+
}) => {
22+
const intl = useIntl();
23+
const [isOpen, open, close] = useToggle(false);
24+
const [additionMessage, setAdditionMessage] = useState<string | null>(null);
25+
const [formValues, setFormValues] = useState(DEFAULT_FORM_VALUES);
26+
27+
const { mutate: addTeamMember, isPending: isAddingNewTeamMember } = useAddTeamMember();
28+
29+
const handleChangeForm = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLSelectElement>) => {
30+
const { name, value } = e.target;
31+
setFormValues((prev) => ({
32+
...prev,
33+
[name]: value,
34+
}));
35+
};
36+
37+
const handleAddTeamMember = () => {
38+
const data = {
39+
users: formValues.users.split(',').map(user => user.trim()),
40+
role: formValues.role,
41+
scope: libraryId,
42+
};
43+
44+
addTeamMember({ data }, {
45+
onSuccess: (successData) => {
46+
if (successData.completed.length) {
47+
setAdditionMessage(
48+
intl.formatMessage(
49+
messages['libraries.authz.manage.add.member.success'],
50+
{ count: successData.completed.length },
51+
),
52+
);
53+
}
54+
55+
if (successData.errors.length) {
56+
setAdditionMessage((prevMessage) => (
57+
`${prevMessage ? `${prevMessage} ` : ''}${intl.formatMessage(
58+
messages['libraries.authz.manage.add.member.failure'],
59+
{ count: successData.errors.length },
60+
)}`
61+
));
62+
} else {
63+
close();
64+
setFormValues(DEFAULT_FORM_VALUES);
65+
}
66+
},
67+
});
68+
};
69+
70+
return (
71+
<>
72+
<Button
73+
key="authz-header-action-new-team-member"
74+
iconBefore={Plus}
75+
onClick={open}
76+
>
77+
{intl.formatMessage(messages['libraries.authz.manage.add.member.title'])}
78+
</Button>
79+
80+
{isOpen && (
81+
<AddNewTeamMemberModal
82+
isOpen={isOpen}
83+
close={close}
84+
onSave={handleAddTeamMember}
85+
isLoading={isAddingNewTeamMember}
86+
formValues={formValues}
87+
handleChangeForm={handleChangeForm}
88+
/>
89+
)}
90+
91+
{additionMessage && (
92+
<Toast
93+
onClose={() => setAdditionMessage(null)}
94+
show={!!additionMessage}
95+
>
96+
{additionMessage}
97+
</Toast>
98+
)}
99+
</>
100+
);
101+
};
102+
103+
export default AddNewTeamMemberTrigger;

src/authz-module/libraries-manager/components/messages.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,51 @@ const messages = defineMessages({
3131
defaultMessage: 'Edit',
3232
description: 'Edit action',
3333
},
34+
'libraries.authz.manage.add.member.title': {
35+
id: 'libraries.authz.manage.add.member.title',
36+
defaultMessage: 'Add New Team Member',
37+
description: 'Title for the add new team member modal',
38+
},
39+
'libraries.authz.manage.add.member.users.label': {
40+
id: 'libraries.authz.manage.add.member.users.label',
41+
defaultMessage: 'Add users by username or email',
42+
description: 'Label for the users input field in the add new team member modal',
43+
},
44+
'libraries.authz.manage.add.member.roles.label': {
45+
id: 'libraries.authz.manage.add.member.roles.label',
46+
defaultMessage: 'Roles',
47+
description: 'Label for the roles select field in the add new team member modal',
48+
},
49+
'libraries.authz.manage.add.member.cancel.button': {
50+
id: 'libraries.authz.manage.add.member.cancel.button',
51+
defaultMessage: 'Cancel',
52+
description: 'Label for the cancel button in the add new team member modal',
53+
},
54+
'libraries.authz.manage.add.member.save.button': {
55+
id: 'libraries.authz.manage.add.member.save.button',
56+
defaultMessage: 'Save',
57+
description: 'Label for the save button in the add new team member modal',
58+
},
59+
'libraries.authz.manage.add.member.saving.button': {
60+
id: 'libraries.authz.manage.add.member.saving.button',
61+
defaultMessage: 'Saving...',
62+
description: 'Label for the save button in the add new team member modal when saving',
63+
},
64+
'libraries.authz.manage.add.member.description': {
65+
id: 'libraries.authz.manage.add.member.description',
66+
defaultMessage: 'Add new members to this library\'s team and assign them a role to define their permissions.',
67+
description: 'Description for the add new team member modal',
68+
},
69+
'libraries.authz.manage.add.member.success': {
70+
id: 'libraries.authz.manage.add.member.success',
71+
defaultMessage: '{count, plural, one {# team member added successfully.} other {# team members added successfully.}}',
72+
description: 'Success message when adding new team members',
73+
},
74+
'libraries.authz.manage.add.member.failure': {
75+
id: 'libraries.authz.manage.add.member.failure',
76+
defaultMessage: 'We couldn\'t find a user for {count, plural, one {# email address or username.} other {# email addresses or usernames.}} Please check the email and try again, or invite them to join your organization first.',
77+
description: 'Error message when adding new team members',
78+
},
3479
});
3580

3681
export default messages;

0 commit comments

Comments
 (0)