Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
745210b
feat: add program dashboard directory (#1)
MaxFrank13 Oct 15, 2025
91b6925
feat: add program dashboard directory (#1)
MaxFrank13 Oct 15, 2025
17ae415
feat: add program list page
MaxFrank13 Oct 17, 2025
51c4db5
feat: add program list view
MaxFrank13 Oct 21, 2025
5f0999f
fix: deps
MaxFrank13 Oct 21, 2025
6b99660
fix: tests
MaxFrank13 Oct 21, 2025
0d9be86
Merge branch 'program-dashboard-feature' into mfrank/add-program-list…
MaxFrank13 Oct 21, 2025
2b77225
fix: removed comment
MaxFrank13 Oct 21, 2025
1a901f5
feat: add program list page
MaxFrank13 Nov 18, 2025
de68d2d
feat: program dashboard
MaxFrank13 Nov 25, 2025
6f6d16b
Merge branch 'master' into mfrank/add-program-list-page
MaxFrank13 Nov 25, 2025
3b83401
fix: test coverage
MaxFrank13 Nov 26, 2025
fd3ff70
fix: code cov
MaxFrank13 Nov 26, 2025
0bb1231
fix: requested changes
MaxFrank13 Feb 13, 2026
ce0942d
feat: added the ability for instances to use local translations fro…
jajjibhai008 Dec 3, 2025
af9924e
chore(deps): update dependency @openedx/paragon to v23.18.1 (#755)
renovate[bot] Dec 8, 2025
cd37458
fix(deps): update dependency core-js to v3.47.0 (#757)
renovate[bot] Dec 8, 2025
68dcf1a
chore(deps): update dependency @reduxjs/toolkit to v2.11.1 (#756)
renovate[bot] Dec 8, 2025
3829dd7
chore(deps): update dependency @reduxjs/toolkit to v2.11.2 (#761)
renovate[bot] Dec 15, 2025
1bd4485
fix: env variables fetching issue for translations (#766)
jajjibhai008 Dec 17, 2025
5e4e347
fix(deps): remove filesize dependency (#767)
MaxFrank13 Dec 18, 2025
9f542d0
chore(deps): bump actions/checkout from 5 to 6 (#750)
dependabot[bot] Dec 18, 2025
fd7d4e4
chore(deps): update dependency @openedx/paragon to v23.18.2 (#771)
renovate[bot] Dec 22, 2025
c3a7c6d
fix(deps): update dependency react-router-dom to v6.30.3 (#780)
renovate[bot] Jan 12, 2026
b4adf16
chore(deps): update dependency @openedx/paragon to v23.19.1 (#781)
renovate[bot] Jan 12, 2026
9982ac4
chore(deps): update dependency lodash to v4.17.23 [security] (#783)
renovate[bot] Jan 22, 2026
23f23a4
chore(deps): update dependency @edx/frontend-platform to v8.5.4 (#784)
renovate[bot] Jan 26, 2026
26ce874
fix: include frontend component header translation (#793)
DeimerM Feb 10, 2026
1927792
fix: remove unused universal-cookie dep (#794)
MaxFrank13 Feb 11, 2026
17e316d
fix: update react-share to v5 (#795)
MaxFrank13 Feb 12, 2026
0d8c0ee
fix(deps): regenerate `package-lock.json` (#788)
brian-smith-tcril Feb 12, 2026
ff000f1
fix(deps): update dependency core-js to v3.48.0 (#799)
renovate[bot] Feb 16, 2026
5478431
chore(deps): update dependency @edx/frontend-platform to v8.5.5 (#798)
renovate[bot] Feb 16, 2026
d99e44a
feat: add program dashboard directory (#1)
MaxFrank13 Oct 15, 2025
19448cb
feat: add program list page
MaxFrank13 Oct 17, 2025
14406a8
fix: deps
MaxFrank13 Oct 21, 2025
489742d
feat: add program dashboard directory (#1)
MaxFrank13 Oct 15, 2025
f42207b
fix: requested changes
MaxFrank13 Feb 25, 2026
ae08970
fix: requested changes
MaxFrank13 Feb 25, 2026
bcd0c33
Merge branch 'master' into mfrank/add-program-list-page
MaxFrank13 Feb 25, 2026
b96c3ef
Merge branch 'master' into mfrank/add-program-list-page
asharma12-sonata Apr 7, 2026
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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"access": "public"
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^8.0.0",
"@edx/frontend-enterprise-hotjar": "7.2.0",
Expand Down
38 changes: 14 additions & 24 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import React from 'react';
import { Helmet } from 'react-helmet';

import { useIntl } from '@edx/frontend-platform/i18n';
import { logError } from '@edx/frontend-platform/logging';
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';

import { ErrorPage } from '@edx/frontend-platform/react';
import { ErrorPage, AppContext } from '@edx/frontend-platform/react';
import { FooterSlot } from '@edx/frontend-component-footer';
import { Alert } from '@openedx/paragon';

import Dashboard from 'containers/Dashboard';

import track from 'tracking';

import fakeData from 'data/services/lms/fakeData/courses';
import AppWrapper from 'containers/AppWrapper';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';

Expand Down Expand Up @@ -42,28 +44,16 @@ export const App = () => {
}
}, []);
return (
<>
<Helmet>
<title>{formatMessage(messages.pageTitle)}</title>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
<div>
<AppWrapper>
Comment thread
MaxFrank13 marked this conversation as resolved.
<LearnerDashboardHeader />
<main id="main">
{hasNetworkFailure
? (
<Alert variant="danger">
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
</Alert>
) : (
<Dashboard />
)}
</main>
</AppWrapper>
<FooterSlot />
</div>
</>
<main id="main">
{hasNetworkFailure
? (
<Alert variant="danger">
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
</Alert>
) : (
<Dashboard />
)}
</main>
);
};

Expand Down
37 changes: 12 additions & 25 deletions src/App.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,18 @@ import { useInitializeLearnerHome } from 'data/hooks';
import { App } from './App';
import messages from './messages';

jest.mock('data/hooks', () => ({
useInitializeLearnerHome: jest.fn(),
jest.mock('containers/Dashboard', () => jest.fn(() => <div>Dashboard</div>));
jest.mock('data/redux', () => ({
selectors: 'redux.selectors',
actions: 'redux.actions',
thunkActions: 'redux.thunkActions',
}));
jest.mock('hooks', () => ({
reduxHooks: {
useRequestIsFailed: jest.fn(),
usePlatformSettingsData: jest.fn(),
useLoadData: jest.fn(),
},
}));

jest.mock('data/context', () => ({
Expand Down Expand Up @@ -43,31 +53,12 @@ useInitializeLearnerHome.mockReturnValue({

describe('App router component', () => {
describe('component', () => {
const runBasicTests = () => {
it('displays title in helmet component', async () => {
await waitFor(() => expect(document.title).toEqual(messages.pageTitle.defaultMessage));
});
it('displays learner dashboard header', () => {
const learnerDashboardHeader = screen.getByText('LearnerDashboardHeader');
expect(learnerDashboardHeader).toBeInTheDocument();
});
it('wraps the header and main components in an AppWrapper widget container', () => {
const appWrapper = screen.getByText('LearnerDashboardHeader').parentElement;
expect(appWrapper).toHaveClass('AppWrapper');
expect(appWrapper.children[1].id).toEqual('main');
});
it('displays footer slot', () => {
const footerSlot = screen.getByText('FooterSlot');
expect(footerSlot).toBeInTheDocument();
});
};
describe('no network failure', () => {
beforeEach(() => {
jest.clearAllMocks();
getConfig.mockReturnValue({});
render(<IntlProvider locale="en"><App /></IntlProvider>);
});
runBasicTests();
it('loads dashboard', () => {
const dashboard = screen.getByText('Dashboard');
expect(dashboard).toBeInTheDocument();
Expand All @@ -79,7 +70,6 @@ describe('App router component', () => {
getConfig.mockReturnValue({ OPTIMIZELY_URL: 'fake.url' });
render(<IntlProvider locale="en"><App /></IntlProvider>);
});
runBasicTests();
it('loads dashboard', () => {
const dashboard = screen.getByText('Dashboard');
expect(dashboard).toBeInTheDocument();
Expand All @@ -91,7 +81,6 @@ describe('App router component', () => {
getConfig.mockReturnValue({ OPTIMIZELY_PROJECT_ID: 'fakeId' });
render(<IntlProvider locale="en"><App /></IntlProvider>);
});
runBasicTests();
it('loads dashboard', () => {
const dashboard = screen.getByText('Dashboard');
expect(dashboard).toBeInTheDocument();
Expand All @@ -107,7 +96,6 @@ describe('App router component', () => {
getConfig.mockReturnValue({});
render(<IntlProvider locale="en" messages={messages}><App /></IntlProvider>);
});
runBasicTests();
it('loads error page', () => {
const alert = screen.getByRole('alert');
expect(alert).toBeInTheDocument();
Expand All @@ -120,7 +108,6 @@ describe('App router component', () => {
getConfig.mockReturnValue({});
render(<IntlProvider locale="en"><App /></IntlProvider>);
});
runBasicTests();
it('loads error page', () => {
const alert = screen.getByRole('alert');
expect(alert).toBeInTheDocument();
Expand Down
13 changes: 0 additions & 13 deletions src/containers/AppWrapper/index.jsx

This file was deleted.

7 changes: 0 additions & 7 deletions src/containers/Dashboard/index.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ jest.mock('./LoadingView', () => jest.fn(() => <div>LoadingView</div>));
jest.mock('containers/SelectSessionModal', () => jest.fn(() => <div>SelectSessionModal</div>));
jest.mock('./DashboardLayout', () => jest.fn(() => <div>DashboardLayout</div>));

const pageTitle = 'test-page-title';

describe('Dashboard', () => {
const createWrapper = (props = {}) => {
const {
Expand All @@ -42,11 +40,6 @@ describe('Dashboard', () => {
};

describe('render', () => {
it('page title is displayed in sr-only h1 tag', () => {
createWrapper();
const heading = screen.getByText(pageTitle);
expect(heading).toHaveClass('sr-only');
});
describe('initIsPending false', () => {
it('should render DashboardModalSlot', () => {
createWrapper({ initIsPending: false });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@ const getLearnerHeaderMenu = (
courseSearchUrl,
authenticatedUser,
exploreCoursesClick,
pathname,
) => ({
mainMenu: [
{
type: 'item',
href: '/',
content: formatMessage(messages.course),
isActive: true,
isActive: pathname === '/',
},
...(getConfig().ENABLE_PROGRAMS ? [{
type: 'item',
href: `${urls.programsUrl()}`,
href: getConfig().ENABLE_PROGRAM_DASHBOARD ? '/programs' : `${urls.programsUrl()}`,
content: formatMessage(messages.program),
isActive: pathname === '/programs',
}] : []),
...(!getConfig().NON_BROWSABLE_COURSES ? [{
type: 'item',
Expand Down
4 changes: 2 additions & 2 deletions src/containers/LearnerDashboardHeader/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ export const findCoursesNavClicked = (href) => track.findCourses.findCoursesClic
});

export const useLearnerDashboardHeaderMenu = ({
courseSearchUrl, authenticatedUser, exploreCoursesClick,
courseSearchUrl, authenticatedUser, exploreCoursesClick, pathname,
}) => {
const { formatMessage } = useIntl();
return getLearnerHeaderMenu(formatMessage, courseSearchUrl, authenticatedUser, exploreCoursesClick);
return getLearnerHeaderMenu(formatMessage, courseSearchUrl, authenticatedUser, exploreCoursesClick, pathname);
};

export default {
Expand Down
19 changes: 17 additions & 2 deletions src/containers/LearnerDashboardHeader/index.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import React from 'react';
import { Helmet } from 'react-helmet';

import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import MasqueradeBar from 'containers/MasqueradeBar';
import { AppContext } from '@edx/frontend-platform/react';
import Header from '@edx/frontend-component-header';
import { useInitializeLearnerHome } from 'data/hooks';
import urls from 'data/services/lms/urls';

import { useLocation } from 'react-router-dom';
import { useDashboardMessages } from 'containers/Dashboard/hooks';
import ConfirmEmailBanner from './ConfirmEmailBanner';

import appMessages from '../../messages';
import { useLearnerDashboardHeaderMenu, findCoursesNavClicked } from './hooks';

import './index.scss';

export const LearnerDashboardHeader = () => {
const { authenticatedUser } = React.useContext(AppContext);
const { formatMessage } = useIntl();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const { pageTitle } = useDashboardMessages();
const location = useLocation();
const { pathname } = location;
const { data: learnerData } = useInitializeLearnerHome();
const courseSearchUrl = learnerData?.platformSettings?.courseSearchUrl || '';

Expand All @@ -25,16 +34,22 @@ export const LearnerDashboardHeader = () => {
courseSearchUrl,
authenticatedUser,
exploreCoursesClick,
pathname,
});

return (
<>
<Helmet>
<title>{formatMessage(appMessages.pageTitle)}</title>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
<ConfirmEmailBanner />
<Header
mainMenuItems={learnerHomeHeaderMenu.mainMenu}
secondaryMenuItems={learnerHomeHeaderMenu.secondaryMenu}
userMenuItems={learnerHomeHeaderMenu.userMenu}
/>
<h1 className="sr-only">{pageTitle}</h1>
<MasqueradeBar />
</>
);
Expand Down
40 changes: 40 additions & 0 deletions src/containers/LearnerDashboardHeader/index.test.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { mergeConfig } from '@edx/frontend-platform';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { useLocation } from 'react-router-dom';

import urls from 'data/services/lms/urls';
import { useDashboardMessages } from 'containers/Dashboard/hooks';
import LearnerDashboardHeader from '.';
import { findCoursesNavClicked } from './hooks';

Expand All @@ -22,16 +24,34 @@ jest.mock('./hooks', () => ({
findCoursesNavClicked: jest.fn(),
}));

jest.mock('react-router-dom', () => ({
useLocation: jest.fn(() => ({
pathname: '/',
})),
}));

const mockedHeaderProps = jest.fn();
jest.mock('containers/MasqueradeBar', () => jest.fn(() => <div>MasqueradeBar</div>));
jest.mock('./ConfirmEmailBanner', () => jest.fn(() => <div>ConfirmEmailBanner</div>));
jest.mock('@edx/frontend-component-header', () => jest.fn((props) => {
mockedHeaderProps(props);
return <div>Header</div>;
}));
jest.mock('containers/Dashboard/hooks', () => ({
useDashboardMessages: jest.fn(),
}));

const pageTitle = 'test-page-title';

describe('LearnerDashboardHeader', () => {
beforeEach(() => jest.clearAllMocks());

it('page title is displayed in sr-only h1 tag', () => {
useDashboardMessages.mockReturnValue({ pageTitle });
render(<IntlProvider locale="en"><LearnerDashboardHeader /></IntlProvider>);
const heading = screen.getByText(pageTitle);
expect(heading).toHaveClass('sr-only');
});
it('renders and discover url is correct', () => {
mergeConfig({ ORDER_HISTORY_URL: 'test-url' });
render(<IntlProvider locale="en"><LearnerDashboardHeader /></IntlProvider>);
Expand Down Expand Up @@ -60,6 +80,26 @@ describe('LearnerDashboardHeader', () => {
const { mainMenuItems } = props;
expect(mainMenuItems.length).toBe(3);
});

it('should highlight the active tab depending on the pathname', () => {
render(<IntlProvider locale="en"><LearnerDashboardHeader /></IntlProvider>);
const props = mockedHeaderProps.mock.calls[0][0];
const { mainMenuItems } = props;
expect(mainMenuItems[0].isActive).toBe(true);
});

it('should highlight the programs tab if dashboard is enabled and on the programs page', () => {
mergeConfig({ ENABLE_PROGRAMS: true, ENABLE_PROGRAM_DASHBOARD: true });
useLocation.mockReturnValueOnce({
pathname: '/programs',
});
render(<IntlProvider locale="en"><LearnerDashboardHeader /></IntlProvider>);
const props = mockedHeaderProps.mock.calls[0][0];
const { mainMenuItems } = props;
expect(mainMenuItems[0].isActive).toBe(false);
expect(mainMenuItems[1].isActive).toBe(true);
});

it('should not display Discover New tab if it is disabled by configuration', () => {
mergeConfig({ NON_BROWSABLE_COURSES: true });
render(<IntlProvider locale="en"><LearnerDashboardHeader /></IntlProvider>);
Expand Down
Loading