Compare commits
23 Commits
teak-desig
...
jwesson/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3663efb283 | ||
|
|
9ef5840700 | ||
|
|
3c8c92ab92 | ||
|
|
7488fe55f0 | ||
|
|
fe6c726306 | ||
|
|
8c3d62c12b | ||
|
|
66794acf17 | ||
|
|
2f3f3bcd8b | ||
|
|
dcab4f1b75 | ||
|
|
66fdd79bdf | ||
|
|
0ea9f6d193 | ||
|
|
fee6a26366 | ||
|
|
1301fddbc9 | ||
|
|
5dcd596ac9 | ||
|
|
a5522faa42 | ||
|
|
3542c38472 | ||
|
|
14c03d8461 | ||
|
|
5562d8a339 | ||
|
|
a9194261c8 | ||
|
|
11a7512fea | ||
|
|
be620a80fa | ||
|
|
1d9eb08e59 | ||
|
|
05e9626e57 |
1
.env
1
.env
@@ -41,3 +41,4 @@ ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||
ENABLE_PROGRAMS=false
|
||||
NON_BROWSABLE_COURSES=false
|
||||
|
||||
@@ -47,3 +47,4 @@ ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||
ENABLE_PROGRAMS=false
|
||||
NON_BROWSABLE_COURSES=false
|
||||
|
||||
@@ -46,3 +46,4 @@ ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=true
|
||||
ENABLE_PROGRAMS=false
|
||||
NON_BROWSABLE_COURSES=false
|
||||
|
||||
87
package-lock.json
generated
87
package-lock.json
generated
@@ -14,7 +14,7 @@
|
||||
"@edx/frontend-component-header": "^6.2.0",
|
||||
"@edx/frontend-enterprise-hotjar": "7.2.0",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@edx/openedx-atlas": "^0.7.0",
|
||||
"@edx/react-unit-test-utils": "^4.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||
@@ -25,14 +25,13 @@
|
||||
"@redux-devtools/extension": "3.3.0",
|
||||
"@reduxjs/toolkit": "^2.0.0",
|
||||
"classnames": "^2.3.1",
|
||||
"core-js": "3.40.0",
|
||||
"core-js": "3.42.0",
|
||||
"filesize": "^10.0.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"history": "5.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "7.1.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
@@ -2066,9 +2065,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer": {
|
||||
"version": "14.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-14.6.0.tgz",
|
||||
"integrity": "sha512-cgRhom6W/WErQ9yvLmfgB6ANBs+rBDLOH73NcvJIhfwWgAg67q+MLUscIbcX9N/9Yykk+kb7Ytr3CDefiKS7HA==",
|
||||
"version": "14.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-14.8.0.tgz",
|
||||
"integrity": "sha512-m/EFvGgiS0r2YtTg9YfUnMNjIRL2ZjQG51VTDNAdqjcNLB+OZ93402Y+173RRkvZU5VnHPTf+bTf1EmzKbMoNA==",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
@@ -2232,16 +2231,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-platform": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.3.4.tgz",
|
||||
"integrity": "sha512-V3XtTo3KP8QSmId+Vvi4+qzpOVkxvTMNA6jH/i3Bfz+/jHjHBRnmp/Cc2pjTxiTgGNoKX4D1twiZkOBO+kWw1Q==",
|
||||
"version": "8.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.3.8.tgz",
|
||||
"integrity": "sha512-wr3HKzDcYGNuHcM7HuZ/mqBdqnY/A7eYa3JDVZEsceyjy0PJyVKHWFu3yneGpD1zufguQCvS0q53C8gJbVzIgw==",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@cospired/i18n-iso-languages": "4.2.0",
|
||||
"@formatjs/intl-pluralrules": "4.3.3",
|
||||
"@formatjs/intl-relativetimeformat": "10.0.1",
|
||||
"axios": "1.8.4",
|
||||
"axios-cache-interceptor": "1.6.2",
|
||||
"axios": "1.9.0",
|
||||
"axios-cache-interceptor": "1.8.0",
|
||||
"form-urlencoded": "4.1.4",
|
||||
"glob": "7.2.3",
|
||||
"history": "4.10.1",
|
||||
@@ -2296,9 +2295,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/openedx-atlas": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@edx/openedx-atlas/-/openedx-atlas-0.6.2.tgz",
|
||||
"integrity": "sha512-28Q8vzJDMS4wUxdkbIUBQpzWJ3HTdMaGlaEhFjrVGfuZkh++1AG6Tn/7FMD88cegalYAkphu530VQCHEkMZQhw==",
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/openedx-atlas/-/openedx-atlas-0.7.0.tgz",
|
||||
"integrity": "sha512-jqv0IV1pHsSn9+RO8Rdsr8jm3SOd84CCzzmo2QC9yvh1MK1+p4YDURQLpmmgKJ0JzE5Cb6ImhnNL/ogpJ2wetQ==",
|
||||
"license": "AGPL-3.0",
|
||||
"bin": {
|
||||
"atlas": "atlas"
|
||||
@@ -3652,9 +3651,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@openedx/frontend-build": {
|
||||
"version": "14.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.5.0.tgz",
|
||||
"integrity": "sha512-HY0PdXvXBxrvJHj8HsRA+VNCHDePENFhqOIvbSz9Ke7HDwHpfDjg2CeKk41aU/8iTyj3eESfPwKQr5fTE0A3Ww==",
|
||||
"version": "14.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.6.0.tgz",
|
||||
"integrity": "sha512-lQn/IYC2xZxmYtm9AsrEXm7o3Ei0voZNTKFDGiBbZARsh27b2/e8hx3ToJ4B3rwxnSHLNYbxVYqAWSiAXG06dw==",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@babel/cli": "7.24.8",
|
||||
@@ -3736,9 +3735,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@openedx/frontend-build/node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -3872,9 +3871,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@openedx/paragon": {
|
||||
"version": "22.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.17.0.tgz",
|
||||
"integrity": "sha512-MzOLQ0myaOErwumPJwxVZXTw7zJKrARtu4YMSaISF5Sz6pE1/dYz9qfRcqaraYRcJGNdbPRzOG0v3iqbZo1uHQ==",
|
||||
"version": "22.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.18.1.tgz",
|
||||
"integrity": "sha512-h/nZkViIONjEwTdFOyTvXYjqT//flrHjUwh6iI6MasRcgPkngoGwQF+EVjT/vT3bxjX0mtB2QV+nwICaR8Ghgw==",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"example",
|
||||
@@ -4381,9 +4380,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.7.0.tgz",
|
||||
"integrity": "sha512-XVwolG6eTqwV0N8z/oDlN93ITCIGIop6leXlGJI/4EKy+0POYkR+ABHRSdGXY+0MQvJBP8yAzh+EYFxTuvmBiQ==",
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz",
|
||||
"integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
@@ -6600,9 +6599,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.8.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
|
||||
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
@@ -6611,9 +6610,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios-cache-interceptor": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-1.6.2.tgz",
|
||||
"integrity": "sha512-YLbAODIHZZIcD4b3WYFVQOa5W2TY/WnJ6sBHqAg6Z+hx+RVj8/OcjQyRopO6awn7/kOkGL5X9TP16AucnlJ/lw==",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-1.8.0.tgz",
|
||||
"integrity": "sha512-cTNnPGJyQkxnWp0EWvE3NRvgURU5cWw/Qx3dIhXyHSM4Ip0c7EEe0I3an0Jwa549m1CAOg57ibj27YRNLmQCcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cache-parser": "1.2.5",
|
||||
@@ -7931,9 +7930,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.40.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz",
|
||||
"integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==",
|
||||
"version": "3.42.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz",
|
||||
"integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -16469,24 +16468,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/query-string": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
|
||||
"integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decode-uri-component": "^0.2.2",
|
||||
"filter-obj": "^1.1.0",
|
||||
"split-on-first": "^1.0.0",
|
||||
"strict-uri-encode": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/querystringify": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"@edx/frontend-component-header": "^6.2.0",
|
||||
"@edx/frontend-enterprise-hotjar": "7.2.0",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@edx/openedx-atlas": "^0.7.0",
|
||||
"@edx/react-unit-test-utils": "^4.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||
@@ -45,14 +45,13 @@
|
||||
"@redux-devtools/extension": "3.3.0",
|
||||
"@reduxjs/toolkit": "^2.0.0",
|
||||
"classnames": "^2.3.1",
|
||||
"core-js": "3.40.0",
|
||||
"core-js": "3.42.0",
|
||||
"filesize": "^10.0.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"history": "5.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "7.1.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
|
||||
@@ -74,11 +74,9 @@ 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];
|
||||
const dashboard = main.children[0].el;
|
||||
expect(dashboard.type).toEqual('Dashboard');
|
||||
expect(
|
||||
dashboard.matches(shallow(<Dashboard />)),
|
||||
).toEqual(true);
|
||||
expect(dashboard).toEqual(shallow(<Dashboard />));
|
||||
});
|
||||
});
|
||||
describe('no network failure with optimizely url', () => {
|
||||
@@ -91,11 +89,9 @@ 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];
|
||||
const dashboard = main.children[0].el;
|
||||
expect(dashboard.type).toEqual('Dashboard');
|
||||
expect(
|
||||
dashboard.matches(shallow(<Dashboard />)),
|
||||
).toEqual(true);
|
||||
expect(dashboard).toEqual(shallow(<Dashboard />));
|
||||
});
|
||||
});
|
||||
describe('no network failure with optimizely project id', () => {
|
||||
@@ -108,11 +104,9 @@ 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];
|
||||
const dashboard = main.children[0].el;
|
||||
expect(dashboard.type).toEqual('Dashboard');
|
||||
expect(
|
||||
dashboard.matches(shallow(<Dashboard />)),
|
||||
).toEqual(true);
|
||||
expect(dashboard).toEqual(shallow(<Dashboard />));
|
||||
});
|
||||
});
|
||||
describe('initialize failure', () => {
|
||||
|
||||
@@ -3,16 +3,15 @@ import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-
|
||||
import { logError, logInfo } from '@edx/frontend-platform/logging';
|
||||
|
||||
export const noticesUrl = `${getConfig().LMS_BASE_URL}/notices/api/v1/unacknowledged`;
|
||||
export const error404Message = 'This probably happened because the notices plugin is not installed on platform.';
|
||||
|
||||
export const getNotices = ({ onLoad }) => {
|
||||
export const getNotices = ({ onLoad, notFoundMessage }) => {
|
||||
const authenticatedUser = getAuthenticatedUser();
|
||||
|
||||
const handleError = async (e) => {
|
||||
// Error probably means that notices is not installed, which is fine.
|
||||
const { customAttributes: { httpErrorStatus } } = e;
|
||||
if (httpErrorStatus === 404) {
|
||||
logInfo(`${e}. ${error404Message}`);
|
||||
logInfo(`${e}. ${notFoundMessage}`);
|
||||
} else {
|
||||
logError(e);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { getNotices } from './api';
|
||||
import * as module from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* This component uses the platform-plugin-notices plugin to function.
|
||||
@@ -17,6 +19,8 @@ export const state = StrictDict({
|
||||
|
||||
export const useNoticesWrapperData = () => {
|
||||
const [isRedirected, setIsRedirected] = module.state.isRedirected();
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (getConfig().ENABLE_NOTICES) {
|
||||
getNotices({
|
||||
@@ -26,9 +30,10 @@ export const useNoticesWrapperData = () => {
|
||||
window.location.replace(`${data.data.results[0]}?next=${window.location.href}`);
|
||||
}
|
||||
},
|
||||
notFoundMessage: formatMessage(messages.error404Message),
|
||||
});
|
||||
}
|
||||
}, [setIsRedirected]);
|
||||
}, [setIsRedirected, formatMessage]);
|
||||
return { isRedirected };
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,12 @@ import * as hooks from './hooks';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn() }));
|
||||
jest.mock('./api', () => ({ getNotices: jest.fn() }));
|
||||
const mockFormatMessage = jest.fn(message => message.defaultMessage || 'translated-string');
|
||||
jest.mock('react-intl', () => ({
|
||||
useIntl: () => ({
|
||||
formatMessage: mockFormatMessage,
|
||||
}),
|
||||
}));
|
||||
|
||||
getConfig.mockReturnValue({ ENABLE_NOTICES: true });
|
||||
const state = new MockUseState(hooks);
|
||||
@@ -34,7 +40,7 @@ describe('NoticesWrapper hooks', () => {
|
||||
getConfig.mockReturnValueOnce({ ENABLE_NOTICES: false });
|
||||
hooks.useNoticesWrapperData();
|
||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||
expect(prereqs).toEqual([state.setState.isRedirected]);
|
||||
expect(prereqs).toEqual([state.setState.isRedirected, mockFormatMessage]);
|
||||
cb();
|
||||
expect(getNotices).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -43,7 +49,7 @@ describe('NoticesWrapper hooks', () => {
|
||||
hooks.useNoticesWrapperData();
|
||||
expect(React.useEffect).toHaveBeenCalled();
|
||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||
expect(prereqs).toEqual([state.setState.isRedirected]);
|
||||
expect(prereqs).toEqual([state.setState.isRedirected, mockFormatMessage]);
|
||||
cb();
|
||||
expect(getNotices).toHaveBeenCalled();
|
||||
const { onLoad } = getNotices.mock.calls[0][0];
|
||||
@@ -59,7 +65,7 @@ describe('NoticesWrapper hooks', () => {
|
||||
window.location = { replace: jest.fn(), href: 'test-old-href' };
|
||||
hooks.useNoticesWrapperData();
|
||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||
expect(prereqs).toEqual([state.setState.isRedirected]);
|
||||
expect(prereqs).toEqual([state.setState.isRedirected, mockFormatMessage]);
|
||||
cb();
|
||||
expect(getNotices).toHaveBeenCalled();
|
||||
const { onLoad } = getNotices.mock.calls[0][0];
|
||||
|
||||
11
src/components/NoticesWrapper/messages.js
Normal file
11
src/components/NoticesWrapper/messages.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
error404Message: {
|
||||
id: 'learner-dash.notices.error404Message',
|
||||
defaultMessage: 'This probably happened because the notices plugin is not installed on platform.',
|
||||
description: 'Error message when notices API returns 404',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -19,6 +19,7 @@ const configuration = {
|
||||
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',
|
||||
NON_BROWSABLE_COURSES: process.env.NON_BROWSABLE_COURSES === 'true',
|
||||
};
|
||||
|
||||
const features = {};
|
||||
|
||||
@@ -10,8 +10,8 @@ export const useAccessMessage = ({ cardId }) => {
|
||||
const courseRun = reduxHooks.useCardCourseRunData(cardId);
|
||||
const formatDate = utilHooks.useFormatDate();
|
||||
if (!courseRun.isStarted) {
|
||||
if (!courseRun.startDate) { return null; }
|
||||
const startDate = formatDate(courseRun.startDate);
|
||||
if (!courseRun.startDate && !courseRun.advertisedStart) { return null; }
|
||||
const startDate = courseRun.advertisedStart ? courseRun.advertisedStart : formatDate(courseRun.startDate);
|
||||
return formatMessage(messages.courseStarts, { startDate });
|
||||
}
|
||||
if (enrollment.isEnrolled) {
|
||||
|
||||
@@ -9,9 +9,11 @@ exports[`NoCoursesView snapshot 1`] = `
|
||||
alt="No Courses view banner"
|
||||
src="icon/mock/path"
|
||||
/>
|
||||
<h1>
|
||||
<h3
|
||||
className="h1"
|
||||
>
|
||||
Looking for a new challenge?
|
||||
</h1>
|
||||
</h3>
|
||||
<p>
|
||||
Explore our courses to add them to your dashboard.
|
||||
</p>
|
||||
|
||||
@@ -19,9 +19,9 @@ export const NoCoursesView = () => {
|
||||
className="d-flex align-items-center justify-content-center mb-4.5"
|
||||
>
|
||||
<Image src={emptyCourseSVG} alt={formatMessage(messages.bannerAlt)} />
|
||||
<h1>
|
||||
<h3 className="h1">
|
||||
{formatMessage(messages.lookingForChallengePrompt)}
|
||||
</h1>
|
||||
</h3>
|
||||
<p>
|
||||
{formatMessage(messages.exploreCoursesPrompt)}
|
||||
</p>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { ListPageSize, SortKeys } from 'data/constants/app';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { StrictDict } from 'utils';
|
||||
@@ -27,12 +25,13 @@ export const useCourseListData = () => {
|
||||
|
||||
const [sortBy, setSortBy] = module.state.sortBy(SortKeys.enrolled);
|
||||
|
||||
const querySearch = queryString.parse(window.location.search, { parseNumbers: true });
|
||||
const querySearch = new URLSearchParams(window.location.search);
|
||||
const disablePagination = querySearch.get('disable_pagination');
|
||||
|
||||
const { numPages, visibleList } = reduxHooks.useCurrentCourseList({
|
||||
sortBy,
|
||||
filters,
|
||||
pageSize: querySearch?.disable_pagination === 1 ? 0 : ListPageSize,
|
||||
pageSize: Number(disablePagination) === 1 ? 0 : ListPageSize,
|
||||
});
|
||||
|
||||
const handleRemoveFilter = (filter) => () => removeFilter(filter);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { MockUseState } from 'testUtils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { ListPageSize, SortKeys } from 'data/constants/app';
|
||||
@@ -15,8 +13,10 @@ jest.mock('hooks', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('query-string', () => ({
|
||||
parse: jest.fn(() => ({})),
|
||||
const mockGet = jest.fn(() => ({}));
|
||||
|
||||
global.URLSearchParams = jest.fn().mockImplementation(() => ({
|
||||
get: mockGet,
|
||||
}));
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
@@ -67,7 +67,7 @@ describe('CourseList hooks', () => {
|
||||
it('loads current course list with page size 0 if/when there is query param disable_pagination=1', () => {
|
||||
state.mock();
|
||||
state.mockVal(state.keys.sortBy, testSortBy);
|
||||
queryString.parse.mockReturnValueOnce({ disable_pagination: 1 });
|
||||
mockGet.mockReturnValueOnce('1');
|
||||
out = hooks.useCourseListData();
|
||||
expect(reduxHooks.useCurrentCourseList).toHaveBeenCalledWith({
|
||||
sortBy: testSortBy,
|
||||
|
||||
@@ -40,8 +40,7 @@ export const DashboardLayout = ({ children }) => {
|
||||
<Col {...courseListColumnProps} className="course-list-column">
|
||||
{children}
|
||||
</Col>
|
||||
<Col {...columnConfig.sidebar} className="sidebar-column">
|
||||
{!isCollapsed && (<h2 className="course-list-title"> </h2>)}
|
||||
<Col {...columnConfig.sidebar} className={['sidebar-column', !isCollapsed && 'not-collapsed']}>
|
||||
<WidgetSidebarSlot />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -84,10 +84,9 @@ describe('DashboardLayout', () => {
|
||||
|
||||
describe('not collapsed', () => {
|
||||
const testWidgetSpacing = () => {
|
||||
it('shows a blank (nbsp) h2 spacer component above widget sidebar', () => {
|
||||
it('shows not-collapsed class on widget sidebar', () => {
|
||||
const columns = el.instance.findByType(Col);
|
||||
// nonbreaking space equivalent
|
||||
expect(columns[1].findByType('h2')[0].children[0].el).toEqual('\xA0');
|
||||
expect(columns[1].props.className).toContain('not-collapsed');
|
||||
});
|
||||
};
|
||||
describe('sidebar showing', () => {
|
||||
|
||||
@@ -24,7 +24,12 @@ exports[`DashboardLayout collapsed sidebar not showing snapshot 1`] = `
|
||||
test-children
|
||||
</Col>
|
||||
<Col
|
||||
className="sidebar-column"
|
||||
className={
|
||||
[
|
||||
"sidebar-column",
|
||||
false,
|
||||
]
|
||||
}
|
||||
lg={
|
||||
{
|
||||
"offset": 0,
|
||||
@@ -68,7 +73,12 @@ exports[`DashboardLayout collapsed sidebar showing snapshot 1`] = `
|
||||
test-children
|
||||
</Col>
|
||||
<Col
|
||||
className="sidebar-column"
|
||||
className={
|
||||
[
|
||||
"sidebar-column",
|
||||
false,
|
||||
]
|
||||
}
|
||||
lg={
|
||||
{
|
||||
"offset": 0,
|
||||
@@ -112,7 +122,12 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
|
||||
test-children
|
||||
</Col>
|
||||
<Col
|
||||
className="sidebar-column"
|
||||
className={
|
||||
[
|
||||
"sidebar-column",
|
||||
"not-collapsed",
|
||||
]
|
||||
}
|
||||
lg={
|
||||
{
|
||||
"offset": 0,
|
||||
@@ -126,11 +141,6 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<h2
|
||||
className="course-list-title"
|
||||
>
|
||||
|
||||
</h2>
|
||||
<WidgetSidebarSlot />
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -161,7 +171,12 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
|
||||
test-children
|
||||
</Col>
|
||||
<Col
|
||||
className="sidebar-column"
|
||||
className={
|
||||
[
|
||||
"sidebar-column",
|
||||
"not-collapsed",
|
||||
]
|
||||
}
|
||||
lg={
|
||||
{
|
||||
"offset": 0,
|
||||
@@ -175,11 +190,6 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<h2
|
||||
className="course-list-title"
|
||||
>
|
||||
|
||||
</h2>
|
||||
<WidgetSidebarSlot />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -6,6 +6,14 @@
|
||||
|
||||
.sidebar-column {
|
||||
padding: 0 map-get($spacers, 3) 0 map-get($spacers, 1);
|
||||
|
||||
&.not-collapsed {
|
||||
padding-top: map-get($spacers, 2);
|
||||
|
||||
& >:first-child {
|
||||
margin-top: map-get($spacers, 5\.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import messages from './messages';
|
||||
|
||||
export const BrandLogo = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const dashboard = reduxHooks.useEnterpriseDashboardData();
|
||||
|
||||
return (
|
||||
<a href={dashboard?.url || '/'} className="mx-auto">
|
||||
<img
|
||||
className="logo py-3"
|
||||
src={getConfig().LOGO_URL}
|
||||
alt={formatMessage(messages.logoAltText)}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
BrandLogo.propTypes = {};
|
||||
|
||||
export default BrandLogo;
|
||||
@@ -1,28 +0,0 @@
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import BrandLogo from './BrandLogo';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useEnterpriseDashboardData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('BrandLogo', () => {
|
||||
test('dashboard defined', () => {
|
||||
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce({
|
||||
url: 'url',
|
||||
});
|
||||
const wrapper = shallow(<BrandLogo />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
expect(wrapper.instance.findByType('a')[0].props.href).toEqual('url');
|
||||
});
|
||||
|
||||
test('dashboard undefined', () => {
|
||||
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(null);
|
||||
const wrapper = shallow(<BrandLogo />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
expect(wrapper.instance.findByType('a')[0].props.href).toEqual('/');
|
||||
});
|
||||
});
|
||||
@@ -55,11 +55,11 @@ exports[`ConfirmEmailBanner snapshot Show on unverified 1`] = `
|
||||
onClose={[MockFunction closeConfirmModal]}
|
||||
title=""
|
||||
>
|
||||
<h1
|
||||
className="text-center p-3"
|
||||
<h2
|
||||
className="text-center p-3 h1"
|
||||
>
|
||||
Confirm your email
|
||||
</h1>
|
||||
</h2>
|
||||
<p
|
||||
className="text-center"
|
||||
>
|
||||
|
||||
@@ -64,7 +64,7 @@ export const ConfirmEmailBanner = () => {
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
<h1 className="text-center p-3">{formatMessage(messages.confirmEmailModalHeader)}</h1>
|
||||
<h2 className="text-center p-3 h1">{formatMessage(messages.confirmEmailModalHeader)}</h2>
|
||||
<p className="text-center">{formatMessage(messages.confirmEmailModalBody)}</p>
|
||||
</MarketingModal>
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
@@ -1,27 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BrandLogo dashboard defined 1`] = `
|
||||
<a
|
||||
className="mx-auto"
|
||||
href="url"
|
||||
>
|
||||
<img
|
||||
alt="edX, Inc. Dashboard"
|
||||
className="logo py-3"
|
||||
src="https://edx-cdn.org/v3/default/logo.svg"
|
||||
/>
|
||||
</a>
|
||||
`;
|
||||
|
||||
exports[`BrandLogo dashboard undefined 1`] = `
|
||||
<a
|
||||
className="mx-auto"
|
||||
href="/"
|
||||
>
|
||||
<img
|
||||
alt="edX, Inc. Dashboard"
|
||||
className="logo py-3"
|
||||
src="https://edx-cdn.org/v3/default/logo.svg"
|
||||
/>
|
||||
</a>
|
||||
`;
|
||||
@@ -3,59 +3,7 @@
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<Header />
|
||||
<MasqueradeBar />
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
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({
|
||||
isOpen: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
|
||||
export const useIsCollapsed = () => {
|
||||
const { width } = useWindowSize();
|
||||
const isCollapsed = React.useMemo(() => (width <= breakpoints.large.minWidth), [width]);
|
||||
return isCollapsed;
|
||||
};
|
||||
|
||||
export const findCoursesNavClicked = (href) => track.findCourses.findCoursesClicked(href, {
|
||||
linkName: linkNames.learnerHomeNavExplore,
|
||||
});
|
||||
|
||||
export const findCoursesNavDropdownClicked = (href) => track.findCourses.findCoursesClicked(href, {
|
||||
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);
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
toggleIsOpen,
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
useIsCollapsed,
|
||||
findCoursesNavClicked,
|
||||
findCoursesNavDropdownClicked,
|
||||
useLearnerDashboardHeaderData,
|
||||
useLearnerDashboardHeaderMenu,
|
||||
};
|
||||
@@ -1,81 +0,0 @@
|
||||
import { useWindowSize, breakpoints } from '@openedx/paragon';
|
||||
import track from 'tracking';
|
||||
import { linkNames } from 'tracking/constants';
|
||||
|
||||
import { MockUseState } from 'testUtils';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
const {
|
||||
useIsCollapsed,
|
||||
findCoursesNavClicked,
|
||||
findCoursesNavDropdownClicked,
|
||||
useLearnerDashboardHeaderData,
|
||||
useLearnerDashboardHeaderMenu,
|
||||
} = hooks;
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
findCourses: {
|
||||
findCoursesClicked: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const url = 'http://example.com';
|
||||
|
||||
describe('LearnerDashboardHeader hooks', () => {
|
||||
describe('state values', () => {
|
||||
state.testGetter(state.keys.isOpen);
|
||||
});
|
||||
|
||||
describe('useIsCollapsed', () => {
|
||||
test('large screen is not collapsed', () => {
|
||||
useWindowSize.mockReturnValueOnce({ width: breakpoints.large.minWidth + 1 });
|
||||
expect(useIsCollapsed()).toEqual(false);
|
||||
});
|
||||
test('small screen is collapsed', () => {
|
||||
useWindowSize.mockReturnValueOnce({ width: breakpoints.large.minWidth - 1 });
|
||||
expect(useIsCollapsed()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findCoursesNavClicked', () => {
|
||||
test('calls tracking with nav link name', () => {
|
||||
findCoursesNavClicked(url);
|
||||
expect(track.findCourses.findCoursesClicked).toHaveBeenCalledWith(url, {
|
||||
linkName: linkNames.learnerHomeNavExplore,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
expect(track.findCourses.findCoursesClicked).toHaveBeenCalledWith(url, {
|
||||
linkName: linkNames.learnerHomeNavDropdownExplore,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useLearnerDashboardHeaderData', () => {
|
||||
test('default state', () => {
|
||||
state.mock();
|
||||
const out = useLearnerDashboardHeaderData();
|
||||
state.expectInitializedWith(state.keys.isOpen, false);
|
||||
out.toggleIsOpen();
|
||||
expect(state.values.isOpen).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,43 +1,16 @@
|
||||
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 MasqueradeBar from 'containers/MasqueradeBar';
|
||||
import ConfirmEmailBanner from './ConfirmEmailBanner';
|
||||
|
||||
import { useLearnerDashboardHeaderMenu, findCoursesNavClicked } from './hooks';
|
||||
|
||||
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 />
|
||||
<Header />
|
||||
<MasqueradeBar />
|
||||
</>
|
||||
);
|
||||
|
||||
LearnerDashboardHeader.propTypes = {};
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
.dropdown-menu-collapse {
|
||||
width: 100vw;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.learner-variant-header {
|
||||
a {
|
||||
// needed to make the link not resize the header
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
.course-link {
|
||||
border-bottom: 2px solid !important;
|
||||
}
|
||||
|
||||
.course-link:hover {
|
||||
border-bottom: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-small-menu {
|
||||
> * {
|
||||
justify-content: flex-start !important;
|
||||
|
||||
border-radius: 0 !important;
|
||||
border-top: 1px solid #ddd !important;
|
||||
|
||||
&::after {
|
||||
content: '\00BB';
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
// copy from legacy dashboard
|
||||
height: 40px;
|
||||
}
|
||||
@@ -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('./ConfirmEmailBanner', () => 'ConfirmEmailBanner');
|
||||
jest.mock('@edx/frontend-component-header', () => 'Header');
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
dashboard: {
|
||||
id: 'learnerVariantDashboard.menu.dashboard.label',
|
||||
defaultMessage: 'Dashboard',
|
||||
description: 'The text for the user menu Dashboard navigation link.',
|
||||
},
|
||||
dashboardPersonal: {
|
||||
id: 'learnerVariantDashboard.menu.dashboardPersonal.label',
|
||||
defaultMessage: 'Personal',
|
||||
description: 'Link to personal dashboard in user menu',
|
||||
},
|
||||
dashboardSwitch: {
|
||||
id: 'learnerVariantDashboard.menu.dashboardSwitch.label',
|
||||
defaultMessage: 'SWITCH DASHBOARD',
|
||||
description: 'Switch Dashboard header in the user menu',
|
||||
},
|
||||
help: {
|
||||
id: 'learnerVariantDashboard.help.label',
|
||||
defaultMessage: 'Help',
|
||||
description: 'The text for the link to the Help Center',
|
||||
},
|
||||
profile: {
|
||||
id: 'learnerVariantDashboard.menu.profile.label',
|
||||
defaultMessage: 'Profile',
|
||||
description: 'The text for the user menu Profile navigation link.',
|
||||
},
|
||||
viewPrograms: {
|
||||
id: 'learnerVariantDashboard.menu.viewPrograms.label',
|
||||
defaultMessage: 'View Programs',
|
||||
description: 'The text for the user menu View Programs navigation link.',
|
||||
},
|
||||
account: {
|
||||
id: 'learnerVariantDashboard.menu.account.label',
|
||||
defaultMessage: 'Account',
|
||||
description: 'The text for the user menu Account navigation link.',
|
||||
},
|
||||
orderHistory: {
|
||||
id: 'learnerVariantDashboard.menu.orderHistory.label',
|
||||
defaultMessage: 'Order History',
|
||||
description: 'The text for the user menu Order History navigation link.',
|
||||
},
|
||||
signOut: {
|
||||
id: 'learnerVariantDashboard.menu.signOut.label',
|
||||
defaultMessage: 'Sign Out',
|
||||
description: 'The label for the user menu Sign Out action.',
|
||||
},
|
||||
course: {
|
||||
id: 'learnerVariantDashboard.course',
|
||||
defaultMessage: 'Courses',
|
||||
description: 'Header link for switching to dashboard page.',
|
||||
},
|
||||
program: {
|
||||
id: 'learnerVariantDashboard.program',
|
||||
defaultMessage: 'Programs',
|
||||
description: 'Header link for switching to program page.',
|
||||
},
|
||||
discoverNew: {
|
||||
id: 'learnerVariantDashboard.discoverNew',
|
||||
defaultMessage: 'Discover New',
|
||||
description: 'Header link for switching to discover page.',
|
||||
},
|
||||
logoAltText: {
|
||||
id: 'learnerVariantDashboard.logoAltText',
|
||||
defaultMessage: 'edX, Inc. Dashboard',
|
||||
description: 'Alt text for the edX logo.',
|
||||
},
|
||||
collapseMenuOpenAltText: {
|
||||
id: 'learnerVariantDashboard.collapseMenuOpenAltText',
|
||||
defaultMessage: 'Menu',
|
||||
description: 'Alt text for the collapse menu icon when the menu is open.',
|
||||
},
|
||||
collapseMenuClosedAltText: {
|
||||
id: 'learnerVariantDashboard.collapseMenuClosedAltText',
|
||||
defaultMessage: 'Close',
|
||||
description: 'Alt text for the collapse menu icon when the menu is closed.',
|
||||
},
|
||||
career: {
|
||||
id: 'leanerDashboard.menu.career.label',
|
||||
defaultMessage: 'Career',
|
||||
description: 'The text for the user menu Career navigation link.',
|
||||
},
|
||||
newAlert: {
|
||||
id: 'header.menu.new.label',
|
||||
defaultMessage: 'New',
|
||||
description: 'The text announcing that an item in the user menu is New',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -43,6 +43,7 @@ export const courseCard = StrictDict({
|
||||
(courseRun) => (courseRun === null ? {} : {
|
||||
endDate: module.loadDateVal(courseRun.endDate),
|
||||
startDate: module.loadDateVal(courseRun.startDate),
|
||||
advertisedStart: courseRun.advertisedStart,
|
||||
|
||||
courseId: courseRun.courseId,
|
||||
isArchived: courseRun.isArchived,
|
||||
|
||||
@@ -147,6 +147,7 @@ describe('courseCard selectors module', () => {
|
||||
loadSelector(courseCard.courseRun, {
|
||||
endDate: '3000-10-20',
|
||||
startDate: '2000-10-20',
|
||||
advertisedStart: 'Mid June',
|
||||
|
||||
courseId: 'test-course-id',
|
||||
isArchived: 'test-is-archived',
|
||||
@@ -172,6 +173,9 @@ describe('courseCard selectors module', () => {
|
||||
expect(selected.endDate).toEqual(new Date(testData.endDate));
|
||||
expect(selected.startDate).toEqual(new Date(testData.startDate));
|
||||
});
|
||||
it('passes advertised start date', () => {
|
||||
expect(selected.advertisedStart).toEqual(testData.advertisedStart);
|
||||
});
|
||||
it('passes [courseId, isArchived, isStarted]', () => {
|
||||
expect(selected.courseId).toEqual(testData.courseId);
|
||||
expect(selected.isArchived).toEqual(testData.isArchived);
|
||||
|
||||
@@ -47,5 +47,16 @@ describe('requests reducer', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('clearRequest', () => {
|
||||
it('cleanup status and error', () => {
|
||||
expect(reducer(
|
||||
testingState,
|
||||
actions.clearRequest({ requestKey: testKey, error: testValue }),
|
||||
)).toEqual({
|
||||
...testingState,
|
||||
[testKey]: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
import queryString from 'query-string';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
/**
|
||||
* stringify(query, existingQuery)
|
||||
* simple wrapper to convert an object to a query string
|
||||
* @param {object} query - object to convert
|
||||
* @param {string} existingQuery - existing query string
|
||||
* @returns {string} - query string
|
||||
*/
|
||||
|
||||
export const stringify = (query, existingQuery = '') => {
|
||||
const searchParams = new URLSearchParams(existingQuery);
|
||||
|
||||
Object.entries(query).forEach(([key, value]) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
searchParams.delete(key);
|
||||
} else if (Array.isArray(value)) {
|
||||
searchParams.delete(key);
|
||||
value.forEach((val) => {
|
||||
if (val !== undefined && val !== null && val !== '') {
|
||||
searchParams.append(key, val);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
searchParams.set(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return searchParams.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* get(url)
|
||||
* simple wrapper providing an authenticated Http client get action
|
||||
@@ -10,21 +38,23 @@ export const get = (...args) => getAuthenticatedHttpClient().get(...args);
|
||||
/**
|
||||
* post(url, data)
|
||||
* simple wrapper providing an authenticated Http client post action
|
||||
* queryString.stringify is used to convert the object to query string with = and &
|
||||
* stringify is used to convert the object to query string with = and &
|
||||
* @param {string} url - target url
|
||||
* @param {object|string} body - post payload
|
||||
*/
|
||||
export const post = (url, body) => getAuthenticatedHttpClient().post(url, queryString.stringify(body));
|
||||
export const post = (url, body = {}) => getAuthenticatedHttpClient().post(url, stringify(body));
|
||||
|
||||
export const client = getAuthenticatedHttpClient;
|
||||
|
||||
/**
|
||||
* stringifyUrl(url, query)
|
||||
* simple wrapper around queryString.stringifyUrl that sets skip behavior
|
||||
* simple wrapper to convert a url and query object to a full url
|
||||
* @param {string} url - base url string
|
||||
* @param {object} query - query parameters
|
||||
* @returns {string} - full url
|
||||
*/
|
||||
export const stringifyUrl = (url, query) => queryString.stringifyUrl(
|
||||
{ url, query },
|
||||
{ skipNull: true, skipEmptyString: true },
|
||||
);
|
||||
export const stringifyUrl = (url, query) => {
|
||||
const [baseUrl, existingQuery = ''] = url.split('?');
|
||||
const queryString = stringify(query, existingQuery);
|
||||
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import queryString from 'query-string';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import * as utils from './utils';
|
||||
|
||||
jest.mock('query-string', () => ({
|
||||
stringifyUrl: jest.fn((url, options) => ({ url, options })),
|
||||
stringify: jest.fn((data) => data),
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedHttpClient: jest.fn(),
|
||||
}));
|
||||
@@ -20,28 +15,25 @@ describe('lms service utils', () => {
|
||||
});
|
||||
});
|
||||
describe('post', () => {
|
||||
it('forwards arguments to authenticatedHttpClient().post', () => {
|
||||
it('forwards arguments to authenticatedHttpClient().post, removes undefined attributes and appends array values', () => {
|
||||
const post = jest.fn((...args) => ({ post: args }));
|
||||
getAuthenticatedHttpClient.mockReturnValue({ post });
|
||||
const url = 'some url';
|
||||
const body = {
|
||||
some: 'body',
|
||||
for: 'the',
|
||||
for: undefined,
|
||||
test: 'yay',
|
||||
array: ['one', 'two', undefined],
|
||||
};
|
||||
const expectedUrl = utils.post(url, body);
|
||||
expect(queryString.stringify).toHaveBeenCalledWith(body);
|
||||
expect(expectedUrl).toEqual(post(url, body));
|
||||
expect(expectedUrl).toEqual(post(url, 'some=body&test=yay&array=one&array=two'));
|
||||
});
|
||||
});
|
||||
describe('stringifyUrl', () => {
|
||||
it('forwards url and query to stringifyUrl with options to skip null and ""', () => {
|
||||
it('forwards url and query to stringifyUrl skipping null and ""', () => {
|
||||
const url = 'here.com';
|
||||
const query = { some: 'set', of: 'queryParams' };
|
||||
const options = { skipNull: true, skipEmptyString: true };
|
||||
expect(utils.stringifyUrl(url, query)).toEqual(
|
||||
queryString.stringifyUrl({ url, query }, options),
|
||||
);
|
||||
expect(utils.stringifyUrl(url, query)).toEqual('here.com?some=set&of=queryParams');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ config.resolve.modules = [
|
||||
'node_modules',
|
||||
];
|
||||
|
||||
config.module.rules[0].exclude = /node_modules\/(?!(query-string|split-on-first|strict-uri-encode|@edx))/;
|
||||
config.module.rules[0].exclude = /node_modules\/(?!(split-on-first|strict-uri-encode|@edx))/;
|
||||
|
||||
config.plugins.push(
|
||||
new CopyPlugin({
|
||||
|
||||
@@ -9,7 +9,7 @@ config.resolve.modules = [
|
||||
'node_modules',
|
||||
];
|
||||
|
||||
config.module.rules[0].exclude = /node_modules\/(?!(query-string|split-on-first|strict-uri-encode|@edx))/;
|
||||
config.module.rules[0].exclude = /node_modules\/(?!(split-on-first|strict-uri-encode|@edx))/;
|
||||
|
||||
config.plugins.push(
|
||||
new CopyPlugin({
|
||||
|
||||
Reference in New Issue
Block a user