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

feat: create zero state for bnr #1182

Merged
merged 3 commits into from
Mar 8, 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
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Button, Card } from '@edx/paragon';
import {
formatDate, formatPrice, useBudgetId, usePathToCatalogTab, useSubsidyAccessPolicy,
} from './data';
import nameYourLearner from './assets/nameYourLearners.svg';
import nameYourLearner from './assets/reading.svg';

const AssignMoreCoursesEmptyStateMinimal = () => {
const { subsidyAccessPolicyId } = useBudgetId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { Stack, Skeleton } from '@edx/paragon';
import BudgetDetailRedemptions from './BudgetDetailRedemptions';
import BudgetDetailAssignments from './BudgetDetailAssignments';
import { useBudgetDetailActivityOverview, useBudgetId, useSubsidyAccessPolicy } from './data';
import NoBudgetActivityEmptyState from './NoBudgetActivityEmptyState';
import NoAssignableBudgetActivity from './empty-state/NoAssignableBudgetActivity';
import NoBnEBudgetActivity from './empty-state/NoBnEBudgetActivity';

const BudgetDetailActivityTabContents = ({ enterpriseUUID, enterpriseFeatures }) => {
const isTopDownAssignmentEnabled = enterpriseFeatures.topDownAssignmentRealTimeLcm;
Expand All @@ -32,19 +33,22 @@ const BudgetDetailActivityTabContents = ({ enterpriseUUID, enterpriseFeatures })
);
}

const hasSpentTransactions = budgetActivityOverview.spentTransactions?.count > 0;
const hasContentAssignments = budgetActivityOverview.contentAssignments?.count > 0;

if (!isTopDownAssignmentEnabled || !subsidyAccessPolicy?.isAssignable) {
return <BudgetDetailRedemptions />;
return (
<>
{!hasSpentTransactions && (<NoBnEBudgetActivity />)}
<BudgetDetailRedemptions />
</>
);
}

const hasContentAssignments = !!budgetActivityOverview.contentAssignments?.count > 0;
const hasSpentTransactions = !!budgetActivityOverview.spentTransactions?.count > 0;

// If there is no activity whatsoever (no assignments, no spent transactions), show the
// full empty state.
if (!hasContentAssignments && !hasSpentTransactions) {
return (
<NoBudgetActivityEmptyState />
);
return <NoAssignableBudgetActivity />;
}

// Otherwise, render the contents of the "Activity" tab.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';

import { useIsLargeOrGreater, usePathToCatalogTab } from './data';
import nameYourLearners from './assets/nameYourLearners.svg';
import findTheRightCourse from './assets/findTheRightCourse.svg';
import confirmSpend from './assets/confirmSpend.svg';
import EVENT_NAMES from '../../eventTracking';
import { useIsLargeOrGreater, usePathToCatalogTab } from '../data';
import findTheRightCourse from '../assets/phoneScroll.svg';
import nameYourLearners from '../assets/reading.svg';
import confirmSpend from '../assets/wallet.svg';
import EVENT_NAMES from '../../../eventTracking';

