Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Grant all Enterprise/B2B squads merge permission to entire repo.
* @edx/enterprise-titans @edx/enterprise-markhors @edx/enterprise-sunrise @edx/enterprise-lakshy
52 changes: 52 additions & 0 deletions src/components/PeopleManagement/AdminActionsMenu.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Dropdown, IconButton, Icon } from '@openedx/paragon';
import { MoreVert, RemoveCircle, ContentCopy } from '@openedx/paragon/icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n';

const AdminActionsMenu = ({ onRemove, onCopy }) => (
<Dropdown drop="top">
<Dropdown.Toggle
id="admin-kabob-menu"
data-testid="admin-kabob-menu"
as={IconButton}
src={MoreVert}
iconAs={Icon}
variant="primary"
aria-label="Admin actions"
/>

<Dropdown.Menu>
<Dropdown.Item onClick={onRemove}>
<Icon
src={RemoveCircle}
className="mr-2 text-danger-500"
/>
<FormattedMessage
id="adminPortal.peopleManagement.admins.remove"
defaultMessage="Remove admin"
description="Remove admin option in the kabob menu"
/>
</Dropdown.Item>

<Dropdown.Item onClick={onCopy}>
<Icon
src={ContentCopy}
className="mr-2"
/>
<FormattedMessage
id="adminPortal.peopleManagement.admins.copyInvite"
defaultMessage="Copy invite link"
description="Copy invite link in the kabob menu"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
);

AdminActionsMenu.propTypes = {
onRemove: PropTypes.func.isRequired,
onCopy: PropTypes.func.isRequired,
};

export default AdminActionsMenu;
91 changes: 91 additions & 0 deletions src/components/PeopleManagement/InviteAdminsTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React from 'react';
import { CardView, DataTable } from '@openedx/paragon';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';

import TableTextFilter from '../learner-credit-management/TableTextFilter';
import CustomDataTableEmptyState from '../learner-credit-management/CustomDataTableEmptyState';
import OrgInviteAdminCard from './OrgInviteAdminCard';
import useEnterpriseAdminsTableData from './data/hooks/useEnterpriseAdminsTableData';

const FilterStatus = (rest) => (
<DataTable.FilterStatus showFilteredFields={false} {...rest} />
);
const InviteAdminsTable = ({ enterpriseId }) => {
const {
isLoading: isTableLoading,
enterpriseAdminsTableData,
fetchEnterpriseAdminsTableData,
// fetchAllEnterpriseAdminsData,
} = useEnterpriseAdminsTableData({ enterpriseId });

const tableColumns = [
{ Header: 'admin details', accessor: 'name' },
];

return (
<>
{/* ================= Header ================= */}
<h3 className="mt-3">
<FormattedMessage
id="adminPortal.peopleManagement.inviteAdmin.title"
defaultMessage="Your organization's admins"
description="Title for people management invite admin data table."
/>
</h3>
<p className="mb-2">
<FormattedMessage
id="adminPortal.peopleManagement.inviteAdmin.subtitle"
defaultMessage="View all admins of your organization."
description="Subtitle for people management admins data table."
/>
</p>

{/* ================= Table ================= */}
<DataTable
isSortable
manualSortBy
isPaginated
manualPagination
isFilterable
manualFilters
isLoading={isTableLoading}
defaultColumnValues={{ Filter: TableTextFilter }}
FilterStatusComponent={FilterStatus}
numBreakoutFilters={2}
columns={tableColumns}
initialState={{
pageSize: 10,
pageIndex: 0,
sortBy: [{ id: 'name', desc: true }],
filters: [],
}}
fetchData={fetchEnterpriseAdminsTableData}
data={enterpriseAdminsTableData.results}
itemCount={enterpriseAdminsTableData.itemCount}
pageCount={enterpriseAdminsTableData.pageCount}
EmptyTableComponent={CustomDataTableEmptyState}

>
<DataTable.TableControlBar />
<CardView
className="d-block"
CardComponent={OrgInviteAdminCard}
columnSizes={{ xs: 12 }}
/>
<DataTable.TableFooter />
</DataTable>
</>
);
};

InviteAdminsTable.propTypes = {
enterpriseId: PropTypes.string.isRequired,
};

const mapStateToProps = (state) => ({
enterpriseId: state.portalConfiguration.enterpriseId,
});

export default connect(mapStateToProps)(InviteAdminsTable);
83 changes: 83 additions & 0 deletions src/components/PeopleManagement/OrgInviteAdminCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import PropTypes from 'prop-types';
import {
Avatar, Card, Col, Row,
} from '@openedx/paragon';

import { FormattedMessage } from '@edx/frontend-platform/i18n';
import AdminActionsMenu from './AdminActionsMenu';

