Skip to content

Commit

Permalink
feat: adding flex group card grid
Browse files Browse the repository at this point in the history
  • Loading branch information
kiram15 committed Oct 16, 2024
1 parent 57c70ff commit b82c0f2
Show file tree
Hide file tree
Showing 11 changed files with 302 additions and 42 deletions.
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

0 comments on commit b82c0f2

Please sign in to comment.