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

Creating flex group cards for People Management tab #1336

Merged
merged 2 commits into from
Oct 16, 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
59 changes: 59 additions & 0 deletions src/components/PeopleManagement/GroupCardGrid.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { CardGrid, Collapsible } from '@openedx/paragon';

import GroupDetailCard from './GroupDetailCard';

const GroupCardGrid = ({ groups }) => {
const [previewGroups, setPreviewGroups] = useState();
const [overflowGroups, setOverflowGroups] = useState();
useEffect(() => {
if (groups.length > 3) {
setPreviewGroups(groups.slice(0, 3));
setOverflowGroups(groups.slice(3));
} else {
setPreviewGroups(groups);
}
}, [groups]);
return (
<>
<CardGrid
columnSizes={{
xs: 6,
lg: 6,
xl: 4,
}}
hasEqualColumnHeights="true"
>
{previewGroups?.map((group) => (
<GroupDetailCard group={group} />
))}
</CardGrid>
{overflowGroups && (
<Collapsible
styling="basic"
title={`Show all ${groups.length} groups`}
>
<CardGrid
columnSizes={{
xs: 6,
lg: 6,
xl: 4,
}}
hasEqualColumnHeights="true"
>
{overflowGroups.map((group) => (
<GroupDetailCard group={group} />
))}
</CardGrid>
</Collapsible>
)}
</>
);
};

GroupCardGrid.propTypes = {
groups: PropTypes.shape.isRequired,
};

export default GroupCardGrid;
34 changes: 34 additions & 0 deletions src/components/PeopleManagement/GroupDetailCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import PropTypes from 'prop-types';
import { useParams } from 'react-router';
import { Card, Hyperlink } from '@openedx/paragon';
import { ROUTE_NAMES } from '../EnterpriseApp/data/constants';

const GroupDetailCard = ({ group }) => {
const { enterpriseSlug } = useParams();
return (
<Card className="group-detail-card">
<Card.Header title={group.name} />
<Card.Section>
{group.acceptedMembersCount} members
</Card.Section>
<Card.Footer className="card-button">
<Hyperlink
className="btn btn-outline-primary"
destination={`/${enterpriseSlug}/admin/${ROUTE_NAMES.peopleManagement}/${group.uuid}`}
>
View group
</Hyperlink>
</Card.Footer>
</Card>
);
};

GroupDetailCard.propTypes = {
group: PropTypes.shape({
acceptedMembersCount: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
uuid: PropTypes.string.isRequired,
}).isRequired,
};

export default GroupDetailCard;
54 changes: 54 additions & 0 deletions src/components/PeopleManagement/ZeroState.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React, { useContext } from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Card } from '@openedx/paragon';

import cardImage from './images/ZeroStateImage.svg';
import { SUBSIDY_TYPES } from '../../data/constants/subsidyTypes';
import { EnterpriseSubsidiesContext } from '../EnterpriseSubsidiesContext';

const ZeroState = () => {
const { enterpriseSubsidyTypes } = useContext(EnterpriseSubsidiesContext);

const hasLearnerCredit = enterpriseSubsidyTypes.includes(
SUBSIDY_TYPES.budget,
);
const hasOtherSubsidyTypes = enterpriseSubsidyTypes.includes(SUBSIDY_TYPES.license)
|| enterpriseSubsidyTypes.includes(SUBSIDY_TYPES.coupon);

return (
<Card>
<Card.ImageCap
className="mh-100"
src={cardImage}
srcAlt="Two people carrying a cartoon arrow"
/>
<span className="text-center align-self-center">
<h2 className="h3 mb-3 mt-3">
<FormattedMessage
id="adminPortal.peopleManagement.zeroState.card.header"
defaultMessage="You don't have any groups yet."
description="Header message shown to admin there's no groups created yet."
/>
</h2>
<p className="mx-2">
{hasLearnerCredit && (
<FormattedMessage
id="adminPortal.peopleManagement.zeroState.card.subtitle.lc"
defaultMessage="Once a group is created, you can track members' progress, assign extra courses, and invite them to additional budgets."
description="Detail message shown to admin benefits of creating a group with learner credit."
/>
)}
{!hasLearnerCredit && hasOtherSubsidyTypes && (
<FormattedMessage
id="admin.portal.people.management.page.zerostate.card.subtitle.noLc"
defaultMessage="Once a group is created, you can track members' progress."
description="Detail message shown to admin benefits of creating a group without learner credit."
/>
)}
</p>
</span>
</Card>
);
};

