Skip to content
Open
Show file tree
Hide file tree
Changes from 56 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
741c9ce
chore: updating the workflows
deborahgu Oct 15, 2025
745210b
feat: add program dashboard directory (#1)
MaxFrank13 Oct 15, 2025
91b6925
feat: add program dashboard directory (#1)
MaxFrank13 Oct 15, 2025
2dc71dc
Merge pull request #2 from edx/dkaplan1/modify-workflow-branches
deborahgu 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
63568ee
feat: sync master branch to openedx master (#10)
MaxFrank13 Feb 24, 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
e6a265d
feat!: sync react query conversion (#11)
MaxFrank13 Apr 6, 2026
b96c3ef
Merge branch 'master' into mfrank/add-program-list-page
asharma12-sonata Apr 7, 2026
f9346d3
feat: refactor programs dashboard to use react querygit push
asharma12-sonata Apr 9, 2026
cd8e283
Resolved merge conflicts
asharma12-sonata Apr 9, 2026
6d961e6
feat: resolved merge conflicts
asharma12-sonata Apr 9, 2026
ca10f5d
feat: removed unused code and uncommented the required piece of code
asharma12-sonata Apr 9, 2026
0795e45
feat: reinstalled npm packages to update package.lock.json to resolve…
asharma12-sonata Apr 10, 2026
dfc751f
feat: reverted github workflow changes
asharma12-sonata Apr 10, 2026
bb2cad6
feat: fixed eslint reported issues
asharma12-sonata Apr 10, 2026
104f90d
feat: fixed workflow for openedx
asharma12-sonata Apr 10, 2026
2164115
feat: added footer as per jason comment in PR review
asharma12-sonata Apr 13, 2026
9f04f71
feat: fixed lint issue
asharma12-sonata Apr 13, 2026
f3ae632
feat: handled test case failures
asharma12-sonata Apr 14, 2026
de32fe2
feat: updated test cases as per Max's comment
asharma12-sonata Apr 17, 2026
310835c
feat: changes done as per the review comment
asharma12-sonata Apr 21, 2026
be8cd42
feat: updates made as per the PR review comments also updated the key…
asharma12-sonata Apr 22, 2026
593e7ce
Merge branch 'master' into abhishek/refactor-program-dashboard
asharma12-sonata Apr 22, 2026
f48e233
Merge branch 'master' into abhishek/refactor-program-dashboard
asharma12-sonata Apr 23, 2026
a79674c
feat: updated package.lock.json as it was causing issue in ci
asharma12-sonata Apr 23, 2026
65251b0
fix: fixed test cases failure issue
asharma12-sonata Apr 23, 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
123 changes: 107 additions & 16 deletions package-lock.json

Large diffs are not rendered by default.

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
37 changes: 10 additions & 27 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
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 { FooterSlot } from '@edx/frontend-component-footer';
import { Alert } from '@openedx/paragon';

import Dashboard from 'containers/Dashboard';

import AppWrapper from 'containers/AppWrapper';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';

import { getConfig } from '@edx/frontend-platform';
import { useInitializeLearnerHome } from 'data/hooks';
import { useMasquerade } from 'data/context';
Expand Down Expand Up @@ -42,28 +37,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>
<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
29 changes: 1 addition & 28 deletions src/App.test.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen } from '@testing-library/react';

import { IntlProvider } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
Expand All @@ -15,12 +15,8 @@ jest.mock('data/context', () => ({
useMasquerade: jest.fn(() => ({ masqueradeUser: null })),
}));

jest.mock('@edx/frontend-component-footer', () => ({
FooterSlot: jest.fn(() => <div>FooterSlot</div>),
}));
jest.mock('containers/Dashboard', () => jest.fn(() => <div>Dashboard</div>));
jest.mock('containers/LearnerDashboardHeader', () => jest.fn(() => <div>LearnerDashboardHeader</div>));
jest.mock('containers/AppWrapper', () => jest.fn(({ children }) => <div className="AppWrapper">{children}</div>));

jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(() => ({})),
Expand All @@ -43,31 +39,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 +56,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 +67,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 +82,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 +94,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.

15 changes: 0 additions & 15 deletions src/containers/AppWrapper/index.test.tsx
Comment thread
asharma12-sonata marked this conversation as resolved.
Outdated

This file was deleted.

9 changes: 1 addition & 8 deletions src/containers/Dashboard/index.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,21 @@ 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 {
hasCourses = true,
initIsPending = true,
showSelectSessionModal = true,
} = props;
hooks.useDashboardMessages.mockReturnValue({ pageTitle });
hooks.useDashboardMessages.mockReturnValue({ pageTitle: 'Dashboard' });
const dataMocked = { data: hasCourses ? { courses: [1, 2] } : { courses: [] }, isPending: initIsPending };
useInitializeLearnerHome.mockReturnValue(dataMocked);
useSelectSessionModal.mockReturnValue({ selectSessionModal: showSelectSessionModal ? { cardId: 1 } : null });
return render(<IntlProvider locale="en"><Dashboard /></IntlProvider>);
};

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
18 changes: 16 additions & 2 deletions src/containers/LearnerDashboardHeader/index.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
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 { pageTitle } = useDashboardMessages();
const location = useLocation();
const { pathname } = location;
const { data: learnerData } = useInitializeLearnerHome();
const courseSearchUrl = learnerData?.platformSettings?.courseSearchUrl || '';

Expand All @@ -25,16 +33,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
Loading