const OrgInviteAdminCard = ({
original, onRemoveAdmin,
onCopyInviteLink,
}) => {
const { enterpriseCustomerUser, inviteLink } = original;
const {
name, joinedOrg, email, role,
} = enterpriseCustomerUser;

return (
<Card orientation="horizontal">
<Card.Body>
<Card.Section className="pb-1">
<Row className="d-flex flex-row">
<Col xs={2}>
<Avatar size="lg" />
</Col>
<Col>
<Row>
<h3 className="pt-2">{name}</h3>
</Row>
<Row>
<p>{email}</p>
</Row>
</Col>
<Col>
<h5 className="pt-2 text-uppercase">
<FormattedMessage
id="adminPortal.peopleManagement.joinedOrg"
defaultMessage="Joined org"
description="Title for people management invite admin joined org date."
/>
</h5>
{joinedOrg}
</Col>
<Col>
<h5 className="pt-2 text-uppercase">
<FormattedMessage
id="adminPortal.peopleManagement.role"
defaultMessage="Role"
description="Title for people management invite admin Role status."
/>
</h5>
{role}
</Col>
<div>
<AdminActionsMenu
onRemove={() => onRemoveAdmin(original)}
onCopy={() => onCopyInviteLink(inviteLink)}
/>
</div>

</Row>
</Card.Section>
</Card.Body>
</Card>
);
};

OrgInviteAdminCard.propTypes = {
original: PropTypes.shape({
enterpriseCustomerUser: PropTypes.shape({
userId: PropTypes.number.isRequired,
email: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
joinedOrg: PropTypes.string.isRequired,
role: PropTypes.string.isRequired,
}).isRequired,
inviteLink: PropTypes.string,
}).isRequired,
onRemoveAdmin: PropTypes.func.isRequired,
onCopyInviteLink: PropTypes.func.isRequired,
};

export default OrgInviteAdminCard;
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useCallback, useMemo, useState } from 'react';
import { camelCaseObject } from '@edx/frontend-platform/utils';
import { logError } from '@edx/frontend-platform/logging';
import { debounce, snakeCase } from 'lodash-es';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Organize imports into external (react and lodash-es in this case) and then edx packages.

import LmsApiService from '../../../../data/services/LmsApiService';

const useEnterpriseAdminsTableData = ({ enterpriseId }) => {
const [isLoading, setIsLoading] = useState(true);
const [enterpriseAdminsTableData, setEnterpriseAdminsTableData] = useState({
itemCount: 0,
pageCount: 0,
results: [],
});
/** Download records */
// const fetchAllEnterpriseAdminsData = useCallback(async () => {
// const { options, itemCount } = enterpriseAdminsTableData;
// // Take the existing filters but specify we're taking all results on one page
// const fetchAllOptions = { ...options, page: 1, page_size: itemCount };
// const response = await LmsApiService.fetchEnterpriseCustomerMembers(enterpriseId, fetchAllOptions);
// return camelCaseObject(response.data);
// }, [enterpriseId, enterpriseAdminsTableData]);

const fetchEnterpriseAdminsData = useCallback((args) => {
const fetch = async () => {
try {
setIsLoading(true);
const options = {};
args.filters.forEach((filter) => {
const { id, value } = filter;
if (id === 'name') {
options.user_query = value;
}
});
if (args?.sortBy.length > 0) {
const sortByValue = args.sortBy[0].id;
options.sort_by = snakeCase(sortByValue);
if (!args.sortBy[0].desc) {
options.is_reversed = !args.sortBy[0].desc;
}
}
options.page = args.pageIndex + 1;
const response = await LmsApiService.fetchEnterpriseAdminMembers(enterpriseId, options);
const data = camelCaseObject(response.data);
setEnterpriseAdminsTableData({
itemCount: data.count,
pageCount: data.numPages ?? Math.ceil(data.count / options.pageSize),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look at other pageCount instances and follow their example

results: data.results,
options,
});
} catch (error) {
logError(error);
} finally {
setIsLoading(false);
}
};
if (args.filters.length && args.filters[0].value.length > 2) {
fetch();
} else if (!args.filters.length) {
fetch();
}
}, [enterpriseId]);

const debouncedFetchEnterpriseAdminsData = useMemo(
() => debounce(fetchEnterpriseAdminsData, 300),
[fetchEnterpriseAdminsData],
);

return {
isLoading,
enterpriseAdminsTableData,
fetchEnterpriseAdminsTableData: debouncedFetchEnterpriseAdminsData,
// fetchAllEnterpriseAdminsData,
};
};

export default useEnterpriseAdminsTableData;
Loading