export default ZeroState;
13 changes: 13 additions & 0 deletions src/components/PeopleManagement/_PeopleManagement.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.group-detail-card {
background-color: $light-200;
.card-button {
justify-content: flex-start !important;
}
.pgn__card-section {
padding: .25rem 1.25rem 1.25rem 1.25rem;
}
}

.collapsible-basic .collapsible-trigger {
justify-content: right;
}
68 changes: 28 additions & 40 deletions src/components/PeopleManagement/index.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import React, { useContext } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import {
ActionRow, Button, Card, useToggle,
} from '@openedx/paragon';
import { ActionRow, Button, useToggle } from '@openedx/paragon';
import { Add } from '@openedx/paragon/icons';

import cardImage from './images/ZeroStateImage.svg';
import Hero from '../Hero';
import { SUBSIDY_TYPES } from '../../data/constants/subsidyTypes';
import { EnterpriseSubsidiesContext } from '../EnterpriseSubsidiesContext';
import CreateGroupModal from './CreateGroupModal';
import { useAllEnterpriseGroups } from '../learner-credit-management/data';
import ZeroState from './ZeroState';
import GroupCardGrid from './GroupCardGrid';

const PeopleManagementPage = () => {
const PeopleManagementPage = ({ enterpriseId }) => {
const intl = useIntl();
const PAGE_TITLE = intl.formatMessage({
id: 'admin.portal.people.management.page',
Expand All @@ -21,13 +23,21 @@ const PeopleManagementPage = () => {
});

const { enterpriseSubsidyTypes } = useContext(EnterpriseSubsidiesContext);
const { data } = useAllEnterpriseGroups(enterpriseId);

const hasLearnerCredit = enterpriseSubsidyTypes.includes(SUBSIDY_TYPES.budget);
const hasOtherSubsidyTypes = enterpriseSubsidyTypes.includes(SUBSIDY_TYPES.license)
|| enterpriseSubsidyTypes.includes(SUBSIDY_TYPES.coupon);

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [isModalOpen, openModal, closeModal] = useToggle(false);
const [groups, setGroups] = useState();

useEffect(() => {
if (data !== undefined) {
setGroups(data.results);
}
}, [data]);

return (
<>
Expand All @@ -41,7 +51,7 @@ const PeopleManagementPage = () => {
<FormattedMessage
id="adminPortal.peopleManagement.title"
defaultMessage="Your organization's groups"
description="Title for people management zero state."
description="Title for people management page."
/>
</h3>
</span>
Expand Down Expand Up @@ -70,41 +80,19 @@ const PeopleManagementPage = () => {
</Button>
<CreateGroupModal isModalOpen={isModalOpen} openModel={openModal} closeModal={closeModal} />
</ActionRow>
<Card>
<Card.ImageCap
className="mh-100"
src={cardImage}
srcAlt="Two people carrying a cartoon arrow"
/>
<span className="text-center align-self-center">
<h2 className="h3 mb-3 mt-3">
<FormattedMessage
id="adminPortal.peopleManagement.zeroStateCard.header"
defaultMessage="You don't have any groups yet."
description="Header message shown to admin there's no groups created yet."
/>
</h2>
<p className="mx-2">
{hasLearnerCredit && (
<FormattedMessage
id="adminPortal.peopleManagement.zeroStateCard.subtitle.lc"
defaultMessage="Once a group is created, you can track members' progress, assign extra courses, and invite them to additional budgets."
description="Detail message shown to admin benefits of creating a group with learner credit."
/>
)}
{!hasLearnerCredit && hasOtherSubsidyTypes && (
<FormattedMessage
id="adminPortal.peopleManagement.zeroStateCard.subtitle.noLc"
defaultMessage="Once a group is created, you can track members' progress."
description="Detail message shown to admin benefits of creating a group without learner credit."
/>
)}
</p>
</span>
</Card>
{groups && groups.length > 0 ? (
<GroupCardGrid groups={groups} />) : <ZeroState />}
</div>
</>
);
};

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

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

export default connect(mapStateToProps)(PeopleManagementPage);
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { render, screen } from '@testing-library/react';
import {
render, screen, waitFor,
} from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import { IntlProvider } from '@edx/frontend-platform/i18n';

import { useAllEnterpriseGroups } from '../../learner-credit-management/data';
import { EnterpriseSubsidiesContext } from '../../EnterpriseSubsidiesContext';
import PeopleManagementPage from '..';

Expand All @@ -30,6 +33,39 @@ const subsEnterpriseSubsidiesContextValue = {
isLoading: false,
};

jest.mock('../../learner-credit-management/data', () => ({
...jest.requireActual('../../learner-credit-management/data'),
useAllEnterpriseGroups: jest.fn(),
}));

const mockGroupsResponse = [{
enterpriseCustomer: enterpriseUUID,
name: 'only cool people',
uuid: '12345',
acceptedMembersCount: 4,
groupType: 'flex',
created: '2024-10-31T03:33:33.292361Z',
}];

const mockMultipleGroupsResponse = [
{
name: 'captain crunch',
acceptedMembersCount: 4,
},
{
name: 'cinnamon toast crunch',
acceptedMembersCount: 5,
},
{
name: 'cocoa puffs',
acceptedMembersCount: 10,
},
{
name: 'fruity pebbles',
acceptedMembersCount: 5,
},
];

const PeopleManagementPageWrapper = ({
initialState = initialStoreState,
enterpriseSubsidiesContextValue = defaultEnterpriseSubsidiesContextValue,
Expand All @@ -48,6 +84,7 @@ const PeopleManagementPageWrapper = ({

describe('<PeopleManagementPage >', () => {
it('renders the PeopleManagementPage zero state', () => {
useAllEnterpriseGroups.mockReturnValue({ data: { results: {} } });
render(<PeopleManagementPageWrapper />);
expect(document.querySelector('h3').textContent).toEqual("Your organization's groups");
expect(screen.getByText("You don't have any groups yet.")).toBeInTheDocument();
Expand All @@ -56,6 +93,7 @@ describe('<PeopleManagementPage >', () => {
)).toBeInTheDocument();
});
it('renders the PeopleManagementPage zero state without LC', () => {
useAllEnterpriseGroups.mockReturnValue({ data: { results: [] } });
const store = getMockStore(initialStoreState);
render(
<IntlProvider locale="en">
Expand All @@ -72,4 +110,41 @@ describe('<PeopleManagementPage >', () => {
expect(screen.getByText("You don't have any groups yet.")).toBeInTheDocument();
expect(screen.getByText("Once a group is created, you can track members' progress.")).toBeInTheDocument();
});
it('renders the PeopleManagementPage group card grid', () => {
useAllEnterpriseGroups.mockReturnValue({ data: { results: mockGroupsResponse } });
const store = getMockStore(initialStoreState);
render(
<IntlProvider locale="en">
<Provider store={store}>
<EnterpriseSubsidiesContext.Provider value={subsEnterpriseSubsidiesContextValue}>
<PeopleManagementPage />
</EnterpriseSubsidiesContext.Provider>
</Provider>
</IntlProvider>,
);
expect(screen.getByText('only cool people')).toBeInTheDocument();
expect(screen.getByText('4 members')).toBeInTheDocument();
});
it('renders the PeopleManagementPage group card grid with collapsible', async () => {
useAllEnterpriseGroups.mockReturnValue({ data: { results: mockMultipleGroupsResponse } });
const store = getMockStore(initialStoreState);
render(
<IntlProvider locale="en">
<Provider store={store}>
<EnterpriseSubsidiesContext.Provider value={subsEnterpriseSubsidiesContextValue}>
<PeopleManagementPage />
</EnterpriseSubsidiesContext.Provider>
</Provider>
</IntlProvider>,
);
expect(screen.getByText('captain crunch')).toBeInTheDocument();
expect(screen.getByText('Show all 4 groups')).toBeInTheDocument();
expect(screen.queryByText('fruity pebbles')).not.toBeInTheDocument();

const collapsible = screen.getByText('Show all 4 groups');
collapsible.click();
await waitFor(() => {
expect(screen.getByText('fruity pebbles')).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ export { default as useEnterpriseGroupMembersTableData } from './useEnterpriseGr
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';
Loading