Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding people management data table #1364

Merged
merged 5 commits into from
Dec 5, 2024
Merged
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: 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;

Check warning on line 9 in src/components/PeopleManagement/OrgMemberCard.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/OrgMemberCard.jsx#L8-L9

Added lines #L8 - L9 were not covered by tests

return (

Check warning on line 11 in src/components/PeopleManagement/OrgMemberCard.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/OrgMemberCard.jsx#L11

Added line #L11 was not covered by tests
<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 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],

Check warning on line 15 in src/components/PeopleManagement/constants.js

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/constants.js#L15

Added line #L15 was not covered by tests
};
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;

Check warning on line 23 in src/components/PeopleManagement/data/hooks/useEnterpriseMembersTableData.js

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/data/hooks/useEnterpriseMembersTableData.js#L23

Added line #L23 was not covered by tests
if (id === 'name') {
options.user_query = value;

Check warning on line 25 in src/components/PeopleManagement/data/hooks/useEnterpriseMembersTableData.js

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/data/hooks/useEnterpriseMembersTableData.js#L25

Added line #L25 was not covered by tests
}
});

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);

Check warning on line 38 in src/components/PeopleManagement/data/hooks/useEnterpriseMembersTableData.js

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/data/hooks/useEnterpriseMembersTableData.js#L38

Added line #L38 was not covered by tests
} finally {
setIsLoading(false);
}
};
if (args.filters.length && args.filters[0].value.length > 2) {
fetch();

Check warning on line 44 in src/components/PeopleManagement/data/hooks/useEnterpriseMembersTableData.js

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/data/hooks/useEnterpriseMembersTableData.js#L44

Added line #L44 was not covered by tests
} 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
Loading