Compare commits

..

2 Commits

Author SHA1 Message Date
Adolfo R. Brandes
bc68a8c674 fix: Remove edX-specific reference
Remove edx-Specific reference from email confirmation banner.
2024-06-10 11:35:01 -03:00
Adolfo R. Brandes
15f9969993 feat: use frontend-plugin-framework to provide a FooterSlot (#356)
Co-authored-by: Brian Smith <bsmith@axim.org>
2024-06-06 15:05:35 -03:00
161 changed files with 19687 additions and 6019 deletions

2
.env
View File

@@ -40,5 +40,5 @@ ACCOUNT_SETTINGS_URL=''
ACCOUNT_PROFILE_URL=''
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY=''
ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false

View File

@@ -20,7 +20,7 @@ LMS_CLIENT_ID='login-service-client-id'
SEGMENT_KEY=''
FEATURE_FLAGS={}
MARKETING_SITE_BASE_URL='http://localhost:18000'
SUPPORT_URL=''
SUPPORT_URL='http://localhost:18000/support'
CONTACT_URL='http://localhost:18000/contact'
OPEN_SOURCE_URL='http://localhost:18000/openedx'
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
@@ -46,5 +46,5 @@ ACCOUNT_SETTINGS_URL='http://localhost:1997'
ACCOUNT_PROFILE_URL='http://localhost:1995'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY=''
ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false

View File

@@ -20,7 +20,7 @@ LMS_CLIENT_ID='login-service-client-id'
SEGMENT_KEY=''
FEATURE_FLAGS={}
MARKETING_SITE_BASE_URL='http://localhost:18000'
SUPPORT_URL=''
SUPPORT_URL='http://localhost:18000/support'
CONTACT_URL='http://localhost:18000/contact'
OPEN_SOURCE_URL='http://localhost:18000/openedx'
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
@@ -45,5 +45,5 @@ ACCOUNT_SETTINGS_URL='http://account-settings-url.test'
ACCOUNT_PROFILE_URL='http://account-profile-url.test'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY='SDK Key'
ENABLE_EDX_PERSONAL_DASHBOARD=true
ENABLE_PROGRAMS=false

7
.github/CODEOWNERS vendored
View File

@@ -1 +1,8 @@
# Root app is developed and owned by Aurora
* @openedx/2U-aperture
# WIDGETS and experiments are developed and owned by separate teams below
# Recommendations panel
/src/widgets/RecommendationsPanel @openedx/2U-vanguards
/src/widgets/LookingForChallengeWidget @openedx/2U-vanguards

View File

@@ -1,7 +0,0 @@
version: 2
updates:
# Adding new check for github-actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -10,19 +10,18 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20]
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Nodejs
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
node-version: ${{ env.NODE_VER }}
- name: Install dependencies
run: npm ci
@@ -40,10 +39,7 @@ jobs:
run: npm run build
- name: Run Coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
uses: codecov/codecov-action@v3
- name: Send failure notification
if: ${{ failure() }}

View File

@@ -7,10 +7,10 @@ on:
jobs:
release:
name: Release
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
fetch-depth: 0
@@ -18,7 +18,7 @@ jobs:
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VER }}

2
.nvmrc
View File

@@ -1 +1 @@
20
18.20

View File

@@ -69,5 +69,6 @@ module.exports = {
ACCOUNT_PROFILE_URL: 'http://localhost:1995',
ENABLE_NOTICES: '',
CAREER_LINK_URL: '',
OPTIMIZELY_FULL_STACK_SDK_KEY: '',
EXPERIMENT_08_23_VAN_PAINTED_DOOR: true,
};