const FindTheRightCourseIllustration = (props) => (
<img data-testid="find-the-right-course-illustration" src={findTheRightCourse} alt="" {...props} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from 'react';
import classNames from 'classnames';
import {
Button, Card, Row, Col,
} from '@edx/paragon';
import { Link } from 'react-router-dom';

import { useIsLargeOrGreater } from '../data';
import nameYourMembers from '../assets/reading.svg';
import memberBrowse from '../assets/phoneScroll.svg';
import enrollAndSpend from '../assets/wallet.svg';

const NameYourMembersIllustration = (props) => (
<img data-testid="name-your-members-illustration" src={nameYourMembers} alt="" {...props} />
);

const MemberBrowseIllustration = (props) => (
<img data-testid="members-browse-illustration" src={memberBrowse} alt="" {...props} />
);

const EnrollAndSpendIllustration = (props) => (
<img data-testid="enroll-and-spend-illustration" src={enrollAndSpend} alt="" {...props} />
);

const NoBnEBudgetActivity = () => {
const isLargeOrGreater = useIsLargeOrGreater();

return (
<Card className="mb-4">
<Card.Section className={classNames('text-center', { 'bg-light-300': isLargeOrGreater })}>
<h3 className={classNames({ 'mb-4.5': isLargeOrGreater })}>
No budget activity yet? Invite members to browse the catalog and enroll!
</h3>
{isLargeOrGreater && (
<Row>
<Col>
<NameYourMembersIllustration />
</Col>
<Col>
<MemberBrowseIllustration />
</Col>
<Col>
<EnrollAndSpendIllustration />
</Col>
</Row>
)}
</Card.Section>
<Card.Section className="text-center">
<Row className={classNames({ 'mb-5': isLargeOrGreater })}>
<Col className="mb-5 mb-lg-0">
{!isLargeOrGreater && <NameYourMembersIllustration className="mb-5" />}
<h4>
<span className="d-block text-brand mb-2">01</span>
Name your members
</h4>
<span>
Upload or enter email addresses to invite people to browse and enroll
using this budget.
</span>
</Col>
<Col className="mb-5 mb-lg-0">
{!isLargeOrGreater && <MemberBrowseIllustration className="mb-5" />}
<h4>
<span className="d-block text-brand mb-2">02</span>
Members find the right course
</h4>
<span>
Members can then browse the catalog associated with this budget and
find a course that aligns with their interests.
</span>
</Col>
<Col className="mb-5 mb-lg-0">
{!isLargeOrGreater && <EnrollAndSpendIllustration className="mb-5" />}
<h4>
<span className="d-block text-brand mb-2">03</span>
Members can enroll and spend
</h4>
<span>
Members can enroll in courses, subject to any limits in this budget&apos;s
settings. The deducted costs from this budget will be visible right here
in your budget activity!
</span>
</Col>
</Row>
<Row>
<Col>
<Button
as={Link}
>
Get started
</Button>
</Col>
</Row>
</Card.Section>
</Card>
);
};

export default NoBnEBudgetActivity;
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ describe('<BudgetDetailPage />', () => {
it.each([
{ isLargeViewport: true },
{ isLargeViewport: false },
])('displays budget activity overview empty state', async ({ isLargeViewport }) => {
])('displays assignable budget activity overview empty state', async ({ isLargeViewport }) => {
useIsLargeOrGreater.mockReturnValue(isLargeViewport);
useParams.mockReturnValue({
enterpriseSlug: 'test-enterprise-slug',
Expand All @@ -472,6 +472,39 @@ describe('<BudgetDetailPage />', () => {
await waitFor(() => expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1));
});

it.each([
{ isLargeViewport: true },
{ isLargeViewport: false },
])('displays bnr budget activity overview empty state', async ({ isLargeViewport }) => {
useIsLargeOrGreater.mockReturnValue(isLargeViewport);
useParams.mockReturnValue({
enterpriseSlug: 'test-enterprise-slug',
enterpriseAppPage: 'test-enterprise-page',
budgetId: 'a52e6548-649f-4576-b73f-c5c2bee25e9c',
activeTabKey: 'activity',
});
useSubsidyAccessPolicy.mockReturnValue({
isInitialLoading: false,
data: mockPerLearnerSpendLimitSubsidyAccessPolicy,
});
useBudgetDetailActivityOverview.mockReturnValue({
isLoading: false,
data: mockEmptyStateBudgetDetailActivityOverview,
});
useBudgetRedemptions.mockReturnValue({
isLoading: false,
budgetRedemptions: mockEmptyBudgetRedemptions,
fetchBudgetRedemptions: jest.fn(),
});
renderWithRouter(<BudgetDetailPageWrapper />);

// Overview empty state (no content assignments, no spent transactions)
expect(screen.getByText('No budget activity yet? Invite members to browse the catalog and enroll!')).toBeInTheDocument();
const illustrationTestIds = ['name-your-members-illustration', 'members-browse-illustration', 'enroll-and-spend-illustration'];
illustrationTestIds.forEach(testId => expect(screen.getByTestId(testId)).toBeInTheDocument());
expect(screen.getByText('Get started', { selector: 'a' })).toBeInTheDocument();
});

it.each([
{
budgetId: mockEnterpriseOfferId,
Expand Down
Loading