+ {!isLoading ? (
+
+
+
+
+
+
+ )}
+ >
+
-
- )}
- >
-
-
-
- >
+
+
+
+ ) : null}
+
);
};
-const mapStateToProps = state => ({
- enterpriseUUID: state.portalConfiguration.enterpriseId,
-});
-
AddMembersModal.propTypes = {
enterpriseUUID: PropTypes.string.isRequired,
isModalOpen: PropTypes.bool.isRequired,
closeModal: PropTypes.func.isRequired,
+ groupUuid: PropTypes.string.isRequired,
+ groupName: PropTypes.string,
};
+const mapStateToProps = state => ({
+ enterpriseUUID: state.portalConfiguration.enterpriseId,
+});
+
export default connect(mapStateToProps)(AddMembersModal);
diff --git a/src/components/PeopleManagement/AddGroupModalContent.jsx b/src/components/PeopleManagement/AddMembersModalContent.jsx
similarity index 92%
rename from src/components/PeopleManagement/AddGroupModalContent.jsx
rename to src/components/PeopleManagement/AddMembersModalContent.jsx
index 81465c474..2cfb1c802 100644
--- a/src/components/PeopleManagement/AddGroupModalContent.jsx
+++ b/src/components/PeopleManagement/AddMembersModalContent.jsx
@@ -4,7 +4,7 @@ import React, {
import PropTypes from 'prop-types';
import debounce from 'lodash.debounce';
import {
- Col, Container, Form, Row,
+ Col, Container, Row,
} from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
@@ -12,15 +12,15 @@ import InviteModalSummary from '../learner-credit-management/invite-modal/Invite
import InviteSummaryCount from '../learner-credit-management/invite-modal/InviteSummaryCount';
import FileUpload from '../learner-credit-management/invite-modal/FileUpload';
import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY, isInviteEmailAddressesInputValueValid } from '../learner-credit-management/cards/data';
-import { MAX_LENGTH_GROUP_NAME } from './constants';
-import EnterpriseCustomerUserDatatable from '../learner-credit-management/invite-modal/EnterpriseCustomerUserDatatable';
+import EnterpriseCustomerUserDatatable from './EnterpriseCustomerUserDatatable';
import { useEnterpriseLearners } from '../learner-credit-management/data';
-const AddGroupModalContent = ({
+const AddMembersModalContent = ({
onEmailAddressesChange,
isGroupInvite,
enterpriseUUID,
groupName,
+ enterpriseGroupLearners,
}) => {
const [learnerEmails, setLearnerEmails] = useState([]);
const [emailAddressesInputValue, setEmailAddressesInputValue] = useState('');
@@ -32,7 +32,6 @@ const AddGroupModalContent = ({
});
const { allEnterpriseLearners } = useEnterpriseLearners({ enterpriseUUID });
-
const handleAddMembersBulkAction = useCallback((value) => {
if (!value) {
setLearnerEmails([]);
@@ -129,15 +128,18 @@ const AddGroupModalContent = ({
onHandleAddMembersBulkAction={handleAddMembersBulkAction}
onHandleRemoveMembersBulkAction={handleRemoveMembersBulkAction}
learnerEmails={learnerEmails}
+ enterpriseGroupLearners={enterpriseGroupLearners}
/>
);
};
-AddGroupModalContent.propTypes = {
+AddMembersModalContent.propTypes = {
onEmailAddressesChange: PropTypes.func.isRequired,
isGroupInvite: PropTypes.bool,
enterpriseUUID: PropTypes.string.isRequired,
+ groupName: PropTypes.string,
+ enterpriseGroupLearners: PropTypes.arrayOf(PropTypes.shape({})),
};
-export default AddGroupModalContent;
+export default AddMembersModalContent;
diff --git a/src/components/PeopleManagement/CreateGroupModalContent.jsx b/src/components/PeopleManagement/CreateGroupModalContent.jsx
index a7b504450..827375e97 100644
--- a/src/components/PeopleManagement/CreateGroupModalContent.jsx
+++ b/src/components/PeopleManagement/CreateGroupModalContent.jsx
@@ -13,7 +13,7 @@ import InviteSummaryCount from '../learner-credit-management/invite-modal/Invite
import FileUpload from '../learner-credit-management/invite-modal/FileUpload';
import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY, isInviteEmailAddressesInputValueValid } from '../learner-credit-management/cards/data';
import { MAX_LENGTH_GROUP_NAME } from './constants';
-import EnterpriseCustomerUserDatatable from '../learner-credit-management/invite-modal/EnterpriseCustomerUserDatatable';
+import EnterpriseCustomerUserDatatable from './EnterpriseCustomerUserDatatable';
import { useEnterpriseLearners } from '../learner-credit-management/data';
const CreateGroupModalContent = ({
diff --git a/src/components/PeopleManagement/EnrollmentsTableColumnHeader.jsx b/src/components/PeopleManagement/EnrollmentsTableColumnHeader.jsx
new file mode 100644
index 000000000..939b0e0b7
--- /dev/null
+++ b/src/components/PeopleManagement/EnrollmentsTableColumnHeader.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import {
+ OverlayTrigger,
+ Tooltip,
+ Stack,
+ Icon,
+} from '@openedx/paragon';
+import { InfoOutline } from '@openedx/paragon/icons';
+import { FormattedMessage } from '@edx/frontend-platform/i18n';
+
+const EnrollmentsTableColumnHeader = () => (
+
+
+
+
+
+
+
+
+
+ )}
+ >
+
+
+
+);
+
+export default EnrollmentsTableColumnHeader;
diff --git a/src/components/PeopleManagement/EnterpriseCustomerUserDatatable.jsx b/src/components/PeopleManagement/EnterpriseCustomerUserDatatable.jsx
new file mode 100644
index 000000000..cc6e2a68c
--- /dev/null
+++ b/src/components/PeopleManagement/EnterpriseCustomerUserDatatable.jsx
@@ -0,0 +1,135 @@
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import {
+ DataTable,
+ TextFilter,
+ CheckboxControl,
+} from '@openedx/paragon';
+import { useEnterpriseLearnersTableData } from './data/hooks/useEnterpriseLearnersTableData';
+import { GROUP_MEMBERS_TABLE_DEFAULT_PAGE, GROUP_MEMBERS_TABLE_PAGE_SIZE } from './constants';
+import MemberDetailsCell from './MemberDetailsCell';
+import AddMembersBulkAction from './AddMembersBulkAction';
+import RemoveMembersBulkAction from './RemoveMembersBulkAction';
+import MemberJoinedDateCell from './MemberJoinedDateCell';
+
+export const BaseSelectWithContext = ({ row, enterpriseGroupLearners }) => {
+ const {
+ indeterminate,
+ checked,
+ ...toggleRowSelectedProps
+ } = row.getToggleRowSelectedProps();
+ const isAddedMember = enterpriseGroupLearners.find(learner => learner.enterpriseCustomerUserId === Number(row.id));
+ return (
+
+
+
+ );
+};
+
+// TO-DO: add search functionality on member details once the learner endpoint is updated
+// to support search
+const EnterpriseCustomerUserDatatable = ({
+ enterpriseId,
+ learnerEmails,
+ onHandleAddMembersBulkAction,
+ onHandleRemoveMembersBulkAction,
+ enterpriseGroupLearners,
+}) => {
+ const {
+ isLoading,
+ enterpriseCustomerUserTableData,
+ fetchEnterpriseLearnersData,
+ } = useEnterpriseLearnersTableData(enterpriseId, enterpriseGroupLearners);
+
+ return (
+ ,
+ ,
+ ]}
+ columns={[
+ {
+ Header: 'Member details',
+ accessor: 'user.email',
+ Cell: MemberDetailsCell,
+ },
+ {
+ Header: 'Joined organization',
+ accessor: 'created',
+ Cell: MemberJoinedDateCell,
+ disableFilters: true,
+ },
+ ]}
+ initialState={{
+ pageIndex: GROUP_MEMBERS_TABLE_DEFAULT_PAGE,
+ pageSize: GROUP_MEMBERS_TABLE_PAGE_SIZE,
+ }}
+ data={enterpriseCustomerUserTableData.results}
+ defaultColumnValues={{ Filter: TextFilter }}
+ fetchData={fetchEnterpriseLearnersData}
+ isFilterable
+ isLoading={isLoading}
+ isPaginated
+ isSelectable
+ itemCount={enterpriseCustomerUserTableData.itemCount}
+ manualFilters
+ manualPagination
+ initialTableOptions={{
+ getRowId: row => row.id.toString(),
+ }}
+ pageCount={enterpriseCustomerUserTableData.pageCount}
+ SelectionStatusComponent={DataTable.ControlledSelectionStatus}
+ manualSelectColumn={
+ {
+ id: 'selection',
+ Header: DataTable.ControlledSelectHeader,
+ /* eslint-disable react/no-unstable-nested-components */
+ Cell: (props) => ,
+ disableSortBy: true,
+ }
+ }
+ />
+ );
+};
+
+EnterpriseCustomerUserDatatable.defaultProps = {
+ enterpriseGroupLearners: [],
+};
+
+EnterpriseCustomerUserDatatable.propTypes = {
+ enterpriseId: PropTypes.string.isRequired,
+ learnerEmails: PropTypes.arrayOf(PropTypes.string).isRequired,
+ onHandleRemoveMembersBulkAction: PropTypes.func.isRequired,
+ onHandleAddMembersBulkAction: PropTypes.func.isRequired,
+ enterpriseGroupLearners: PropTypes.arrayOf(PropTypes.shape({})),
+};
+
+BaseSelectWithContext.propTypes = {
+ row: PropTypes.shape({
+ getToggleRowSelectedProps: PropTypes.func.isRequired,
+ id: PropTypes.string,
+ }).isRequired,
+ contextKey: PropTypes.string.isRequired,
+ enterpriseGroupLearners: PropTypes.arrayOf(PropTypes.shape({})),
+};
+
+const mapStateToProps = state => ({
+ enterpriseId: state.portalConfiguration.enterpriseId,
+});
+
+export default connect(mapStateToProps)(EnterpriseCustomerUserDatatable);
diff --git a/src/components/PeopleManagement/GroupDetailPage.jsx b/src/components/PeopleManagement/GroupDetailPage.jsx
index b0a099280..6a1ba25d3 100644
--- a/src/components/PeopleManagement/GroupDetailPage.jsx
+++ b/src/components/PeopleManagement/GroupDetailPage.jsx
@@ -7,11 +7,13 @@ import {
import { Delete, Edit } from '@openedx/paragon/icons';
import { useEnterpriseGroupUuid } from '../learner-credit-management/data';
+import { useEnterpriseGroupLearnersTableData } from './data/hooks';
import { ROUTE_NAMES } from '../EnterpriseApp/data/constants';
import DeleteGroupModal from './DeleteGroupModal';
import EditGroupNameModal from './EditGroupNameModal';
import formatDates from './utils';
import AddMembersModal from './AddMembersModal';
+import GroupMembersTable from './GroupMembersTable';
const GroupDetailPage = () => {
const intl = useIntl();
@@ -22,6 +24,11 @@ const GroupDetailPage = () => {
const [isLoading, setIsLoading] = useState(true);
const [groupName, setGroupName] = useState(enterpriseGroup?.name);
const [isAddMembersModalOpen, openAddMembersModal, closeAddMembersModal] = useToggle(false);
+ const {
+ isLoading: isTableLoading,
+ enterpriseGroupLearnersTableData,
+ fetchEnterpriseGroupLearnersTableData,
+ } = useEnterpriseGroupLearnersTableData({ groupUuid, isAddMembersModalOpen });
const handleNameUpdate = (name) => {
setGroupName(name);
};
@@ -117,21 +124,39 @@ const GroupDetailPage = () => {
>
View group progress
-
-
>
) : }
+
+
+
);
};
diff --git a/src/components/PeopleManagement/GroupMembersTable.jsx b/src/components/PeopleManagement/GroupMembersTable.jsx
new file mode 100644
index 000000000..9ccd2e275
--- /dev/null
+++ b/src/components/PeopleManagement/GroupMembersTable.jsx
@@ -0,0 +1,147 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+ DataTable, Dropdown, Icon, IconButton,
+} from '@openedx/paragon';
+import { MoreVert, RemoveCircle } from '@openedx/paragon/icons';
+import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
+import TableTextFilter from '../learner-credit-management/TableTextFilter';
+import CustomDataTableEmptyState from '../learner-credit-management/CustomDataTableEmptyState';
+import MemberDetailsTableCell from '../learner-credit-management/members-tab/MemberDetailsTableCell';
+import EnrollmentsTableColumnHeader from './EnrollmentsTableColumnHeader';
+import { GROUP_MEMBERS_TABLE_DEFAULT_PAGE, GROUP_MEMBERS_TABLE_PAGE_SIZE } from './constants';
+import RecentActionTableCell from './RecentActionTableCell';
+import AddMemberTableAction from './AddMemberTableAction';
+
+const FilterStatus = (rest) => ;
+
+const KabobMenu = () => (
+
+
+
+
+
+
+
+
+
+);
+
+const selectColumn = {
+ id: 'selection',
+ Header: DataTable.ControlledSelectHeader,
+ Cell: DataTable.ControlledSelect,
+ disableSortBy: true,
+};
+
+const GroupMembersTable = ({
+ isLoading,
+ tableData,
+ fetchTableData,
+ groupUuid,
+ openAddMembersModal,
+}) => {
+ const intl = useIntl();
+ return (
+
+ ,
+ ]}
+ columns={[
+ {
+ Header: intl.formatMessage({
+ id: 'people.management.groups.detail.page.members.columns.memberDetails',
+ defaultMessage: 'Member details',
+ description: 'Column header for the Member details column in the People management Groups detail page',
+ }),
+ accessor: 'memberDetails',
+ Cell: MemberDetailsTableCell,
+ },
+ {
+ Header: intl.formatMessage({
+ id: 'people.management.groups.detail.page.members.columns.recentAction',
+ defaultMessage: 'Recent action',
+ description: 'Column header for the Recent action column in the People management Groups detail page',
+ }),
+ accessor: 'recentAction',
+ Cell: RecentActionTableCell,
+ disableFilters: true,
+ },
+ {
+ Header: EnrollmentsTableColumnHeader,
+ accessor: 'enrollmentCount',
+ Cell: ({ row }) => row.original.enrollments,
+ disableFilters: true,
+ },
+ ]}
+ initialTableOptions={{
+ getRowId: row => row?.memberDetails.userEmail,
+ autoResetPage: true,
+ }}
+ initialState={{
+ pageSize: GROUP_MEMBERS_TABLE_PAGE_SIZE,
+ pageIndex: GROUP_MEMBERS_TABLE_DEFAULT_PAGE,
+ sortBy: [
+ { id: 'memberDetails', desc: true },
+ ],
+ filters: [],
+ }}
+ additionalColumns={[
+ {
+ id: 'action',
+ Header: '',
+ // eslint-disable-next-line react/no-unstable-nested-components
+ Cell: (props) => (
+
+ ),
+ },
+ ]}
+ fetchData={fetchTableData}
+ data={tableData.results}
+ itemCount={tableData.itemCount}
+ pageCount={tableData.pageCount}
+ EmptyTableComponent={CustomDataTableEmptyState}
+ />
+
+ );
+};
+
+GroupMembersTable.propTypes = {
+ isLoading: PropTypes.bool.isRequired,
+ tableData: PropTypes.shape({
+ results: PropTypes.arrayOf(PropTypes.shape({
+ })),
+ itemCount: PropTypes.number.isRequired,
+ pageCount: PropTypes.number.isRequired,
+ }).isRequired,
+ fetchTableData: PropTypes.func.isRequired,
+ groupUuid: PropTypes.string.isRequired,
+ openAddMembersModal: PropTypes.func.isRequired,
+};
+
+export default GroupMembersTable;
diff --git a/src/components/PeopleManagement/MemberDetailsCell.jsx b/src/components/PeopleManagement/MemberDetailsCell.jsx
new file mode 100644
index 000000000..153061887
--- /dev/null
+++ b/src/components/PeopleManagement/MemberDetailsCell.jsx
@@ -0,0 +1,26 @@
+import PropTypes from 'prop-types';
+import { Stack } from '@openedx/paragon';
+
+const MemberDetailsCell = ({ row }) => (
+
+
+ {row.original?.user?.username}
+
+
+ {row.original?.user?.email}
+
+
+);
+
+MemberDetailsCell.propTypes = {
+ row: PropTypes.shape({
+ original: PropTypes.shape({
+ user: PropTypes.shape({
+ email: PropTypes.string.isRequired,
+ username: PropTypes.string.isRequired,
+ }).isRequired,
+ }).isRequired,
+ }).isRequired,
+};
+
+export default MemberDetailsCell;
diff --git a/src/components/PeopleManagement/MemberJoinedDateCell.jsx b/src/components/PeopleManagement/MemberJoinedDateCell.jsx
new file mode 100644
index 000000000..a68c3b9ee
--- /dev/null
+++ b/src/components/PeopleManagement/MemberJoinedDateCell.jsx
@@ -0,0 +1,18 @@
+import PropTypes from 'prop-types';
+import { formatTimestamp } from '../../utils';
+
+const MemberJoinedDateCell = ({ row }) => (
+
+ {formatTimestamp({ timestamp: row.original.created, format: 'MMM DD, YYYY' })}
+
+);
+
+MemberJoinedDateCell.propTypes = {
+ row: PropTypes.shape({
+ original: PropTypes.shape({
+ created: PropTypes.string.isRequired,
+ }).isRequired,
+ }).isRequired,
+};
+
+export default MemberJoinedDateCell;
diff --git a/src/components/PeopleManagement/RecentActionTableCell.jsx b/src/components/PeopleManagement/RecentActionTableCell.jsx
new file mode 100644
index 000000000..cbc2fb75b
--- /dev/null
+++ b/src/components/PeopleManagement/RecentActionTableCell.jsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import formatDates from './utils';
+
+const RecentActionTableCell = ({
+ row,
+}) => (
+ Added: {formatDates(row.original.activatedAt)}
+);
+
+RecentActionTableCell.propTypes = {
+ row: PropTypes.shape({
+ original: PropTypes.shape({
+ activatedAt: PropTypes.string.isRequired,
+ }).isRequired,
+ }).isRequired,
+};
+
+export default RecentActionTableCell;
diff --git a/src/components/PeopleManagement/RemoveMembersBulkAction.jsx b/src/components/PeopleManagement/RemoveMembersBulkAction.jsx
new file mode 100644
index 000000000..99da09899
--- /dev/null
+++ b/src/components/PeopleManagement/RemoveMembersBulkAction.jsx
@@ -0,0 +1,33 @@
+import PropTypes from 'prop-types';
+import { Button } from '@openedx/paragon';
+import { getSelectedEmailsByRow } from './utils';
+
+const RemoveMembersBulkAction = ({
+ isEntireTableSelected,
+ selectedFlatRows,
+ onHandleRemoveMembersBulkAction,
+ learnerEmails,
+}) => {
+ const handleOnClick = async () => {
+ if (isEntireTableSelected) {
+ onHandleRemoveMembersBulkAction(learnerEmails);
+ }
+ const emails = getSelectedEmailsByRow(selectedFlatRows);
+ onHandleRemoveMembersBulkAction(emails);
+ };
+
+ return (
+
+ );
+};
+
+RemoveMembersBulkAction.propTypes = {
+ learnerEmails: PropTypes.arrayOf(PropTypes.string).isRequired,
+ selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()).isRequired,
+ onHandleRemoveMembersBulkAction: PropTypes.func.isRequired,
+ isEntireTableSelected: PropTypes.bool,
+};
+
+export default RemoveMembersBulkAction;
diff --git a/src/components/PeopleManagement/constants.js b/src/components/PeopleManagement/constants.js
index abd76681b..2c8a9de50 100644
--- a/src/components/PeopleManagement/constants.js
+++ b/src/components/PeopleManagement/constants.js
@@ -3,3 +3,6 @@ export const MAX_LENGTH_GROUP_NAME = 60;
export const GROUP_TYPE_BUDGET = 'budget';
export const GROUP_TYPE_FLEX = 'flex';
export const GROUP_DROPDOWN_TEXT = 'Select group';
+
+export const GROUP_MEMBERS_TABLE_PAGE_SIZE = 10;
+export const GROUP_MEMBERS_TABLE_DEFAULT_PAGE = 0; // `DataTable` uses zero-index array
diff --git a/src/components/PeopleManagement/data/hooks/index.js b/src/components/PeopleManagement/data/hooks/index.js
new file mode 100644
index 000000000..f2c42923e
--- /dev/null
+++ b/src/components/PeopleManagement/data/hooks/index.js
@@ -0,0 +1,2 @@
+export { default as useEnterpriseGroupLearnersTableData } from './useEnterpriseGroupLearnersTableData';
+export { default as useAllEnterpriseGroupLearners } from './useAllEnterpriseGroupLearners';
diff --git a/src/components/PeopleManagement/data/hooks/useAllEnterpriseGroupLearners.js b/src/components/PeopleManagement/data/hooks/useAllEnterpriseGroupLearners.js
new file mode 100644
index 000000000..43c3752c7
--- /dev/null
+++ b/src/components/PeopleManagement/data/hooks/useAllEnterpriseGroupLearners.js
@@ -0,0 +1,35 @@
+import {
+ useEffect, useState,
+} from 'react';
+import { logError } from '@edx/frontend-platform/logging';
+
+import LmsApiService from '../../../../data/services/LmsApiService';
+
+const useAllEnterpriseGroupLearners = (groupUuid) => {
+ const [isLoading, setIsLoading] = useState(true);
+ const [enterpriseGroupLearners, setEnterpriseGroupLearners] = useState([]);
+
+ useEffect(() => {
+ const fetch = async () => {
+ try {
+ setIsLoading(true);
+ const response = await LmsApiService.fetchAllEnterpriseGroupLearners(groupUuid);
+ setEnterpriseGroupLearners(
+ response,
+ );
+ } catch (error) {
+ logError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetch();
+ }, [groupUuid]);
+
+ return {
+ isLoading,
+ enterpriseGroupLearners,
+ };
+};
+
+export default useAllEnterpriseGroupLearners;
diff --git a/src/components/PeopleManagement/data/hooks/useEnterpriseGroupLearnersTableData.js b/src/components/PeopleManagement/data/hooks/useEnterpriseGroupLearnersTableData.js
new file mode 100644
index 000000000..d3161ef3f
--- /dev/null
+++ b/src/components/PeopleManagement/data/hooks/useEnterpriseGroupLearnersTableData.js
@@ -0,0 +1,70 @@
+import {
+ useCallback, useMemo, useState,
+} from 'react';
+import _ from 'lodash';
+import { camelCaseObject } from '@edx/frontend-platform/utils';
+import { logError } from '@edx/frontend-platform/logging';
+import debounce from 'lodash.debounce';
+
+import LmsApiService from '../../../../data/services/LmsApiService';
+
+const useEnterpriseGroupLearnersTableData = ({ groupUuid, isAddMembersModalOpen }) => {
+ const [isLoading, setIsLoading] = useState(true);
+ const [enterpriseGroupLearnersTableData, setEnterpriseGroupLearnersTableData] = useState({
+ itemCount: 0,
+ pageCount: 0,
+ results: [],
+ });
+ const fetchEnterpriseGroupLearnersData = useCallback((args) => {
+ const fetch = async () => {
+ try {
+ setIsLoading(true);
+ const options = {};
+ 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;
+ }
+ }
+ args.filters.forEach((filter) => {
+ const { id, value } = filter;
+ if (id === 'status') {
+ options.show_removed = value;
+ } else if (id === 'memberDetails') {
+ options.user_query = value;
+ }
+ });
+
+ options.page = args.pageIndex + 1;
+ const response = await LmsApiService.fetchEnterpriseGroupLearners(groupUuid, options);
+ const data = camelCaseObject(response.data);
+
+ setEnterpriseGroupLearnersTableData({
+ itemCount: data.count,
+ pageCount: data.numPages ?? Math.floor(data.count / options.pageSize),
+ results: data.results.filter(result => result.activatedAt),
+ });
+ } catch (error) {
+ logError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetch();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [groupUuid, isAddMembersModalOpen]);
+
+ const debouncedFetchEnterpriseGroupLearnersData = useMemo(
+ () => debounce(fetchEnterpriseGroupLearnersData, 300),
+ [fetchEnterpriseGroupLearnersData],
+ );
+
+ return {
+ isLoading,
+ enterpriseGroupLearnersTableData,
+ fetchEnterpriseGroupLearnersTableData: debouncedFetchEnterpriseGroupLearnersData,
+ };
+};
+
+export default useEnterpriseGroupLearnersTableData;
diff --git a/src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js b/src/components/PeopleManagement/data/hooks/useEnterpriseLearnersTableData.js
similarity index 85%
rename from src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js
rename to src/components/PeopleManagement/data/hooks/useEnterpriseLearnersTableData.js
index 78fd7df4c..824d2cc03 100644
--- a/src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js
+++ b/src/components/PeopleManagement/data/hooks/useEnterpriseLearnersTableData.js
@@ -11,6 +11,7 @@ import { fetchPaginatedData } from '../../../../data/services/apiServiceUtils';
export const useGetAllEnterpriseLearnerEmails = ({
enterpriseId,
onHandleAddMembersBulkAction,
+ enterpriseGroupLearners,
}) => {
const [isLoading, setIsLoading] = useState(true);
const [addButtonState, setAddButtonState] = useState('default');
@@ -20,7 +21,11 @@ export const useGetAllEnterpriseLearnerEmails = ({
try {
const url = `${LmsApiService.enterpriseLearnerUrl}?enterprise_customer=${enterpriseId}`;
const { results } = await fetchPaginatedData(url);
- const learnerEmails = results.map(result => result?.user?.email).filter(email => email !== undefined);
+ const addedMemberEmails = enterpriseGroupLearners.map(learner => learner.memberDetails.userEmail);
+ const learnerEmails = results
+ .map(result => result?.user?.email)
+ .filter(email => email !== undefined)
+ .filter(email => !addedMemberEmails.includes(email));
onHandleAddMembersBulkAction(learnerEmails);
} catch (error) {
logError(error);
@@ -29,7 +34,7 @@ export const useGetAllEnterpriseLearnerEmails = ({
setIsLoading(false);
setAddButtonState('complete');
}
- }, [enterpriseId, onHandleAddMembersBulkAction]);
+ }, [enterpriseId, onHandleAddMembersBulkAction, enterpriseGroupLearners]);
return {
isLoading,
diff --git a/src/components/PeopleManagement/tests/CreateGroupModal.test.jsx b/src/components/PeopleManagement/tests/CreateGroupModal.test.jsx
index ca0d39fe0..384b2917e 100644
--- a/src/components/PeopleManagement/tests/CreateGroupModal.test.jsx
+++ b/src/components/PeopleManagement/tests/CreateGroupModal.test.jsx
@@ -16,7 +16,7 @@ import CreateGroupModal from '../CreateGroupModal';
import {
useEnterpriseLearnersTableData,
useGetAllEnterpriseLearnerEmails,
-} from '../../learner-credit-management/data/hooks/useEnterpriseLearnersTableData';
+} from '../data/hooks/useEnterpriseLearnersTableData';
import { useEnterpriseLearners } from '../../learner-credit-management/data';
jest.mock('@tanstack/react-query', () => ({
diff --git a/src/components/PeopleManagement/tests/GroupDetailPage.test.jsx b/src/components/PeopleManagement/tests/GroupDetailPage.test.jsx
index 7e4125230..2280b2df9 100644
--- a/src/components/PeopleManagement/tests/GroupDetailPage.test.jsx
+++ b/src/components/PeopleManagement/tests/GroupDetailPage.test.jsx
@@ -5,9 +5,11 @@ import '@testing-library/jest-dom/extend-expect';
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
import { Provider } from 'react-redux';
+import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { useEnterpriseGroupUuid } from '../../learner-credit-management/data';
+import { useEnterpriseGroupLearnersTableData } from '../data/hooks';
import GroupDetailPage from '../GroupDetailPage';
import LmsApiService from '../../../data/services/LmsApiService';
@@ -25,6 +27,7 @@ const getMockStore = store => mockStore(store);
jest.mock('../../learner-credit-management/data', () => ({
...jest.requireActual('../../learner-credit-management/data'),
useEnterpriseGroupUuid: jest.fn(),
+ useEnterpriseGroupLearnersTableData: jest.fn(),
}));
jest.mock('../../../data/services/LmsApiService');
jest.mock('react-router-dom', () => ({
@@ -60,11 +63,50 @@ describe('', () => {
beforeEach(() => {
useEnterpriseGroupUuid.mockReturnValue({ data: TEST_GROUP });
});
- it('renders the GroupDetailPage', () => {
+ it('renders the GroupDetailPage', async () => {
+ const mockFetchEnterpriseGroupLearnersTableData = jest.fn();
+ useEnterpriseGroupLearnersTableData.mockReturnValue({
+ fetchEnterpriseGroupLearnersTableData: mockFetchEnterpriseGroupLearnersTableData,
+ isLoading: false,
+ enterpriseGroupLearnersTableData: {
+ count: 1,
+ currentPage: 1,
+ next: null,
+ numPages: 1,
+ results: [{
+ activatedAt: '2024-11-06T21:01:32.953901Z',
+ enterprise_group_membership_uuid: TEST_GROUP,
+ memberDetails: {
+ userEmail: 'test@2u.com',
+ userName: 'Test 2u',
+ },
+ recentAction: 'Accepted: November 06, 2024',
+ status: 'accepted',
+ enrollments: 1,
+ }],
+ },
+ });
render();
expect(screen.queryAllByText(TEST_GROUP.name)).toHaveLength(2);
expect(screen.getByText('0 accepted members')).toBeInTheDocument();
expect(screen.getByText('View group progress')).toBeInTheDocument();
+ expect(screen.getByText('Add and remove group members.')).toBeInTheDocument();
+ expect(screen.getByText('Test 2u')).toBeInTheDocument();
+ userEvent.click(screen.getByText('Member details'));
+ await waitFor(() => expect(mockFetchEnterpriseGroupLearnersTableData).toHaveBeenCalledWith({
+ filters: [],
+ pageIndex: 0,
+ pageSize: 10,
+ sortBy: [{ desc: true, id: 'memberDetails' }],
+ }));
+
+ userEvent.click(screen.getByTestId('members-table-enrollments-column-header'));
+ await waitFor(() => expect(mockFetchEnterpriseGroupLearnersTableData).toHaveBeenCalledWith({
+ filters: [],
+ pageIndex: 0,
+ pageSize: 10,
+ sortBy: [{ desc: false, id: 'enrollmentCount' }],
+ }));
});
it('edit flex group name', async () => {
const spy = jest.spyOn(LmsApiService, 'updateEnterpriseGroup');
diff --git a/src/components/PeopleManagement/utils.js b/src/components/PeopleManagement/utils.js
index 141d82600..eb8cfe4f6 100644
--- a/src/components/PeopleManagement/utils.js
+++ b/src/components/PeopleManagement/utils.js
@@ -10,3 +10,14 @@ export default function formatDates(timestamp) {
const DATE_FORMAT = 'MMMM DD, YYYY';
return dayjs(timestamp).format(DATE_FORMAT);
}
+
+export const getSelectedEmailsByRow = (selectedFlatRows) => {
+ const emails = [];
+ Object.keys(selectedFlatRows).forEach(key => {
+ const { original } = selectedFlatRows[key];
+ if (original.user !== null) {
+ emails.push(original.user.email);
+ }
+ });
+ return emails;
+};
diff --git a/src/components/learner-credit-management/data/hooks/tests/useEnterpriseGroupLearnersTableData.test.jsx b/src/components/learner-credit-management/data/hooks/tests/useEnterpriseGroupLearnersTableData.test.jsx
new file mode 100644
index 000000000..a7bdf78be
--- /dev/null
+++ b/src/components/learner-credit-management/data/hooks/tests/useEnterpriseGroupLearnersTableData.test.jsx
@@ -0,0 +1,45 @@
+import { renderHook } from '@testing-library/react-hooks';
+import { camelCaseObject } from '@edx/frontend-platform/utils';
+import LmsApiService from '../../../../../data/services/LmsApiService';
+import { useEnterpriseGroupLearnersTableData } from '../../../../PeopleManagement/data/hooks';
+
+describe('useEnterpriseGroupLearnersTableData', () => {
+ it('should fetch and return enterprise learners', async () => {
+ const mockGroupUUID = 'test-uuid';
+ const mockData = {
+ count: 1,
+ current_page: 1,
+ next: null,
+ num_pages: 1,
+ previous: null,
+ results: [{
+ activated_at: '2024-11-06T21:01:32.953901Z',
+ enterprise_customer_user_id: 1,
+ enterprise_group_membership_uuid: 'test-uuid',
+ member_details: {
+ user_email: 'test@2u.com',
+ user_name: 'Test 2u',
+ },
+ recent_action: 'Accepted: November 06, 2024',
+ status: 'accepted',
+ enrollments: 1,
+ }],
+ };
+ const mockEnterpriseGroupLearners = jest.spyOn(LmsApiService, 'fetchEnterpriseGroupLearners');
+ mockEnterpriseGroupLearners.mockResolvedValue({ data: mockData });
+
+ const { result, waitForNextUpdate } = renderHook(
+ () => useEnterpriseGroupLearnersTableData({ groupUuid: mockGroupUUID }),
+ );
+ result.current.fetchEnterpriseGroupLearnersTableData({
+ pageIndex: 0,
+ pageSize: 10,
+ filters: [],
+ sortBy: [],
+ });
+ await waitForNextUpdate();
+ expect(LmsApiService.fetchEnterpriseGroupLearners).toHaveBeenCalledWith(mockGroupUUID, { page: 1 });
+ expect(result.current.isLoading).toEqual(false);
+ expect(result.current.enterpriseGroupLearnersTableData.results).toEqual(camelCaseObject(mockData.results));
+ });
+});
diff --git a/src/components/learner-credit-management/invite-modal/EnterpriseCustomerUserDatatable.jsx b/src/components/learner-credit-management/invite-modal/EnterpriseCustomerUserDatatable.jsx
deleted file mode 100644
index eeee91067..000000000
--- a/src/components/learner-credit-management/invite-modal/EnterpriseCustomerUserDatatable.jsx
+++ /dev/null
@@ -1,230 +0,0 @@
-import { connect } from 'react-redux';
-import PropTypes from 'prop-types';
-import {
- Button,
- DataTable,
- Stack,
- StatefulButton,
- TextFilter,
-} from '@openedx/paragon';
-import { useIntl } from '@edx/frontend-platform/i18n';
-import { useGetAllEnterpriseLearnerEmails, useEnterpriseLearnersTableData } from '../data/hooks/useEnterpriseLearnersTableData';
-import { formatTimestamp } from '../../../utils';
-import { DEFAULT_PAGE, MEMBERS_TABLE_PAGE_SIZE } from '../data';
-
-const getSelectedEmailsByRow = (selectedFlatRows) => {
- const emails = [];
- Object.keys(selectedFlatRows).forEach(key => {
- const { original } = selectedFlatRows[key];
- if (original.user !== null) {
- emails.push(original.user.email);
- }
- });
- return emails;
-};
-
-const MemberDetailsCell = ({ row }) => (
-
-
- {row.original?.user?.username}
-
-
- {row.original?.user?.email}
-
-
-);
-
-const MemberJoinedDateCell = ({ row }) => (
-
- {formatTimestamp({ timestamp: row.original.created, format: 'MMM DD, YYYY' })}
-
-);
-
-const AddMembersBulkAction = ({
- isEntireTableSelected,
- selectedFlatRows,
- onHandleAddMembersBulkAction,
- enterpriseId,
-}) => {
- const intl = useIntl();
- const { fetchLearnerEmails, addButtonState } = useGetAllEnterpriseLearnerEmails({
- enterpriseId,
- isEntireTableSelected,
- onHandleAddMembersBulkAction,
- });
- const handleOnClick = () => {
- if (isEntireTableSelected) {
- fetchLearnerEmails();
- return;
- }
- const emails = getSelectedEmailsByRow(selectedFlatRows);
- onHandleAddMembersBulkAction(emails);
- };
-
- return (
-
- );
-};
-
-const RemoveMembersBulkAction = ({
- isEntireTableSelected,
- selectedFlatRows,
- onHandleRemoveMembersBulkAction,
- learnerEmails,
-}) => {
- const handleOnClick = async () => {
- if (isEntireTableSelected) {
- onHandleRemoveMembersBulkAction(learnerEmails);
- }
- const emails = getSelectedEmailsByRow(selectedFlatRows);
- onHandleRemoveMembersBulkAction(emails);
- };
-
- return (
-
- );
-};
-
-const selectColumn = {
- id: 'selection',
- Header: DataTable.ControlledSelectHeader,
- Cell: DataTable.ControlledSelect,
-};
-
-// TO-DO: add search functionality on member details once the learner endpoint is updated
-// to support search
-const EnterpriseCustomerUserDatatable = ({
- enterpriseId,
- learnerEmails,
- onHandleAddMembersBulkAction,
- onHandleRemoveMembersBulkAction,
-}) => {
- const {
- isLoading,
- enterpriseCustomerUserTableData,
- fetchEnterpriseLearnersData,
- } = useEnterpriseLearnersTableData(enterpriseId);
-
- return (
- ,
- ,
- ]}
- columns={[
- {
- Header: 'Member details',
- accessor: 'user.email',
- Cell: MemberDetailsCell,
- },
- {
- Header: 'Joined organization',
- accessor: 'created',
- Cell: MemberJoinedDateCell,
- disableFilters: true,
- },
- ]}
- initialState={{
- pageIndex: DEFAULT_PAGE,
- pageSize: MEMBERS_TABLE_PAGE_SIZE,
- }}
- data={enterpriseCustomerUserTableData.results}
- defaultColumnValues={{ Filter: TextFilter }}
- fetchData={fetchEnterpriseLearnersData}
- isFilterable
- isLoading={isLoading}
- isPaginated
- isSelectable
- itemCount={enterpriseCustomerUserTableData.itemCount}
- manualFilters
- manualPagination
- initialTableOptions={{
- getRowId: row => row.id.toString(),
- }}
- pageCount={enterpriseCustomerUserTableData.pageCount}
- SelectionStatusComponent={DataTable.ControlledSelectionStatus}
- manualSelectColumn={selectColumn}
- />
- );
-};
-
-MemberDetailsCell.propTypes = {
- row: PropTypes.shape({
- original: PropTypes.shape({
- user: PropTypes.shape({
- email: PropTypes.string.isRequired,
- username: PropTypes.string.isRequired,
- }).isRequired,
- }).isRequired,
- }).isRequired,
-};
-
-MemberJoinedDateCell.propTypes = {
- row: PropTypes.shape({
- original: PropTypes.shape({
- created: PropTypes.string.isRequired,
- }).isRequired,
- }).isRequired,
-};
-
-AddMembersBulkAction.propTypes = {
- isEntireTableSelected: PropTypes.bool.isRequired,
- selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()).isRequired,
- enterpriseId: PropTypes.string.isRequired,
- onHandleAddMembersBulkAction: PropTypes.func.isRequired,
-};
-
-RemoveMembersBulkAction.propTypes = {
- isEntireTableSelected: PropTypes.bool.isRequired,
- learnerEmails: PropTypes.arrayOf(PropTypes.string).isRequired,
- selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()).isRequired,
- onHandleRemoveMembersBulkAction: PropTypes.func.isRequired,
-};
-
-EnterpriseCustomerUserDatatable.propTypes = {
- enterpriseId: PropTypes.string.isRequired,
- learnerEmails: PropTypes.arrayOf(PropTypes.string).isRequired,
- onHandleRemoveMembersBulkAction: PropTypes.func.isRequired,
- onHandleAddMembersBulkAction: PropTypes.func.isRequired,
-};
-
-const mapStateToProps = state => ({
- enterpriseId: state.portalConfiguration.enterpriseId,
-});
-
-export default connect(mapStateToProps)(EnterpriseCustomerUserDatatable);
diff --git a/src/data/services/LmsApiService.js b/src/data/services/LmsApiService.js
index 40a388e1a..19955c52a 100644
--- a/src/data/services/LmsApiService.js
+++ b/src/data/services/LmsApiService.js
@@ -462,6 +462,15 @@ class LmsApiService {
return LmsApiService.apiClient().get(enterpriseGroupLearnersEndpoint);
};
+ static fetchAllEnterpriseGroupLearners = async (groupUuid) => {
+ const queryParams = new URLSearchParams({
+ page: 1,
+ });
+ const url = `${LmsApiService.enterpriseGroupUrl}${groupUuid}/learners?${queryParams.toString()}`;
+ const response = await LmsApiService.fetchData(url);
+ return response;
+ };
+
static removeEnterpriseGroup = async (groupUuid) => {
const removeGroupEndpoint = `${LmsApiService.enterpriseGroupListUrl}${groupUuid}/`;
return LmsApiService.apiClient().delete(removeGroupEndpoint);