Skip to content

Commit 5f667e2

Browse files
feat(ui): add invite admins header, table, and kebab menu
1 parent 835a942 commit 5f667e2

12 files changed

Lines changed: 944 additions & 62 deletions
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { Dropdown, IconButton, Icon } from '@openedx/paragon';
4+
import { MoreVert, RemoveCircle, ContentCopy } from '@openedx/paragon/icons';
5+
import { FormattedMessage } from '@edx/frontend-platform/i18n';
6+
7+
const AdminActionsMenu = ({ onRemove, onCopy }) => (
8+
<Dropdown drop="top">
9+
<Dropdown.Toggle
10+
id="admin-kabob-menu"
11+
data-testid="admin-kabob-menu"
12+
as={IconButton}
13+
src={MoreVert}
14+
iconAs={Icon}
15+
variant="primary"
16+
aria-label="Admin actions"
17+
/>
18+
19+
<Dropdown.Menu>
20+
<Dropdown.Item onClick={onRemove}>
21+
<Icon
22+
src={RemoveCircle}
23+
className="mr-2 text-danger-500"
24+
/>
25+
<FormattedMessage
26+
id="adminPortal.peopleManagement.admins.remove"
27+
defaultMessage="Remove admin"
28+
description="Remove admin option in the kabob menu"
29+
/>
30+
</Dropdown.Item>
31+
32+
<Dropdown.Item onClick={onCopy}>
33+
<Icon
34+
src={ContentCopy}
35+
className="mr-2"
36+
/>
37+
<FormattedMessage
38+
id="adminPortal.peopleManagement.admins.copyInvite"
39+
defaultMessage="Copy invite link"
40+
description="Copy invite link in the kabob menu"
41+
/>
42+
</Dropdown.Item>
43+
</Dropdown.Menu>
44+
</Dropdown>
45+
);
46+
47+
AdminActionsMenu.propTypes = {
48+
onRemove: PropTypes.func.isRequired,
49+
onCopy: PropTypes.func.isRequired,
50+
};
51+
52+
export default AdminActionsMenu;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import React from 'react';
2+
import { CardView, DataTable } from '@openedx/paragon';
3+
import { connect } from 'react-redux';
4+
import PropTypes from 'prop-types';
5+
import { FormattedMessage } from '@edx/frontend-platform/i18n';
6+
7+
import TableTextFilter from '../learner-credit-management/TableTextFilter';
8+
import CustomDataTableEmptyState from '../learner-credit-management/CustomDataTableEmptyState';
9+
import OrgInviteAdminCard from './OrgInviteAdminCard';
10+
import useEnterpriseAdminsTableData from './data/hooks/useEnterpriseAdminsTableData';
11+
12+
const FilterStatus = (rest) => (
13+
<DataTable.FilterStatus showFilteredFields={false} {...rest} />
14+
);
15+
const InviteAdminsTable = ({ enterpriseId }) => {
16+
const {
17+
isLoading: isTableLoading,
18+
enterpriseAdminsTableData,
19+
fetchEnterpriseAdminsTableData,
20+
// fetchAllEnterpriseAdminsData,
21+
} = useEnterpriseAdminsTableData({ enterpriseId });
22+
23+
const tableColumns = [
24+
{ Header: 'admin details', accessor: 'name' },
25+
];
26+
27+
return (
28+
<>
29+
{/* ================= Header ================= */}
30+
<h3 className="mt-3">
31+
<FormattedMessage
32+
id="adminPortal.peopleManagement.inviteAdmin.title"
33+
defaultMessage="Your organization's admins"
34+
description="Title for people management invite admin data table."
35+
/>
36+
</h3>
37+
<p className="mb-2">
38+
<FormattedMessage
39+
id="adminPortal.peopleManagement.inviteAdmin.subtitle"
40+
defaultMessage="View all admins of your organization."
41+
description="Subtitle for people management admins data table."
42+
/>
43+
</p>
44+
45+
{/* ================= Table ================= */}
46+
<DataTable
47+
isSortable
48+
manualSortBy
49+
isPaginated
50+
manualPagination
51+
isFilterable
52+
manualFilters
53+
isLoading={isTableLoading}
54+
defaultColumnValues={{ Filter: TableTextFilter }}
55+
FilterStatusComponent={FilterStatus}
56+
numBreakoutFilters={2}
57+
columns={tableColumns}
58+
initialState={{
59+
pageSize: 10,
60+
pageIndex: 0,
61+
sortBy: [{ id: 'name', desc: true }],
62+
filters: [],
63+
}}
64+
fetchData={fetchEnterpriseAdminsTableData}
65+
data={enterpriseAdminsTableData.results}
66+
itemCount={enterpriseAdminsTableData.itemCount}
67+
pageCount={enterpriseAdminsTableData.pageCount}
68+
EmptyTableComponent={CustomDataTableEmptyState}
69+
70+
>
71+
<DataTable.TableControlBar />
72+
<CardView
73+
className="d-block"
74+
CardComponent={OrgInviteAdminCard}
75+
columnSizes={{ xs: 12 }}
76+
/>
77+
<DataTable.TableFooter />
78+
</DataTable>
79+
</>
80+
);
81+
};
82+
83+
InviteAdminsTable.propTypes = {
84+
enterpriseId: PropTypes.string.isRequired,
85+
};
86+
87+
const mapStateToProps = (state) => ({
88+
enterpriseId: state.portalConfiguration.enterpriseId,
89+
});
90+
91+
export default connect(mapStateToProps)(InviteAdminsTable);
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import PropTypes from 'prop-types';
2+
import {
3+
Avatar, Card, Col, Row,
4+
} from '@openedx/paragon';
5+
6+
import { FormattedMessage } from '@edx/frontend-platform/i18n';
7+
import AdminActionsMenu from './AdminActionsMenu';
8+
9+
const OrgInviteAdminCard = ({
10+
original, onRemoveAdmin,
11+
onCopyInviteLink,
12+
}) => {
13+
const { enterpriseCustomerUser, inviteLink } = original;
14+
const {
15+
name, joinedOrg, email, role,
16+
} = enterpriseCustomerUser;
17+
18+
return (
19+
<Card orientation="horizontal">
20+
<Card.Body>
21+
<Card.Section className="pb-1">
22+
<Row className="d-flex flex-row">
23+
<Col xs={2}>
24+
<Avatar size="lg" />
25+
</Col>
26+
<Col>
27+
<Row>
28+
<h3 className="pt-2">{name}</h3>
29+
</Row>
30+
<Row>
31+
<p>{email}</p>
32+
</Row>
33+
</Col>
34+
<Col>
35+
<h5 className="pt-2 text-uppercase">
36+
<FormattedMessage
37+
id="adminPortal.peopleManagement.joinedOrg"
38+
defaultMessage="Joined org"
39+
description="Title for people management invite admin joined org date."
40+
/>
41+
</h5>
42+
{joinedOrg}
43+
</Col>
44+
<Col>
45+
<h5 className="pt-2 text-uppercase">
46+
<FormattedMessage
47+
id="adminPortal.peopleManagement.role"
48+
defaultMessage="Role"
49+
description="Title for people management invite admin Role status."
50+
/>
51+
</h5>
52+
{role}
53+
</Col>
54+
<div>
55+
<AdminActionsMenu
56+
onRemove={() => onRemoveAdmin(original)}
57+
onCopy={() => onCopyInviteLink(inviteLink)}
58+
/>
59+
</div>
60+
61+
</Row>
62+
</Card.Section>
63+
</Card.Body>
64+
</Card>
65+
);
66+
};
67+
68+
OrgInviteAdminCard.propTypes = {
69+
original: PropTypes.shape({
70+
enterpriseCustomerUser: PropTypes.shape({
71+
userId: PropTypes.number.isRequired,
72+
email: PropTypes.string.isRequired,
73+
name: PropTypes.string.isRequired,
74+
joinedOrg: PropTypes.string.isRequired,
75+
role: PropTypes.string.isRequired,
76+
}).isRequired,
77+
inviteLink: PropTypes.string,
78+
}).isRequired,
79+
onRemoveAdmin: PropTypes.func.isRequired,
80+
onCopyInviteLink: PropTypes.func.isRequired,
81+
};
82+
83+
export default OrgInviteAdminCard;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { useCallback, useMemo, useState } from 'react';
2+
import { camelCaseObject } from '@edx/frontend-platform/utils';
3+
import { logError } from '@edx/frontend-platform/logging';
4+
import { debounce, snakeCase } from 'lodash-es';
5+
import LmsApiService from '../../../../data/services/LmsApiService';
6+
7+
const useEnterpriseAdminsTableData = ({ enterpriseId }) => {
8+
const [isLoading, setIsLoading] = useState(true);
9+
const [enterpriseAdminsTableData, setEnterpriseAdminsTableData] = useState({
10+
itemCount: 0,
11+
pageCount: 0,
12+
results: [],
13+
});
14+
/** Download records */
15+
// const fetchAllEnterpriseAdminsData = useCallback(async () => {
16+
// const { options, itemCount } = enterpriseAdminsTableData;
17+
// // Take the existing filters but specify we're taking all results on one page
18+
// const fetchAllOptions = { ...options, page: 1, page_size: itemCount };
19+
// const response = await LmsApiService.fetchEnterpriseCustomerMembers(enterpriseId, fetchAllOptions);
20+
// return camelCaseObject(response.data);
21+
// }, [enterpriseId, enterpriseAdminsTableData]);
22+
23+
const fetchEnterpriseAdminsData = useCallback((args) => {
24+
const fetch = async () => {
25+
try {
26+
setIsLoading(true);
27+
const options = {};
28+
args.filters.forEach((filter) => {
29+
const { id, value } = filter;
30+
if (id === 'name') {
31+
options.user_query = value;
32+
}
33+
});
34+
if (args?.sortBy.length > 0) {
35+
const sortByValue = args.sortBy[0].id;
36+
options.sort_by = snakeCase(sortByValue);
37+
if (!args.sortBy[0].desc) {
38+
options.is_reversed = !args.sortBy[0].desc;
39+
}
40+
}
41+
options.page = args.pageIndex + 1;
42+
const response = await LmsApiService.fetchEnterpriseAdminMembers(enterpriseId, options);
43+
const data = camelCaseObject(response.data);
44+
setEnterpriseAdminsTableData({
45+
itemCount: data.count,
46+
pageCount: data.numPages ?? Math.ceil(data.count / options.pageSize),
47+
results: data.results,
48+
options,
49+
});
50+
} catch (error) {
51+
logError(error);
52+
} finally {
53+
setIsLoading(false);
54+
}
55+
};
56+
if (args.filters.length && args.filters[0].value.length > 2) {
57+
fetch();
58+
} else if (!args.filters.length) {
59+
fetch();
60+
}
61+
}, [enterpriseId]);
62+
63+
const debouncedFetchEnterpriseAdminsData = useMemo(
64+
() => debounce(fetchEnterpriseAdminsData, 300),
65+
[fetchEnterpriseAdminsData],
66+
);
67+
68+
return {
69+
isLoading,
70+
enterpriseAdminsTableData,
71+
fetchEnterpriseAdminsTableData: debouncedFetchEnterpriseAdminsData,
72+
// fetchAllEnterpriseAdminsData,
73+
};
74+
};
75+
76+
export default useEnterpriseAdminsTableData;

0 commit comments

Comments
 (0)