19941
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,6 @@
"type": "git",
"url": "git+https://github.com/edx/frontend-app-learner-dashboard.git"
},
"browserslist": [
"extends @edx/browserslist-config"
],
"scripts": {
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
@@ -30,68 +27,70 @@
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-header": "^5.6.0",
"@edx/browserslist-config": "^1.1.0",
"@edx/frontend-enterprise-hotjar": "3.0.0",
"@edx/frontend-platform": "8.1.2",
"@edx/frontend-platform": "^7.1.4",
"@edx/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "3.0.0",
"@edx/react-unit-test-utils": "2.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-plugin-framework": "^1.1.2",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.2.2",
"@optimizely/react-sdk": "^2.9.2",
"@redux-beacon/segment": "^1.1.0",
"@redux-devtools/extension": "3.3.0",
"@reduxjs/toolkit": "^1.6.1",
"@testing-library/user-event": "^13.5.0",
"axios": "^0.28.0",
"classnames": "^2.3.1",
"core-js": "3.38.1",
"core-js": "3.16.2",
"dompurify": "^2.3.1",
"email-prop-type": "^3.0.1",
"file-saver": "^2.0.5",
"filesize": "^8.0.6",
"font-awesome": "4.7.0",
"history": "5.3.0",
"history": "5.0.1",
"html-react-parser": "^1.3.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-when": "^3.6.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"prop-types": "^15.7.2",
"query-string": "7.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-intl": "6.8.0",
"react-intl": "6.4.7",
"react-pdf": "^7.0.0",
"react-redux": "^7.2.4",
"react-router-dom": "6.27.0",
"react-router-dom": "6.15.0",
"react-share": "^4.4.0",
"react-zendesk": "^0.1.13",
"redux": "4.2.1",
"redux": "4.1.1",
"redux-beacon": "^2.1.0",
"redux-devtools-extension": "2.13.9",
"redux-logger": "3.0.6",
"redux-thunk": "2.4.2",
"regenerator-runtime": "^0.14.0",
"redux-thunk": "2.3.0",
"regenerator-runtime": "^0.13.9",
"reselect": "^4.0.0",
"universal-cookie": "^4.0.4",
"util": "^0.12.4",
"whatwg-fetch": "^3.6.2"
},
"devDependencies": {
"@edx/browserslist-config": "^1.3.0",
"@edx/reactifex": "^2.1.1",
"@openedx/frontend-build": "14.1.5",
"@openedx/frontend-build": "13.1.4",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.1.0",
"copy-webpack-plugin": "^12.0.0",
"husky": "^9.0.0",
"axios-mock-adapter": "^1.20.0",
"copy-webpack-plugin": "^11.0.0",
"fetch-mock": "^9.11.0",
"husky": "^7.0.0",
"identity-obj-proxy": "^3.0.0",
"jest-expect-message": "^1.1.3",
"react-dev-utils": "^12.0.0",
"jest-expect-message": "^1.0.2",
"react-dev-utils": "^11.0.4",
"react-test-renderer": "^17.0.2",
"redux-mock-store": "^1.5.4",
"semantic-release": "^20.1.3"

View File

@@ -17,6 +17,8 @@ import {
} from 'data/redux';
import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import ZendeskFab from 'components/ZendeskFab';
import { ExperimentProvider } from 'ExperimentContext';
import track from 'tracking';
@@ -40,6 +42,19 @@ export const App = () => {
const { supportEmail } = reduxHooks.usePlatformSettingsData();
const loadData = reduxHooks.useLoadData();
const optimizelyScript = () => {
if (getConfig().OPTIMIZELY_URL) {
return <script src={getConfig().OPTIMIZELY_URL} />;
} if (getConfig().OPTIMIZELY_PROJECT_ID) {
return (
<script
src={`${getConfig().MARKETING_SITE_BASE_URL}/optimizelyjs/${getConfig().OPTIMIZELY_PROJECT_ID}.js`}
/>
);
}
return null;
};
React.useEffect(() => {
if (authenticatedUser?.administrator || getConfig().NODE_ENV === 'development') {
window.loadEmptyData = () => {
@@ -76,6 +91,7 @@ export const App = () => {
<Helmet>
<title>{formatMessage(messages.pageTitle)}</title>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
{optimizelyScript()}
</Helmet>
<div>
<AppWrapper>
@@ -87,11 +103,14 @@ export const App = () => {
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
</Alert>
) : (
<Dashboard />
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
)}
</main>
</AppWrapper>
<FooterSlot />
<ZendeskFab />
</div>
</>
);

View File

@@ -9,7 +9,6 @@ $fa-font-path: "~font-awesome/fonts";
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work
@import "~@edx/frontend-component-header/dist/index";
@import "~@edx/frontend-component-footer/dist/_footer";
.text-ellipsis {

View File

@@ -10,6 +10,7 @@ import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
import { ExperimentProvider } from 'ExperimentContext';
import { App } from './App';
import messages from './messages';
@@ -18,6 +19,9 @@ jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'Footer' }));
jest.mock('containers/Dashboard', () => 'Dashboard');
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
jest.mock('components/ZendeskFab', () => 'ZendeskFab');
jest.mock('ExperimentContext', () => ({
ExperimentProvider: 'ExperimentProvider',
}));
jest.mock('containers/WidgetContainers/AppWrapper', () => 'AppWrapper');
jest.mock('data/redux', () => ({
selectors: 'redux.selectors',
@@ -75,10 +79,11 @@ describe('App router component', () => {
it('loads dashboard', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const dashboard = main.children[0];
expect(dashboard.type).toEqual('Dashboard');
const expProvider = main.children[0];
expect(expProvider.type).toEqual('ExperimentProvider');
expect(expProvider.children.length).toEqual(1);
expect(
dashboard.matches(shallow(<Dashboard />)),
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
).toEqual(true);
});
});
@@ -92,10 +97,11 @@ describe('App router component', () => {
it('loads dashboard', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const dashboard = main.children[0];
expect(dashboard.type).toEqual('Dashboard');
const expProvider = main.children[0];
expect(expProvider.type).toEqual('ExperimentProvider');
expect(expProvider.children.length).toEqual(1);
expect(
dashboard.matches(shallow(<Dashboard />)),
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
).toEqual(true);
});
});
@@ -109,10 +115,11 @@ describe('App router component', () => {
it('loads dashboard', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const dashboard = main.children[0];
expect(dashboard.type).toEqual('Dashboard');
const expProvider = main.children[0];
expect(expProvider.type).toEqual('ExperimentProvider');
expect(expProvider.children.length).toEqual(1);
expect(
dashboard.matches(shallow(<Dashboard />)),
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
).toEqual(true);
});
});

64
src/ExperimentContext.jsx Normal file
View File

@@ -0,0 +1,64 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useWindowSize, breakpoints } from '@openedx/paragon';
import { StrictDict } from 'utils';
import api from 'widgets/ProductRecommendations/api';
import * as module from './ExperimentContext';
export const state = StrictDict({
experiment: (val) => React.useState(val), // eslint-disable-line
countryCode: (val) => React.useState(val), // eslint-disable-line
});
export const useCountryCode = (setCountryCode) => {
React.useEffect(() => {
api
.fetchRecommendationsContext()
.then((response) => {
setCountryCode(response.data.countryCode);
})
.catch(() => {
setCountryCode('');
});
/* eslint-disable */
}, []);
};
export const ExperimentContext = React.createContext();
export const ExperimentProvider = ({ children }) => {
const [countryCode, setCountryCode] = module.state.countryCode(null);
const [experiment, setExperiment] = module.state.experiment({
isExperimentActive: false,
inRecommendationsVariant: true,
});
module.useCountryCode(setCountryCode);
const { width } = useWindowSize();
const isMobile = width < breakpoints.small.minWidth;
const contextValue = React.useMemo(
() => ({
experiment,
countryCode,
setExperiment,
setCountryCode,
isMobile,
}),
[experiment, countryCode, setExperiment, setCountryCode, isMobile]
);
return (
<ExperimentContext.Provider value={contextValue}>
{children}
</ExperimentContext.Provider>
);
};
export const useExperimentContext = () => React.useContext(ExperimentContext);
ExperimentProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export default { useCountryCode, useExperimentContext };

View File

@@ -0,0 +1,122 @@
import React from 'react';
import { waitFor, render } from '@testing-library/react';
import { useWindowSize } from '@openedx/paragon';
import api from 'widgets/ProductRecommendations/api';
import { MockUseState } from 'testUtils';
import * as experiment from 'ExperimentContext';
const state = new MockUseState(experiment);
jest.unmock('react');
jest.spyOn(React, 'useEffect').mockImplementation((cb, prereqs) => ({ useEffect: { cb, prereqs } }));
jest.mock('widgets/ProductRecommendations/api', () => ({
fetchRecommendationsContext: jest.fn(),
}));
describe('experiments context', () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe('useCountryCode', () => {
describe('behaviour', () => {
describe('useEffect call', () => {
let calls;
let cb;
const setCountryCode = jest.fn();
const successfulFetch = { data: { countryCode: 'ZA' } };
beforeEach(() => {
experiment.useCountryCode(setCountryCode);
({ calls } = React.useEffect.mock);
[[cb]] = calls;
});
it('calls useEffect once', () => {
expect(calls.length).toEqual(1);
});
describe('successful fetch', () => {
it('sets the country code', async () => {
let resolveFn;
api.fetchRecommendationsContext.mockReturnValueOnce(
new Promise((resolve) => {
resolveFn = resolve;
}),
);
cb();
expect(api.fetchRecommendationsContext).toHaveBeenCalled();
expect(setCountryCode).not.toHaveBeenCalled();
resolveFn(successfulFetch);
await waitFor(() => {
expect(setCountryCode).toHaveBeenCalledWith(successfulFetch.data.countryCode);
});
});
});
describe('unsuccessful fetch', () => {
it('sets the country code to an empty string', async () => {
let rejectFn;
api.fetchRecommendationsContext.mockReturnValueOnce(
new Promise((resolve, reject) => {
rejectFn = reject;
}),
);
cb();
expect(api.fetchRecommendationsContext).toHaveBeenCalled();
expect(setCountryCode).not.toHaveBeenCalled();
rejectFn();
await waitFor(() => {
expect(setCountryCode).toHaveBeenCalledWith('');
});
});
});
});
});
});
describe('ExperimentProvider', () => {
const { ExperimentProvider } = experiment;
const TestComponent = () => {
const {
experiment: exp,
setExperiment,
countryCode,
setCountryCode,
isMobile,
} = experiment.useExperimentContext();
expect(exp.isExperimentActive).toBeFalsy();
expect(exp.inRecommendationsVariant).toBeTruthy();
expect(countryCode).toBeNull();
expect(isMobile).toBe(false);
expect(setExperiment).toBeDefined();
expect(setCountryCode).toBeDefined();
return (
<div />
);
};
it('allows access to child components with the context stateful values', () => {
const countryCodeSpy = jest.spyOn(experiment, 'useCountryCode').mockImplementationOnce(() => {});
useWindowSize.mockImplementationOnce(() => ({ width: 577, height: 943 }));
state.mock();
render(
<ExperimentProvider>
<TestComponent />
</ExperimentProvider>,
);
expect(countryCodeSpy).toHaveBeenCalledWith(state.setState.countryCode);
state.expectInitializedWith(state.keys.countryCode, null);
state.expectInitializedWith(state.keys.experiment, { isExperimentActive: false, inRecommendationsVariant: true });
});
});
});

View File

@@ -28,6 +28,7 @@ exports[`App router component component initialize failure snapshot 1`] = `
</main>
</AppWrapper>
<FooterSlot />
<ZendeskFab />
</div>
</Fragment>
`;
@@ -50,10 +51,13 @@ exports[`App router component component no network failure snapshot 1`] = `
<AppWrapper>
<LearnerDashboardHeader />
<main>
<Dashboard />
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
</main>
</AppWrapper>
<FooterSlot />
<ZendeskFab />
</div>
</Fragment>
`;
@@ -71,15 +75,21 @@ exports[`App router component component no network failure with optimizely proje
rel="shortcut icon"
type="image/x-icon"
/>
<script
src="undefined/optimizelyjs/fakeId.js"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<Dashboard />
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
</main>
</AppWrapper>
<FooterSlot />
<ZendeskFab />
</div>
</Fragment>
`;
@@ -97,15 +107,21 @@ exports[`App router component component no network failure with optimizely url s
rel="shortcut icon"
type="image/x-icon"
/>
<script
src="fake.url"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<Dashboard />
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
</main>
</AppWrapper>
<FooterSlot />
<ZendeskFab />
</div>
</Fragment>
`;
@@ -138,6 +154,7 @@ exports[`App router component component refresh failure snapshot 1`] = `
</main>
</AppWrapper>
<FooterSlot />
<ZendeskFab />
</div>
</Fragment>
`;

View File

@@ -9,7 +9,7 @@ exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPag
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
<AppProvider
store={
{
Object {
"redux": "store",
}
}

View File

@@ -5,23 +5,23 @@ exports[`ZendeskFab snapshot 1`] = `
cookies={true}
defer={true}
webWidget={
{
"answerBot": {
"avatar": {
"name": {
Object {
"answerBot": Object {
"avatar": Object {
"name": Object {
"*": "edX Support",
},
"url": "https://edx-cdn.org/v3/prod/favicon.ico",
},
"contactOnlyAfterQuery": true,
"suppress": false,
"title": {
"title": Object {
"*": "edX Support",
},
},
"chat": {
"departments": {
"enabled": [
"chat": Object {
"departments": Object {
"enabled": Array [
"account settings",
"billing and payments",
"certificates",
@@ -33,17 +33,17 @@ exports[`ZendeskFab snapshot 1`] = `
},
"suppress": false,
},
"contactForm": {
"contactForm": Object {
"attachments": true,
"selectTicketForm": {
"selectTicketForm": Object {
"*": "Please choose your request type:",
},
"ticketForms": [
{
"fields": [
{
"ticketForms": Array [
Object {
"fields": Array [
Object {
"id": "description",
"prefill": {
"prefill": Object {
"*": "",
},
},
@@ -53,10 +53,10 @@ exports[`ZendeskFab snapshot 1`] = `
},
],
},
"contactOptions": {
"contactOptions": Object {
"enabled": false,
},
"helpCenter": {
"helpCenter": Object {
"originalArticleButton": true,
},
}

View File

@@ -18,8 +18,6 @@ const configuration = {
CAREER_LINK_URL: process.env.CAREER_LINK_URL || null,
LOGO_URL: process.env.LOGO_URL,
ENABLE_EDX_PERSONAL_DASHBOARD: process.env.ENABLE_EDX_PERSONAL_DASHBOARD === 'true',
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
ENABLE_PROGRAMS: process.env.ENABLE_PROGRAMS === 'true',
};
const features = {};

View File

@@ -6,8 +6,8 @@ exports[`BeginCourseButton snapshot disabled snapshot 1`] = `
disabled={true}
href="#"
onClick={
{
"trackCourseEvent": {
Object {
"trackCourseEvent": Object {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
@@ -25,8 +25,8 @@ exports[`BeginCourseButton snapshot enabled snapshot 1`] = `
disabled={false}
href="#"
onClick={
{
"trackCourseEvent": {
Object {
"trackCourseEvent": Object {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",

View File

@@ -6,8 +6,8 @@ exports[`ResumeButton snapshot disabled snapshot 1`] = `
disabled={true}
href="#"
onClick={
{
"trackCourseEvent": {
Object {
"trackCourseEvent": Object {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
@@ -25,8 +25,8 @@ exports[`ResumeButton snapshot enabled snapshot 1`] = `
disabled={false}
href="#"
onClick={
{
"trackCourseEvent": {
Object {
"trackCourseEvent": Object {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",

View File

@@ -7,8 +7,8 @@ exports[`UpgradeButton snapshot can upgrade 1`] = `
href="upgradeUrl"
iconBefore={[MockFunction icons.Locked]}
onClick={
{
"trackCourseEvent": {
Object {
"trackCourseEvent": Object {
"cardId": "cardId",
"eventName": [MockFunction segment.trackUpgradeClicked],
"upgradeUrl": "upgradeUrl",

View File

@@ -6,8 +6,8 @@ exports[`ViewCourseButton learner can view course 1`] = `
disabled={false}
href="#"
onClick={
{
"trackCourseEvent": {
Object {
"trackCourseEvent": Object {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "homeUrl",
@@ -25,8 +25,8 @@ exports[`ViewCourseButton learner cannot view course 1`] = `
disabled={true}
href="#"
onClick={
{
"trackCourseEvent": {
Object {
"trackCourseEvent": Object {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "homeUrl",

View File

@@ -5,7 +5,7 @@ import { ActionRow } from '@openedx/paragon';
import { reduxHooks } from 'hooks';
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
import UpgradeButton from './UpgradeButton';
import SelectSessionButton from './SelectSessionButton';
import BeginCourseButton from './BeginCourseButton';
import ResumeButton from './ResumeButton';
@@ -14,13 +14,15 @@ import ViewCourseButton from './ViewCourseButton';
export const CourseCardActions = ({ cardId }) => {
const { isEntitlement, isFulfilled } = reduxHooks.useCardEntitlementData(cardId);
const {
isVerified,
hasStarted,
isExecEd2UCourse,
} = reduxHooks.useCardEnrollmentData(cardId);
const { isArchived } = reduxHooks.useCardCourseRunData(cardId);
return (
<ActionRow data-test-id="CourseCardActions">
<CourseCardActionSlot cardId={cardId} />
{!(isEntitlement || isVerified || isExecEd2UCourse) && <UpgradeButton cardId={cardId} />}
{isEntitlement && (isFulfilled
? <ViewCourseButton cardId={cardId} />
: <SelectSessionButton cardId={cardId} />

View File

@@ -2,7 +2,6 @@ import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
import UpgradeButton from './UpgradeButton';
import SelectSessionButton from './SelectSessionButton';
import BeginCourseButton from './BeginCourseButton';
@@ -20,7 +19,6 @@ jest.mock('hooks', () => ({
},
}));
jest.mock('plugin-slots/CourseCardActionSlot', () => 'CustomActionButton');
jest.mock('./UpgradeButton', () => 'UpgradeButton');
jest.mock('./SelectSessionButton', () => 'SelectSessionButton');
jest.mock('./ViewCourseButton', () => 'ViewCourseButton');
@@ -90,18 +88,18 @@ describe('CourseCardActions', () => {
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
});
});
describe('not entitlement, verified, or exec ed', () => {
describe('not entielement, verified, or exec ed', () => {
it('renders UpgradeButton and ViewCourseButton for archived courses', () => {
mockHooks({ isArchived: true });
render();
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId);
});
describe('unstarted courses', () => {
it('renders UpgradeButton and BeginCourseButton', () => {
mockHooks();
render();
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
expect(el.instance.findByType(BeginCourseButton)[0].props.cardId).toEqual(cardId);
});
});
@@ -109,7 +107,7 @@ describe('CourseCardActions', () => {
it('renders UpgradeButton and ResumeButton', () => {
mockHooks({ hasStarted: true });
render();
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
expect(el.instance.findByType(ResumeButton)[0].props.cardId).toEqual(cardId);
});
});

View File

@@ -10,14 +10,14 @@ exports[`CreditBanner component render with error state snapshot 1`] = `
>
<format-message-function
message={
{
Object {
"defaultMessage": "An error occurred with this transaction. For help, contact {supportEmailLink}.",
"description": "",
"id": "learner-dash.courseCard.banners.credit.error",
}
}
values={
{
Object {
"supportEmailLink": <MailtoLink
to="test-support-email"
>

View File

@@ -27,8 +27,8 @@ exports[`CreditContent component render with action snapshot 1`] = `
</ActionRow>
<CreditRequestForm
requestData={
{
"parameters": {
Object {
"parameters": Object {
"key1": "val1",
},
"url": "test-request-data-url",
@@ -48,8 +48,8 @@ exports[`CreditContent component render without action snapshot 1`] = `
</div>
<CreditRequestForm
requestData={
{
"parameters": {
Object {
"parameters": Object {
"key1": "val1",
},
"url": "test-request-data-url",

View File

@@ -13,12 +13,12 @@ exports[`RelatedProgramsBanner render with programs 1`] = `
</span>
<ProgramsList
programs={
[
{
Array [
Object {
"title": "Program 1",
"url": "http://example.com/program1",
},
{
Object {
"title": "Program 2",
"url": "http://example.com/program2",
},

View File

@@ -43,14 +43,14 @@ exports[`CertificateBanner snapshot is restricted and verified with billing emai
<format-message-function
message={
{
Object {
"defaultMessage": "If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}",
"description": "Message to learners to contact billing for certificate refunds",
"id": "learner-dash.courseCard.banners.certificateRefundContactBilling",
}
}
values={
{
Object {
"billingEmail": <MailtoLink
to="billing@email"
>
@@ -68,14 +68,14 @@ exports[`CertificateBanner snapshot is restricted and verified with support and
>
<format-message-function
message={
{
Object {
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
{
Object {
"supportEmail": <MailtoLink
to="suport@email"
>
@@ -87,14 +87,14 @@ exports[`CertificateBanner snapshot is restricted and verified with support and
<format-message-function
message={
{
Object {
"defaultMessage": "If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}",
"description": "Message to learners to contact billing for certificate refunds",
"id": "learner-dash.courseCard.banners.certificateRefundContactBilling",
}
}
values={
{
Object {
"billingEmail": <MailtoLink
to="billing@email"
>
@@ -112,14 +112,14 @@ exports[`CertificateBanner snapshot is restricted and verified with support emai
>
<format-message-function
message={
{
Object {
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
{
Object {
"supportEmail": <MailtoLink
to="suport@email"
>
@@ -147,14 +147,14 @@ exports[`CertificateBanner snapshot is restricted with support email 1`] = `
>
<format-message-function
message={
{
Object {
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
{
Object {
"supportEmail": <MailtoLink
to="suport@email"
>

View File

@@ -4,14 +4,14 @@ exports[`EntitlementBanner snapshot: expiration warning 1`] = `
<Banner>
<format-message-function
message={
{
Object {
"defaultMessage": "You must {selectSessionButton} by {changeDeadline} to access the course.",
"description": "Entitlement course message when the entitlement is expiring soon.",
"id": "learner-dash.courseCard.banners.entitlementExpiringSoon",
}
}
values={
{
Object {
"changeDeadline": "11/11/2022",
"selectSessionButton": <Button
className="m-0 p-0"
@@ -33,14 +33,14 @@ exports[`EntitlementBanner snapshot: no sessions available 1`] = `
>
<format-message-function
message={
{
Object {
"defaultMessage": "There are no sessions available at the moment. The course team will create new sessions soon. If no sessions appear, please contact {emailLink} for information.",
"description": "Entitlement course message when no sessions are available",
"id": "learner-dash.courseCard.banners.entitlementUnavailable",
}
}
values={
{
Object {
"emailLink": <MailtoLink
to="test-support-email"
>

View File

@@ -17,7 +17,7 @@ exports[`CourseCardMenu render show dropdown hide unenroll item and disable emai
<SocialShareMenu
cardId="test-card-id"
emailSettings={
{
Object {
"hide": [MockFunction emailSettingHide],
"isVisible": false,
"show": [MockFunction emailSettingShow],
@@ -58,7 +58,7 @@ exports[`CourseCardMenu render show dropdown show unenroll and enable email snap
<SocialShareMenu
cardId="test-card-id"
emailSettings={
{
Object {
"hide": [MockFunction emailSettingHide],
"isVisible": false,
"show": [MockFunction emailSettingShow],

View File

@@ -5,8 +5,8 @@ exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
className="pgn__card-wrapper-image-cap overflow-visible orientation"
href="home-url"
onClick={
{
"trackCourseEvent": {
Object {
"trackCourseEvent": Object {
"cardId": "cardId",
"eventName": [MockFunction segment.courseImageClicked],
"upgradeUrl": "home-url",

View File

@@ -7,8 +7,8 @@ exports[`CourseCardTitle snapshot renders clickable link course title 1`] = `
data-testid="CourseCardTitle"
href="home-url"
onClick={
{
"trackCourseEvent": {
Object {
"trackCourseEvent": Object {
"cardId": "cardId",
"eventName": [MockFunction segment.courseTitleClicked],
"upgradeUrl": "home-url",

View File

@@ -28,7 +28,7 @@ exports[`CourseFilterControls is not mobile snapshot 1`] = `
>
<FilterForm
filters={
[
Array [
"test-filter",
]
}
@@ -84,7 +84,7 @@ exports[`CourseFilterControls mobile snapshot 1`] = `
>
<FilterForm
filters={
[
Array [
"test-filter",
]
}
@@ -144,7 +144,7 @@ exports[`CourseFilterControls no courses snapshot 1`] = `
>
<FilterForm
filters={
[
Array [
"test-filter",
]
}

View File

@@ -11,7 +11,7 @@ exports[`FilterForm snapshot renders 1`] = `
name="course-status-filters"
onChange={[MockFunction handleFilterChange]}
value={
[
Array [
"test-filter",
]
}

View File

@@ -9,10 +9,9 @@ import CourseCard from 'containers/CourseCard';
import { useIsCollapsed } from './hooks';
export const CourseList = ({ courseListData }) => {
const {
filterOptions, setPageNumber, numPages, showFilters, visibleList,
} = courseListData;
export const CourseList = ({
filterOptions, setPageNumber, numPages, showFilters, visibleList,
}) => {
const isCollapsed = useIsCollapsed();
return (
<>
@@ -39,16 +38,14 @@ export const CourseList = ({ courseListData }) => {
);
};
export const courseListDataShape = PropTypes.shape({
CourseList.propTypes = {
showFilters: PropTypes.bool.isRequired,
visibleList: PropTypes.arrayOf(PropTypes.shape()).isRequired,
filterOptions: PropTypes.shape().isRequired,
// eslint-disable-next-line react/forbid-prop-types
visibleList: PropTypes.arrayOf(PropTypes.object).isRequired,
// eslint-disable-next-line react/forbid-prop-types
filterOptions: PropTypes.object.isRequired,
numPages: PropTypes.number.isRequired,
setPageNumber: PropTypes.func.isRequired,
});
CourseList.propTypes = {
courseListData: courseListDataShape,
};
export default CourseList;

View File

@@ -23,7 +23,7 @@ describe('CourseList', () => {
useIsCollapsed.mockReturnValue(false);
const createWrapper = (courseListData = defaultCourseListData) => (
shallow(<CourseList courseListData={courseListData} />)
shallow(<CourseList {...courseListData} />)
);
describe('no courses or filters', () => {

View File

@@ -18,7 +18,11 @@ exports[`CoursesPanel no courses snapshot 1`] = `
<CourseFilterControls />
</div>
</div>
<NoCoursesViewSlot />
<PluginSlot
id="no_courses_view"
>
<NoCoursesView />
</PluginSlot>
</div>
`;
@@ -40,16 +44,16 @@ exports[`CoursesPanel with courses snapshot 1`] = `
<CourseFilterControls />
</div>
</div>
<CourseListSlot
courseListData={
{
"filterOptions": {},
"numPages": 1,
"setPageNumber": [MockFunction setPageNumber],
"showFilters": false,
"visibleList": [],
}
}
/>
<PluginSlot
id="course_list"
>
<CourseList
filterOptions={Object {}}
numPages={1}
setPageNumber={[MockFunction setPageNumber]}
showFilters={false}
visibleList={Array []}
/>
</PluginSlot>
</div>
`;

View File

@@ -1,13 +1,15 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import {
CourseFilterControls,
} from 'containers/CourseFilterControls';
import CourseListSlot from 'plugin-slots/CourseListSlot';
import NoCoursesViewSlot from 'plugin-slots/NoCoursesViewSlot';
import NoCoursesView from './NoCoursesView';
import CourseList from './CourseList';
import { useCourseListData } from './hooks';
@@ -32,7 +34,19 @@ export const CoursesPanel = () => {
<CourseFilterControls {...courseListData.filterOptions} />
</div>
</div>
{hasCourses ? <CourseListSlot courseListData={courseListData} /> : <NoCoursesViewSlot />}
{hasCourses ? (
<PluginSlot
id="course_list"
>
<CourseList {...courseListData} />
</PluginSlot>
) : (
<PluginSlot
id="no_courses_view"
>
<NoCoursesView />
</PluginSlot>
)}
</div>
);
};

View File

@@ -3,8 +3,7 @@ import PropTypes from 'prop-types';
import { Container, Col, Row } from '@openedx/paragon';
import WidgetSidebarSlot from 'plugin-slots/WidgetSidebarSlot';
import WidgetFooter from 'containers/WidgetContainers/WidgetFooter';
import hooks from './hooks';
export const columnConfig = {
@@ -24,10 +23,11 @@ export const columnConfig = {
},
};
export const DashboardLayout = ({ children }) => {
export const DashboardLayout = ({ children, sidebar: Sidebar }) => {
const {
isCollapsed,
sidebarShowing,
setSidebarShowing,
} = hooks.useDashboardLayoutData();
const courseListColumnProps = sidebarShowing
@@ -42,7 +42,12 @@ export const DashboardLayout = ({ children }) => {
</Col>
<Col {...columnConfig.sidebar} className="sidebar-column">
{!isCollapsed && (<h2 className="course-list-title">&nbsp;</h2>)}
<WidgetSidebarSlot />
<Sidebar setSidebarShowing={setSidebarShowing} />
</Col>
</Row>
<Row>
<Col>
<WidgetFooter />
</Col>
</Row>
</Container>
@@ -50,6 +55,7 @@ export const DashboardLayout = ({ children }) => {
};
DashboardLayout.propTypes = {
children: PropTypes.node.isRequired,
sidebar: PropTypes.func.isRequired,
};
export default DashboardLayout;

View File

@@ -16,13 +16,17 @@ const hookProps = {
};
hooks.useDashboardLayoutData.mockReturnValue(hookProps);
const props = {
sidebar: jest.fn(() => 'test-sidebar-content'),
};
const children = 'test-children';
let el;
describe('DashboardLayout', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<DashboardLayout>{children}</DashboardLayout>);
el = shallow(<DashboardLayout {...props}>{children}</DashboardLayout>);
});
const testColumns = () => {
@@ -36,13 +40,17 @@ describe('DashboardLayout', () => {
const columns = el.instance.findByType(Row)[0].findByType(Col);
expect(columns[0].children).not.toHaveLength(0);
});
it('displays WidgetSidebarSlot in second column', () => {
it('displays sidebar prop in second column', () => {
const columns = el.instance.findByType(Row)[0].findByType(Col);
expect(columns[1].findByType('WidgetSidebarSlot')).toHaveLength(1);
expect(columns[1].findByType(props.sidebar)).toHaveLength(1);
});
it('displays a footer in the second row', () => {
const columns = el.instance.findByType(Row)[1].findByType(Col);
expect(columns[0].children[0].type).toEqual('WidgetFooter');
});
};
const testSidebarLayout = () => {
it('displays withSidebar width for course list column', () => {
it('displays widthSidebar width for course list column', () => {
const columns = el.instance.findByType(Row)[0].findByType(Col);
Object.keys(columnConfig.courseList.withSidebar).forEach(size => {
expect(columns[0].props[size]).toEqual(columnConfig.courseList.withSidebar[size]);

View File

@@ -9,13 +9,13 @@ exports[`DashboardLayout collapsed sidebar not showing snapshot 1`] = `
<Col
className="course-list-column"
lg={
{
Object {
"offset": 0,
"span": 12,
}
}
xl={
{
Object {
"offset": 0,
"span": 12,
}
@@ -26,19 +26,26 @@ exports[`DashboardLayout collapsed sidebar not showing snapshot 1`] = `
<Col
className="sidebar-column"
lg={
{
Object {
"offset": 0,
"span": 12,
}
}
xl={
{
Object {
"offset": 0,
"span": 4,
}
}
>
<WidgetSidebarSlot />
<mockConstructor
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
/>
</Col>
</Row>
<Row>
<Col>
<WidgetFooter />
</Col>
</Row>
</Container>
@@ -53,13 +60,13 @@ exports[`DashboardLayout collapsed sidebar showing snapshot 1`] = `
<Col
className="course-list-column"
lg={
{
Object {
"offset": 0,
"span": 12,
}
}
xl={
{
Object {
"offset": 0,
"span": 8,
}
@@ -70,19 +77,26 @@ exports[`DashboardLayout collapsed sidebar showing snapshot 1`] = `
<Col
className="sidebar-column"
lg={
{
Object {
"offset": 0,
"span": 12,
}
}
xl={
{
Object {
"offset": 0,
"span": 4,
}
}
>
<WidgetSidebarSlot />
<mockConstructor
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
/>
</Col>
</Row>
<Row>
<Col>
<WidgetFooter />
</Col>
</Row>
</Container>
@@ -97,13 +111,13 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
<Col
className="course-list-column"
lg={
{
Object {
"offset": 0,
"span": 12,
}
}
xl={
{
Object {
"offset": 0,
"span": 12,
}
@@ -114,13 +128,13 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
<Col
className="sidebar-column"
lg={
{
Object {
"offset": 0,
"span": 12,
}
}
xl={
{
Object {
"offset": 0,
"span": 4,
}
@@ -131,7 +145,14 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
>
 
</h2>
<WidgetSidebarSlot />
<mockConstructor
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
/>
</Col>
</Row>
<Row>
<Col>
<WidgetFooter />
</Col>
</Row>
</Container>
@@ -146,13 +167,13 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
<Col
className="course-list-column"
lg={
{
Object {
"offset": 0,
"span": 12,
}
}
xl={
{
Object {
"offset": 0,
"span": 8,
}
@@ -163,13 +184,13 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
<Col
className="sidebar-column"
lg={
{
Object {
"offset": 0,
"span": 12,
}
}
xl={
{
Object {
"offset": 0,
"span": 4,
}
@@ -180,7 +201,14 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
>
 
</h2>
<WidgetSidebarSlot />
<mockConstructor
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
/>
</Col>
</Row>
<Row>
<Col>
<WidgetFooter />
</Col>
</Row>
</Container>

View File

@@ -17,7 +17,9 @@ exports[`Dashboard snapshots courses loaded, show select session modal, no avail
data-testid="dashboard-content"
id="dashboard-content"
>
<DashboardLayout>
<DashboardLayout
sidebar="LoadedWidgetSidebar"
>
<CoursesPanel />
</DashboardLayout>
</div>
@@ -60,7 +62,9 @@ exports[`Dashboard snapshots there are no courses, there ARE available dashboard
data-testid="dashboard-content"
id="dashboard-content"
>
<DashboardLayout>
<DashboardLayout
sidebar="NoCoursesWidgetSidebar"
>
<CoursesPanel />
</DashboardLayout>
</div>

View File

@@ -26,8 +26,7 @@ export const useDashboardMessages = () => {
export const useDashboardLayoutData = () => {
const { width } = useWindowSize();
const [sidebarShowing, setSidebarShowing] = module.state.sidebarShowing(true);
const [sidebarShowing, setSidebarShowing] = module.state.sidebarShowing(false);
return {
isDashboardCollapsed: width < breakpoints.large.maxWidth,
sidebarShowing,

View File

@@ -40,9 +40,9 @@ describe('CourseCard hooks', () => {
describe('useDashboardLayoutData', () => {
beforeEach(() => { state.mock(); });
describe('behavior', () => {
it('initializes sidebarShowing to default true value', () => {
it('initializes sidebarShowing to default false value', () => {
hooks.useDashboardLayoutData();
state.expectInitializedWith(state.keys.sidebarShowing, true);
state.expectInitializedWith(state.keys.sidebarShowing, false);
});
});
describe('output', () => {

View File

@@ -6,6 +6,9 @@ import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
import SelectSessionModal from 'containers/SelectSessionModal';
import CoursesPanel from 'containers/CoursesPanel';
import LoadedSidebar from 'containers/WidgetContainers/LoadedSidebar';
import NoCoursesSidebar from 'containers/WidgetContainers/NoCoursesSidebar';
import LoadingView from './LoadingView';
import DashboardLayout from './DashboardLayout';
import hooks from './hooks';
@@ -32,7 +35,7 @@ export const Dashboard = () => {
{initIsPending
? (<LoadingView />)
: (
<DashboardLayout>
<DashboardLayout sidebar={hasCourses ? LoadedSidebar : NoCoursesSidebar}>
<CoursesPanel />
</DashboardLayout>
)}

View File

@@ -6,6 +6,9 @@ import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
import SelectSessionModal from 'containers/SelectSessionModal';
import CoursesPanel from 'containers/CoursesPanel';
import LoadedWidgetSidebar from 'containers/WidgetContainers/LoadedSidebar';
import NoCoursesWidgetSidebar from 'containers/WidgetContainers/NoCoursesSidebar';
import DashboardLayout from './DashboardLayout';
import LoadingView from './LoadingView';
import hooks from './hooks';
@@ -22,6 +25,8 @@ jest.mock('hooks', () => ({
jest.mock('containers/EnterpriseDashboardModal', () => 'EnterpriseDashboardModal');
jest.mock('containers/CoursesPanel', () => 'CoursesPanel');
jest.mock('containers/WidgetContainers/LoadedSidebar', () => 'LoadedWidgetSidebar');
jest.mock('containers/WidgetContainers/NoCoursesSidebar', () => 'NoCoursesWidgetSidebar');
jest.mock('./LoadingView', () => 'LoadingView');
jest.mock('./DashboardLayout', () => 'DashboardLayout');
@@ -111,7 +116,7 @@ describe('Dashboard', () => {
showSelectSessionModal: true,
},
content: ['LoadedView', (
<DashboardLayout><CoursesPanel /></DashboardLayout>
<DashboardLayout sidebar={LoadedWidgetSidebar}><CoursesPanel /></DashboardLayout>
)],
showEnterpriseModal: false,
showSelectSessionModal: true,
@@ -127,7 +132,7 @@ describe('Dashboard', () => {
showSelectSessionModal: false,
},
content: ['Dashboard layout with no courses sidebar and content', (
<DashboardLayout><CoursesPanel /></DashboardLayout>
<DashboardLayout sidebar={NoCoursesWidgetSidebar}><CoursesPanel /></DashboardLayout>
)],
showEnterpriseModal: true,
showSelectSessionModal: false,

View File

@@ -10,7 +10,7 @@ exports[`EmailSettingsModal render snapshot: emails disabled, show: false 1`] =
<div
className="bg-white p-3 rounded shadow"
style={
{
Object {
"textAlign": "start",
}
}
@@ -54,7 +54,7 @@ exports[`EmailSettingsModal render snapshot: emails disabled, show: true 1`] = `
<div
className="bg-white p-3 rounded shadow"
style={
{
Object {
"textAlign": "start",
}
}
@@ -98,7 +98,7 @@ exports[`EmailSettingsModal render snapshot: emails enabled, show: true 1`] = `
<div
className="bg-white p-3 rounded shadow"
style={
{
Object {
"textAlign": "start",
}
}

View File

@@ -11,7 +11,7 @@ exports[`EnterpriseDashboard snapshot 1`] = `
<div
className="bg-white p-3 rounded shadow"
style={
{
Object {
"textAlign": "start",
}
}

View File

@@ -0,0 +1,104 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { Button, Badge } from '@openedx/paragon';
import urls from 'data/services/lms/urls';
import { reduxHooks } from 'hooks';
import { findCoursesNavDropdownClicked } from '../hooks';
import messages from '../messages';
export const CollapseMenuBody = ({ isOpen }) => {
const { formatMessage } = useIntl();
const { authenticatedUser } = React.useContext(AppContext);
const dashboard = reduxHooks.useEnterpriseDashboardData();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const exploreCoursesClick = findCoursesNavDropdownClicked(
urls.baseAppUrl(courseSearchUrl),
);
if (!isOpen) {
return null;
}
return (
<div className="d-flex flex-column shadow-sm nav-small-menu">
<Button as="a" href="/" variant="inverse-primary">
{formatMessage(messages.course)}
</Button>
<Button as="a" href={urls.programsUrl()} variant="inverse-primary">
{formatMessage(messages.program)}
</Button>
<Button
as="a"
href={urls.baseAppUrl(courseSearchUrl)}
variant="inverse-primary"
onClick={exploreCoursesClick}
>
{formatMessage(messages.discoverNew)}
</Button>
<Button as="a" href={getConfig().SUPPORT_URL} variant="inverse-primary">
{formatMessage(messages.help)}
</Button>
{authenticatedUser && (
<>
{!!dashboard && (
<Button as="a" href={dashboard.url} variant="inverse-primary">
{formatMessage(messages.dashboard)}
</Button>
)}
{!dashboard && getConfig().CAREER_LINK_URL && (
<Button href={`${getConfig().CAREER_LINK_URL}`}>
{formatMessage(messages.career)}
<Badge className="px-2 mx-2" variant="warning">
{formatMessage(messages.newAlert)}
</Badge>
</Button>
)}
<Button
as="a"
href={`${getConfig().LMS_BASE_URL}/u/${authenticatedUser.username}`}
variant="inverse-primary"
>
{formatMessage(messages.profile)}
</Button>
<Button
as="a"
href={`${getConfig().LMS_BASE_URL}/account/settings`}
variant="inverse-primary"
>
{formatMessage(messages.account)}
</Button>
{getConfig().ORDER_HISTORY_URL && (
<Button
as="a"
variant="inverse-primary"
href={getConfig().ORDER_HISTORY_URL}
>
{formatMessage(messages.orderHistory)}
</Button>
)}
<Button
as="a"
href={getConfig().LOGOUT_URL}
variant="inverse-primary"
>
{formatMessage(messages.signOut)}
</Button>
</>
)}
</div>
);
};
CollapseMenuBody.propTypes = {
isOpen: PropTypes.bool.isRequired,
};
export default CollapseMenuBody;

View File

@@ -0,0 +1,48 @@
import { shallow } from '@edx/react-unit-test-utils';
import { AppContext } from '@edx/frontend-platform/react';
import CollapseMenuBody from './CollapseMenuBody';
jest.mock('@edx/frontend-platform/react', () => ({
AppContext: {
authenticatedUser: {
username: 'username',
},
},
}));
jest.mock('hooks', () => ({
reduxHooks: {
useEnterpriseDashboardData: () => ({
url: 'url',
}),
usePlatformSettingsData: () => ({
courseSearchUrl: '/courseSearchUrl',
}),
},
}));
jest.mock('../hooks', () => ({
findCoursesNavDropdownClicked: (url) => jest.fn().mockName(`findCoursesNavDropdownClicked("${url}")`),
}));
describe('CollapseMenuBody', () => {
test('render', () => {
const wrapper = shallow(<CollapseMenuBody isOpen />);
expect(wrapper.snapshot).toMatchSnapshot();
});
test('render empty if not open', () => {
const wrapper = shallow(<CollapseMenuBody isOpen={false} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.isEmptyRender()).toBe(true);
});
test('render unauthenticated', () => {
const { authenticatedUser } = AppContext;
AppContext.authenticatedUser = null;
const wrapper = shallow(<CollapseMenuBody isOpen />);
expect(wrapper.snapshot).toMatchSnapshot();
AppContext.authenticatedUser = authenticatedUser;
});
});

View File

@@ -0,0 +1,105 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CollapseMenuBody render 1`] = `
<div
className="d-flex flex-column shadow-sm nav-small-menu"
>
<Button
as="a"
href="/"
variant="inverse-primary"
>
Courses
</Button>
<Button
as="a"
href="http://localhost:18000/dashboard/programs"
variant="inverse-primary"
>
Programs
</Button>
<Button
as="a"
href="http://localhost:18000/courseSearchUrl"
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
variant="inverse-primary"
>
Discover New
</Button>
<Button
as="a"
href="http://localhost:18000/support"
variant="inverse-primary"
>
Help
</Button>
<Fragment>
<Button
as="a"
href="url"
variant="inverse-primary"
>
Dashboard
</Button>
<Button
as="a"
href="http://localhost:18000/u/username"
variant="inverse-primary"
>
Profile
</Button>
<Button
as="a"
href="http://localhost:18000/account/settings"
variant="inverse-primary"
>
Account
</Button>
<Button
as="a"
href="http://localhost:18000/logout"
variant="inverse-primary"
>
Sign Out
</Button>
</Fragment>
</div>
`;
exports[`CollapseMenuBody render empty if not open 1`] = `null`;
exports[`CollapseMenuBody render unauthenticated 1`] = `
<div
className="d-flex flex-column shadow-sm nav-small-menu"
>
<Button
as="a"
href="/"
variant="inverse-primary"
>
Courses
</Button>
<Button
as="a"
href="http://localhost:18000/dashboard/programs"
variant="inverse-primary"
>
Programs
</Button>
<Button
as="a"
href="http://localhost:18000/courseSearchUrl"
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
variant="inverse-primary"
>
Discover New
</Button>
<Button
as="a"
href="http://localhost:18000/support"
variant="inverse-primary"
>
Help
</Button>
</div>
`;

View File

@@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CollapsedHeader render nothing if not collapsed 1`] = `false`;
exports[`CollapsedHeader renders 1`] = `
<Fragment>
<header
className="d-flex shadow-sm align-items-center learner-variant-header"
>
<IconButton
alt="Close"
className="p-4"
iconAs="Icon"
invertColors={true}
isActive={true}
onClick={[MockFunction toggleIsOpen]}
variant="primary"
/>
<mockConstructor />
</header>
<mockConstructor
isOpen={false}
/>
</Fragment>
`;
exports[`CollapsedHeader renders with isOpen true 1`] = `
<Fragment>
<header
className="d-flex shadow-sm align-items-center learner-variant-header"
>
<IconButton
alt="Menu"
className="p-4"
iconAs="Icon"
invertColors={true}
isActive={true}
onClick={[MockFunction toggleIsOpen]}
src={[MockFunction icons.Close]}
variant="primary"
/>
<mockConstructor />
</header>
<mockConstructor
isOpen={true}
/>
</Fragment>
`;

View File

@@ -0,0 +1,47 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { MenuIcon, Close } from '@openedx/paragon/icons';
import { IconButton, Icon } from '@openedx/paragon';
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
import CollapseMenuBody from './CollapseMenuBody';
import BrandLogo from '../BrandLogo';
import messages from '../messages';
export const CollapsedHeader = () => {
const { formatMessage } = useIntl();
const isCollapsed = useIsCollapsed();
const { isOpen, toggleIsOpen } = useLearnerDashboardHeaderData();
return (
isCollapsed && (
<>
<header className="d-flex shadow-sm align-items-center learner-variant-header">
<IconButton
invertColors
isActive
src={isOpen ? Close : MenuIcon}
iconAs={Icon}
alt={
isOpen
? formatMessage(messages.collapseMenuOpenAltText)
: formatMessage(messages.collapseMenuClosedAltText)
}
onClick={toggleIsOpen}
variant="primary"
className="p-4"
/>
<BrandLogo />
</header>
<CollapseMenuBody isOpen={isOpen} />
</>
)
);
};
CollapsedHeader.propTypes = {};
export default CollapsedHeader;

View File

@@ -0,0 +1,38 @@
import { shallow } from '@edx/react-unit-test-utils';
import CollapsedHeader from '.';
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
jest.mock('../BrandLogo', () => jest.fn(() => 'BrandLogo'));
jest.mock('./CollapseMenuBody', () => jest.fn(() => 'CollapseMenuBody'));
jest.mock('../hooks', () => ({
useIsCollapsed: jest.fn(() => true),
useLearnerDashboardHeaderData: jest.fn(() => ({
isOpen: false,
toggleIsOpen: jest.fn().mockName('toggleIsOpen'),
})),
}));
describe('CollapsedHeader', () => {
it('renders', () => {
const wrapper = shallow(<CollapsedHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
});
it('render nothing if not collapsed', () => {
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<CollapsedHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
});
it('renders with isOpen true', () => {
useLearnerDashboardHeaderData.mockReturnValueOnce({
isOpen: true,
toggleIsOpen: jest.fn().mockName('toggleIsOpen'),
});
const wrapper = shallow(<CollapsedHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
});
});

View File

@@ -9,14 +9,14 @@ exports[`ConfirmEmailBanner snapshot Show on unverified 1`] = `
>
<format-message-function
message={
{
Object {
"defaultMessage": "Remember to confirm your email so that you can keep learning! {confirmNowButton}.",
"description": "Text for reminding user to confirm email",
"id": "leanerDashboard.confirmEmailTextReminderBanner",
}
}
values={
{
Object {
"confirmNowButton": <Button
className="confirm-email-now-button"
onClick={[MockFunction openConfirmModalButtonClick]}

View File

@@ -0,0 +1,78 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { AvatarButton, Dropdown, Badge } from '@openedx/paragon';
import { reduxHooks } from 'hooks';
import messages from '../messages';
export const AuthenticatedUserDropdown = () => {
const { formatMessage } = useIntl();
const { authenticatedUser } = React.useContext(AppContext);
const dashboard = reduxHooks.useEnterpriseDashboardData();
return (
authenticatedUser && (
<Dropdown className="user-dropdown pr4">
<Dropdown.Toggle
as={AvatarButton}
src={authenticatedUser.profileImage}
id="user"
variant="light"
className="p-4"
>
<span data-hj-suppress className="d-md-inline">
{authenticatedUser.username}
</span>
</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right">
{ getConfig().ENABLE_EDX_PERSONAL_DASHBOARD && (
<>
<Dropdown.Header>{formatMessage(messages.dashboardSwitch)}</Dropdown.Header>
<Dropdown.Item as="a" href="/edx-dashboard" className="active">
{formatMessage(messages.dashboardPersonal)}
</Dropdown.Item>
{!!dashboard && (
<Dropdown.Item as="a" href={dashboard.url} key={dashboard.label}>
{dashboard.label} {formatMessage(messages.dashboard)}
</Dropdown.Item>
)}
<Dropdown.Divider />
</>
)}
{!dashboard && getConfig().CAREER_LINK_URL && (
<Dropdown.Item href={`${getConfig().CAREER_LINK_URL}`}>
{formatMessage(messages.career)}
<Badge className="px-2 mx-2" variant="warning">
{formatMessage(messages.newAlert)}
</Badge>
</Dropdown.Item>
)}
<Dropdown.Item href={`${getConfig().ACCOUNT_PROFILE_URL}/u/${authenticatedUser.username}`}>
{formatMessage(messages.profile)}
</Dropdown.Item>
<Dropdown.Item href={getConfig().ACCOUNT_SETTINGS_URL}>
{formatMessage(messages.account)}
</Dropdown.Item>
{getConfig().ORDER_HISTORY_URL && (
<Dropdown.Item href={getConfig().ORDER_HISTORY_URL}>
{formatMessage(messages.orderHistory)}
</Dropdown.Item>
)}
<Dropdown.Divider />
<Dropdown.Item href={getConfig().LOGOUT_URL}>
{formatMessage(messages.signOut)}
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
)
);
};
AuthenticatedUserDropdown.propTypes = {};
export default AuthenticatedUserDropdown;

View File

@@ -0,0 +1,81 @@
import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import { getConfig } from '@edx/frontend-platform';
import { AppContext } from '@edx/frontend-platform/react';
import { AuthenticatedUserDropdown } from './AuthenticatedUserDropdown';
import { useIsCollapsed } from '../hooks';
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
jest.mock('@edx/frontend-platform/react', () => ({
AppContext: {
authenticatedUser: {
profileImage: 'profileImage',
username: 'username',
},
},
}));
const COURSE_SEARCH_URL = 'test-course-search-url';
jest.mock('hooks', () => ({
reduxHooks: {
useEnterpriseDashboardData: jest.fn(),
usePlatformSettingsData: jest.fn(() => ({
courseSearchUrl: COURSE_SEARCH_URL,
})),
},
}));
jest.mock('../hooks', () => ({
useIsCollapsed: jest.fn(),
findCoursesNavDropdownClicked: (href) => jest.fn().mockName(`findCoursesNavDropdownClicked('${href}')`),
}));
jest.mock('data/services/lms/urls', () => ({
baseAppUrl: (url) => (url),
programsUrl: 'http://localhost:18000/dashboard/programs',
}));
const config = {
ACCOUNT_PROFILE_URL: 'http://account-profile-url.test',
ACCOUNT_SETTINGS_URL: 'http://account-settings-url.test',
LOGOUT_URL: 'http://logout-url.test',
ORDER_HISTORY_URL: 'http://order-history-url.test',
SUPPORT_URL: 'http://localhost:18000/support',
CAREER_LINK_URL: 'http://localhost:18000/career',
LMS_BASE_URL: 'http:/localhost:18000',
ENABLE_EDX_PERSONAL_DASHBOARD: true,
};
getConfig.mockReturnValue(config);
describe('AuthenticatedUserDropdown', () => {
const defaultDashboardData = {
label: 'label',
url: 'url',
};
describe('snapshots', () => {
test('no auth render empty', () => {
const { authenticatedUser } = AppContext;
AppContext.authenticatedUser = null;
const wrapper = shallow(<AuthenticatedUserDropdown />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.isEmptyRender()).toBe(true);
AppContext.authenticatedUser = authenticatedUser;
});
test('with enterprise dashboard', () => {
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(defaultDashboardData);
useIsCollapsed.mockReturnValueOnce(true);
const wrapper = shallow(<AuthenticatedUserDropdown />);
expect(wrapper.snapshot).toMatchSnapshot();
});
test('without enterprise dashboard and expanded', () => {
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(null);
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<AuthenticatedUserDropdown />);
expect(wrapper.snapshot).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,139 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AuthenticatedUserDropdown snapshots no auth render empty 1`] = `null`;
exports[`AuthenticatedUserDropdown snapshots with enterprise dashboard 1`] = `
<Dropdown
className="user-dropdown pr4"
>
<Dropdown.Toggle
className="p-4"
id="user"
src="profileImage"
variant="light"
>
<span
className="d-md-inline"
data-hj-suppress={true}
>
username
</span>
</Dropdown.Toggle>
<Dropdown.Menu
className="dropdown-menu-right"
>
<Fragment>
<Dropdown.Header>
SWITCH DASHBOARD
</Dropdown.Header>
<Dropdown.Item
as="a"
className="active"
href="/edx-dashboard"
>
Personal
</Dropdown.Item>
<Dropdown.Item
as="a"
href="url"
key="label"
>
label
Dashboard
</Dropdown.Item>
<Dropdown.Divider />
</Fragment>
<Dropdown.Item
href="http://account-profile-url.test/u/username"
>
Profile
</Dropdown.Item>
<Dropdown.Item
href="http://account-settings-url.test"
>
Account
</Dropdown.Item>
<Dropdown.Item
href="http://order-history-url.test"
>
Order History
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
href="http://logout-url.test"
>
Sign Out
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
`;
exports[`AuthenticatedUserDropdown snapshots without enterprise dashboard and expanded 1`] = `
<Dropdown
className="user-dropdown pr4"
>
<Dropdown.Toggle
className="p-4"
id="user"
src="profileImage"
variant="light"
>
<span
className="d-md-inline"
data-hj-suppress={true}
>
username
</span>
</Dropdown.Toggle>
<Dropdown.Menu
className="dropdown-menu-right"
>
<Fragment>
<Dropdown.Header>
SWITCH DASHBOARD
</Dropdown.Header>
<Dropdown.Item
as="a"
className="active"
href="/edx-dashboard"
>
Personal
</Dropdown.Item>
<Dropdown.Divider />
</Fragment>
<Dropdown.Item
href="http://localhost:18000/career"
>
Career
<Badge
className="px-2 mx-2"
variant="warning"
>
New
</Badge>
</Dropdown.Item>
<Dropdown.Item
href="http://account-profile-url.test/u/username"
>
Profile
</Dropdown.Item>
<Dropdown.Item
href="http://account-settings-url.test"
>
Account
</Dropdown.Item>
<Dropdown.Item
href="http://order-history-url.test"
>
Order History
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
href="http://logout-url.test"
>
Sign Out
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
`;

View File

@@ -0,0 +1,52 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ExpandedHeader render 1`] = `
<header
className="d-flex shadow-sm align-items-center learner-variant-header pl-4"
>
<div
className="flex-grow-1 d-flex align-items-center"
>
<BrandLogo />
<Button
as="a"
className="p-4 course-link"
href="/"
variant="inverse-primary"
>
Courses
</Button>
<Button
as="a"
className="p-4"
href="programsUrl"
variant="inverse-primary"
>
Programs
</Button>
<Button
as="a"
className="p-4"
href="http://localhost:18000/courseSearchUrl"
onClick={[MockFunction findCoursesNavClicked("http://localhost:18000/courseSearchUrl")]}
variant="inverse-primary"
>
Discover New
</Button>
<span
className="flex-grow-1"
/>
<Button
as="a"
className="p-4"
href="http://localhost:18000/support"
variant="inverse-primary"
>
Help
</Button>
</div>
<AuthenticatedUserDropdown />
</header>
`;
exports[`ExpandedHeader render empty if collapsed 1`] = `null`;

View File

@@ -0,0 +1,76 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import urls from 'data/services/lms/urls';
import { reduxHooks } from 'hooks';
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
import { useIsCollapsed, findCoursesNavClicked } from '../hooks';
import messages from '../messages';
import BrandLogo from '../BrandLogo';
export const ExpandedHeader = () => {
const { formatMessage } = useIntl();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const isCollapsed = useIsCollapsed();
const exploreCoursesClick = findCoursesNavClicked(
urls.baseAppUrl(courseSearchUrl),
);
if (isCollapsed) {
return null;
}
return (
<header className="d-flex shadow-sm align-items-center learner-variant-header pl-4">
<div className="flex-grow-1 d-flex align-items-center">
<BrandLogo />
<Button
as="a"
href="/"
variant="inverse-primary"
className="p-4 course-link"
>
{formatMessage(messages.course)}
</Button>
<Button
as="a"
href={urls.programsUrl()}
variant="inverse-primary"
className="p-4"
>
{formatMessage(messages.program)}
</Button>
<Button
as="a"
href={urls.baseAppUrl(courseSearchUrl)}
variant="inverse-primary"
className="p-4"
onClick={exploreCoursesClick}
>
{formatMessage(messages.discoverNew)}
</Button>
<span className="flex-grow-1" />
<Button
as="a"
href={getConfig().SUPPORT_URL}
variant="inverse-primary"
className="p-4"
>
{formatMessage(messages.help)}
</Button>
</div>
<AuthenticatedUserDropdown />
</header>
);
};
ExpandedHeader.propTypes = {};
export default ExpandedHeader;

View File

@@ -0,0 +1,41 @@
import { shallow } from '@edx/react-unit-test-utils';
import ExpandedHeader from '.';
import { useIsCollapsed } from '../hooks';
jest.mock('data/services/lms/urls', () => ({
programsUrl: () => 'programsUrl',
baseAppUrl: url => (`http://localhost:18000${url}`),
}));
jest.mock('hooks', () => ({
reduxHooks: {
usePlatformSettingsData: () => ({
courseSearchUrl: '/courseSearchUrl',
}),
},
}));
jest.mock('../hooks', () => ({
useIsCollapsed: jest.fn(),
findCoursesNavClicked: (url) => jest.fn().mockName(`findCoursesNavClicked("${url}")`),
}));
jest.mock('./AuthenticatedUserDropdown', () => 'AuthenticatedUserDropdown');
jest.mock('../BrandLogo', () => 'BrandLogo');
describe('ExpandedHeader', () => {
test('render', () => {
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<ExpandedHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
});
test('render empty if collapsed', () => {
useIsCollapsed.mockReturnValueOnce(true);
const wrapper = shallow(<ExpandedHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.isEmptyRender()).toBe(true);
});
});

View File

@@ -1,76 +0,0 @@
import { getConfig } from '@edx/frontend-platform';
import urls from 'data/services/lms/urls';
import messages from './messages';
const getLearnerHeaderMenu = (
formatMessage,
courseSearchUrl,
authenticatedUser,
exploreCoursesClick,
) => ({
mainMenu: [
{
type: 'item',
href: '/',
content: formatMessage(messages.course),
isActive: true,
},
...(getConfig().ENABLE_PROGRAMS ? [{
type: 'item',
href: `${urls.programsUrl()}`,
content: formatMessage(messages.program),
}] : []),
{
type: 'item',
href: `${urls.baseAppUrl(courseSearchUrl)}`,
content: formatMessage(messages.discoverNew),
onClick: (e) => {
exploreCoursesClick(e);
},
},
],
secondaryMenu: [
...(getConfig().SUPPORT_URL ? [{
type: 'item',
href: `${getConfig().SUPPORT_URL}`,
content: formatMessage(messages.help),
}] : []),
],
userMenu: [
{
heading: '',
items: [
{
type: 'item',
href: `${getConfig().ACCOUNT_PROFILE_URL}/u/${authenticatedUser?.username}`,
content: formatMessage(messages.profile),
},
{
type: 'item',
href: `${getConfig().ACCOUNT_SETTINGS_URL}`,
content: formatMessage(messages.account),
},
...(getConfig().ORDER_HISTORY_URL ? [{
type: 'item',
href: getConfig().ORDER_HISTORY_URL,
content: formatMessage(messages.orderHistory),
}] : []),
],
},
{
heading: '',
items: [
{
type: 'item',
href: `${getConfig().LOGOUT_URL}`,
content: formatMessage(messages.signOut),
},
],
},
],
}
);
export default getLearnerHeaderMenu;

View File

@@ -3,59 +3,8 @@
exports[`LearnerDashboardHeader render 1`] = `
<Fragment>
<ConfirmEmailBanner />
<Header
mainMenuItems={
[
{
"content": "Courses",
"href": "/",
"isActive": true,
"type": "item",
},
{
"content": "Discover New",
"href": "http://localhost:18000/course-search-url",
"onClick": [Function],
"type": "item",
},
]
}
secondaryMenuItems={[]}
userMenuItems={
[
{
"heading": "",
"items": [
{
"content": "Profile",
"href": "http://account-profile-url.test/u/undefined",
"type": "item",
},
{
"content": "Account",
"href": "http://account-settings-url.test",
"type": "item",
},
{
"content": "Order History",
"href": "test-url",
"type": "item",
},
],
},
{
"heading": "",
"items": [
{
"content": "Sign Out",
"href": "http://localhost:18000/logout",
"type": "item",
},
],
},
]
}
/>
<CollapsedHeader />
<ExpandedHeader />
<MasqueradeBar />
</Fragment>
`;

View File

@@ -1,12 +1,9 @@
import React from 'react';
import { useWindowSize, breakpoints } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import track from 'tracking';
import { StrictDict } from 'utils';
import { linkNames } from 'tracking/constants';
import getLearnerHeaderMenu from './LearnerDashboardMenu';
import * as module from './hooks';
export const state = StrictDict({
@@ -27,13 +24,6 @@ export const findCoursesNavDropdownClicked = (href) => track.findCourses.findCou
linkName: linkNames.learnerHomeNavDropdownExplore,
});
export const useLearnerDashboardHeaderMenu = ({
courseSearchUrl, authenticatedUser, exploreCoursesClick,
}) => {
const { formatMessage } = useIntl();
return getLearnerHeaderMenu(formatMessage, courseSearchUrl, authenticatedUser, exploreCoursesClick);
};
export const useLearnerDashboardHeaderData = () => {
const [isOpen, setIsOpen] = module.state.isOpen(false);
const toggleIsOpen = () => setIsOpen(!isOpen);
@@ -49,5 +39,4 @@ export default {
findCoursesNavClicked,
findCoursesNavDropdownClicked,
useLearnerDashboardHeaderData,
useLearnerDashboardHeaderMenu,
};

View File

@@ -13,7 +13,6 @@ const {
findCoursesNavClicked,
findCoursesNavDropdownClicked,
useLearnerDashboardHeaderData,
useLearnerDashboardHeaderMenu,
} = hooks;
jest.mock('tracking', () => ({
@@ -49,17 +48,6 @@ describe('LearnerDashboardHeader hooks', () => {
});
});
describe('getLearnerDashboardHeaderMenu', () => {
test('calls header menu data hook', () => {
const courseSearchUrl = '/courses';
const authenticatedUser = {
username: 'test',
};
const learnerHomeHeaderMenu = useLearnerDashboardHeaderMenu({ courseSearchUrl, authenticatedUser });
expect(learnerHomeHeaderMenu.mainMenu.length).toBe(2);
});
});
describe('findCoursesNavDropdownClicked', () => {
test('calls tracking with dropdown link name', () => {
findCoursesNavDropdownClicked(url);

View File

@@ -1,43 +1,21 @@
import React from 'react';
import MasqueradeBar from 'containers/MasqueradeBar';
import { AppContext } from '@edx/frontend-platform/react';
import Header from '@edx/frontend-component-header';
import { reduxHooks } from 'hooks';
import urls from 'data/services/lms/urls';
import ConfirmEmailBanner from './ConfirmEmailBanner';
import { useLearnerDashboardHeaderMenu, findCoursesNavClicked } from './hooks';
import CollapsedHeader from './CollapsedHeader';
import ExpandedHeader from './ExpandedHeader';
import './index.scss';
export const LearnerDashboardHeader = () => {
const { authenticatedUser } = React.useContext(AppContext);
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const exploreCoursesClick = () => {
findCoursesNavClicked(urls.baseAppUrl(courseSearchUrl));
};
const learnerHomeHeaderMenu = useLearnerDashboardHeaderMenu({
courseSearchUrl,
authenticatedUser,
exploreCoursesClick,
});
return (
<>
<ConfirmEmailBanner />
<Header
mainMenuItems={learnerHomeHeaderMenu.mainMenu}
secondaryMenuItems={learnerHomeHeaderMenu.secondaryMenu}
userMenuItems={learnerHomeHeaderMenu.userMenu}
/>
<MasqueradeBar />
</>
);
};
export const LearnerDashboardHeader = () => (
<>
<ConfirmEmailBanner />
<CollapsedHeader />
<ExpandedHeader />
<MasqueradeBar />
</>
);
LearnerDashboardHeader.propTypes = {};

View File

@@ -1,47 +1,18 @@
import { mergeConfig } from '@edx/frontend-platform';
import { shallow } from '@edx/react-unit-test-utils';
import Header from '@edx/frontend-component-header';
import urls from 'data/services/lms/urls';
import LearnerDashboardHeader from '.';
import { findCoursesNavClicked } from './hooks';
jest.mock('hooks', () => ({
reduxHooks: {
usePlatformSettingsData: jest.fn(() => ({
courseSearchUrl: '/course-search-url',
})),
},
}));
jest.mock('./hooks', () => ({
...jest.requireActual('./hooks'),
findCoursesNavClicked: jest.fn(),
}));
jest.mock('containers/MasqueradeBar', () => 'MasqueradeBar');
jest.mock('./CollapsedHeader', () => 'CollapsedHeader');
jest.mock('./ConfirmEmailBanner', () => 'ConfirmEmailBanner');
jest.mock('@edx/frontend-component-header', () => 'Header');
jest.mock('./ExpandedHeader', () => 'ExpandedHeader');
describe('LearnerDashboardHeader', () => {
test('render', () => {
mergeConfig({ ORDER_HISTORY_URL: 'test-url' });
const wrapper = shallow(<LearnerDashboardHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.findByType('ConfirmEmailBanner')).toHaveLength(1);
expect(wrapper.instance.findByType('MasqueradeBar')).toHaveLength(1);
expect(wrapper.instance.findByType(Header)).toHaveLength(1);
wrapper.instance.findByType(Header)[0].props.mainMenuItems[1].onClick();
expect(findCoursesNavClicked).toHaveBeenCalledWith(urls.baseAppUrl('/course-search-url'));
expect(wrapper.instance.findByType(Header)[0].props.secondaryMenuItems.length).toBe(0);
});
test('should display Help link if SUPPORT_URL is set', () => {
mergeConfig({ SUPPORT_URL: 'http://localhost:18000/support' });
const wrapper = shallow(<LearnerDashboardHeader />);
expect(wrapper.instance.findByType(Header)[0].props.secondaryMenuItems.length).toBe(1);
});
test('should display Programs link if it is enabled by configuration', () => {
mergeConfig({ ENABLE_PROGRAMS: true });
const wrapper = shallow(<LearnerDashboardHeader />);
expect(wrapper.instance.findByType(Header)[0].props.mainMenuItems.length).toBe(3);
expect(wrapper.instance.findByType('CollapsedHeader')).toHaveLength(1);
expect(wrapper.instance.findByType('ExpandedHeader')).toHaveLength(1);
});
});

View File

@@ -33,7 +33,7 @@ exports[`MasqueradeBar snapshot can masquerade 1`] = `
className="mr-3"
disabled={true}
labels={
{
Object {
"default": "Submit",
}
}
@@ -79,7 +79,7 @@ exports[`MasqueradeBar snapshot can masquerade with input 1`] = `
className="mr-3"
disabled={false}
labels={
{
Object {
"default": "Submit",
}
}
@@ -131,7 +131,7 @@ exports[`MasqueradeBar snapshot is masquerading failed with error 1`] = `
className="mr-3"
disabled={true}
labels={
{
Object {
"default": "Submit",
}
}
@@ -177,7 +177,7 @@ exports[`MasqueradeBar snapshot is masquerading pending 1`] = `
className="mr-3"
disabled={true}
labels={
{
Object {
"default": "Submit",
}
}

View File

@@ -37,8 +37,8 @@ exports[`RelatedProgramsModal snapshot: closed 1`] = `
>
<ProgramCard
data={
{
"programData": {
Object {
"programData": Object {
"dataFor": "program1",
},
"programUrl": "program-1-url",
@@ -53,8 +53,8 @@ exports[`RelatedProgramsModal snapshot: closed 1`] = `
>
<ProgramCard
data={
{
"programData": {
Object {
"programData": Object {
"dataFor": "program2",
},
"programUrl": "program-2-url",
@@ -69,8 +69,8 @@ exports[`RelatedProgramsModal snapshot: closed 1`] = `
>
<ProgramCard
data={
{
"programData": {
Object {
"programData": Object {
"dataFor": "program3",
},
"programUrl": "program-3-url",
@@ -121,8 +121,8 @@ exports[`RelatedProgramsModal snapshot: open 1`] = `
>
<ProgramCard
data={
{
"programData": {
Object {
"programData": Object {
"dataFor": "program1",
},
"programUrl": "program-1-url",
@@ -137,8 +137,8 @@ exports[`RelatedProgramsModal snapshot: open 1`] = `
>
<ProgramCard
data={
{
"programData": {
Object {
"programData": Object {
"dataFor": "program2",
},
"programUrl": "program-2-url",
@@ -153,8 +153,8 @@ exports[`RelatedProgramsModal snapshot: open 1`] = `
>
<ProgramCard
data={
{
"programData": {
Object {
"programData": Object {
"dataFor": "program3",
},
"programUrl": "program-3-url",

View File

@@ -7,7 +7,7 @@ exports[`RelatedProgramsModal ProgramCard snapshot 1`] = `
href="props.data.programUrl"
isClickable={true}
style={
{
Object {
"color": "white",
"width": "18rem",
}

View File

@@ -11,7 +11,7 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.confirm 1`] = `
<div
className="bg-white p-3 rounded shadow"
style={
{
Object {
"textAlign": "start",
}
}
@@ -35,7 +35,7 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.finished, reason g
<div
className="bg-white p-3 rounded shadow"
style={
{
Object {
"textAlign": "start",
}
}
@@ -59,7 +59,7 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.finished, reason s
<div
className="bg-white p-3 rounded shadow"
style={
{
Object {
"textAlign": "start",
}
}
@@ -83,14 +83,14 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.reason, should be
<div
className="bg-white p-3 rounded"
style={
{
Object {
"textAlign": "start",
}
}
>
<ReasonPane
reason={
{
Object {
"isSkipped": false,
"reasonProps": "other",
}

View File

@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WidgetSidebar snapshots default 1`] = `
<div
className="widget-sidebar"
>
<div
className="d-flex flex-column"
>
<PluginSlot
id="widget_sidebar_plugin_slot"
/>
</div>
</div>
`;

View File

@@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import hooks from 'widgets/ProductRecommendations/hooks';
export const WidgetSidebar = ({ setSidebarShowing }) => {
const { inRecommendationsVariant, isExperimentActive } = hooks.useShowRecommendationsFooter();
if (!inRecommendationsVariant && isExperimentActive) {
setSidebarShowing(true);
return (
<div className="widget-sidebar">
<div className="d-flex flex-column">
<PluginSlot id="widget_sidebar_plugin_slot" />
</div>
</div>
);
}
return null;
};
WidgetSidebar.propTypes = {
setSidebarShowing: PropTypes.func.isRequired,
};
export default WidgetSidebar;

View File

@@ -0,0 +1,52 @@
import { shallow } from '@edx/react-unit-test-utils';
import hooks from 'widgets/ProductRecommendations/hooks';
import { mockFooterRecommendationsHook } from 'widgets/ProductRecommendations/testData';
import WidgetSidebar from '.';
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
jest.mock('widgets/ProductRecommendations/hooks', () => ({
useShowRecommendationsFooter: jest.fn(),
}));
jest.mock('@openedx/frontend-plugin-framework', () => ({
PluginSlot: 'PluginSlot',
}));
describe('WidgetSidebar', () => {
beforeEach(() => jest.resetAllMocks());
const props = {
setSidebarShowing: jest.fn(),
};
describe('snapshots', () => {
test('default', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.activeControl,
);
const wrapper = shallow(<WidgetSidebar {...props} />);
expect(props.setSidebarShowing).toHaveBeenCalledWith(true);
expect(wrapper.snapshot).toMatchSnapshot();
});
});
test('is hidden when the has the default values', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.default,
);
const wrapper = shallow(<WidgetSidebar {...props} />);
expect(props.setSidebarShowing).not.toHaveBeenCalled();
expect(wrapper.shallowWrapper).toBeNull();
});
test('is hidden when the has the treatment values', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.activeTreatment,
);
const wrapper = shallow(<WidgetSidebar {...props} />);
expect(props.setSidebarShowing).not.toHaveBeenCalled();
expect(wrapper.shallowWrapper).toBeNull();
});
});

View File

@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WidgetSidebar snapshots default 1`] = `
<div
className="widget-sidebar px-2"
>
<div
className="d-flex"
>
<PluginSlot
id="widget_sidebar_plugin_slot"
/>
</div>
</div>
`;

View File

@@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import hooks from 'widgets/ProductRecommendations/hooks';
export const WidgetSidebar = ({ setSidebarShowing }) => {
const { inRecommendationsVariant, isExperimentActive } = hooks.useShowRecommendationsFooter();
if (!inRecommendationsVariant && isExperimentActive) {
setSidebarShowing(true);
return (
<div className="widget-sidebar px-2">
<div className="d-flex">
<PluginSlot id="widget_sidebar_plugin_slot" />
</div>
</div>
);
}
return null;
};
WidgetSidebar.propTypes = {
setSidebarShowing: PropTypes.func.isRequired,
};
export default WidgetSidebar;

View File

@@ -0,0 +1,52 @@
import { shallow } from '@edx/react-unit-test-utils';
import hooks from 'widgets/ProductRecommendations/hooks';
import { mockFooterRecommendationsHook } from 'widgets/ProductRecommendations/testData';
import WidgetSidebar from '.';
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
jest.mock('widgets/ProductRecommendations/hooks', () => ({
useShowRecommendationsFooter: jest.fn(),
}));
jest.mock('@openedx/frontend-plugin-framework', () => ({
PluginSlot: 'PluginSlot',
}));
describe('WidgetSidebar', () => {
beforeEach(() => jest.resetAllMocks());
const props = {
setSidebarShowing: jest.fn(),
};
describe('snapshots', () => {
test('default', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.activeControl,
);
const wrapper = shallow(<WidgetSidebar {...props} />);
expect(props.setSidebarShowing).toHaveBeenCalledWith(true);
expect(wrapper.snapshot).toMatchSnapshot();
});
});
test('is hidden when the has the default values', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.default,
);
const wrapper = shallow(<WidgetSidebar {...props} />);
expect(props.setSidebarShowing).not.toHaveBeenCalled();
expect(wrapper.shallowWrapper).toBeNull();
});
test('is hidden when the has the treatment values', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.activeTreatment,
);
const wrapper = shallow(<WidgetSidebar {...props} />);
expect(props.setSidebarShowing).not.toHaveBeenCalled();
expect(wrapper.shallowWrapper).toBeNull();
});
});

View File

@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WidgetFooter snapshots default 1`] = `
<div
className="widget-footer"
>
<ProductRecommendations />
</div>
`;

View File

@@ -0,0 +1,21 @@
import React from 'react';
import ProductRecommendations from 'widgets/ProductRecommendations';
import hooks from 'widgets/ProductRecommendations/hooks';
export const WidgetFooter = () => {
hooks.useActivateRecommendationsExperiment();
const { inRecommendationsVariant, isExperimentActive } = hooks.useShowRecommendationsFooter();
if (inRecommendationsVariant && isExperimentActive) {
return (
<div className="widget-footer">
<ProductRecommendations />
</div>
);
}
return null;
};
export default WidgetFooter;

View File

@@ -0,0 +1,45 @@
import { shallow } from '@edx/react-unit-test-utils';
import hooks from 'widgets/ProductRecommendations/hooks';
import { mockFooterRecommendationsHook } from 'widgets/ProductRecommendations/testData';
import WidgetFooter from '.';
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
jest.mock('widgets/ProductRecommendations/hooks', () => ({
useActivateRecommendationsExperiment: jest.fn(),
useShowRecommendationsFooter: jest.fn(),
}));
describe('WidgetFooter', () => {
describe('snapshots', () => {
test('default', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.activeTreatment,
);
const wrapper = shallow(<WidgetFooter />);
expect(hooks.useActivateRecommendationsExperiment).toHaveBeenCalled();
expect(wrapper.snapshot).toMatchSnapshot();
});
});
test('is hidden when the experiment has the default values', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.default,
);
const wrapper = shallow(<WidgetFooter />);
expect(hooks.useActivateRecommendationsExperiment).toHaveBeenCalled();
expect(wrapper.shallowWrapper).toBeNull();
});
test('is hidden when the experiment has the control values', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.activeControl,
);
const wrapper = shallow(<WidgetFooter />);
expect(hooks.useActivateRecommendationsExperiment).toHaveBeenCalled();
expect(wrapper.shallowWrapper).toBeNull();
});
});

View File

@@ -1,8 +1,6 @@
import * as redux from 'redux';
import thunkMiddleware from 'redux-thunk';
import {
composeWithDevToolsLogOnlyInProduction,
} from '@redux-devtools/extension';
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
import { createLogger } from 'redux-logger';
import apiTestUtils from 'data/services/lms/fakeData/testUtils';
@@ -16,7 +14,7 @@ export const createStore = () => {
const store = redux.createStore(
reducer,
composeWithDevToolsLogOnlyInProduction(redux.applyMiddleware(...middleware)),
composeWithDevTools(redux.applyMiddleware(...middleware)),
);
/**

View File

@@ -1,8 +1,6 @@
import { applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import {
composeWithDevToolsLogOnlyInProduction,
} from '@redux-devtools/extension';
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
import { createLogger } from 'redux-logger';
import rootReducer, { actions, selectors } from 'data/redux';
@@ -24,8 +22,8 @@ jest.mock('redux', () => ({
applyMiddleware: (...middleware) => ({ applied: middleware }),
createStore: (reducer, middleware) => ({ reducer, middleware }),
}));
jest.mock('@redux-devtools/extension', () => ({
composeWithDevToolsLogOnlyInProduction: (middleware) => ({ withDevTools: middleware }),
jest.mock('redux-devtools-extension/logOnlyInProduction', () => ({
composeWithDevTools: (middleware) => ({ withDevTools: middleware }),
}));
describe('store aggregator module', () => {
@@ -39,7 +37,7 @@ describe('store aggregator module', () => {
describe('middleware', () => {
it('exports thunk and logger middleware, composed and applied with dev tools', () => {
expect(createStore().middleware).toEqual(
composeWithDevToolsLogOnlyInProduction(applyMiddleware(thunkMiddleware, createLogger())),
composeWithDevTools(applyMiddleware(thunkMiddleware, createLogger())),
);
});
});

View File

@@ -1,64 +0,0 @@
# Course Card Action Slot
### Slot ID: `course_card_action_slot`
### Props:
* `cardId`
## Description
This slot is used for adding content in the Action buttons section of each Course Card.
## Example
The following `env.config.jsx` will render the `cardId` of the course as `<p>` elements in a `<div>`.
![Screenshot of Content added after the Sequence Container](./images/post_course_card_action.png)
```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
import ActionButton from 'containers/CourseCard/components/CourseCardActions/ActionButton';
const config = {
pluginSlots: {
course_card_action_slot: {
keepDefault: false,
plugins: [
{
// Insert Custom Button in Course Card
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_course_card_action',
priority: 60,
type: DIRECT_PLUGIN,
RenderWidget: ({cardId}) => (
<ActionButton
variant="outline-primary"
>
Custom Button
</ ActionButton>
),
},
},
{
// Insert Another Button in Course Card
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'another_custom_course_card_action',
priority: 70,
type: DIRECT_PLUGIN,
RenderWidget: ({cardId}) => (
<ActionButton
variant="outline-primary"
>
📚: {cardId}
</ ActionButton>
),
},
},
]
}
},
}
export default config;
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 603 KiB

View File

@@ -1,18 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
const CourseCardActionSlot = ({ cardId }) => (
<PluginSlot
id="course_card_action_slot"
pluginProps={{
cardId,
}}
/>
);
CourseCardActionSlot.propTypes = {
cardId: PropTypes.string.isRequired,
};
export default CourseCardActionSlot;

View File

@@ -1,60 +0,0 @@
# Course List Slot
### Slot ID: `course_list_slot`
## Plugin Props
* courseListData
## Description
This slot is used for replacing or adding content around the `CourseList` component. The `CourseListSlot` is only rendered if the learner has enrolled in at least one course.
## Example
The space will show the `CourseList` component by default. This can be disabled in the configuration with the `keepDefault` boolean.
![Screenshot of the CourseListSlot](./images/course_list_slot.png)
Setting the MFE's `env.config.jsx` to the following will replace the default experience with a list of course titles.
![Screenshot of a custom course list](./images/readme_custom_course_list.png)
```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
course_list_slot: {
// Hide the default CourseList component
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_course_list',
type: DIRECT_PLUGIN,
priority: 60,
RenderWidget: ({ courseListData }) => {
// Extract the "visibleList"
const courses = courseListData.visibleList;
// Render a list of course names
return (
<div>
{courses.map(courseData => (
<p>
{courseData.course.courseName}
</p>
))}
</div>
)
},
},
},
],
},
},
}
export default config;
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,16 +0,0 @@
import React from 'react';
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 }}>
<CourseList courseListData={courseListData} />
</PluginSlot>
);
CourseListSlot.propTypes = {
courseListData: courseListDataShape,
};
export default CourseListSlot;

View File

@@ -1,47 +0,0 @@
# No Courses View Slot
### Slot ID: `no_courses_view_slot`
## Description
This slot is used for replacing or adding content around the `NoCoursesView` component. The `NoCoursesViewSlot` only renders if the learner has not yet enrolled in any courses.
## Example
The space will show the `NoCoursesView` by default. This can be disabled in the configuration with the `keepDefault` boolean.
![Screenshot of the no courses view](./images/no_courses_view_slot.png)
Setting the MFE's `env.config.jsx` to the following will replace the default experience with a custom call-to-action component.
![Screenshot of a custom no courses view](./images/readme_custom_no_courses_view.png)
```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
no_courses_view_slot: {
// Hide the default NoCoursesView component
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_no_courses_CTA',
type: DIRECT_PLUGIN,
priority: 60,
RenderWidget: () => (
<h3>
Check out our catalog of courses and start learning today!
</h3>
),
},
},
],
},
},
}
export default config;
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -1,12 +0,0 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import NoCoursesView from 'containers/CoursesPanel/NoCoursesView';
export const NoCoursesViewSlot = () => (
<PluginSlot id="no_courses_view_slot">
<NoCoursesView />
</PluginSlot>
);
export default NoCoursesViewSlot;

View File

@@ -1,7 +1,3 @@
# `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/)

View File

@@ -1,58 +0,0 @@
# Widget Sidebar Slot
### Slot ID: `widget_sidebar_slot`
## Description
This slot is used for adding content to the right-hand sidebar.
## Example
The space will show the `LookingForChallengeWidget` by default. This can be disabled in the configuration with the `keepDefault` boolean.
![Screenshot of the widget sidebar](./images/widget_sidebar_slot.png)
Setting the MFE's `env.config.jsx` to the following will replace the default experience with a custom sidebar component.
![Screenshot of a custom call-to-action in the sidebar](./images/readme_custom_sidebar.png)
```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
widget_sidebar_slot: {
// Hide the default LookingForChallenge component
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_sidebar_panel',
type: DIRECT_PLUGIN,
priority: 60,
RenderWidget: () => (
<div>
<h3>
Sidebar Menu
</h3>
<p>
sidebar item #1
</p>
<p>
sidebar item #2
</p>
<p>
sidebar item #3
</p>
</div>
),
},
},
],
},
},
}
export default config;
```

View File

@@ -1,9 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WidgetSidebar snapshots 1`] = `
<PluginSlot
id="widget_sidebar_slot"
>
<LookingForChallengeWidget />
</PluginSlot>
`;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

Some files were not shown because too many files have changed in this diff Show More