diff --git a/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx b/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx
index 731b34ff9..02ac0d708 100644
--- a/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx
+++ b/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx
@@ -19,7 +19,7 @@ import { EnterpriseSubsidiesContext } from '../EnterpriseSubsidiesContext';
import ContentHighlights from '../ContentHighlights';
import LearnerCreditManagementRoutes from '../learner-credit-management';
import PeopleManagementPage from '../PeopleManagement';
-import GroupDetailPage from '../PeopleManagement/GroupDetailPage';
+import GroupDetailPage from '../PeopleManagement/GroupDetailPage/GroupDetailPage';
const EnterpriseAppRoutes = ({
email,
diff --git a/src/components/PeopleManagement/DeleteGroupModal.jsx b/src/components/PeopleManagement/GroupDetailPage/DeleteGroupModal.jsx
similarity index 93%
rename from src/components/PeopleManagement/DeleteGroupModal.jsx
rename to src/components/PeopleManagement/GroupDetailPage/DeleteGroupModal.jsx
index c0f67802c..ccf98ee65 100644
--- a/src/components/PeopleManagement/DeleteGroupModal.jsx
+++ b/src/components/PeopleManagement/GroupDetailPage/DeleteGroupModal.jsx
@@ -8,10 +8,10 @@ import {
} from '@openedx/paragon';
import { RemoveCircleOutline } from '@openedx/paragon/icons';
-import GeneralErrorModal from './GeneralErrorModal';
-import { ROUTE_NAMES } from '../EnterpriseApp/data/constants';
+import GeneralErrorModal from '../GeneralErrorModal';
+import { ROUTE_NAMES } from '../../EnterpriseApp/data/constants';
-import LmsApiService from '../../data/services/LmsApiService';
+import LmsApiService from '../../../data/services/LmsApiService';
const DeleteGroupModal = ({
group, isOpen, close,
diff --git a/src/components/PeopleManagement/EditGroupNameModal.jsx b/src/components/PeopleManagement/GroupDetailPage/EditGroupNameModal.jsx
similarity index 95%
rename from src/components/PeopleManagement/EditGroupNameModal.jsx
rename to src/components/PeopleManagement/GroupDetailPage/EditGroupNameModal.jsx
index ca43f78bd..b548cb22d 100644
--- a/src/components/PeopleManagement/EditGroupNameModal.jsx
+++ b/src/components/PeopleManagement/GroupDetailPage/EditGroupNameModal.jsx
@@ -6,9 +6,9 @@ import {
ActionRow, Form, ModalDialog, Spinner, StatefulButton, useToggle,
} from '@openedx/paragon';
-import { MAX_LENGTH_GROUP_NAME } from './constants';
-import LmsApiService from '../../data/services/LmsApiService';
-import GeneralErrorModal from './GeneralErrorModal';
+import { MAX_LENGTH_GROUP_NAME } from '../constants';
+import LmsApiService from '../../../data/services/LmsApiService';
+import GeneralErrorModal from '../GeneralErrorModal';
const EditGroupNameModal = ({
group, isOpen, close, handleNameUpdate,
diff --git a/src/components/PeopleManagement/GroupDetailPage.jsx b/src/components/PeopleManagement/GroupDetailPage/GroupDetailPage.jsx
similarity index 96%
rename from src/components/PeopleManagement/GroupDetailPage.jsx
rename to src/components/PeopleManagement/GroupDetailPage/GroupDetailPage.jsx
index d47f54303..84a0f2d78 100644
--- a/src/components/PeopleManagement/GroupDetailPage.jsx
+++ b/src/components/PeopleManagement/GroupDetailPage/GroupDetailPage.jsx
@@ -6,12 +6,12 @@ import {
} from '@openedx/paragon';
import { Delete, Edit } from '@openedx/paragon/icons';
-import { useEnterpriseGroupLearnersTableData, useEnterpriseGroupUuid } from '../learner-credit-management/data';
-import { ROUTE_NAMES } from '../EnterpriseApp/data/constants';
+import { useEnterpriseGroupLearnersTableData, useEnterpriseGroupUuid } from '../data/hooks';
+import { ROUTE_NAMES } from '../../EnterpriseApp/data/constants';
import DeleteGroupModal from './DeleteGroupModal';
import EditGroupNameModal from './EditGroupNameModal';
-import formatDates from './utils';
-import GroupMembersTable from './GroupMembersTable';
+import formatDates from '../utils';
+import GroupMembersTable from '../GroupMembersTable';
const GroupDetailPage = () => {
const intl = useIntl();
diff --git a/src/components/PeopleManagement/OrgMemberCard.jsx b/src/components/PeopleManagement/OrgMemberCard.jsx
new file mode 100644
index 000000000..6ae9e4804
--- /dev/null
+++ b/src/components/PeopleManagement/OrgMemberCard.jsx
@@ -0,0 +1,53 @@
+import PropTypes from 'prop-types';
+
+import {
+ Avatar, Card, Col, Row,
+} from '@openedx/paragon';
+
+const OrgMemberCard = ({ original }) => {
+ const { enterpriseCustomerUser, enrollments } = original;
+ const { name, joinedOrg, email } = enterpriseCustomerUser;
+
+ return (
+
+
+
+
+
+
+
+
+
+ {name}
+
+
+ {email}
+
+
+
+ Joined org
+ {joinedOrg}
+
+
+ Enrollments
+ {enrollments}
+
+
+
+
+
+ );
+};
+
+OrgMemberCard.propTypes = {
+ original: PropTypes.shape({
+ enterpriseCustomerUser: PropTypes.shape({
+ email: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ joinedOrg: PropTypes.string.isRequired,
+ }),
+ enrollments: PropTypes.number.isRequired,
+ }),
+};
+
+export default OrgMemberCard;
diff --git a/src/components/PeopleManagement/PeopleManagementTable.jsx b/src/components/PeopleManagement/PeopleManagementTable.jsx
new file mode 100644
index 000000000..62d0d5745
--- /dev/null
+++ b/src/components/PeopleManagement/PeopleManagementTable.jsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import { CardView, DataTable } from '@openedx/paragon';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+
+import TableTextFilter from '../learner-credit-management/TableTextFilter';
+import CustomDataTableEmptyState from '../learner-credit-management/CustomDataTableEmptyState';
+import OrgMemberCard from './OrgMemberCard';
+import useEnterpriseMembersTableData from './data/hooks/useEnterpriseMembersTableData';
+
+const FilterStatus = (rest) => ;
+
+const PeopleManagementTable = ({ enterpriseId }) => {
+ const {
+ isLoading: isTableLoading,
+ enterpriseMembersTableData,
+ fetchEnterpriseMembersTableData,
+ } = useEnterpriseMembersTableData({ enterpriseId });
+
+ const tableColumns = [{ Header: 'Name', accessor: 'name' }];
+
+ return (
+
+
+
+
+
+ );
+};
+
+PeopleManagementTable.propTypes = {
+ enterpriseId: PropTypes.string.isRequired,
+};
+
+const mapStateToProps = state => ({
+ enterpriseId: state.portalConfiguration.enterpriseId,
+});
+
+export default connect(mapStateToProps)(PeopleManagementTable);
diff --git a/src/components/PeopleManagement/constants.js b/src/components/PeopleManagement/constants.js
index 2c8a9de50..ef2d298c3 100644
--- a/src/components/PeopleManagement/constants.js
+++ b/src/components/PeopleManagement/constants.js
@@ -2,7 +2,15 @@ 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
+
+// Query Key factory for the people management module, intended to be used with `@tanstack/react-query`.
+// Inspired by https://tkdodo.eu/blog/effective-react-query-keys#use-query-key-factories.
+export const peopleManagementQueryKeys = {
+ all: ['people-management'],
+ members: (enterpriseUuid) => [...peopleManagementQueryKeys.all, 'members', enterpriseUuid],
+};
diff --git a/src/components/PeopleManagement/data/hooks/index.js b/src/components/PeopleManagement/data/hooks/index.js
new file mode 100644
index 000000000..04bcc3b90
--- /dev/null
+++ b/src/components/PeopleManagement/data/hooks/index.js
@@ -0,0 +1,3 @@
+export { default as useEnterpriseGroupUuid } from './useEnterpriseGroupUuid';
+export { default as useEnterpriseGroupLearnersTableData } from './useEnterpriseGroupLearnersTableData';
+export { default as useEnterpriseMembersTableData } from './useEnterpriseMembersTableData';
diff --git a/src/components/learner-credit-management/data/hooks/useEnterpriseGroupLearnersTableData.js b/src/components/PeopleManagement/data/hooks/useEnterpriseGroupLearnersTableData.js
similarity index 100%
rename from src/components/learner-credit-management/data/hooks/useEnterpriseGroupLearnersTableData.js
rename to src/components/PeopleManagement/data/hooks/useEnterpriseGroupLearnersTableData.js
diff --git a/src/components/learner-credit-management/data/hooks/useEnterpriseGroupUuid.js b/src/components/PeopleManagement/data/hooks/useEnterpriseGroupUuid.js
similarity index 89%
rename from src/components/learner-credit-management/data/hooks/useEnterpriseGroupUuid.js
rename to src/components/PeopleManagement/data/hooks/useEnterpriseGroupUuid.js
index c2a3dc91e..a8e97e495 100644
--- a/src/components/learner-credit-management/data/hooks/useEnterpriseGroupUuid.js
+++ b/src/components/PeopleManagement/data/hooks/useEnterpriseGroupUuid.js
@@ -1,7 +1,7 @@
import { useQuery } from '@tanstack/react-query';
import { camelCaseObject } from '@edx/frontend-platform/utils';
-import { learnerCreditManagementQueryKeys } from '../constants';
+import { learnerCreditManagementQueryKeys } from '../../../learner-credit-management/data/constants';
import LmsApiService from '../../../../data/services/LmsApiService';
/**
diff --git a/src/components/PeopleManagement/data/hooks/useEnterpriseMembersTableData.js b/src/components/PeopleManagement/data/hooks/useEnterpriseMembersTableData.js
new file mode 100644
index 000000000..d2bccdc74
--- /dev/null
+++ b/src/components/PeopleManagement/data/hooks/useEnterpriseMembersTableData.js
@@ -0,0 +1,62 @@
+import {
+ useCallback, useMemo, useState,
+} from 'react';
+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 useEnterpriseMembersTableData = ({ enterpriseId }) => {
+ const [isLoading, setIsLoading] = useState(true);
+ const [enterpriseMembersTableData, setEnterpriseMembersTableData] = useState({
+ itemCount: 0,
+ pageCount: 0,
+ results: [],
+ });
+ const fetchEnterpriseMembersData = 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;
+ }
+ });
+
+ options.page = args.pageIndex + 1;
+ const response = await LmsApiService.fetchEnterpriseCustomerMembers(enterpriseId, options);
+ const data = camelCaseObject(response.data);
+ setEnterpriseMembersTableData({
+ itemCount: data.count,
+ pageCount: data.numPages ?? Math.floor(data.count / options.pageSize),
+ results: data.results,
+ });
+ } 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 debouncedFetchEnterpriseMembersData = useMemo(
+ () => debounce(fetchEnterpriseMembersData, 300),
+ [fetchEnterpriseMembersData],
+ );
+
+ return {
+ isLoading,
+ enterpriseMembersTableData,
+ fetchEnterpriseMembersTableData: debouncedFetchEnterpriseMembersData,
+ };
+};
+
+export default useEnterpriseMembersTableData;
diff --git a/src/components/PeopleManagement/index.jsx b/src/components/PeopleManagement/index.jsx
index 27e8c8858..7cf661418 100644
--- a/src/components/PeopleManagement/index.jsx
+++ b/src/components/PeopleManagement/index.jsx
@@ -13,6 +13,7 @@ import CreateGroupModal from './CreateGroupModal';
import { useAllEnterpriseGroups } from '../learner-credit-management/data';
import ZeroState from './ZeroState';
import GroupCardGrid from './GroupCardGrid';
+import PeopleManagementTable from './PeopleManagementTable';
const PeopleManagementPage = ({ enterpriseId }) => {
const intl = useIntl();
@@ -78,16 +79,37 @@ const PeopleManagementPage = ({ enterpriseId }) => {
description="CTA button text to open new group modal."
/>
-
+
{groups && groups.length > 0 ? (
- ) : }
+
+ ) : (
+
+ )}
+
+
+
+
+
>
);
};
-const mapStateToProps = state => ({
+const mapStateToProps = (state) => ({
enterpriseId: state.portalConfiguration.enterpriseId,
});
diff --git a/src/components/PeopleManagement/tests/GroupDetailPage.test.jsx b/src/components/PeopleManagement/tests/GroupDetailPage.test.jsx
index e00d2fbd9..d93a06344 100644
--- a/src/components/PeopleManagement/tests/GroupDetailPage.test.jsx
+++ b/src/components/PeopleManagement/tests/GroupDetailPage.test.jsx
@@ -8,8 +8,8 @@ import { Provider } from 'react-redux';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
-import { useEnterpriseGroupUuid, useEnterpriseGroupLearnersTableData } from '../../learner-credit-management/data';
-import GroupDetailPage from '../GroupDetailPage';
+import { useEnterpriseGroupUuid, useEnterpriseGroupLearnersTableData } from '../data/hooks';
+import GroupDetailPage from '../GroupDetailPage/GroupDetailPage';
import LmsApiService from '../../../data/services/LmsApiService';
const TEST_ENTERPRISE_SLUG = 'test-enterprise';
@@ -23,8 +23,8 @@ const TEST_GROUP = {
const mockStore = configureMockStore([thunk]);
const getMockStore = store => mockStore(store);
-jest.mock('../../learner-credit-management/data', () => ({
- ...jest.requireActual('../../learner-credit-management/data'),
+jest.mock('../data/hooks', () => ({
+ ...jest.requireActual('../data/hooks'),
useEnterpriseGroupUuid: jest.fn(),
useEnterpriseGroupLearnersTableData: jest.fn(),
}));
diff --git a/src/components/learner-credit-management/data/hooks/tests/useEnterpriseGroupLearnersTableData.test.jsx b/src/components/PeopleManagement/tests/useEnterpriseGroupLearnersTableData.test.jsx
similarity index 92%
rename from src/components/learner-credit-management/data/hooks/tests/useEnterpriseGroupLearnersTableData.test.jsx
rename to src/components/PeopleManagement/tests/useEnterpriseGroupLearnersTableData.test.jsx
index ec49d431e..a8b923090 100644
--- a/src/components/learner-credit-management/data/hooks/tests/useEnterpriseGroupLearnersTableData.test.jsx
+++ b/src/components/PeopleManagement/tests/useEnterpriseGroupLearnersTableData.test.jsx
@@ -1,7 +1,7 @@
import { renderHook } from '@testing-library/react-hooks';
import { camelCaseObject } from '@edx/frontend-platform/utils';
-import LmsApiService from '../../../../../data/services/LmsApiService';
-import { useEnterpriseGroupLearnersTableData } from '../..';
+import LmsApiService from '../../../data/services/LmsApiService';
+import { useEnterpriseGroupLearnersTableData } from '../data/hooks';
describe('useEnterpriseGroupLearnersTableData', () => {
it('should fetch and return enterprise learners', async () => {
diff --git a/src/components/PeopleManagement/tests/useEnterpriseMembersTableData.test.jsx b/src/components/PeopleManagement/tests/useEnterpriseMembersTableData.test.jsx
new file mode 100644
index 000000000..d12aca1e6
--- /dev/null
+++ b/src/components/PeopleManagement/tests/useEnterpriseMembersTableData.test.jsx
@@ -0,0 +1,42 @@
+import { renderHook } from '@testing-library/react-hooks';
+import { camelCaseObject } from '@edx/frontend-platform/utils';
+import LmsApiService from '../../../data/services/LmsApiService';
+
+import useEnterpriseMembersTableData from '../data/hooks/useEnterpriseMembersTableData';
+
+describe('useEnterpriseMembersTableData', () => {
+ it('should fetch and return members of an enterprise', async () => {
+ const mockEnterpriseUUID = 'uuid-bb';
+ const mockData = {
+ count: 1,
+ current_page: 1,
+ next: null,
+ num_pages: 1,
+ previous: null,
+ results: [{
+ enterprise_customer_user: {
+ email: 'jeez.louise@example.com',
+ joinedOrg: 'Sep 15, 2021',
+ name: 'Jeez Louise',
+ },
+ enrollments: 11,
+ }],
+ };
+ const mockEnterpriseMembers = jest.spyOn(LmsApiService, 'fetchEnterpriseCustomerMembers');
+ mockEnterpriseMembers.mockResolvedValue({ data: mockData });
+
+ const { result, waitForNextUpdate } = renderHook(
+ () => useEnterpriseMembersTableData({ enterpriseId: mockEnterpriseUUID }),
+ );
+ result.current.fetchEnterpriseMembersTableData({
+ pageIndex: 0,
+ pageSize: 10,
+ filters: [],
+ sortBy: [],
+ });
+ await waitForNextUpdate();
+ expect(LmsApiService.fetchEnterpriseCustomerMembers).toHaveBeenCalledWith(mockEnterpriseUUID, { page: 1 });
+ expect(result.current.isLoading).toEqual(false);
+ expect(result.current.enterpriseMembersTableData.results).toEqual(camelCaseObject(mockData.results));
+ });
+});
diff --git a/src/components/learner-credit-management/data/hooks/index.js b/src/components/learner-credit-management/data/hooks/index.js
index cdd1006dd..da1e448ab 100644
--- a/src/components/learner-credit-management/data/hooks/index.js
+++ b/src/components/learner-credit-management/data/hooks/index.js
@@ -17,12 +17,10 @@ export { default as useEnterpriseGroupLearners } from './useEnterpriseGroupLearn
export { default as useEnterpriseGroupMembersTableData } from './useEnterpriseGroupMembersTableData';
export { default as useEnterpriseCustomer } from './useEnterpriseCustomer';
export { default as useEnterpriseGroup } from './useEnterpriseGroup';
-export { default as useEnterpriseGroupUuid } from './useEnterpriseGroupUuid';
export { default as useAllEnterpriseGroups } from './useAllEnterpriseGroups';
export { default as useContentMetadata } from './useContentMetadata';
export { default as useEnterpriseRemovedGroupMembers } from './useEnterpriseRemovedGroupMembers';
export { default as useEnterpriseFlexGroups } from './useEnterpriseFlexGroups';
export { default as useGroupDropdownToggle } from './useGroupDropdownToggle';
-export { default as useEnterpriseGroupLearnersTableData } from './useEnterpriseGroupLearnersTableData';
export { default as useEnterpriseLearners } from './useEnterpriseLearners';
export { default as useCatalogContainsContentItemsMultipleQueries } from './useCatalogContainsContentItemsMultipleQueries';
diff --git a/src/data/services/LmsApiService.js b/src/data/services/LmsApiService.js
index 40a388e1a..3ac99b3c8 100644
--- a/src/data/services/LmsApiService.js
+++ b/src/data/services/LmsApiService.js
@@ -17,6 +17,8 @@ class LmsApiService {
static enterpriseCustomerBrandingUrl = `${LmsApiService.baseUrl}/enterprise/api/v1/enterprise-customer-branding/update-branding/`;
+ static enterpriseCustomerMembersUrl = `${LmsApiService.baseUrl}/enterprise/api/v1/enterprise-customer-members/`;
+
static providerConfigUrl = `${LmsApiService.baseUrl}/auth/saml/v0/provider_config/`;
static providerDataUrl = `${LmsApiService.baseUrl}/auth/saml/v0/provider_data/`;
@@ -353,6 +355,15 @@ class LmsApiService {
return LmsApiService.apiClient().patch(url, options);
}
+ static fetchEnterpriseCustomerMembers(enterpriseUUID, options) {
+ let url = `${LmsApiService.enterpriseCustomerMembersUrl}${enterpriseUUID}/`;
+ if (options) {
+ const queryParams = new URLSearchParams(options);
+ url = `${LmsApiService.enterpriseCustomerMembersUrl}${enterpriseUUID}?${queryParams.toString()}`;
+ }
+ return LmsApiService.apiClient().get(url, options);
+ }
+
/**
* Disables EnterpriseCustomerInviteKey
* @param {string} enterpriseCustomerInviteKeyUUID uuid EnterpriseCustomerInviteKey to disable