Skip to content

Commit

Permalink
Adding people management data table (#1364)
Browse files Browse the repository at this point in the history
* fix: formatting without data

* fix: adding in tests

* fix: teeny fix
  • Loading branch information
kiram15 authored Dec 5, 2024
1 parent 0c35ab3 commit 371f043
Show file tree
Hide file tree
Showing 17 changed files with 290 additions and 23 deletions.
2 changes: 1 addition & 1 deletion src/components/EnterpriseApp/EnterpriseAppRoutes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
53 changes: 53 additions & 0 deletions src/components/PeopleManagement/OrgMemberCard.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<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">Joined org</h5>
{joinedOrg}
</Col>
<Col>
<h5 className="pt-2 text-uppercase">Enrollments</h5>
{enrollments}
</Col>
</Row>
</Card.Section>
</Card.Body>
</Card>
);
};

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;
68 changes: 68 additions & 0 deletions src/components/PeopleManagement/PeopleManagementTable.jsx
Original file line number Diff line number Diff line change
@@ -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) => <DataTable.FilterStatus showFilteredFields={false} {...rest} />;

const PeopleManagementTable = ({ enterpriseId }) => {
const {
isLoading: isTableLoading,
enterpriseMembersTableData,
fetchEnterpriseMembersTableData,
} = useEnterpriseMembersTableData({ enterpriseId });

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

return (
<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: 'enterpriseCustomerUser.name', desc: true },
],
filters: [],
}}
fetchData={fetchEnterpriseMembersTableData}
data={enterpriseMembersTableData.results}
itemCount={enterpriseMembersTableData.itemCount}
pageCount={enterpriseMembersTableData.pageCount}
EmptyTableComponent={CustomDataTableEmptyState}
>
<DataTable.TableControlBar />
<CardView
className="d-block"
CardComponent={OrgMemberCard}
columnSizes={{ xs: 12 }}
/>
<DataTable.TableFooter />
</DataTable>
);
};

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

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

export default connect(mapStateToProps)(PeopleManagementTable);
8 changes: 8 additions & 0 deletions src/components/PeopleManagement/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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],
};
3 changes: 3 additions & 0 deletions src/components/PeopleManagement/data/hooks/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as useEnterpriseGroupUuid } from './useEnterpriseGroupUuid';
export { default as useEnterpriseGroupLearnersTableData } from './useEnterpriseGroupLearnersTableData';
export { default as useEnterpriseMembersTableData } from './useEnterpriseMembersTableData';
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
28 changes: 25 additions & 3 deletions src/components/PeopleManagement/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -78,16 +79,37 @@ const PeopleManagementPage = ({ enterpriseId }) => {
description="CTA button text to open new group modal."
/>
</Button>
<CreateGroupModal isModalOpen={isModalOpen} openModel={openModal} closeModal={closeModal} />
<CreateGroupModal
isModalOpen={isModalOpen}
openModel={openModal}
closeModal={closeModal}
/>
</ActionRow>
{groups && groups.length > 0 ? (
<GroupCardGrid groups={groups} />) : <ZeroState />}
<GroupCardGrid groups={groups} />
) : (
<ZeroState />
)}
<h3 className="mt-3">
<FormattedMessage
id="adminPortal.peopleManagement.dataTable.title"
defaultMessage="Your organization's members"
description="Title for people management data table."
/>
</h3>
<FormattedMessage
className="mb-4"
id="adminPortal.peopleManagement.dataTable.subtitle"
defaultMessage="View all members of your organization."
description="Subtitle for people management members data table."
/>
<PeopleManagementTable />
</div>
</>
);
};

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(),
}));
Expand Down
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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: '[email protected]',
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));
});
});
Loading

0 comments on commit 371f043

Please sign in to comment.