diff --git a/src/components/academies/GoToAcademy.jsx b/src/components/academies/GoToAcademy.jsx
new file mode 100644
index 0000000000..58337c65eb
--- /dev/null
+++ b/src/components/academies/GoToAcademy.jsx
@@ -0,0 +1,36 @@
+import { Link } from 'react-router-dom';
+import { Button } from '@openedx/paragon';
+import { FormattedMessage } from '@edx/frontend-platform/i18n';
+import React from 'react';
+import useEnterpriseCustomer from '../app/data/hooks/useEnterpriseCustomer';
+import { useAcademies } from '../app/data';
+
+const GoToAcademy = () => {
+ const { data: academies } = useAcademies();
+ const { data: enterpriseCustomer } = useEnterpriseCustomer();
+
+ return (
+ <>
+
@@ -30,6 +33,7 @@ const wrapper = ({ children }) => (
describe('useRecommendCoursesForMe', () => {
beforeEach(() => {
jest.clearAllMocks();
+ useEnterpriseCustomer.mockReturnValue({ data: mockEnterpriseCustomer });
});
it('should not show recommend course CTA by default', () => {
@@ -44,24 +48,35 @@ describe('useRecommendCoursesForMe', () => {
mockRouteMatch: null, // simulates non-search page route
canOnlyViewHighlightSets: false,
isAssignmentsOnlyLearner: false,
+ enableOneAcademy: false,
hasRecommendCourseCTA: false,
},
{
mockRouteMatch: { path: '/search' }, // simulates search page route
canOnlyViewHighlightSets: false,
isAssignmentsOnlyLearner: false,
+ enableOneAcademy: false,
hasRecommendCourseCTA: true,
},
{
mockRouteMatch: { path: '/search' }, // simulates search page route
canOnlyViewHighlightSets: true,
isAssignmentsOnlyLearner: false,
+ enableOneAcademy: false,
+ hasRecommendCourseCTA: false,
+ },
+ {
+ mockRouteMatch: { path: '/search' }, // simulates search page route
+ canOnlyViewHighlightSets: false,
+ isAssignmentsOnlyLearner: false,
+ enableOneAcademy: true,
hasRecommendCourseCTA: false,
},
])('should support showing recommend course CTA, when appropriate (%s)', async ({
mockRouteMatch,
canOnlyViewHighlightSets,
isAssignmentsOnlyLearner,
+ enableOneAcademy,
hasRecommendCourseCTA,
}) => {
useMatch.mockReturnValue(mockRouteMatch);
@@ -71,6 +86,7 @@ describe('useRecommendCoursesForMe', () => {
},
});
useIsAssignmentsOnlyLearner.mockReturnValue(isAssignmentsOnlyLearner);
+ useEnterpriseCustomer.mockReturnValue({ data: { ...mockEnterpriseCustomer, enableOneAcademy } });
const { result } = renderHook(() => useRecommendCoursesForMe(), { wrapper });
diff --git a/src/components/app/data/services/data/__factories__/academies.factory.js b/src/components/app/data/services/data/__factories__/academies.factory.js
new file mode 100644
index 0000000000..fd4a27d702
--- /dev/null
+++ b/src/components/app/data/services/data/__factories__/academies.factory.js
@@ -0,0 +1,20 @@
+import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies
+import { faker } from '@faker-js/faker'; // eslint-disable-line import/no-extraneous-dependencies
+import { camelCaseObject } from '@edx/frontend-platform';
+import { v4 as uuidv4 } from 'uuid';
+
+Factory.define('academy')
+ .attr('uuid', uuidv4())
+ .attr('title', faker.lorem.words())
+ .attr('short_description', faker.lorem.sentence(50))
+ .attr('long_description', faker.lorem.sentence(20))
+ .attr('image', faker.image.urlPlaceholder())
+ .attr('tags', []);
+
+export function academyFactory(overrides = {}) {
+ return camelCaseObject(Factory.build('academy', overrides));
+}
+
+export function academiesFactory(count = 1, overrides = {}) {
+ return Array.from({ length: count }, () => academyFactory(overrides));
+}
diff --git a/src/components/app/data/services/data/__factories__/enterpriseCustomerUser.factory.js b/src/components/app/data/services/data/__factories__/enterpriseCustomerUser.factory.js
index 90fe9257fe..c2f6c8f01d 100644
--- a/src/components/app/data/services/data/__factories__/enterpriseCustomerUser.factory.js
+++ b/src/components/app/data/services/data/__factories__/enterpriseCustomerUser.factory.js
@@ -28,6 +28,7 @@ Factory.define('enterpriseCustomer')
.attr('enable_data_sharing_consent', true)
.attr('admin_users', [{ email: faker.internet.email() }])
.attr('disable_search', false)
+ .attr('enable_one_academy', false)
.attr('branding_configuration', {
logo: faker.image.urlPlaceholder(),
primary_color: faker.internet.color(),
diff --git a/src/components/app/data/services/data/__factories__/index.js b/src/components/app/data/services/data/__factories__/index.js
index 44562421a1..e8a3b5462f 100644
--- a/src/components/app/data/services/data/__factories__/index.js
+++ b/src/components/app/data/services/data/__factories__/index.js
@@ -1,3 +1,4 @@
import './enterpriseCustomerUser.factory';
export * from './enterpriseCustomerUser.factory';
+export * from './academies.factory';
diff --git a/src/components/dashboard/main-content/DashboardMainContent.test.jsx b/src/components/dashboard/main-content/DashboardMainContent.test.jsx
index 9e513f240a..d3efef53fc 100644
--- a/src/components/dashboard/main-content/DashboardMainContent.test.jsx
+++ b/src/components/dashboard/main-content/DashboardMainContent.test.jsx
@@ -7,14 +7,24 @@ import { QueryClientProvider } from '@tanstack/react-query';
import DashboardMainContent from './DashboardMainContent';
import { queryClient, renderWithRouter } from '../../../utils/tests';
import { features } from '../../../config';
-import { useCanOnlyViewHighlights, useEnterpriseCourseEnrollments, useEnterpriseCustomer } from '../../app/data';
-import { authenticatedUserFactory, enterpriseCustomerFactory } from '../../app/data/services/data/__factories__';
+import {
+ useCanOnlyViewHighlights,
+ useEnterpriseCourseEnrollments,
+ useEnterpriseCustomer,
+ useAcademies,
+} from '../../app/data';
+import {
+ authenticatedUserFactory,
+ enterpriseCustomerFactory,
+ academiesFactory,
+} from '../../app/data/services/data/__factories__';
jest.mock('../../app/data', () => ({
...jest.requireActual('../../app/data'),
useEnterpriseCustomer: jest.fn(),
useCanOnlyViewHighlights: jest.fn(),
useEnterpriseCourseEnrollments: jest.fn(),
+ useAcademies: jest.fn(),
}));
jest.mock('../../../config', () => ({
@@ -41,6 +51,7 @@ describe('DashboardMainContent', () => {
beforeEach(() => {
jest.clearAllMocks();
useEnterpriseCustomer.mockReturnValue({ data: mockEnterpriseCustomer });
+ useAcademies.mockReturnValue({ data: academiesFactory(3) });
useCanOnlyViewHighlights.mockReturnValue({ data: false });
useEnterpriseCourseEnrollments.mockReturnValue({
data: {
diff --git a/src/components/dashboard/main-content/course-enrollments/CourseEnrollmentsEmptyState.jsx b/src/components/dashboard/main-content/course-enrollments/CourseEnrollmentsEmptyState.jsx
index e0b0eeceb1..498450effb 100644
--- a/src/components/dashboard/main-content/course-enrollments/CourseEnrollmentsEmptyState.jsx
+++ b/src/components/dashboard/main-content/course-enrollments/CourseEnrollmentsEmptyState.jsx
@@ -4,13 +4,16 @@ import { Link } from 'react-router-dom';
import {
useEnterpriseCustomer,
- useCanOnlyViewHighlights,
+ useCanOnlyViewHighlights, useAcademies,
} from '../../../app/data';
import CourseRecommendations from '../CourseRecommendations';
+import GoToAcademy from '../../../academies/GoToAcademy';
const CourseEnrollmentsEmptyState = () => {
const { data: enterpriseCustomer } = useEnterpriseCustomer();
const { data: canOnlyViewHighlightSets } = useCanOnlyViewHighlights();
+ const { data: academies } = useAcademies();
+
if (enterpriseCustomer.disableSearch) {
return (
@@ -25,6 +28,11 @@ const CourseEnrollmentsEmptyState = () => {
);
}
+
+ if (enterpriseCustomer.enableOneAcademy && academies?.length === 1) {
+ return ;
+ }
+
return (
<>
@@ -45,7 +53,6 @@ const CourseEnrollmentsEmptyState = () => {
description="Label for Find a course button on enterprise dashboard's courses tab."
/>
-
{canOnlyViewHighlightSets === false && }
>
diff --git a/src/components/dashboard/tests/DashboardPage.test.jsx b/src/components/dashboard/tests/DashboardPage.test.jsx
index 67cddbf52f..9c31c70af1 100644
--- a/src/components/dashboard/tests/DashboardPage.test.jsx
+++ b/src/components/dashboard/tests/DashboardPage.test.jsx
@@ -25,6 +25,7 @@ import {
useCouponCodes,
useEnterpriseCourseEnrollments,
useEnterpriseCustomer,
+ useAcademies,
useEnterpriseOffers,
useEnterprisePathwaysList,
useEnterpriseProgramsList,
@@ -33,7 +34,11 @@ import {
useSubscriptions,
useHasAvailableSubsidiesOrRequests,
} from '../../app/data';
-import { authenticatedUserFactory, enterpriseCustomerFactory } from '../../app/data/services/data/__factories__';
+import {
+ authenticatedUserFactory,
+ enterpriseCustomerFactory,
+ academiesFactory,
+} from '../../app/data/services/data/__factories__';
const dummyProgramData = {
uuid: 'test-uuid',
@@ -98,6 +103,7 @@ jest.mock('../../app/data', () => ({
useBrowseAndRequest: jest.fn(),
useIsAssignmentsOnlyLearner: jest.fn(),
useHasAvailableSubsidiesOrRequests: jest.fn(),
+ useAcademies: jest.fn(),
}));
jest.mock('@edx/frontend-enterprise-utils', () => ({
@@ -200,6 +206,7 @@ describe('', () => {
beforeEach(() => {
jest.clearAllMocks();
useEnterpriseCustomer.mockReturnValue({ data: mockEnterpriseCustomer });
+ useAcademies.mockReturnValue({ data: academiesFactory(3) });
useSubscriptions.mockReturnValue({
data: {
subscriptionLicense: undefined,
diff --git a/src/components/search/data/searchLoader.js b/src/components/search/data/searchLoader.js
index 1fd7beabd4..8baa5def5c 100644
--- a/src/components/search/data/searchLoader.js
+++ b/src/components/search/data/searchLoader.js
@@ -1,5 +1,6 @@
+import { generatePath, redirect } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform/config';
-import { ensureAuthenticatedUser } from '../../app/routes/data';
+import { ensureAuthenticatedUser } from '../../app/routes/data/utils';
import { extractEnterpriseCustomer, queryAcademiesList, queryContentHighlightSets } from '../../app/data';
export default function makeSearchLoader(queryClient) {
@@ -18,11 +19,10 @@ export default function makeSearchLoader(queryClient) {
authenticatedUser,
enterpriseSlug,
});
- const searchData = [
- queryClient.ensureQueryData(
- queryAcademiesList(enterpriseCustomer.uuid),
- ),
- ];
+
+ const academiesListQuery = queryAcademiesList(enterpriseCustomer.uuid);
+
+ const searchData = [queryClient.ensureQueryData(academiesListQuery)];
if (getConfig().FEATURE_CONTENT_HIGHLIGHTS) {
searchData.push(
queryClient.ensureQueryData(
@@ -33,6 +33,15 @@ export default function makeSearchLoader(queryClient) {
await Promise.all(searchData);
+ const academies = queryClient.getQueryData(academiesListQuery.queryKey);
+ if (enterpriseCustomer.enableOneAcademy && academies.length === 1) {
+ const redirectPath = generatePath('/:enterpriseSlug/academies/:academyUUID', {
+ enterpriseSlug,
+ academyUUID: academies[0].uuid,
+ });
+ return redirect(redirectPath);
+ }
+
return null;
};
}
diff --git a/src/components/search/data/searchLoader.test.jsx b/src/components/search/data/searchLoader.test.jsx
index 89504fb738..860cf45e98 100644
--- a/src/components/search/data/searchLoader.test.jsx
+++ b/src/components/search/data/searchLoader.test.jsx
@@ -1,7 +1,8 @@
-import { screen } from '@testing-library/react';
+import { screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { getConfig } from '@edx/frontend-platform/config';
+import { when } from 'jest-when';
import { renderWithRouterProvider } from '../../../utils/tests';
import makeSearchLoader from './searchLoader';
import {
@@ -9,11 +10,11 @@ import {
queryAcademiesList,
queryContentHighlightSets,
} from '../../app/data';
-import { ensureAuthenticatedUser } from '../../app/routes/data';
+import { ensureAuthenticatedUser } from '../../app/routes/data/utils';
import { enterpriseCustomerFactory } from '../../app/data/services/data/__factories__';
-jest.mock('../../app/routes/data', () => ({
- ...jest.requireActual('../../app/routes/data'),
+jest.mock('../../app/routes/data/utils', () => ({
+ ...jest.requireActual('../../app/routes/data/utils'),
ensureAuthenticatedUser: jest.fn(),
}));
jest.mock('../../app/data', () => ({
@@ -37,8 +38,15 @@ jest.mock('@edx/frontend-platform/config', () => ({
const mockEnterpriseCustomer = enterpriseCustomerFactory();
extractEnterpriseCustomer.mockResolvedValue(mockEnterpriseCustomer);
+const mockAcademies = [
+ {
+ uuid: 'test-academy-uuid-1',
+ },
+];
+
const mockQueryClient = {
ensureQueryData: jest.fn().mockResolvedValue({}),
+ getQueryData: jest.fn().mockReturnValue(mockAcademies),
};
describe('searchLoader', () => {
@@ -94,6 +102,7 @@ describe('searchLoader', () => {
}),
);
});
+
it('ensures the requisite search data is resolved with content highlights', async () => {
getConfig.mockReturnValue({
FEATURE_CONTENT_HIGHLIGHTS: true,
@@ -124,4 +133,65 @@ describe('searchLoader', () => {
}),
);
});
+
+ it('Redirect learners whose enterprise has enabled one academy.', async () => {
+ extractEnterpriseCustomer.mockResolvedValue(enterpriseCustomerFactory({ enable_one_academy: true }));
+ const academiesQuery = queryAcademiesList(mockEnterpriseCustomer.uuid);
+
+ when(mockQueryClient.ensureQueryData).calledWith(
+ expect.objectContaining({
+ queryKey: academiesQuery.queryKey,
+ }),
+ ).mockResolvedValue(mockAcademies);
+ ensureAuthenticatedUser.mockResolvedValue({ userId: 3, username: 'test-user' });
+
+ renderWithRouterProvider({
+ path: '/:enterpriseSlug/search',
+ element:
,
+ loader: makeSearchLoader(mockQueryClient),
+ }, {
+ routes: [
+ {
+ path: '/:enterpriseCustomer/academies/:academyUUID',
+ element: ,
+ },
+ ],
+ initialEntries: [`/${mockEnterpriseCustomer.slug}/search`],
+ });
+
+ // Validate user is redirected to the academy details page.
+ await waitFor(() => {
+ expect(screen.getByTestId('academy-details-page')).toBeInTheDocument();
+ });
+ });
+
+ it('Does not redirect the learners if enterprise one academy is not enabled.', async () => {
+ extractEnterpriseCustomer.mockResolvedValue(mockEnterpriseCustomer);
+ const academiesQuery = queryAcademiesList(mockEnterpriseCustomer.uuid);
+ when(mockQueryClient.ensureQueryData).calledWith(
+ expect.objectContaining({
+ queryKey: academiesQuery.queryKey,
+ }),
+ ).mockResolvedValue(mockAcademies);
+ ensureAuthenticatedUser.mockResolvedValue({ userId: 3, username: 'test-user' });
+
+ renderWithRouterProvider({
+ path: '/:enterpriseSlug/search',
+ element: ,
+ loader: makeSearchLoader(mockQueryClient),
+ }, {
+ routes: [
+ {
+ path: '/:enterpriseCustomer/academies/:academyUUID',
+ element: ,
+ },
+ ],
+ initialEntries: [`/${mockEnterpriseCustomer.slug}/search`],
+ });
+
+ // Validate user is not redirected to the academy details page.
+ await waitFor(() => {
+ expect(screen.getByTestId('search-page')).toBeInTheDocument();
+ });
+ });
});
diff --git a/src/components/site-header/SiteHeaderNavMenu.jsx b/src/components/site-header/SiteHeaderNavMenu.jsx
index 94d265c5e4..6815032be4 100644
--- a/src/components/site-header/SiteHeaderNavMenu.jsx
+++ b/src/components/site-header/SiteHeaderNavMenu.jsx
@@ -1,12 +1,14 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { NavLink } from 'react-router-dom';
import { useEnterpriseCustomer, useIsAssignmentsOnlyLearner } from '../app/data';
+import { useContentDiscoveryNavLink } from './data';
const SiteHeaderNavMenu = () => {
const { data: enterpriseCustomer } = useEnterpriseCustomer();
const isAssignmentOnlyLearner = useIsAssignmentsOnlyLearner();
const intl = useIntl();
const mainMenuLinkClassName = 'nav-link';
+ const contentDiscoveryNavLink = useContentDiscoveryNavLink(mainMenuLinkClassName);
if (enterpriseCustomer.disableSearch) {
return null;
@@ -21,15 +23,7 @@ const SiteHeaderNavMenu = () => {
description: 'Dashboard link title in site header navigation.',
})}
- {!isAssignmentOnlyLearner && (
-
- {intl.formatMessage({
- id: 'site.header.nav.search.title',
- defaultMessage: 'Find a Course',
- description: 'Find a course link in site header navigation.',
- })}
-
- )}
+ {!isAssignmentOnlyLearner && contentDiscoveryNavLink}
>
);
};
diff --git a/src/components/site-header/data/hooks/index.js b/src/components/site-header/data/hooks/index.js
new file mode 100644
index 0000000000..ca435a42bc
--- /dev/null
+++ b/src/components/site-header/data/hooks/index.js
@@ -0,0 +1 @@
+export { default as useContentDiscoveryNavLink } from './useContentDiscoveryNavLink';
diff --git a/src/components/site-header/data/hooks/useContentDiscoveryNavLink.jsx b/src/components/site-header/data/hooks/useContentDiscoveryNavLink.jsx
new file mode 100644
index 0000000000..16b8a250f9
--- /dev/null
+++ b/src/components/site-header/data/hooks/useContentDiscoveryNavLink.jsx
@@ -0,0 +1,28 @@
+import { NavLink } from 'react-router-dom';
+import { FormattedMessage } from '@edx/frontend-platform/i18n';
+import { useAcademies, useEnterpriseCustomer } from '../../../app/data';
+
+export default function useContentDiscoveryNavLink(mainMenuLinkClassName) {
+ const { data: enterpriseCustomer } = useEnterpriseCustomer();
+ const { data: academies } = useAcademies();
+ if (enterpriseCustomer.enableOneAcademy && academies.length === 1) {
+ return (
+
+
+
+ );
+ }
+ return (
+
+
+
+ );
+}
diff --git a/src/components/site-header/data/index.js b/src/components/site-header/data/index.js
new file mode 100644
index 0000000000..4cc90d02bd
--- /dev/null
+++ b/src/components/site-header/data/index.js
@@ -0,0 +1 @@
+export * from './hooks';
diff --git a/src/components/site-header/tests/SiteHeader.test.jsx b/src/components/site-header/tests/SiteHeader.test.jsx
index ecc61e788f..5b7de7385a 100644
--- a/src/components/site-header/tests/SiteHeader.test.jsx
+++ b/src/components/site-header/tests/SiteHeader.test.jsx
@@ -10,12 +10,17 @@ import { getConfig } from '@edx/frontend-platform/config';
import SiteHeader from '../SiteHeader';
import { renderWithRouter, renderWithRouterProvider } from '../../../utils/tests';
-import { useEnterpriseCustomer, useEnterpriseLearner } from '../../app/data';
-import { authenticatedUserFactory, enterpriseCustomerFactory } from '../../app/data/services/data/__factories__';
+import { useAcademies, useEnterpriseCustomer, useEnterpriseLearner } from '../../app/data';
+import {
+ academiesFactory,
+ authenticatedUserFactory,
+ enterpriseCustomerFactory,
+} from '../../app/data/services/data/__factories__';
jest.mock('../../app/data', () => ({
...jest.requireActual('../../app/data'),
useEnterpriseLearner: jest.fn(),
+ useAcademies: jest.fn(),
useEnterpriseCustomer: jest.fn(),
useIsAssignmentsOnlyLearner: jest.fn(),
}));
@@ -88,6 +93,7 @@ describe('', () => {
],
},
});
+ useAcademies.mockReturnValue({ data: academiesFactory(3) });
});
test('renders link with logo to dashboard', () => {