Compare commits
2 Commits
open-relea
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc68a8c674 | ||
|
|
15f9969993 |
2
.env
2
.env
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
7
.github/CODEOWNERS
vendored
@@ -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
|
||||
|
||||
7
.github/dependabot.yml
vendored
7
.github/dependabot.yml
vendored
@@ -1,7 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Adding new check for github-actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
@@ -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() }}
|
||||
|
||||
6
.github/workflows/npm-publish.yml
vendored
6
.github/workflows/npm-publish.yml
vendored
@@ -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 }}
|
||||
|
||||
|
||||
@@ -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
19941
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -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"
|
||||
|
||||
21
src/App.jsx
21
src/App.jsx
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
64
src/ExperimentContext.jsx
Normal 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 };
|
||||
122
src/ExperimentContext.test.jsx
Normal file
122
src/ExperimentContext.test.jsx
Normal 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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ exports[`FilterForm snapshot renders 1`] = `
|
||||
name="course-status-filters"
|
||||
onChange={[MockFunction handleFilterChange]}
|
||||
value={
|
||||
[
|
||||
Array [
|
||||
"test-filter",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"> </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;
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ exports[`EnterpriseDashboard snapshot 1`] = `
|
||||
<div
|
||||
className="bg-white p-3 rounded shadow"
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"textAlign": "start",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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]}
|
||||
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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`;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -7,7 +7,7 @@ exports[`RelatedProgramsModal ProgramCard snapshot 1`] = `
|
||||
href="props.data.programUrl"
|
||||
isClickable={true}
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"color": "white",
|
||||
"width": "18rem",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
29
src/containers/WidgetContainers/LoadedSidebar/index.jsx
Normal file
29
src/containers/WidgetContainers/LoadedSidebar/index.jsx
Normal 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;
|
||||
52
src/containers/WidgetContainers/LoadedSidebar/index.test.jsx
Normal file
52
src/containers/WidgetContainers/LoadedSidebar/index.test.jsx
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
29
src/containers/WidgetContainers/NoCoursesSidebar/index.jsx
Normal file
29
src/containers/WidgetContainers/NoCoursesSidebar/index.jsx
Normal 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;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`WidgetFooter snapshots default 1`] = `
|
||||
<div
|
||||
className="widget-footer"
|
||||
>
|
||||
<ProductRecommendations />
|
||||
</div>
|
||||
`;
|
||||
21
src/containers/WidgetContainers/WidgetFooter/index.jsx
Normal file
21
src/containers/WidgetContainers/WidgetFooter/index.jsx
Normal 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;
|
||||
45
src/containers/WidgetContainers/WidgetFooter/index.test.jsx
Normal file
45
src/containers/WidgetContainers/WidgetFooter/index.test.jsx
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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)),
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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())),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>`.
|
||||
|
||||

|
||||
|
||||
```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 |
@@ -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;
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
Setting the MFE's `env.config.jsx` to the following will replace the default experience with a list of course titles.
|
||||
|
||||

|
||||
|
||||
```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 |
@@ -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;
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
Setting the MFE's `env.config.jsx` to the following will replace the default experience with a custom call-to-action component.
|
||||
|
||||

|
||||
|
||||
```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 |
@@ -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;
|
||||
@@ -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/)
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
Setting the MFE's `env.config.jsx` to the following will replace the default experience with a custom sidebar component.
|
||||
|
||||

|
||||
|
||||
```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;
|
||||
```
|
||||
@@ -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
Reference in New Issue
Block a user