Compare commits
47 Commits
hunia/upgr
...
release/te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe386e31ee | ||
|
|
cb1de82f0a | ||
|
|
2337843d54 | ||
|
|
70da0d38ed | ||
|
|
154a2583f6 | ||
|
|
633050739e | ||
|
|
61d24d29f1 | ||
|
|
a210f23c9f | ||
|
|
b16908842e | ||
|
|
b80cab7a66 | ||
|
|
6b8cd1f780 | ||
|
|
78c5d73900 | ||
|
|
eb3fc9412d | ||
|
|
3caf6fd67a | ||
|
|
d48cb3d9fc | ||
|
|
d3b4a7fc84 | ||
|
|
9e63777c5c | ||
|
|
cf2f3acc51 | ||
|
|
54f8bc86e3 | ||
|
|
10961010ba | ||
|
|
3c1b749395 | ||
|
|
845ee09bf2 | ||
|
|
1efec09f44 | ||
|
|
aa1cae5200 | ||
|
|
77ab48c59f | ||
|
|
5d2b33abd3 | ||
|
|
dd4f61eec3 | ||
|
|
8f7580ec30 | ||
|
|
fbf24e42d3 | ||
|
|
e764e9c502 | ||
|
|
13721f2770 | ||
|
|
960647ce9f | ||
|
|
44c797854f | ||
|
|
3ea088e411 | ||
|
|
4a18c890c3 | ||
|
|
e1c1c51704 | ||
|
|
f83f3a1850 | ||
|
|
b26d4632c9 | ||
|
|
76783133da | ||
|
|
57d09af61d | ||
|
|
86fd29309a | ||
|
|
6a43918b56 | ||
|
|
f110a0ade8 | ||
|
|
93bd883a01 | ||
|
|
61375c9e95 | ||
|
|
c2f4be5063 | ||
|
|
14bde7fc3f |
@@ -1 +0,0 @@
|
||||
npm run lint
|
||||
@@ -17,6 +17,7 @@ metadata:
|
||||
openedx.org/arch-interest-groups: ""
|
||||
# This can be multiple comma-separated projects.
|
||||
openedx.org/add-to-projects: "openedx:23"
|
||||
openedx.org/release: "master"
|
||||
spec:
|
||||
type: 'service'
|
||||
lifecycle: 'production'
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# This file describes this Open edX repo, as described in OEP-2:
|
||||
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
|
||||
|
||||
tags:
|
||||
- frontend-app
|
||||
- masters
|
||||
oeps:
|
||||
oep-2: true # Repository metadata
|
||||
openedx-release: {ref: master}
|
||||
8186
package-lock.json
generated
8186
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -20,8 +20,7 @@
|
||||
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
|
||||
"quality": "npm run lint-fix && npm run test",
|
||||
"watch-tests": "jest --watch",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"prepare": "husky"
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot"
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
@@ -31,18 +30,18 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-header": "^5.6.0",
|
||||
"@edx/frontend-enterprise-hotjar": "7.1.0",
|
||||
"@edx/frontend-platform": "8.1.5",
|
||||
"@edx/frontend-component-footer": "^14.6.0",
|
||||
"@edx/frontend-component-header": "^6.2.0",
|
||||
"@edx/frontend-enterprise-hotjar": "7.2.0",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@edx/react-unit-test-utils": "3.0.0",
|
||||
"@edx/react-unit-test-utils": "^4.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.2.0",
|
||||
"@openedx/frontend-slot-footer": "^1.0.2",
|
||||
"@openedx/paragon": "^22.2.2",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"@openedx/paragon": "^22.16.0",
|
||||
"@redux-devtools/extension": "3.3.0",
|
||||
"@reduxjs/toolkit": "^2.0.0",
|
||||
"classnames": "^2.3.1",
|
||||
@@ -54,8 +53,8 @@
|
||||
"moment": "^2.29.4",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "7.1.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "6.8.9",
|
||||
"react-redux": "^7.2.4",
|
||||
@@ -72,18 +71,17 @@
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.3.0",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@openedx/frontend-build": "14.2.2",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.1.0",
|
||||
"@openedx/frontend-build": "^14.3.3",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"copy-webpack-plugin": "^12.0.0",
|
||||
"husky": "^9.0.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"jest-when": "^3.6.0",
|
||||
"react-dev-utils": "^12.0.0",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"redux-mock-store": "^1.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { logError } from '@edx/frontend-platform/logging';
|
||||
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
|
||||
|
||||
import { ErrorPage, AppContext } from '@edx/frontend-platform/react';
|
||||
import FooterSlot from '@openedx/frontend-slot-footer';
|
||||
import { FooterSlot } from '@edx/frontend-component-footer';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
@@ -80,7 +80,7 @@ export const App = () => {
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<main id="main">
|
||||
{hasNetworkFailure
|
||||
? (
|
||||
<Alert variant="danger">
|
||||
|
||||
@@ -13,7 +13,7 @@ import AppWrapper from 'containers/WidgetContainers/AppWrapper';
|
||||
import { App } from './App';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'Footer' }));
|
||||
jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'FooterSlot' }));
|
||||
|
||||
jest.mock('containers/Dashboard', () => 'Dashboard');
|
||||
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
|
||||
|
||||
@@ -17,7 +17,9 @@ exports[`App router component component initialize failure snapshot 1`] = `
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<main
|
||||
id="main"
|
||||
>
|
||||
<Alert
|
||||
variant="danger"
|
||||
>
|
||||
@@ -49,7 +51,9 @@ exports[`App router component component no network failure snapshot 1`] = `
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<main
|
||||
id="main"
|
||||
>
|
||||
<Dashboard />
|
||||
</main>
|
||||
</AppWrapper>
|
||||
@@ -75,7 +79,9 @@ exports[`App router component component no network failure with optimizely proje
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<main
|
||||
id="main"
|
||||
>
|
||||
<Dashboard />
|
||||
</main>
|
||||
</AppWrapper>
|
||||
@@ -101,7 +107,9 @@ exports[`App router component component no network failure with optimizely url s
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<main
|
||||
id="main"
|
||||
>
|
||||
<Dashboard />
|
||||
</main>
|
||||
</AppWrapper>
|
||||
@@ -127,7 +135,9 @@ exports[`App router component component refresh failure snapshot 1`] = `
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<main
|
||||
id="main"
|
||||
>
|
||||
<Alert
|
||||
variant="danger"
|
||||
>
|
||||
|
||||
@@ -1,40 +1,43 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
|
||||
<ErrorPage
|
||||
message="test-error-message"
|
||||
/>
|
||||
<UNDEFINED>
|
||||
<ErrorPage
|
||||
message="test-error-message"
|
||||
/>
|
||||
</UNDEFINED>
|
||||
`;
|
||||
|
||||
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
||||
<AppProvider
|
||||
store={
|
||||
{
|
||||
"redux": "store",
|
||||
<UNDEFINED>
|
||||
<AppProvider
|
||||
store={
|
||||
{
|
||||
"redux": "store",
|
||||
}
|
||||
}
|
||||
}
|
||||
wrapWithRouter={true}
|
||||
>
|
||||
<NoticesWrapper>
|
||||
<Routes>
|
||||
<Route
|
||||
element={
|
||||
<PageWrap>
|
||||
<App />
|
||||
</PageWrap>
|
||||
}
|
||||
path="/"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<Navigate
|
||||
replace={true}
|
||||
to="/"
|
||||
/>
|
||||
}
|
||||
path="*"
|
||||
/>
|
||||
</Routes>
|
||||
</NoticesWrapper>
|
||||
</AppProvider>
|
||||
>
|
||||
<NoticesWrapper>
|
||||
<Routes>
|
||||
<Route
|
||||
element={
|
||||
<PageWrap>
|
||||
<App />
|
||||
</PageWrap>
|
||||
}
|
||||
path="/"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<Navigate
|
||||
replace={true}
|
||||
to="/"
|
||||
/>
|
||||
}
|
||||
path="*"
|
||||
/>
|
||||
</Routes>
|
||||
</NoticesWrapper>
|
||||
</AppProvider>
|
||||
</UNDEFINED>
|
||||
`;
|
||||
|
||||
@@ -12,7 +12,6 @@ export const CourseBanner = ({ cardId }) => {
|
||||
const {
|
||||
isVerified,
|
||||
isAuditAccessExpired,
|
||||
canUpgrade,
|
||||
coursewareAccess = {},
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const courseRun = reduxHooks.useCardCourseRunData(cardId);
|
||||
@@ -26,13 +25,7 @@ export const CourseBanner = ({ cardId }) => {
|
||||
return (
|
||||
<>
|
||||
{isAuditAccessExpired
|
||||
&& (canUpgrade ? (
|
||||
<Banner>
|
||||
{formatMessage(messages.auditAccessExpired)}
|
||||
{' '}
|
||||
{formatMessage(messages.upgradeToAccess)}
|
||||
</Banner>
|
||||
) : (
|
||||
&& (
|
||||
<Banner>
|
||||
{formatMessage(messages.auditAccessExpired)}
|
||||
{' '}
|
||||
@@ -40,17 +33,7 @@ export const CourseBanner = ({ cardId }) => {
|
||||
{formatMessage(messages.findAnotherCourse)}
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
))}
|
||||
|
||||
{courseRun.isActive && !canUpgrade && (
|
||||
<Banner>
|
||||
{formatMessage(messages.upgradeDeadlinePassed)}
|
||||
{' '}
|
||||
<Hyperlink isInline destination={courseRun.marketingUrl || ''}>
|
||||
{formatMessage(messages.exploreCourseDetails)}
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
)}
|
||||
)}
|
||||
|
||||
{(!isStaff && isTooEarly && courseRun.startDate) && (
|
||||
<Banner>
|
||||
@@ -59,6 +42,7 @@ export const CourseBanner = ({ cardId }) => {
|
||||
})}
|
||||
</Banner>
|
||||
)}
|
||||
|
||||
{(!isStaff && hasUnmetPrerequisites) && (
|
||||
<Banner>{formatMessage(messages.prerequisitesNotMet)}</Banner>
|
||||
)}
|
||||
|
||||
@@ -25,7 +25,6 @@ let el;
|
||||
|
||||
const enrollmentData = {
|
||||
isVerified: false,
|
||||
canUpgrade: false,
|
||||
isAuditAccessExpired: false,
|
||||
coursewareAccess: {
|
||||
hasUnmetPrerequisites: false,
|
||||
@@ -65,51 +64,18 @@ describe('CourseBanner', () => {
|
||||
render({ enrollment: { isVerified: true } });
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
describe('audit access expired, can upgrade', () => {
|
||||
beforeEach(() => {
|
||||
render({ enrollment: { isAuditAccessExpired: true, canUpgrade: true } });
|
||||
});
|
||||
test('snapshot: (auditAccessExpired, upgradeToAccess)', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
|
||||
expect(el.instance.children[0].children[0].el).toContain(messages.auditAccessExpired.defaultMessage);
|
||||
expect(el.instance.children[0].children[2].el).toContain(messages.upgradeToAccess.defaultMessage);
|
||||
});
|
||||
});
|
||||
describe('audit access expired, cannot upgrade', () => {
|
||||
describe('audit access expired', () => {
|
||||
beforeEach(() => {
|
||||
render({ enrollment: { isAuditAccessExpired: true } });
|
||||
});
|
||||
test('snapshot: (auditAccessExpired, findAnotherCourse hyperlink)', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
|
||||
test('messages: auditAccessExpired', () => {
|
||||
expect(el.instance.children[0].children[0].el).toContain(messages.auditAccessExpired.defaultMessage);
|
||||
expect(el.instance.findByType(Hyperlink)[0].children[0].el).toEqual(messages.findAnotherCourse.defaultMessage);
|
||||
});
|
||||
});
|
||||
describe('course run active and cannot upgrade', () => {
|
||||
beforeEach(() => {
|
||||
render({ courseRun: { isActive: true } });
|
||||
});
|
||||
test('snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('messages: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
|
||||
expect(el.instance.children[0].children[0].el).toContain(messages.upgradeDeadlinePassed.defaultMessage);
|
||||
const link = el.instance.findByType(Hyperlink);
|
||||
expect(link[0].children[0].el).toEqual(messages.exploreCourseDetails.defaultMessage);
|
||||
expect(link[0].props.destination).toEqual(courseRunData.marketingUrl);
|
||||
});
|
||||
});
|
||||
test('no display if audit access not expired and (course is not active or can upgrade)', () => {
|
||||
render();
|
||||
// isEmptyRender() isn't true because the minimal is <Fragment />
|
||||
expect(el.instance.children).toEqual([]);
|
||||
render({ enrollment: { canUpgrade: true }, courseRun: { isActive: true } });
|
||||
expect(el.instance.children).toEqual([]);
|
||||
});
|
||||
describe('unmet prerequisites', () => {
|
||||
beforeEach(() => {
|
||||
render({ enrollment: { coursewareAccess: { hasUnmetPrerequisites: true } } });
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseBanner audit access expired, can upgrade snapshot: (auditAccessExpired, upgradeToAccess) 1`] = `
|
||||
<Fragment>
|
||||
<Banner>
|
||||
Your audit access to this course has expired.
|
||||
|
||||
Upgrade now to access your course again.
|
||||
</Banner>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`CourseBanner audit access expired, cannot upgrade snapshot: (auditAccessExpired, findAnotherCourse hyperlink) 1`] = `
|
||||
exports[`CourseBanner audit access expired snapshot: (auditAccessExpired, findAnotherCourse hyperlink) 1`] = `
|
||||
<Fragment>
|
||||
<Banner>
|
||||
Your audit access to this course has expired.
|
||||
@@ -25,21 +15,6 @@ exports[`CourseBanner audit access expired, cannot upgrade snapshot: (auditAcces
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`CourseBanner course run active and cannot upgrade snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink) 1`] = `
|
||||
<Fragment>
|
||||
<Banner>
|
||||
Your upgrade deadline for this course has passed. To upgrade, enroll in a session that is farther in the future.
|
||||
|
||||
<Hyperlink
|
||||
destination="marketing-url"
|
||||
isInline={true}
|
||||
>
|
||||
Explore course details.
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`CourseBanner snapshot: stacking banners 1`] = `<Fragment />`;
|
||||
|
||||
exports[`CourseBanner staff snapshot: isStaff 1`] = `<Fragment />`;
|
||||
|
||||
@@ -6,26 +6,11 @@ const messages = defineMessages({
|
||||
description: 'Audit access expiration banner message',
|
||||
defaultMessage: 'Your audit access to this course has expired.',
|
||||
},
|
||||
upgradeToAccess: {
|
||||
id: 'learner-dash.courseCard.banners.upgradeToAccess',
|
||||
description: 'Upgrade prompt for audit-expired learners that can still upgrade',
|
||||
defaultMessage: 'Upgrade now to access your course again.',
|
||||
},
|
||||
findAnotherCourse: {
|
||||
id: 'learner-dash.courseCard.banners.findAnotherCourse',
|
||||
description: 'Action prompt taking learners to course exploration',
|
||||
defaultMessage: 'Find another course',
|
||||
},
|
||||
upgradeDeadlinePassed: {
|
||||
id: 'learner-dash.courseCard.banners.upgradeDeadlinePassed',
|
||||
description: 'Audit upgrade deadline passed banner message',
|
||||
defaultMessage: 'Your upgrade deadline for this course has passed. To upgrade, enroll in a session that is farther in the future.',
|
||||
},
|
||||
exploreCourseDetails: {
|
||||
id: 'learner-dash.courseCard.banners.exploreCourseDetails',
|
||||
description: 'Action prompt taking learners to course details page',
|
||||
defaultMessage: 'Explore course details.',
|
||||
},
|
||||
certRestricted: {
|
||||
id: 'learner-dash.courseCard.banners.certificateRestricted',
|
||||
description: 'Restricted certificate warning message',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Dashboard snapshots courses loaded, show select session modal, no available dashboards snapshot 1`] = `
|
||||
exports[`Dashboard snapshots courses loaded, show select session modal snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex flex-column p-2 pt-0"
|
||||
id="dashboard-container"
|
||||
@@ -11,6 +11,7 @@ exports[`Dashboard snapshots courses loaded, show select session modal, no avail
|
||||
test-page-title
|
||||
</h1>
|
||||
<Fragment>
|
||||
<DashboardModalSlot />
|
||||
<SelectSessionModal />
|
||||
</Fragment>
|
||||
<div
|
||||
@@ -43,7 +44,7 @@ exports[`Dashboard snapshots courses still loading snapshot 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Dashboard snapshots there are no courses, there ARE available dashboards snapshot 1`] = `
|
||||
exports[`Dashboard snapshots there are no courses snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex flex-column p-2 pt-0"
|
||||
id="dashboard-container"
|
||||
@@ -54,7 +55,7 @@ exports[`Dashboard snapshots there are no courses, there ARE available dashboard
|
||||
test-page-title
|
||||
</h1>
|
||||
<Fragment>
|
||||
<EnterpriseDashboardModal />
|
||||
<DashboardModalSlot />
|
||||
</Fragment>
|
||||
<div
|
||||
data-testid="dashboard-content"
|
||||
|
||||
@@ -2,9 +2,9 @@ import React from 'react';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
|
||||
import SelectSessionModal from 'containers/SelectSessionModal';
|
||||
import CoursesPanel from 'containers/CoursesPanel';
|
||||
import DashboardModalSlot from 'plugin-slots/DashboardModalSlot';
|
||||
|
||||
import LoadingView from './LoadingView';
|
||||
import DashboardLayout from './DashboardLayout';
|
||||
@@ -15,7 +15,6 @@ export const Dashboard = () => {
|
||||
hooks.useInitializeDashboard();
|
||||
const { pageTitle } = hooks.useDashboardMessages();
|
||||
const hasCourses = reduxHooks.useHasCourses();
|
||||
const hasAvailableDashboards = reduxHooks.useHasAvailableDashboards();
|
||||
const initIsPending = reduxHooks.useRequestIsPending(RequestKeys.initialize);
|
||||
const showSelectSessionModal = reduxHooks.useShowSelectSessionModal();
|
||||
|
||||
@@ -24,7 +23,7 @@ export const Dashboard = () => {
|
||||
<h1 className="sr-only">{pageTitle}</h1>
|
||||
{!initIsPending && (
|
||||
<>
|
||||
{hasAvailableDashboards && <EnterpriseDashboardModal />}
|
||||
<DashboardModalSlot />
|
||||
{(hasCourses && showSelectSessionModal) && <SelectSessionModal />}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
|
||||
import SelectSessionModal from 'containers/SelectSessionModal';
|
||||
import CoursesPanel from 'containers/CoursesPanel';
|
||||
|
||||
@@ -14,13 +13,12 @@ import Dashboard from '.';
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useHasCourses: jest.fn(),
|
||||
useHasAvailableDashboards: jest.fn(),
|
||||
useShowSelectSessionModal: jest.fn(),
|
||||
useRequestIsPending: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('containers/EnterpriseDashboardModal', () => 'EnterpriseDashboardModal');
|
||||
jest.mock('plugin-slots/DashboardModalSlot', () => 'DashboardModalSlot');
|
||||
jest.mock('containers/CoursesPanel', () => 'CoursesPanel');
|
||||
jest.mock('./LoadingView', () => 'LoadingView');
|
||||
jest.mock('./DashboardLayout', () => 'DashboardLayout');
|
||||
@@ -38,12 +36,10 @@ describe('Dashboard', () => {
|
||||
});
|
||||
const createWrapper = ({
|
||||
hasCourses,
|
||||
hasAvailableDashboards,
|
||||
initIsPending,
|
||||
showSelectSessionModal,
|
||||
}) => {
|
||||
reduxHooks.useHasCourses.mockReturnValueOnce(hasCourses);
|
||||
reduxHooks.useHasAvailableDashboards.mockReturnValueOnce(hasAvailableDashboards);
|
||||
reduxHooks.useRequestIsPending.mockReturnValueOnce(initIsPending);
|
||||
reduxHooks.useShowSelectSessionModal.mockReturnValueOnce(showSelectSessionModal);
|
||||
return shallow(<Dashboard />);
|
||||
@@ -71,7 +67,6 @@ describe('Dashboard', () => {
|
||||
const testView = ({
|
||||
props,
|
||||
content: [contentName, contentEl],
|
||||
showEnterpriseModal,
|
||||
showSelectSessionModal,
|
||||
}) => {
|
||||
beforeEach(() => { wrapper = createWrapper(props); });
|
||||
@@ -80,10 +75,6 @@ describe('Dashboard', () => {
|
||||
it(`renders ${contentName}`, () => {
|
||||
testContent(contentEl);
|
||||
});
|
||||
it(`${renderString(showEnterpriseModal)} dashbaord modal`, () => {
|
||||
expect(wrapper.instance.findByType(EnterpriseDashboardModal).length)
|
||||
.toEqual(showEnterpriseModal ? 1 : 0);
|
||||
});
|
||||
it(`${renderString(showSelectSessionModal)} select session modal`, () => {
|
||||
expect(wrapper.instance.findByType(SelectSessionModal).length).toEqual(showSelectSessionModal ? 1 : 0);
|
||||
});
|
||||
@@ -92,44 +83,38 @@ describe('Dashboard', () => {
|
||||
testView({
|
||||
props: {
|
||||
hasCourses: false,
|
||||
hasAvailableDashboards: false,
|
||||
initIsPending: true,
|
||||
showSelectSessionModal: false,
|
||||
},
|
||||
content: ['LoadingView', <LoadingView />],
|
||||
showEnterpriseModal: false,
|
||||
showSelectSessionModal: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe('courses loaded, show select session modal, no available dashboards', () => {
|
||||
describe('courses loaded, show select session modal', () => {
|
||||
testView({
|
||||
props: {
|
||||
hasCourses: true,
|
||||
hasAvailableDashboards: false,
|
||||
initIsPending: false,
|
||||
showSelectSessionModal: true,
|
||||
},
|
||||
content: ['LoadedView', (
|
||||
<DashboardLayout><CoursesPanel /></DashboardLayout>
|
||||
)],
|
||||
showEnterpriseModal: false,
|
||||
showSelectSessionModal: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('there are no courses, there ARE available dashboards', () => {
|
||||
describe('there are no courses', () => {
|
||||
testView({
|
||||
props: {
|
||||
hasCourses: false,
|
||||
hasAvailableDashboards: true,
|
||||
initIsPending: false,
|
||||
showSelectSessionModal: false,
|
||||
},
|
||||
content: ['Dashboard layout with no courses sidebar and content', (
|
||||
<DashboardLayout><CoursesPanel /></DashboardLayout>
|
||||
)],
|
||||
showEnterpriseModal: true,
|
||||
showSelectSessionModal: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EnterpriseDashboard empty snapshot 1`] = `null`;
|
||||
|
||||
exports[`EnterpriseDashboard snapshot 1`] = `
|
||||
<ModalDialog
|
||||
hasCloseButton={false}
|
||||
onClose={[MockFunction useEnterpriseDashboardHook.handleEscape]}
|
||||
title=""
|
||||
>
|
||||
<div
|
||||
className="bg-white p-3 rounded shadow"
|
||||
style={
|
||||
{
|
||||
"textAlign": "start",
|
||||
}
|
||||
}
|
||||
>
|
||||
<h4>
|
||||
You have access to the edX, Inc. dashboard
|
||||
</h4>
|
||||
<p>
|
||||
To access the courses available to you through edX, Inc., visit the edX, Inc. dashboard now.
|
||||
</p>
|
||||
<ActionRow>
|
||||
<Button
|
||||
onClick={[MockFunction useEnterpriseDashboardHook.handleClose]}
|
||||
variant="tertiary"
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
<Button
|
||||
href="/edx-dashboard"
|
||||
onClick={[MockFunction useEnterpriseDashboardHook.handleCTAClick]}
|
||||
type="a"
|
||||
>
|
||||
Go to dashboard
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
`;
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import * as module from './hooks';
|
||||
|
||||
export const state = StrictDict({
|
||||
showModal: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
|
||||
const { modalOpened, modalClosed, modalCTAClicked } = track.enterpriseDashboard;
|
||||
|
||||
export const useEnterpriseDashboardHook = () => {
|
||||
const [showModal, setShowModal] = module.state.showModal(true);
|
||||
const dashboard = reduxHooks.useEnterpriseDashboardData();
|
||||
|
||||
const trackOpened = modalOpened(dashboard.enterpriseUUID);
|
||||
const trackClose = modalClosed(dashboard.enterpriseUUID, 'Cancel button');
|
||||
const trackEscape = modalClosed(dashboard.enterpriseUUID, 'Escape');
|
||||
|
||||
const handleCTAClick = modalCTAClicked(dashboard.enterpriseUUID, dashboard.url);
|
||||
const handleClose = () => {
|
||||
trackClose();
|
||||
setShowModal(false);
|
||||
};
|
||||
const handleEscape = () => {
|
||||
trackEscape();
|
||||
setShowModal(false);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (dashboard && dashboard.label) {
|
||||
trackOpened();
|
||||
}
|
||||
}, []); // eslint-disable-line
|
||||
|
||||
return {
|
||||
showModal,
|
||||
handleCTAClick,
|
||||
handleClose,
|
||||
handleEscape,
|
||||
dashboard,
|
||||
};
|
||||
};
|
||||
|
||||
export default useEnterpriseDashboardHook;
|
||||
@@ -1,75 +0,0 @@
|
||||
import { MockUseState } from 'testUtils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useEnterpriseDashboardData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('tracking', () => {
|
||||
const modalOpenedEvent = jest.fn();
|
||||
const modalClosedEvent = jest.fn();
|
||||
const modalCTAClickedEvent = jest.fn();
|
||||
return {
|
||||
__esModule: true,
|
||||
default: {
|
||||
enterpriseDashboard: {
|
||||
modalOpenedEvent,
|
||||
modalClosedEvent,
|
||||
modalCTAClickedEvent,
|
||||
modalOpened: jest.fn(() => modalOpenedEvent),
|
||||
modalClosed: jest.fn(() => modalClosedEvent),
|
||||
modalCTAClicked: jest.fn(() => modalCTAClickedEvent),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
const enterpriseDashboardData = { label: 'edX, Inc.', url: '/edx-dashboard' };
|
||||
|
||||
describe('EnterpriseDashboard hooks', () => {
|
||||
reduxHooks.useEnterpriseDashboardData.mockReturnValue({ ...enterpriseDashboardData });
|
||||
|
||||
describe('state values', () => {
|
||||
state.testGetter(state.keys.showModal);
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
let out;
|
||||
|
||||
beforeEach(() => {
|
||||
state.mock();
|
||||
out = hooks.useEnterpriseDashboardHook();
|
||||
});
|
||||
afterEach(state.restore);
|
||||
|
||||
test('useEnterpriseDashboardHook to return dashboard data from redux hooks', () => {
|
||||
expect(out.dashboard).toMatchObject(enterpriseDashboardData);
|
||||
});
|
||||
|
||||
test('modal initializes to shown when rendered and closes on click', () => {
|
||||
state.expectInitializedWith(state.keys.showModal, true);
|
||||
out.handleClose();
|
||||
expect(state.values.showModal).toEqual(false);
|
||||
});
|
||||
|
||||
test('modal initializes to shown when rendered and closes on escape', () => {
|
||||
state.expectInitializedWith(state.keys.showModal, true);
|
||||
out.handleEscape();
|
||||
expect(state.values.showModal).toEqual(false);
|
||||
});
|
||||
|
||||
test('CTA click tracks modalCTAClicked', () => {
|
||||
out.handleCTAClick();
|
||||
expect(track.enterpriseDashboard.modalCTAClicked).toHaveBeenCalledWith(
|
||||
enterpriseDashboardData.enterpriseUUID,
|
||||
enterpriseDashboardData.url,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
import React from 'react';
|
||||
// import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ModalDialog, ActionRow, Button,
|
||||
} from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
import useEnterpriseDashboardHook from './hooks';
|
||||
|
||||
export const EnterpriseDashboardModal = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
showModal,
|
||||
handleClose,
|
||||
handleCTAClick,
|
||||
handleEscape,
|
||||
dashboard,
|
||||
} = useEnterpriseDashboardHook();
|
||||
if (!dashboard || !dashboard.label) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ModalDialog
|
||||
isOpen={showModal}
|
||||
onClose={handleEscape}
|
||||
hasCloseButton={false}
|
||||
title=""
|
||||
>
|
||||
<div
|
||||
className="bg-white p-3 rounded shadow"
|
||||
style={{ textAlign: 'start' }}
|
||||
>
|
||||
<h4>
|
||||
{formatMessage(messages.enterpriseDialogHeader, {
|
||||
label: dashboard.label,
|
||||
})}
|
||||
</h4>
|
||||
<p>
|
||||
{formatMessage(messages.enterpriseDialogBody, {
|
||||
label: dashboard.label,
|
||||
})}
|
||||
</p>
|
||||
<ActionRow>
|
||||
<Button variant="tertiary" onClick={handleClose}>
|
||||
{formatMessage(messages.enterpriseDialogDismissButton)}
|
||||
</Button>
|
||||
<Button type="a" href={dashboard.url} onClick={handleCTAClick}>
|
||||
{formatMessage(messages.enterpriseDialogConfirmButton)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
);
|
||||
};
|
||||
|
||||
EnterpriseDashboardModal.propTypes = {};
|
||||
|
||||
export default EnterpriseDashboardModal;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import EnterpriseDashboard from '.';
|
||||
|
||||
import useEnterpriseDashboardHook from './hooks';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('EnterpriseDashboard', () => {
|
||||
test('snapshot', () => {
|
||||
const hookData = {
|
||||
dashboard: { label: 'edX, Inc.', url: '/edx-dashboard' },
|
||||
showDialog: false,
|
||||
handleClose: jest.fn().mockName('useEnterpriseDashboardHook.handleClose'),
|
||||
handleCTAClick: jest.fn().mockName('useEnterpriseDashboardHook.handleCTAClick'),
|
||||
handleEscape: jest.fn().mockName('useEnterpriseDashboardHook.handleEscape'),
|
||||
};
|
||||
useEnterpriseDashboardHook.mockReturnValueOnce({ ...hookData });
|
||||
const el = shallow(<EnterpriseDashboard />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('empty snapshot', () => {
|
||||
useEnterpriseDashboardHook.mockReturnValueOnce({});
|
||||
const el = shallow(<EnterpriseDashboard />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
enterpriseDialogHeader: {
|
||||
id: 'leanerDashboard.enterpriseDialogHeader',
|
||||
defaultMessage: 'You have access to the {label} dashboard',
|
||||
description: 'title for enterpise dashboard dialog',
|
||||
},
|
||||
enterpriseDialogBody: {
|
||||
id: 'leanerDashboard.enterpriseDialogBody',
|
||||
defaultMessage: 'To access the courses available to you through {label}, visit the {label} dashboard now.',
|
||||
description: 'Body text for enterpise dashboard dialog',
|
||||
},
|
||||
enterpriseDialogDismissButton: {
|
||||
id: 'leanerDashboard.enterpriseDialogDismissButton',
|
||||
defaultMessage: 'Dismiss',
|
||||
description: 'Dismiss button to cancel visiting dashboard',
|
||||
},
|
||||
enterpriseDialogConfirmButton: {
|
||||
id: 'leanerDashboard.enterpriseDialogConfirmButton',
|
||||
defaultMessage: 'Go to dashboard',
|
||||
description: 'Confirm button to go to the dashboard url',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable quotes */
|
||||
import { StrictDict } from 'utils';
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export const reasonKeys = StrictDict({
|
||||
prereqs: 'prereqs',
|
||||
@@ -26,7 +27,7 @@ export const order = [
|
||||
reasonKeys.easy,
|
||||
];
|
||||
|
||||
const messages = StrictDict({
|
||||
const messages = defineMessages({
|
||||
[reasonKeys.prereqs]: {
|
||||
id: 'learner-dash.unenrollConfirm.reasons.prereqs',
|
||||
description: 'Unenroll reason option - missing prerequisites',
|
||||
|
||||
@@ -10,10 +10,7 @@ export const numCourses = createSelector(
|
||||
(courseData) => Object.keys(courseData).length,
|
||||
);
|
||||
export const hasCourses = createSelector([module.numCourses], (num) => num > 0);
|
||||
export const hasAvailableDashboards = createSelector(
|
||||
[simpleSelectors.enterpriseDashboard],
|
||||
(data) => data !== null && data.isLearnerPortalEnabled === true,
|
||||
);
|
||||
|
||||
export const showSelectSessionModal = createSelector(
|
||||
[simpleSelectors.selectSessionModal],
|
||||
(data) => data.cardId != null,
|
||||
@@ -22,6 +19,5 @@ export const showSelectSessionModal = createSelector(
|
||||
export default StrictDict({
|
||||
numCourses,
|
||||
hasCourses,
|
||||
hasAvailableDashboards,
|
||||
showSelectSessionModal,
|
||||
});
|
||||
|
||||
@@ -17,15 +17,6 @@ describe('basic app selectors', () => {
|
||||
expect(cb(0)).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('hasAvailableDashboards', () => {
|
||||
it('returns true iff the enterpriseDashboard field is populated and learner portal is enabled', () => {
|
||||
const { preSelectors, cb } = appSelectors.hasAvailableDashboards;
|
||||
expect(preSelectors).toEqual([simpleSelectors.enterpriseDashboard]);
|
||||
expect(cb({ isLearnerPortalEnabled: true })).toEqual(true);
|
||||
expect(cb({ isLearnerPortalEnabled: false })).toEqual(false);
|
||||
expect(cb(null)).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('showSelectSessionModal', () => {
|
||||
it('returns true if the selectSessionModal cardId is not null', () => {
|
||||
const { preSelectors, cb } = appSelectors.showSelectSessionModal;
|
||||
|
||||
@@ -36,12 +36,6 @@ describe('app simple selectors', () => {
|
||||
expect(preSelectors).toEqual([appSelector]);
|
||||
expect(cb(testState.app)).toEqual(testString);
|
||||
});
|
||||
test('enterpriseDashboard returns empty object if data returns null', () => {
|
||||
testState = { app: { enterpriseDashboard: null } };
|
||||
const { preSelectors, cb } = simpleSelectors.enterpriseDashboard;
|
||||
expect(preSelectors).toEqual([appSelector]);
|
||||
expect(cb(testState.app)).toEqual({});
|
||||
});
|
||||
describe('cardSimpleSelectors', () => {
|
||||
keys = keyStore(cardSimpleSelectors);
|
||||
test.each([
|
||||
|
||||
@@ -18,7 +18,6 @@ export const useSocialShareSettings = () => useSelector(selectors.socialShareSet
|
||||
|
||||
/** global-level meta-selectors **/
|
||||
export const useHasCourses = () => useSelector(selectors.hasCourses);
|
||||
export const useHasAvailableDashboards = () => useSelector(selectors.hasAvailableDashboards);
|
||||
export const useCurrentCourseList = (opts) => useSelector(
|
||||
state => selectors.currentList(state, opts),
|
||||
);
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import React, { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import {
|
||||
Route, Navigate, Routes,
|
||||
} from 'react-router-dom';
|
||||
@@ -30,23 +30,29 @@ import App from './App';
|
||||
import NoticesWrapper from './components/NoticesWrapper';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
<AppProvider store={store}>
|
||||
<NoticesWrapper>
|
||||
<Routes>
|
||||
<Route path="/" element={<PageWrap><App /></PageWrap>} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</NoticesWrapper>
|
||||
</AppProvider>,
|
||||
document.getElementById('root'),
|
||||
const root = createRoot(document.getElementById('root'));
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<AppProvider store={store}>
|
||||
<NoticesWrapper>
|
||||
<Routes>
|
||||
<Route path="/" element={<PageWrap><App /></PageWrap>} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</NoticesWrapper>
|
||||
</AppProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
});
|
||||
|
||||
subscribe(APP_INIT_ERROR, (error) => {
|
||||
ReactDOM.render(
|
||||
<ErrorPage message={error.message} />,
|
||||
document.getElementById('root'),
|
||||
const root = createRoot(document.getElementById('root'));
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<ErrorPage message={error.message} />
|
||||
</StrictMode>,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { render } from 'react-dom';
|
||||
|
||||
import {
|
||||
APP_INIT_ERROR,
|
||||
APP_READY,
|
||||
@@ -11,9 +9,20 @@ import {
|
||||
import { configuration } from './config';
|
||||
import * as app from '.';
|
||||
|
||||
jest.mock('react-dom', () => ({
|
||||
render: jest.fn(),
|
||||
}));
|
||||
// These need to be var not let so they get hoisted
|
||||
// and can be used by jest.mock (which is also hoisted)
|
||||
var mockRender; // eslint-disable-line no-var
|
||||
var mockCreateRoot; // eslint-disable-line no-var
|
||||
jest.mock('react-dom/client', () => {
|
||||
mockRender = jest.fn();
|
||||
mockCreateRoot = jest.fn(() => ({
|
||||
render: mockRender,
|
||||
}));
|
||||
|
||||
return ({
|
||||
createRoot: mockCreateRoot,
|
||||
});
|
||||
});
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
mergeConfig: jest.fn(),
|
||||
@@ -32,7 +41,9 @@ describe('app registry', () => {
|
||||
let getElement;
|
||||
|
||||
beforeEach(() => {
|
||||
render.mockClear();
|
||||
mockCreateRoot.mockClear();
|
||||
mockRender.mockClear();
|
||||
|
||||
getElement = window.document.getElementById;
|
||||
window.document.getElementById = jest.fn(id => ({ id }));
|
||||
});
|
||||
@@ -44,18 +55,16 @@ describe('app registry', () => {
|
||||
const callArgs = subscribe.mock.calls[0];
|
||||
expect(callArgs[0]).toEqual(APP_READY);
|
||||
callArgs[1]();
|
||||
const [rendered, target] = render.mock.calls[0];
|
||||
const [rendered] = mockRender.mock.calls[0];
|
||||
expect(rendered).toMatchSnapshot();
|
||||
expect(target).toEqual(document.getElementById('root'));
|
||||
});
|
||||
test('subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element', () => {
|
||||
const callArgs = subscribe.mock.calls[1];
|
||||
expect(callArgs[0]).toEqual(APP_INIT_ERROR);
|
||||
const error = { message: 'test-error-message' };
|
||||
callArgs[1](error);
|
||||
const [rendered, target] = render.mock.calls[0];
|
||||
const [rendered] = mockRender.mock.calls[0];
|
||||
expect(rendered).toMatchSnapshot();
|
||||
expect(target).toEqual(document.getElementById('root'));
|
||||
});
|
||||
test('initialize is called with requireAuthenticatedUser', () => {
|
||||
expect(initialize).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Course Card Action Slot
|
||||
|
||||
### Slot ID: `course_banner_slot`
|
||||
### Slot ID: `org.openedx.frontend.learner_dashboard.course_card_banner.v1`
|
||||
### Props:
|
||||
* `cardId`
|
||||
|
||||
@@ -19,16 +19,17 @@ The following `env.config.jsx` will render a custom implemenation of a CourseBan
|
||||
|
||||
```js
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
course_banner_slot: {
|
||||
'org.openedx.frontend.learner_dashboard.course_card_banner.v1': {
|
||||
keepDefault: false,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_course_banner',
|
||||
id: 'org.openedx.frontend.learner_dashboard.course_card_banner.v1',
|
||||
type: DIRECT_PLUGIN,
|
||||
priority: 60,
|
||||
RenderWidget: ({ cardId }) => (
|
||||
|
||||
@@ -5,7 +5,7 @@ import CourseBanner from 'containers/CourseCard/components/CourseCardBanners/Cou
|
||||
|
||||
const CourseBannerSlot = ({ cardId }) => (
|
||||
<PluginSlot
|
||||
id="course_banner_slot"
|
||||
id="org.openedx.frontend.learner_dashboard.course_card_banner.v1"
|
||||
pluginProps={{
|
||||
cardId,
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# Course Card Action Slot
|
||||
|
||||
### Slot ID: `course_card_action_slot`
|
||||
### Slot ID: `org.openedx.frontend.learner_dashboard.course_card_action.v1`
|
||||
|
||||
### Slot ID Aliases
|
||||
* `course_card_action_slot`
|
||||
|
||||
### Props:
|
||||
* `cardId`
|
||||
|
||||
@@ -20,7 +24,7 @@ import ActionButton from 'containers/CourseCard/components/CourseCardActions/Act
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
course_card_action_slot: {
|
||||
'org.openedx.frontend.learner_dashboard.course_card_action.v1': {
|
||||
keepDefault: false,
|
||||
plugins: [
|
||||
{
|
||||
|
||||
@@ -4,7 +4,8 @@ import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const CourseCardActionSlot = ({ cardId }) => (
|
||||
<PluginSlot
|
||||
id="course_card_action_slot"
|
||||
id="org.openedx.frontend.learner_dashboard.course_card_action.v1"
|
||||
idAliases={['course_card_action_slot']}
|
||||
pluginProps={{
|
||||
cardId,
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Course List Slot
|
||||
|
||||
### Slot ID: `course_list_slot`
|
||||
### Slot ID: `org.openedx.frontend.learner_dashboard.course_list.v1`
|
||||
|
||||
### Slot ID Aliases
|
||||
* `course_list_slot`
|
||||
|
||||
## Plugin Props
|
||||
|
||||
@@ -25,7 +28,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
course_list_slot: {
|
||||
'org.openedx.frontend.learner_dashboard.course_list.v1': {
|
||||
// Hide the default CourseList component
|
||||
keepDefault: false,
|
||||
plugins: [
|
||||
|
||||
@@ -4,7 +4,11 @@ import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import { CourseList, courseListDataShape } from 'containers/CoursesPanel/CourseList';
|
||||
|
||||
export const CourseListSlot = ({ courseListData }) => (
|
||||
<PluginSlot id="course_list_slot" pluginProps={{ courseListData }}>
|
||||
<PluginSlot
|
||||
id="org.openedx.frontend.learner_dashboard.course_list.v1"
|
||||
idAliases={['course_list_slot']}
|
||||
pluginProps={{ courseListData }}
|
||||
>
|
||||
<CourseList courseListData={courseListData} />
|
||||
</PluginSlot>
|
||||
);
|
||||
|
||||
41
src/plugin-slots/DashboardModalSlot/README.md
Normal file
41
src/plugin-slots/DashboardModalSlot/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Dashboard Modal Slot
|
||||
|
||||
### Slot ID: `org.openedx.frontend.learner_dashboard.dashboard_modal.v1`
|
||||
|
||||
### https://github.com/openedx/frontend-plugin-framework/blob/master/docs/decisions/0003-slot-naming-and-life-cycle.rst#1-naming-format
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used for the modal on a dashboard.
|
||||
The following `env.config.jsx` will render the modal.
|
||||
|
||||
## Example
|
||||
|
||||
Learner dashboard will show modal
|
||||

|
||||
|
||||
```js
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
import { ModalDialog } from '@openedx/paragon';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
org.openedx.frontend.learner_dashboard.dashboard_modal.v1: {
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'dashboard_modal',
|
||||
type: DIRECT_PLUGIN,
|
||||
priority: 60,
|
||||
RenderWidget:
|
||||
<ModalDialog title="Modal that appears on learner dashboard" />,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 187 KiB |
8
src/plugin-slots/DashboardModalSlot/index.jsx
Normal file
8
src/plugin-slots/DashboardModalSlot/index.jsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const DashboardModalSlot = () => (
|
||||
<PluginSlot id="org.openedx.frontend.learner_dashboard.dashboard_modal.v1" />
|
||||
);
|
||||
|
||||
export default DashboardModalSlot;
|
||||
@@ -1,12 +1,15 @@
|
||||
# Footer Slot
|
||||
|
||||
### Slot ID: `footer_slot`
|
||||
### Slot ID: `org.openedx.frontend.layout.footer.v1`
|
||||
|
||||
### Slot ID Aliases
|
||||
* `footer_slot`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used to replace/modify/hide the footer.
|
||||
|
||||
The implementation of the `FooterSlot` component lives in [the `frontend-slot-footer` repository](https://github.com/openedx/frontend-slot-footer/).
|
||||
The implementation of the `FooterSlot` component lives in [the `frontend-component-footer` repository](https://github.com/openedx/frontend-component-footer/).
|
||||
|
||||
## Example
|
||||
|
||||
@@ -23,7 +26,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
footer_slot: {
|
||||
'org.openedx.frontend.layout.footer.v1': {
|
||||
plugins: [
|
||||
{
|
||||
// Hide the default footer
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# No Courses View Slot
|
||||
|
||||
### Slot ID: `no_courses_view_slot`
|
||||
### Slot ID: `org.openedx.frontend.learner_dashboard.no_courses_view.v1`
|
||||
|
||||
### Slot ID Aliases
|
||||
* `no_courses_view_slot`
|
||||
|
||||
## Description
|
||||
|
||||
@@ -21,7 +24,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
no_courses_view_slot: {
|
||||
'org.openedx.frontend.learner_dashboard.no_courses_view.v1': {
|
||||
// Hide the default NoCoursesView component
|
||||
keepDefault: false,
|
||||
plugins: [
|
||||
|
||||
@@ -4,7 +4,10 @@ import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import NoCoursesView from 'containers/CoursesPanel/NoCoursesView';
|
||||
|
||||
export const NoCoursesViewSlot = () => (
|
||||
<PluginSlot id="no_courses_view_slot">
|
||||
<PluginSlot
|
||||
id="org.openedx.frontend.learner_dashboard.no_courses_view.v1"
|
||||
idAliases={['no_courses_view_slot']}
|
||||
>
|
||||
<NoCoursesView />
|
||||
</PluginSlot>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# `frontend-app-learner-dashboard` Plugin Slots
|
||||
|
||||
* [`course_card_action_slot`](./CourseCardActionSlot/)
|
||||
* [`footer_slot`](./FooterSlot/)
|
||||
* [`widget_sidebar_slot`](./WidgetSidebarSlot/)
|
||||
* [`course_list_slot`](./CourseListSlot/)
|
||||
* [`no_courses_view_slot`](./NoCoursesViewSlot/)
|
||||
* [`org.openedx.frontend.learner_dashboard.course_card_action.v1`](./CourseCardActionSlot/)
|
||||
* [`org.openedx.frontend.layout.footer.v1`](./FooterSlot/)
|
||||
* [`org.openedx.frontend.learner_dashboard.widget_sidebar.v1`](./WidgetSidebarSlot/)
|
||||
* [`org.openedx.frontend.learner_dashboard.course_list.v1`](./CourseListSlot/)
|
||||
* [`org.openedx.frontend.learner_dashboard.no_courses_view.v1`](./NoCoursesViewSlot/)
|
||||
* [`org.openedx.frontend.learner_dashboard.dashboard_modal.v1`](./DashboardModalSlot)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Widget Sidebar Slot
|
||||
|
||||
### Slot ID: `widget_sidebar_slot`
|
||||
### Slot ID: `org.openedx.frontend.learner_dashboard.widget_sidebar.v1`
|
||||
|
||||
### Slot ID Aliases
|
||||
* `widget_sidebar_slot`
|
||||
|
||||
## Description
|
||||
|
||||
@@ -21,7 +24,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
widget_sidebar_slot: {
|
||||
'org.openedx.frontend.learner_dashboard.widget_sidebar.v1': {
|
||||
// Hide the default LookingForChallenge component
|
||||
keepDefault: false,
|
||||
plugins: [
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
exports[`WidgetSidebar snapshots 1`] = `
|
||||
<PluginSlot
|
||||
id="widget_sidebar_slot"
|
||||
id="org.openedx.frontend.learner_dashboard.widget_sidebar.v1"
|
||||
idAliases={
|
||||
[
|
||||
"widget_sidebar_slot",
|
||||
]
|
||||
}
|
||||
>
|
||||
<LookingForChallengeWidget />
|
||||
</PluginSlot>
|
||||
|
||||
@@ -5,7 +5,10 @@ import LookingForChallengeWidget from 'widgets/LookingForChallengeWidget';
|
||||
|
||||
// eslint-disable-next-line arrow-body-style
|
||||
export const WidgetSidebarSlot = () => (
|
||||
<PluginSlot id="widget_sidebar_slot">
|
||||
<PluginSlot
|
||||
id="org.openedx.frontend.learner_dashboard.widget_sidebar.v1"
|
||||
idAliases={['widget_sidebar_slot']}
|
||||
>
|
||||
<LookingForChallengeWidget />
|
||||
</PluginSlot>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import '@testing-library/jest-dom';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
|
||||
@@ -20,13 +20,8 @@ export const events = StrictDict({
|
||||
leaveSession: 'leaveSession',
|
||||
unenrollReason: 'unenrollReason',
|
||||
entitlementUnenrollReason: 'entitlementUnenrollReason',
|
||||
enterpriseDashboardModalOpened: 'enterpriseDashboardModalOpened',
|
||||
enterpriseDashboardModalCTAClicked: 'enterpriseDashboardModalCTAClicked',
|
||||
enterpriseDashboardModalClosed: 'enterpriseDashboardModalClosed',
|
||||
});
|
||||
|
||||
const learnerPortal = 'edx.ui.enterprise.lms.dashboard.learner_portal_modal';
|
||||
|
||||
export const eventNames = StrictDict({
|
||||
enterCourseClicked: 'edx.bi.dashboard.enter_course.clicked',
|
||||
courseImageClicked: 'edx.bi.dashboard.course_image.clicked',
|
||||
@@ -39,9 +34,6 @@ export const eventNames = StrictDict({
|
||||
leaveSession: 'course-dashboard.leave-session',
|
||||
unenrollReason: 'unenrollment_reason.selected',
|
||||
entitlementUnenrollReason: 'entitlement_unenrollment_reason.selected',
|
||||
enterpriseDashboardModalOpened: `${learnerPortal}.opened`,
|
||||
enterpriseDashboardModalCTAClicked: `${learnerPortal}.dashboard_cta.clicked`,
|
||||
enterpriseDashboardModalClosed: `${learnerPortal}.closed`,
|
||||
findCoursesClicked: 'edx.bi.dashboard.find_courses_button.clicked',
|
||||
purchaseCredit: 'edx.bi.credit.clicked_purchase_credit',
|
||||
filterClicked: 'course-dashboard.filter.clicked',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import course from './trackers/course';
|
||||
import credit from './trackers/credit';
|
||||
import engagement from './trackers/engagement';
|
||||
import enterpriseDashboard from './trackers/enterpriseDashboard';
|
||||
import entitlements from './trackers/entitlements';
|
||||
import socialShare from './trackers/socialShare';
|
||||
import findCourses from './trackers/findCourses';
|
||||
@@ -11,7 +10,6 @@ export default {
|
||||
course,
|
||||
credit,
|
||||
engagement,
|
||||
enterpriseDashboard,
|
||||
entitlements,
|
||||
socialShare,
|
||||
findCourses,
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { createEventTracker, createLinkTracker } from 'data/services/segment/utils';
|
||||
import { eventNames } from '../constants';
|
||||
|
||||
/** Enterprise Dashboard events**/
|
||||
/**
|
||||
* Creates tracking callback for Enterprise Dashboard Modal open event
|
||||
* @param {string} enterpriseUUID - enterprise identifier
|
||||
* @return {func} - Callback that tracks the event when fired.
|
||||
*/
|
||||
export const modalOpened = (enterpriseUUID) => () => createEventTracker(
|
||||
eventNames.enterpriseDashboardModalOpened,
|
||||
{ enterpriseUUID },
|
||||
);
|
||||
|
||||
/**
|
||||
* Creates tracking callback for Enterprise Dashboard Modal Call-to-action click-event
|
||||
* @param {string} enterpriseUUID - enterprise identifier
|
||||
* @param {string} href - destination url
|
||||
* @return {func} - Callback that tracks the event when fired and then loads the passed href.
|
||||
*/
|
||||
export const modalCTAClicked = (enterpriseUUID, href) => createLinkTracker(
|
||||
createEventTracker(
|
||||
eventNames.enterpriseDashboardModalCTAClicked,
|
||||
{ enterpriseUUID },
|
||||
),
|
||||
href,
|
||||
);
|
||||
|
||||
/**
|
||||
* Creates tracking callback for Enterprise Dashboard Modal close event
|
||||
* @param {string} enterpriseUUID - enterprise identifier
|
||||
* @param {string} source - close event soruce ("Cancel button" vs "Escape")
|
||||
* @return {func} - Callback that tracks the event when fired.
|
||||
*/
|
||||
export const modalClosed = (enterpriseUUID, source) => createEventTracker(
|
||||
eventNames.enterpriseDashboardModalClosed,
|
||||
{ enterpriseUUID, source },
|
||||
);
|
||||
|
||||
export default {
|
||||
modalOpened,
|
||||
modalCTAClicked,
|
||||
modalClosed,
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
import { createEventTracker } from 'data/services/segment/utils';
|
||||
import { eventNames } from '../constants';
|
||||
import * as trackers from './enterpriseDashboard';
|
||||
|
||||
jest.mock('data/services/segment/utils', () => ({
|
||||
createEventTracker: jest.fn(args => ({ createEventTracker: args })),
|
||||
createLinkTracker: jest.fn((cb, href) => ({ createLinkTracker: { cb, href } })),
|
||||
}));
|
||||
|
||||
const enterpriseUUID = 'test-enterprise-uuid';
|
||||
const source = 'test-source';
|
||||
|
||||
describe('enterpriseDashboard trackers', () => {
|
||||
describe('modalOpened', () => {
|
||||
it('creates event tracker for dashboard modal opened event', () => {
|
||||
expect(trackers.modalOpened(enterpriseUUID, source)()).toEqual(
|
||||
createEventTracker(
|
||||
eventNames.enterpriseDashboardModalOpened,
|
||||
{ enterpriseUUID, source },
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('modalCTAClicked', () => {
|
||||
const testHref = 'test-href';
|
||||
it('creates link tracker for dashboard modal cta click event', () => {
|
||||
const { cb, href } = trackers.modalCTAClicked(enterpriseUUID, testHref).createLinkTracker;
|
||||
expect(href).toEqual(testHref);
|
||||
expect(cb).toEqual(
|
||||
createEventTracker(
|
||||
eventNames.enterpriseDashboardModalCTAClicked,
|
||||
{ enterpriseUUID, source },
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('modalClosed', () => {
|
||||
it('creates event tracker for dashboard modal closed event with close source', () => {
|
||||
expect(trackers.modalClosed(enterpriseUUID, source)).toEqual(
|
||||
createEventTracker(
|
||||
eventNames.enterpriseDashboardModalClosed,
|
||||
{ enterpriseUUID, source },
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user