From 09da1fff1ea056b1527b564be044b0ebbf9d2f10 Mon Sep 17 00:00:00 2001 From: Kira Miller <31229189+kiram15@users.noreply.github.com> Date: Thu, 7 Mar 2024 12:42:07 -0700 Subject: [PATCH 1/4] feat: create zero state for bnr (#1179) * feat: create zero state for bnr * fix: PR requests --- .../AssignMoreCoursesEmptyStateMinimal.jsx | 2 +- .../BudgetDetailActivityTabContents.jsx | 20 ++-- ...findTheRightCourse.svg => phoneScroll.svg} | 0 .../{nameYourLearners.svg => reading.svg} | 0 .../assets/{confirmSpend.svg => wallet.svg} | 0 .../NoAssignableBudgetActivity.jsx} | 10 +- .../empty-state/NoBnEBudgetActivity.jsx | 99 +++++++++++++++++++ .../tests/BudgetDetailPage.test.jsx | 35 ++++++- 8 files changed, 151 insertions(+), 15 deletions(-) rename src/components/learner-credit-management/assets/{findTheRightCourse.svg => phoneScroll.svg} (100%) rename src/components/learner-credit-management/assets/{nameYourLearners.svg => reading.svg} (100%) rename src/components/learner-credit-management/assets/{confirmSpend.svg => wallet.svg} (100%) rename src/components/learner-credit-management/{NoBudgetActivityEmptyState.jsx => empty-state/NoAssignableBudgetActivity.jsx} (92%) create mode 100644 src/components/learner-credit-management/empty-state/NoBnEBudgetActivity.jsx diff --git a/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx b/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx index d51595c5e9..25d1a02a6b 100644 --- a/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx +++ b/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx @@ -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(); diff --git a/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx b/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx index 21e65d2de1..9720c607a9 100644 --- a/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx +++ b/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx @@ -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; @@ -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 ; + return ( + <> + {!hasSpentTransactions && ()} + + + ); } - 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 ( - - ); + return ; } // Otherwise, render the contents of the "Activity" tab. diff --git a/src/components/learner-credit-management/assets/findTheRightCourse.svg b/src/components/learner-credit-management/assets/phoneScroll.svg similarity index 100% rename from src/components/learner-credit-management/assets/findTheRightCourse.svg rename to src/components/learner-credit-management/assets/phoneScroll.svg diff --git a/src/components/learner-credit-management/assets/nameYourLearners.svg b/src/components/learner-credit-management/assets/reading.svg similarity index 100% rename from src/components/learner-credit-management/assets/nameYourLearners.svg rename to src/components/learner-credit-management/assets/reading.svg diff --git a/src/components/learner-credit-management/assets/confirmSpend.svg b/src/components/learner-credit-management/assets/wallet.svg similarity index 100% rename from src/components/learner-credit-management/assets/confirmSpend.svg rename to src/components/learner-credit-management/assets/wallet.svg diff --git a/src/components/learner-credit-management/NoBudgetActivityEmptyState.jsx b/src/components/learner-credit-management/empty-state/NoAssignableBudgetActivity.jsx similarity index 92% rename from src/components/learner-credit-management/NoBudgetActivityEmptyState.jsx rename to src/components/learner-credit-management/empty-state/NoAssignableBudgetActivity.jsx index 53559092e1..a59c245f88 100644 --- a/src/components/learner-credit-management/NoBudgetActivityEmptyState.jsx +++ b/src/components/learner-credit-management/empty-state/NoAssignableBudgetActivity.jsx @@ -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) => ( diff --git a/src/components/learner-credit-management/empty-state/NoBnEBudgetActivity.jsx b/src/components/learner-credit-management/empty-state/NoBnEBudgetActivity.jsx new file mode 100644 index 0000000000..ecfd3f3c06 --- /dev/null +++ b/src/components/learner-credit-management/empty-state/NoBnEBudgetActivity.jsx @@ -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) => ( + +); + +const MemberBrowseIllustration = (props) => ( + +); + +const EnrollAndSpendIllustration = (props) => ( + +); + +const NoBnEBudgetActivity = () => { + const isLargeOrGreater = useIsLargeOrGreater(); + + return ( + + +

+ No budget activity yet? Invite members to browse the catalog and enroll! +

+ {isLargeOrGreater && ( + + + + + + + + + + + + )} +
+ + + + {!isLargeOrGreater && } +

+ 01 + Name your members +

+ + Upload or enter email addresses to invite people to browse and enroll + using this budget. + + + + {!isLargeOrGreater && } +

+ 02 + Members find the right course +

+ + Members can then browse the catalog associated with this budget and + find a course that aligns with their interests. + + + + {!isLargeOrGreater && } +

+ 03 + Members can enroll and spend +

+ + Members can enroll in courses, subject to any limits in this budget's + settings. The deducted costs from this budget will be visible right here + in your budget activity! + + +
+ + + + + +
+
+ ); +}; + +export default NoBnEBudgetActivity; diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index f05394b572..07423a77d2 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -445,7 +445,7 @@ describe('', () => { 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', @@ -472,6 +472,39 @@ describe('', () => { 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(); + + // 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, From 56ef352435ef6f98aaf32d2a4ac9aa0c37cb33b8 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Thu, 7 Mar 2024 18:02:22 -0500 Subject: [PATCH 2/4] Revert "feat: create zero state for bnr (#1179)" (#1181) This reverts commit 09da1fff1ea056b1527b564be044b0ebbf9d2f10. --- .../AssignMoreCoursesEmptyStateMinimal.jsx | 2 +- .../BudgetDetailActivityTabContents.jsx | 20 ++-- ...ity.jsx => NoBudgetActivityEmptyState.jsx} | 10 +- .../assets/{wallet.svg => confirmSpend.svg} | 0 ...phoneScroll.svg => findTheRightCourse.svg} | 0 .../{reading.svg => nameYourLearners.svg} | 0 .../empty-state/NoBnEBudgetActivity.jsx | 99 ------------------- .../tests/BudgetDetailPage.test.jsx | 35 +------ 8 files changed, 15 insertions(+), 151 deletions(-) rename src/components/learner-credit-management/{empty-state/NoAssignableBudgetActivity.jsx => NoBudgetActivityEmptyState.jsx} (92%) rename src/components/learner-credit-management/assets/{wallet.svg => confirmSpend.svg} (100%) rename src/components/learner-credit-management/assets/{phoneScroll.svg => findTheRightCourse.svg} (100%) rename src/components/learner-credit-management/assets/{reading.svg => nameYourLearners.svg} (100%) delete mode 100644 src/components/learner-credit-management/empty-state/NoBnEBudgetActivity.jsx diff --git a/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx b/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx index 25d1a02a6b..d51595c5e9 100644 --- a/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx +++ b/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx @@ -5,7 +5,7 @@ import { Button, Card } from '@edx/paragon'; import { formatDate, formatPrice, useBudgetId, usePathToCatalogTab, useSubsidyAccessPolicy, } from './data'; -import nameYourLearner from './assets/reading.svg'; +import nameYourLearner from './assets/nameYourLearners.svg'; const AssignMoreCoursesEmptyStateMinimal = () => { const { subsidyAccessPolicyId } = useBudgetId(); diff --git a/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx b/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx index 9720c607a9..21e65d2de1 100644 --- a/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx +++ b/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx @@ -6,8 +6,7 @@ import { Stack, Skeleton } from '@edx/paragon'; import BudgetDetailRedemptions from './BudgetDetailRedemptions'; import BudgetDetailAssignments from './BudgetDetailAssignments'; import { useBudgetDetailActivityOverview, useBudgetId, useSubsidyAccessPolicy } from './data'; -import NoAssignableBudgetActivity from './empty-state/NoAssignableBudgetActivity'; -import NoBnEBudgetActivity from './empty-state/NoBnEBudgetActivity'; +import NoBudgetActivityEmptyState from './NoBudgetActivityEmptyState'; const BudgetDetailActivityTabContents = ({ enterpriseUUID, enterpriseFeatures }) => { const isTopDownAssignmentEnabled = enterpriseFeatures.topDownAssignmentRealTimeLcm; @@ -33,22 +32,19 @@ const BudgetDetailActivityTabContents = ({ enterpriseUUID, enterpriseFeatures }) ); } - const hasSpentTransactions = budgetActivityOverview.spentTransactions?.count > 0; - const hasContentAssignments = budgetActivityOverview.contentAssignments?.count > 0; - if (!isTopDownAssignmentEnabled || !subsidyAccessPolicy?.isAssignable) { - return ( - <> - {!hasSpentTransactions && ()} - - - ); + return ; } + 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 ; + return ( + + ); } // Otherwise, render the contents of the "Activity" tab. diff --git a/src/components/learner-credit-management/empty-state/NoAssignableBudgetActivity.jsx b/src/components/learner-credit-management/NoBudgetActivityEmptyState.jsx similarity index 92% rename from src/components/learner-credit-management/empty-state/NoAssignableBudgetActivity.jsx rename to src/components/learner-credit-management/NoBudgetActivityEmptyState.jsx index a59c245f88..53559092e1 100644 --- a/src/components/learner-credit-management/empty-state/NoAssignableBudgetActivity.jsx +++ b/src/components/learner-credit-management/NoBudgetActivityEmptyState.jsx @@ -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 findTheRightCourse from '../assets/phoneScroll.svg'; -import nameYourLearners from '../assets/reading.svg'; -import confirmSpend from '../assets/wallet.svg'; -import EVENT_NAMES from '../../../eventTracking'; +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'; const FindTheRightCourseIllustration = (props) => ( diff --git a/src/components/learner-credit-management/assets/wallet.svg b/src/components/learner-credit-management/assets/confirmSpend.svg similarity index 100% rename from src/components/learner-credit-management/assets/wallet.svg rename to src/components/learner-credit-management/assets/confirmSpend.svg diff --git a/src/components/learner-credit-management/assets/phoneScroll.svg b/src/components/learner-credit-management/assets/findTheRightCourse.svg similarity index 100% rename from src/components/learner-credit-management/assets/phoneScroll.svg rename to src/components/learner-credit-management/assets/findTheRightCourse.svg diff --git a/src/components/learner-credit-management/assets/reading.svg b/src/components/learner-credit-management/assets/nameYourLearners.svg similarity index 100% rename from src/components/learner-credit-management/assets/reading.svg rename to src/components/learner-credit-management/assets/nameYourLearners.svg diff --git a/src/components/learner-credit-management/empty-state/NoBnEBudgetActivity.jsx b/src/components/learner-credit-management/empty-state/NoBnEBudgetActivity.jsx deleted file mode 100644 index ecfd3f3c06..0000000000 --- a/src/components/learner-credit-management/empty-state/NoBnEBudgetActivity.jsx +++ /dev/null @@ -1,99 +0,0 @@ -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) => ( - -); - -const MemberBrowseIllustration = (props) => ( - -); - -const EnrollAndSpendIllustration = (props) => ( - -); - -const NoBnEBudgetActivity = () => { - const isLargeOrGreater = useIsLargeOrGreater(); - - return ( - - -

- No budget activity yet? Invite members to browse the catalog and enroll! -

- {isLargeOrGreater && ( - - - - - - - - - - - - )} -
- - - - {!isLargeOrGreater && } -

- 01 - Name your members -

- - Upload or enter email addresses to invite people to browse and enroll - using this budget. - - - - {!isLargeOrGreater && } -

- 02 - Members find the right course -

- - Members can then browse the catalog associated with this budget and - find a course that aligns with their interests. - - - - {!isLargeOrGreater && } -

- 03 - Members can enroll and spend -

- - Members can enroll in courses, subject to any limits in this budget's - settings. The deducted costs from this budget will be visible right here - in your budget activity! - - -
- - - - - -
-
- ); -}; - -export default NoBnEBudgetActivity; diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 07423a77d2..f05394b572 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -445,7 +445,7 @@ describe('', () => { it.each([ { isLargeViewport: true }, { isLargeViewport: false }, - ])('displays assignable budget activity overview empty state', async ({ isLargeViewport }) => { + ])('displays budget activity overview empty state', async ({ isLargeViewport }) => { useIsLargeOrGreater.mockReturnValue(isLargeViewport); useParams.mockReturnValue({ enterpriseSlug: 'test-enterprise-slug', @@ -472,39 +472,6 @@ describe('', () => { 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(); - - // 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, From 7c271638103677fec4d2222900190f6925633be7 Mon Sep 17 00:00:00 2001 From: Kira Miller <31229189+kiram15@users.noreply.github.com> Date: Fri, 8 Mar 2024 08:06:01 -0700 Subject: [PATCH 3/4] feat: create zero state for bnr (#1182) * feat: create zero state for bnr * fix: PR requests --- .../AssignMoreCoursesEmptyStateMinimal.jsx | 2 +- .../BudgetDetailActivityTabContents.jsx | 20 ++-- ...findTheRightCourse.svg => phoneScroll.svg} | 0 .../{nameYourLearners.svg => reading.svg} | 0 .../assets/{confirmSpend.svg => wallet.svg} | 0 .../NoAssignableBudgetActivity.jsx} | 10 +- .../empty-state/NoBnEBudgetActivity.jsx | 99 +++++++++++++++++++ .../tests/BudgetDetailPage.test.jsx | 35 ++++++- 8 files changed, 151 insertions(+), 15 deletions(-) rename src/components/learner-credit-management/assets/{findTheRightCourse.svg => phoneScroll.svg} (100%) rename src/components/learner-credit-management/assets/{nameYourLearners.svg => reading.svg} (100%) rename src/components/learner-credit-management/assets/{confirmSpend.svg => wallet.svg} (100%) rename src/components/learner-credit-management/{NoBudgetActivityEmptyState.jsx => empty-state/NoAssignableBudgetActivity.jsx} (92%) create mode 100644 src/components/learner-credit-management/empty-state/NoBnEBudgetActivity.jsx diff --git a/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx b/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx index d51595c5e9..25d1a02a6b 100644 --- a/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx +++ b/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx @@ -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(); diff --git a/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx b/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx index 21e65d2de1..9720c607a9 100644 --- a/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx +++ b/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx @@ -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; @@ -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 ; + return ( + <> + {!hasSpentTransactions && ()} + + + ); } - 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 ( - - ); + return ; } // Otherwise, render the contents of the "Activity" tab. diff --git a/src/components/learner-credit-management/assets/findTheRightCourse.svg b/src/components/learner-credit-management/assets/phoneScroll.svg similarity index 100% rename from src/components/learner-credit-management/assets/findTheRightCourse.svg rename to src/components/learner-credit-management/assets/phoneScroll.svg diff --git a/src/components/learner-credit-management/assets/nameYourLearners.svg b/src/components/learner-credit-management/assets/reading.svg similarity index 100% rename from src/components/learner-credit-management/assets/nameYourLearners.svg rename to src/components/learner-credit-management/assets/reading.svg diff --git a/src/components/learner-credit-management/assets/confirmSpend.svg b/src/components/learner-credit-management/assets/wallet.svg similarity index 100% rename from src/components/learner-credit-management/assets/confirmSpend.svg rename to src/components/learner-credit-management/assets/wallet.svg diff --git a/src/components/learner-credit-management/NoBudgetActivityEmptyState.jsx b/src/components/learner-credit-management/empty-state/NoAssignableBudgetActivity.jsx similarity index 92% rename from src/components/learner-credit-management/NoBudgetActivityEmptyState.jsx rename to src/components/learner-credit-management/empty-state/NoAssignableBudgetActivity.jsx index 53559092e1..a59c245f88 100644 --- a/src/components/learner-credit-management/NoBudgetActivityEmptyState.jsx +++ b/src/components/learner-credit-management/empty-state/NoAssignableBudgetActivity.jsx @@ -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) => ( diff --git a/src/components/learner-credit-management/empty-state/NoBnEBudgetActivity.jsx b/src/components/learner-credit-management/empty-state/NoBnEBudgetActivity.jsx new file mode 100644 index 0000000000..ecfd3f3c06 --- /dev/null +++ b/src/components/learner-credit-management/empty-state/NoBnEBudgetActivity.jsx @@ -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) => ( + +); + +const MemberBrowseIllustration = (props) => ( + +); + +const EnrollAndSpendIllustration = (props) => ( + +); + +const NoBnEBudgetActivity = () => { + const isLargeOrGreater = useIsLargeOrGreater(); + + return ( + + +

+ No budget activity yet? Invite members to browse the catalog and enroll! +

+ {isLargeOrGreater && ( + + + + + + + + + + + + )} +
+ + + + {!isLargeOrGreater && } +

+ 01 + Name your members +

+ + Upload or enter email addresses to invite people to browse and enroll + using this budget. + + + + {!isLargeOrGreater && } +

+ 02 + Members find the right course +

+ + Members can then browse the catalog associated with this budget and + find a course that aligns with their interests. + + + + {!isLargeOrGreater && } +

+ 03 + Members can enroll and spend +

+ + Members can enroll in courses, subject to any limits in this budget's + settings. The deducted costs from this budget will be visible right here + in your budget activity! + + +
+ + + + + +
+
+ ); +}; + +export default NoBnEBudgetActivity; diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index f05394b572..07423a77d2 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -445,7 +445,7 @@ describe('', () => { 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', @@ -472,6 +472,39 @@ describe('', () => { 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(); + + // 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, From 135cb6ec381b0a36db7b19d214030aaa4dfba4b3 Mon Sep 17 00:00:00 2001 From: Katrina Nguyen <71999631+katrinan029@users.noreply.github.com> Date: Fri, 8 Mar 2024 08:43:10 -0800 Subject: [PATCH 4/4] feat: add filtering and search to budget landing page (#1180) * feat: add filtering and search to budget landing page --- .../learner-credit-management/BudgetCard.jsx | 60 +++++---- .../MultipleBudgetsPicker.jsx | 90 ++++++++++---- .../SubBudgetCard.jsx | 7 +- .../tests/BudgetCard.test.jsx | 117 +++++++++--------- .../tests/MultipleBudgetsPage.test.jsx | 10 +- 5 files changed, 174 insertions(+), 110 deletions(-) diff --git a/src/components/learner-credit-management/BudgetCard.jsx b/src/components/learner-credit-management/BudgetCard.jsx index 78d22f57c7..5eb4f2d678 100644 --- a/src/components/learner-credit-management/BudgetCard.jsx +++ b/src/components/learner-credit-management/BudgetCard.jsx @@ -17,46 +17,55 @@ import { BUDGET_TYPES } from '../EnterpriseApp/data/constants'; * * @returns Budget card component(s). */ -const BudgetCard = ({ - budget, - enterpriseUUID, - enterpriseSlug, -}) => { +const BudgetCard = ({ original }) => { + const { + aggregates, + end, + enterpriseSlug, + enterpriseUUID, + id, + isAssignable, + isRetired, + name, + source, + start, + } = original; + const { isLoading: isLoadingSubsidySummaryAnalyticsApi, subsidySummary: subsidySummaryAnalyticsApi, - } = useSubsidySummaryAnalyticsApi(enterpriseUUID, budget.id, budget.source); + } = useSubsidySummaryAnalyticsApi(enterpriseUUID, id, source); // Subsidy Access Policies will always have a single budget, so we can render a single card // without relying on `useSubsidySummaryAnalyticsApi`. - if (budget.source === BUDGET_TYPES.policy) { + if (source === BUDGET_TYPES.policy) { return ( ); } // Enterprise Offers (ecommerce) will always have a single budget, so we can render a single card. - if (budget.source === BUDGET_TYPES.ecommerce) { + if (source === BUDGET_TYPES.ecommerce) { return ( ); @@ -74,8 +83,8 @@ const BudgetCard = ({ isLoading={isLoadingSubsidySummaryAnalyticsApi} key={subBudget.subsidyAccessPolicyUuid} id={subBudget.subsidyAccessPolicyUuid} - start={budget.start} - end={budget.end} + start={start} + end={end} available={subBudget.remainingFunds} spent={subBudget.redeemedFunds} displayName={subBudget.subsidyAccessPolicyDisplayName} @@ -85,7 +94,7 @@ const BudgetCard = ({ }; BudgetCard.propTypes = { - budget: PropTypes.shape({ + original: PropTypes.shape({ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, name: PropTypes.string.isRequired, start: PropTypes.string.isRequired, @@ -98,9 +107,10 @@ BudgetCard.propTypes = { }), isAssignable: PropTypes.bool, isRetired: PropTypes.bool, + enterpriseUUID: PropTypes.string.isRequired, + enterpriseSlug: PropTypes.string.isRequired, + status: PropTypes.string, }).isRequired, - enterpriseUUID: PropTypes.string.isRequired, - enterpriseSlug: PropTypes.string.isRequired, }; export default BudgetCard; diff --git a/src/components/learner-credit-management/MultipleBudgetsPicker.jsx b/src/components/learner-credit-management/MultipleBudgetsPicker.jsx index 2477b74281..0af1fe06a5 100644 --- a/src/components/learner-credit-management/MultipleBudgetsPicker.jsx +++ b/src/components/learner-credit-management/MultipleBudgetsPicker.jsx @@ -1,13 +1,17 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; import { - Stack, + DataTable, + CardView, + TextFilter, + CheckboxFilter, Row, Col, } from '@edx/paragon'; +import groupBy from 'lodash/groupBy'; import BudgetCard from './BudgetCard'; -import { orderBudgets } from './data/utils'; +import { getBudgetStatus, orderBudgets } from './data/utils'; const MultipleBudgetsPicker = ({ budgets, @@ -16,27 +20,71 @@ const MultipleBudgetsPicker = ({ enableLearnerPortal, }) => { const orderedBudgets = orderBudgets(budgets); + + const rows = useMemo( + () => orderedBudgets.map(budget => { + const budgetLabel = getBudgetStatus({ + startDateStr: budget.start, + endDateStr: budget.end, + isBudgetRetired: budget.isRetired, + }); + + return ({ + ...budget, + status: budgetLabel.status, + enterpriseUUID, + enterpriseSlug, + enableLearnerPortal, + }); + }), + [orderedBudgets, enterpriseUUID, enterpriseSlug, enableLearnerPortal], + ); + + const budgetLabels = orderedBudgets.map(budget => ( + getBudgetStatus({ + startDateStr: budget.start, + endDateStr: budget.end, + isBudgetRetired: budget.isRetired, + }) + )); + const budgetLabelsByStatus = groupBy(budgetLabels, 'status'); + const reducedChoices = Object.keys(budgetLabelsByStatus).map(budgetLabel => ({ + name: budgetLabel, + number: budgetLabelsByStatus[budgetLabel].length, + value: budgetLabel, + })); + return ( - - + <> +

Budgets

- - - - {orderedBudgets.map(budget => ( - - ))} - - - -
+ + + + + + ); }; diff --git a/src/components/learner-credit-management/SubBudgetCard.jsx b/src/components/learner-credit-management/SubBudgetCard.jsx index 16a2b1cc7a..f27e0f0c77 100644 --- a/src/components/learner-credit-management/SubBudgetCard.jsx +++ b/src/components/learner-credit-management/SubBudgetCard.jsx @@ -6,7 +6,6 @@ import classNames from 'classnames'; import { Card, Button, - Row, Col, Badge, Stack, @@ -123,8 +122,8 @@ const BaseSubBudgetCard = ({ title={

Balance

} muted > - - + +
Available
{isFetchingBudgets ? : formatPrice(available)} @@ -144,7 +143,7 @@ const BaseSubBudgetCard = ({ {isFetchingBudgets ? : formatPrice(spent)} -
+ ); diff --git a/src/components/learner-credit-management/tests/BudgetCard.test.jsx b/src/components/learner-credit-management/tests/BudgetCard.test.jsx index 80ec4dc7db..43687867db 100644 --- a/src/components/learner-credit-management/tests/BudgetCard.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetCard.test.jsx @@ -90,18 +90,20 @@ describe('', () => { }); it('displays correctly for a scheduled Enterprise Offers (ecommerce)', () => { + const mockBudgetAggregates = { + total: 5000, + spent: 200, + available: 4800, + }; const mockBudget = { id: mockEnterpriseOfferId, name: mockBudgetDisplayName, start: '3022-01-01', end: '3023-01-01', source: BUDGET_TYPES.ecommerce, + aggregates: mockBudgetAggregates, }; - const mockBudgetAggregates = { - total: 5000, - spent: 200, - available: 4800, - }; + useSubsidySummaryAnalyticsApi.mockReturnValue({ isLoading: false, subsidySummary: { @@ -116,9 +118,7 @@ describe('', () => { }); render(); expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); @@ -130,17 +130,18 @@ describe('', () => { }); it('displays correctly for a scheduled Subsidy (enterprise-subsidy)', () => { + const mockBudgetAggregates = { + total: 5000, + spent: 200, + available: 4800, + }; const mockBudget = { id: mockEnterpriseOfferId, name: mockBudgetDisplayName, start: '3022-01-01', end: '4023-01-01', source: BUDGET_TYPES.subsidy, - }; - const mockBudgetAggregates = { - total: 5000, - spent: 200, - available: 4800, + aggregates: mockBudgetAggregates, }; useSubsidySummaryAnalyticsApi.mockReturnValue({ isLoading: false, @@ -167,9 +168,7 @@ describe('', () => { }); render(); expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); @@ -202,6 +201,8 @@ describe('', () => { spent: mockBudgetAggregates.spent, }, isAssignable: isAssignableBudget, + enterpriseSlug, + enterpriseUUID, }; useSubsidySummaryAnalyticsApi.mockReturnValue({ isLoading: false, @@ -209,9 +210,7 @@ describe('', () => { }); render(); expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); @@ -223,17 +222,20 @@ describe('', () => { }); it('displays correctly for an expired Enterprise Offers (ecommerce)', () => { + const mockBudgetAggregates = { + total: 5000, + spent: 200, + available: 4800, + }; const mockBudget = { id: mockEnterpriseOfferId, name: mockBudgetDisplayName, start: '2022-01-01', end: '2023-01-01', source: BUDGET_TYPES.ecommerce, - }; - const mockBudgetAggregates = { - total: 5000, - spent: 200, - available: 4800, + aggregates: mockBudgetAggregates, + enterpriseSlug, + enterpriseUUID, }; useSubsidySummaryAnalyticsApi.mockReturnValue({ isLoading: false, @@ -249,9 +251,7 @@ describe('', () => { }); render(); expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); @@ -268,17 +268,20 @@ describe('', () => { }); it('displays correctly for an expired Subsidy (enterprise-subsidy)', () => { + const mockBudgetAggregates = { + total: 5000, + spent: 200, + available: 4800, + }; const mockBudget = { id: mockEnterpriseOfferId, name: mockBudgetDisplayName, start: '2022-01-01', end: '2023-01-01', source: BUDGET_TYPES.subsidy, - }; - const mockBudgetAggregates = { - total: 5000, - spent: 200, - available: 4800, + aggregates: mockBudgetAggregates, + enterpriseSlug, + enterpriseUUID, }; useSubsidySummaryAnalyticsApi.mockReturnValue({ isLoading: false, @@ -305,9 +308,7 @@ describe('', () => { }); render(); expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); @@ -345,6 +346,8 @@ describe('', () => { spent: mockBudgetAggregates.spent, }, isAssignable: isAssignableBudget, + enterpriseSlug, + enterpriseUUID, }; useSubsidySummaryAnalyticsApi.mockReturnValue({ isLoading: false, @@ -352,9 +355,7 @@ describe('', () => { }); render(); expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); @@ -371,17 +372,20 @@ describe('', () => { }); it('displays correctly for a current Enterprise Offers (ecommerce)', () => { + const mockBudgetAggregates = { + total: 5000, + spent: 200, + available: 4800, + }; const mockBudget = { id: mockEnterpriseOfferId, name: mockBudgetDisplayName, start: '2022-01-01', end: '3022-01-01', source: BUDGET_TYPES.ecommerce, - }; - const mockBudgetAggregates = { - total: 5000, - spent: 200, - available: 4800, + aggregates: mockBudgetAggregates, + enterpriseSlug, + enterpriseUUID, }; useSubsidySummaryAnalyticsApi.mockReturnValue({ isLoading: false, @@ -397,9 +401,7 @@ describe('', () => { }); render(); expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); @@ -423,17 +425,20 @@ describe('', () => { }); it('displays correctly for a current Subsidy (enterprise-subsidy)', () => { + const mockBudgetAggregates = { + total: 5000, + spent: 200, + available: 4800, + }; const mockBudget = { id: mockEnterpriseOfferId, name: mockBudgetDisplayName, start: '2022-01-01', end: '3023-01-01', source: BUDGET_TYPES.subsidy, - }; - const mockBudgetAggregates = { - total: 5000, - spent: 200, - available: 4800, + aggregates: mockBudgetAggregates, + enterpriseSlug, + enterpriseUUID, }; useSubsidySummaryAnalyticsApi.mockReturnValue({ isLoading: false, @@ -460,9 +465,7 @@ describe('', () => { }); render(); expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); @@ -507,6 +510,8 @@ describe('', () => { spent: mockBudgetAggregates.spent, }, isAssignable: isAssignableBudget, + enterpriseSlug, + enterpriseUUID, }; useSubsidySummaryAnalyticsApi.mockReturnValue({ isLoading: false, @@ -514,9 +519,7 @@ describe('', () => { }); render(); expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); diff --git a/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx b/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx index f81d9afeb5..041114312b 100644 --- a/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx +++ b/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx @@ -10,6 +10,8 @@ import { } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + import { EnterpriseSubsidiesContext } from '../../EnterpriseSubsidiesContext'; import MultipleBudgetsPage from '../MultipleBudgetsPage'; import { queryClient } from '../../test/testUtils'; @@ -61,9 +63,11 @@ const MultipleBudgetsPageWrapper = ({ }) => ( - - - + + + + + );