Compare commits
1 Commits
release/te
...
rijuma/cou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a5ae3939b |
@@ -8,7 +8,7 @@ APP_ID='learning'
|
||||
BASE_URL='http://localhost:2000'
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-college-credit-or-credit-hours-for-my-course'
|
||||
CREDIT_HELP_LINK_URL='https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_credit_courses.html#keep-track-of-credit-requirements'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
||||
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
||||
|
||||
@@ -8,7 +8,7 @@ APP_ID='learning'
|
||||
BASE_URL='http://localhost:2000'
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-college-credit-or-credit-hours-for-my-course'
|
||||
CREDIT_HELP_LINK_URL='https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_credit_courses.html#keep-track-of-credit-requirements'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
||||
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
||||
|
||||
@@ -41,8 +41,9 @@ Cloning and Setup
|
||||
|
||||
git clone https://github.com/openedx/frontend-app-learning.git
|
||||
|
||||
2. Use the version of Node specified in ``.nvmrc``.
|
||||
2. Use node v20.x.
|
||||
|
||||
The current version of the micro-frontend build scripts supports node 18.
|
||||
Using other major versions of node *may* work, but this is unsupported. For
|
||||
convenience, this repository includes an ``.nvmrc`` file to help in setting the
|
||||
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
|
||||
@@ -130,7 +131,7 @@ Deployment
|
||||
|
||||
The Learning MFE is similar to all the other Open edX MFEs. Read the Open
|
||||
edX Developer Guide's section on
|
||||
`MFE applications <https://openedx.github.io/frontend-platform/>`_.
|
||||
`MFE applications <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html>`_.
|
||||
|
||||
Plugins
|
||||
=======
|
||||
@@ -144,7 +145,7 @@ Environment Variables
|
||||
This MFE is configured via environment variables supplied at build time.
|
||||
All micro-frontends have a shared set of required environment variables,
|
||||
as documented in the Open edX Developer Guide under
|
||||
`Required Environment Variables <https://openedx.github.io/frontend-platform/>`_.
|
||||
`Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`_.
|
||||
|
||||
The learning micro-frontend also supports the following additional variables:
|
||||
|
||||
|
||||
7954
package-lock.json
generated
7954
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@@ -15,6 +15,7 @@
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .",
|
||||
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx .",
|
||||
"postinstall": "patch-package",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"dev": "PUBLIC_PATH=/learning/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
|
||||
@@ -34,20 +35,20 @@
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/browserslist-config": "1.5.0",
|
||||
"@edx/frontend-component-footer": "^14.6.0",
|
||||
"@edx/frontend-component-header": "^6.2.0",
|
||||
"@edx/frontend-lib-learning-assistant": "^2.20.0",
|
||||
"@edx/frontend-lib-special-exams": "^3.5.0",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@edx/frontend-component-header": "^5.8.0",
|
||||
"@edx/frontend-lib-learning-assistant": "^2.19.2",
|
||||
"@edx/frontend-lib-special-exams": "^3.1.3",
|
||||
"@edx/frontend-platform": "^8.0.0",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@edx/react-unit-test-utils": "^4.0.0",
|
||||
"@edx/react-unit-test-utils": "3.0.0",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.1.4",
|
||||
"@openedx/frontend-build": "^14.5.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"@openedx/paragon": "^22.16.0",
|
||||
"@openedx/frontend-build": "^14.2.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.2.1",
|
||||
"@openedx/frontend-slot-footer": "^1.0.2",
|
||||
"@openedx/paragon": "^22.3.0",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@reduxjs/toolkit": "1.9.7",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -57,11 +58,12 @@
|
||||
"js-cookie": "3.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "^7.1.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-router": "6.15.0",
|
||||
@@ -77,8 +79,9 @@
|
||||
"devDependencies": {
|
||||
"@edx/reactifex": "2.2.0",
|
||||
"@pact-foundation/pact": "^13.0.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/jest-dom": "5.17.0",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"axios-mock-adapter": "2.1.0",
|
||||
"bundlewatch": "^0.4.0",
|
||||
@@ -92,7 +95,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "dist/*.js",
|
||||
"maxSize": "1450kB"
|
||||
"maxSize": "1400kB"
|
||||
}
|
||||
],
|
||||
"normalizeFilenames": "^.+?(\\..+?)\\.\\w+$"
|
||||
|
||||
36
patches/@openedx+frontend-build+13.0.30.patch
Normal file
36
patches/@openedx+frontend-build+13.0.30.patch
Normal file
@@ -0,0 +1,36 @@
|
||||
diff --git a/node_modules/@openedx/frontend-build/config/webpack.prod.config.js b/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
|
||||
index 2879dd9..9efd0fc 100644
|
||||
--- a/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
|
||||
+++ b/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
|
||||
@@ -12,6 +12,7 @@ const NewRelicSourceMapPlugin = require('@edx/new-relic-source-map-webpack-plugi
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const path = require('path');
|
||||
+const fs = require('fs');
|
||||
const PostCssAutoprefixerPlugin = require('autoprefixer');
|
||||
const PostCssRTLCSS = require('postcss-rtlcss');
|
||||
const PostCssCustomMediaCSS = require('postcss-custom-media');
|
||||
@@ -23,6 +24,23 @@ const HtmlWebpackNewRelicPlugin = require('../lib/plugins/html-webpack-new-relic
|
||||
const commonConfig = require('./webpack.common.config');
|
||||
const presets = require('../lib/presets');
|
||||
|
||||
+/**
|
||||
+ * This condition confirms whether the configuration for the MFE has switched to a JS-based configuration
|
||||
+ * as previously implemented in frontend-build and frontend-platform. If the environment variable JS_CONFIG_FILEPATH
|
||||
+ * exists, then an env.config.js(x) file will be copied from the location referenced by the environment variable to the
|
||||
+ * root directory. Its env variables can be accessed with getConfig().
|
||||
+ *
|
||||
+ * https://github.com/openedx/frontend-build/blob/master/docs/0002-js-environment-config.md
|
||||
+ * https://github.com/openedx/frontend-platform/blob/master/docs/decisions/0007-javascript-file-configuration.rst
|
||||
+ */
|
||||
+
|
||||
+const envConfigPath = process.env.JS_CONFIG_FILEPATH;
|
||||
+
|
||||
+if (envConfigPath) {
|
||||
+ const envConfigFilename = envConfigPath.slice(envConfigPath.indexOf('env.config'));
|
||||
+ fs.copyFileSync(envConfigPath, envConfigFilename);
|
||||
+}
|
||||
+
|
||||
// Add process env vars. Currently used only for setting the PUBLIC_PATH.
|
||||
dotenv.config({
|
||||
path: path.resolve(process.cwd(), '.env'),
|
||||
@@ -1,224 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
|
||||
<React Strict Mode>
|
||||
<ErrorPage
|
||||
message="test-error-message"
|
||||
/>
|
||||
</React Strict Mode>
|
||||
`;
|
||||
|
||||
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
||||
<React Strict Mode>
|
||||
<AppProvider
|
||||
store={
|
||||
{
|
||||
"dispatch": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
Symbol(Symbol.observable): [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<link
|
||||
href="favicon-url"
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<PathFixesProvider>
|
||||
<NoticesProvider>
|
||||
<UserMessagesProvider>
|
||||
<Routes>
|
||||
<Route
|
||||
element={
|
||||
<PageWrap>
|
||||
<Page Not Found />
|
||||
</PageWrap>
|
||||
}
|
||||
path="*"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<PageWrap>
|
||||
<Goal Unsubscribe />
|
||||
</PageWrap>
|
||||
}
|
||||
path="/goal-unsubscribe/:token"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<PageWrap>
|
||||
<Courseware Redirect Landing Page />
|
||||
</PageWrap>
|
||||
}
|
||||
path="/redirect/*"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<PageWrap>
|
||||
<Preferences Unsubscribe />
|
||||
</PageWrap>
|
||||
}
|
||||
path="/preferences-unsubscribe/:userToken/:updatePatch"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Course Access Error Page />
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/access-denied"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Tab Container
|
||||
fetch={[Function]}
|
||||
slice="courseHome"
|
||||
tab="outline"
|
||||
>
|
||||
<Outline Tab />
|
||||
</Tab Container>
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/home"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Tab Container
|
||||
fetch={[Function]}
|
||||
slice="courseHome"
|
||||
tab="lti_live"
|
||||
>
|
||||
<Live Tab />
|
||||
</Tab Container>
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/live"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Tab Container
|
||||
fetch={[Function]}
|
||||
slice="courseHome"
|
||||
tab="dates"
|
||||
>
|
||||
<Dates Tab />
|
||||
</Tab Container>
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/dates"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Tab Container
|
||||
fetch={[Function]}
|
||||
slice="courseHome"
|
||||
tab="discussion"
|
||||
>
|
||||
<Discussion Tab />
|
||||
</Tab Container>
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/discussion/:path/*"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Tab Container
|
||||
fetch={[Function]}
|
||||
isProgressTab={true}
|
||||
slice="courseHome"
|
||||
tab="progress"
|
||||
>
|
||||
<Progress Tab />
|
||||
</Tab Container>
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/progress/:targetUserId/"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Tab Container
|
||||
fetch={[Function]}
|
||||
isProgressTab={true}
|
||||
slice="courseHome"
|
||||
tab="progress"
|
||||
>
|
||||
<Progress Tab />
|
||||
</Tab Container>
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/progress"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Tab Container
|
||||
fetch={[Function]}
|
||||
slice="courseware"
|
||||
tab="courseware"
|
||||
>
|
||||
<Course Exit />
|
||||
</Tab Container>
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/course-end"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Courseware Container />
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/:sequenceId/:unitId"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Courseware Container />
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/:sequenceId"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Courseware Container />
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Courseware Container />
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/preview/course/:courseId/:sequenceId/:unitId"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Courseware Container />
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/preview/course/:courseId/:sequenceId"
|
||||
/>
|
||||
</Routes>
|
||||
</UserMessagesProvider>
|
||||
</NoticesProvider>
|
||||
</PathFixesProvider>
|
||||
</AppProvider>
|
||||
</React Strict Mode>
|
||||
`;
|
||||
@@ -1,13 +1,14 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { FormattedMessage, FormattedDate, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
FormattedMessage, FormattedDate, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
||||
import { Info } from '@openedx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const AccessExpirationAlert = ({ payload }) => {
|
||||
const intl = useIntl();
|
||||
const AccessExpirationAlert = ({ intl, payload }) => {
|
||||
const {
|
||||
accessExpiration,
|
||||
courseId,
|
||||
@@ -118,6 +119,7 @@ const AccessExpirationAlert = ({ payload }) => {
|
||||
};
|
||||
|
||||
AccessExpirationAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
payload: PropTypes.shape({
|
||||
accessExpiration: PropTypes.shape({
|
||||
expirationDate: PropTypes.string.isRequired,
|
||||
@@ -132,4 +134,4 @@ AccessExpirationAlert.propTypes = {
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default AccessExpirationAlert;
|
||||
export default injectIntl(AccessExpirationAlert);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
||||
import { WarningFilled } from '@openedx/paragon/icons';
|
||||
@@ -7,8 +7,7 @@ import { WarningFilled } from '@openedx/paragon/icons';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import genericMessages from './messages';
|
||||
|
||||
const ActiveEnterpriseAlert = ({ payload }) => {
|
||||
const intl = useIntl();
|
||||
const ActiveEnterpriseAlert = ({ intl, payload }) => {
|
||||
const { text, courseId } = payload;
|
||||
const changeActiveEnterprise = (
|
||||
<Hyperlink
|
||||
@@ -39,10 +38,11 @@ const ActiveEnterpriseAlert = ({ payload }) => {
|
||||
};
|
||||
|
||||
ActiveEnterpriseAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
payload: PropTypes.shape({
|
||||
text: PropTypes.string,
|
||||
courseId: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default ActiveEnterpriseAlert;
|
||||
export default injectIntl(ActiveEnterpriseAlert);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert, Button } from '@openedx/paragon';
|
||||
import { Info, WarningFilled } from '@openedx/paragon/icons';
|
||||
@@ -11,8 +11,7 @@ import { useModel } from '../../generic/model-store';
|
||||
import messages from './messages';
|
||||
import useEnrollClickHandler from './clickHook';
|
||||
|
||||
const EnrollmentAlert = ({ payload }) => {
|
||||
const intl = useIntl();
|
||||
const EnrollmentAlert = ({ intl, payload }) => {
|
||||
const {
|
||||
canEnroll,
|
||||
courseId,
|
||||
@@ -59,6 +58,7 @@ const EnrollmentAlert = ({ payload }) => {
|
||||
};
|
||||
|
||||
EnrollmentAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
payload: PropTypes.shape({
|
||||
canEnroll: PropTypes.bool,
|
||||
courseId: PropTypes.string,
|
||||
@@ -67,4 +67,4 @@ EnrollmentAlert.propTypes = {
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default EnrollmentAlert;
|
||||
export default injectIntl(EnrollmentAlert);
|
||||
|
||||
@@ -9,12 +9,13 @@ import {
|
||||
Icon,
|
||||
} from '@openedx/paragon';
|
||||
import { Check, ArrowForward } from '@openedx/paragon/icons';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { sendActivationEmail } from '../../courseware/data';
|
||||
import messages from './messages';
|
||||
|
||||
const AccountActivationAlert = () => {
|
||||
const intl = useIntl();
|
||||
const AccountActivationAlert = ({
|
||||
intl,
|
||||
}) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [showSpinner, setShowSpinner] = useState(false);
|
||||
const [showCheck, setShowCheck] = useState(false);
|
||||
@@ -124,4 +125,8 @@ const AccountActivationAlert = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountActivationAlert;
|
||||
AccountActivationAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AccountActivationAlert);
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
||||
import { WarningFilled } from '@openedx/paragon/icons';
|
||||
|
||||
import genericMessages from '../../generic/messages';
|
||||
|
||||
const LogistrationAlert = () => {
|
||||
const intl = useIntl();
|
||||
const LogistrationAlert = ({ intl }) => {
|
||||
const signIn = (
|
||||
<Hyperlink
|
||||
style={{ textDecoration: 'underline' }}
|
||||
@@ -44,4 +43,8 @@ const LogistrationAlert = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default LogistrationAlert;
|
||||
LogistrationAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(LogistrationAlert);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Tabs, Tab } from '@openedx/paragon';
|
||||
|
||||
import { useParams } from 'react-router';
|
||||
@@ -13,8 +13,7 @@ const filterTypes = ['text', 'video', 'sequence'];
|
||||
const filterOther = 'other';
|
||||
const validFilters = [filterAll, ...filterTypes, filterOther];
|
||||
|
||||
export const CoursewareSearchResultsFilter = () => {
|
||||
const intl = useIntl();
|
||||
export const CoursewareSearchResultsFilter = ({ intl }) => {
|
||||
const { courseId } = useParams();
|
||||
const lastSearch = useModel('contentSearchResults', courseId);
|
||||
const { filter: filterKeyword, setFilter } = useCoursewareSearchParams();
|
||||
@@ -64,6 +63,7 @@ export const CoursewareSearchResultsFilter = () => {
|
||||
variant="tabs"
|
||||
activeKey={activeKey}
|
||||
onSelect={setFilter}
|
||||
aria-label={intl.formatMessage(messages.searchResultsFilterDescription)}
|
||||
>
|
||||
{filters.filter(({ count }) => (count > 0)).map(({ key, label }) => (
|
||||
<Tab key={key} eventKey={key} title={label} data-testid={`courseware-search-results-tabs-${key}`}>
|
||||
@@ -74,4 +74,8 @@ export const CoursewareSearchResultsFilter = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CoursewareSearchResultsFilter;
|
||||
CoursewareSearchResultsFilter.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CoursewareSearchResultsFilter);
|
||||
|
||||
@@ -141,7 +141,15 @@ const CoursewareSearch = ({ ...sectionProps }) => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="courseware-search__results" aria-live="polite" data-testid="courseware-search-results">
|
||||
<div
|
||||
key={status}
|
||||
className="courseware-search__results"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
aria-busy={status === 'loading'}
|
||||
data-testid="courseware-search-results"
|
||||
role={status === 'results' ? 'alert' : 'none'}
|
||||
>
|
||||
{status === 'loading' ? (
|
||||
<div className="courseware-search__spinner" data-testid="courseware-search-spinner">
|
||||
<Spinner animation="border" variant="light" screenReaderText={formatMessage(messages.loading)} />
|
||||
@@ -157,8 +165,6 @@ const CoursewareSearch = ({ ...sectionProps }) => {
|
||||
{total > 0 ? (
|
||||
<div
|
||||
className="courseware-search__results-summary"
|
||||
aria-relevant="all"
|
||||
aria-atomic="true"
|
||||
data-testid="courseware-search-summary"
|
||||
>{formatMessage(messages.searchResultsLabel, { total, keyword: lastSearchKeyword })}
|
||||
</div>
|
||||
|
||||
@@ -244,7 +244,7 @@ describe('CoursewareSearch', () => {
|
||||
expect(screen.queryByTestId('courseware-search-summary')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show a summary for the results within a container with aria-live="polite"', () => {
|
||||
it('should show a wrapper div with proper aria attributes', () => {
|
||||
mockModels({
|
||||
searchKeyword: 'fubar',
|
||||
total: 1,
|
||||
@@ -253,7 +253,9 @@ describe('CoursewareSearch', () => {
|
||||
|
||||
const results = screen.queryByTestId('courseware-search-results');
|
||||
|
||||
expect(results).toHaveAttribute('aria-live', 'polite');
|
||||
expect(results).toHaveAttribute('aria-live', 'assertive');
|
||||
expect(results).toHaveAttribute('aria-atomic', 'true');
|
||||
expect(results).toHaveAttribute('role', 'alert');
|
||||
expect(within(results).queryByTestId('courseware-search-summary').textContent).toBe('Results for "fubar":');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
|
||||
const CoursewareSearchEmpty = () => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<div className="courseware-search-results">
|
||||
<p className="courseware-search-results__empty" data-testid="no-results">{intl.formatMessage(messages.searchResultsNone)}</p>
|
||||
</div>
|
||||
);
|
||||
const CoursewareSearchEmpty = ({ intl }) => (
|
||||
<div className="courseware-search-results">
|
||||
<p className="courseware-search-results__empty" data-testid="no-results">{intl.formatMessage(messages.searchResultsNone)}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
CoursewareSearchEmpty.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default CoursewareSearchEmpty;
|
||||
export default injectIntl(CoursewareSearchEmpty);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Folder, TextFields, VideoCamera, Article,
|
||||
} from '@openedx/paragon/icons';
|
||||
@@ -6,6 +6,7 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
|
||||
import messages from './messages';
|
||||
|
||||
const iconTypeMapping = {
|
||||
text: TextFields,
|
||||
@@ -21,6 +22,8 @@ const CoursewareSearchResults = ({ results = [] }) => {
|
||||
return <CoursewareSearchEmpty />;
|
||||
}
|
||||
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const baseUrl = `${getConfig().LMS_BASE_URL}`;
|
||||
|
||||
return (
|
||||
@@ -42,24 +45,30 @@ const CoursewareSearchResults = ({ results = [] }) => {
|
||||
rel: 'nofollow',
|
||||
} : { href: `${baseUrl}${url}` };
|
||||
|
||||
const ariaSeaparator = formatMessage(messages.searchResultsBreadcrumbSeparator);
|
||||
const ariaLocation = location?.length ? formatMessage(messages.searchResultsBreadcrumb, { path: location.join(ariaSeaparator) }) : '';
|
||||
|
||||
return (
|
||||
<a key={id} className="courseware-search-results__item" {...linkProps}>
|
||||
<div className="courseware-search-results__icon"><Icon src={icon} /></div>
|
||||
<div className="courseware-search-results__info">
|
||||
<div className="courseware-search-results__title">
|
||||
<span>{title}</span>
|
||||
{contentHits ? (<em>{contentHits}</em>) : null }
|
||||
<h3>{title}</h3>
|
||||
{contentHits ? (<em aria-hidden="true">{contentHits}</em>) : null }
|
||||
</div>
|
||||
{location?.length ? (
|
||||
<ul className="courseware-search-results__breadcrumbs">
|
||||
{
|
||||
|
||||
<div aria-label={ariaLocation}>
|
||||
{location?.length ? (
|
||||
<ul className="courseware-search-results__breadcrumbs" aria-hidden="true">
|
||||
{
|
||||
// This ignore is necessary because the breadcrumb texts might have duplicates.
|
||||
// The breadcrumbs are not expected to change.
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
location.map((breadcrumb, i) => (<li key={`${i}:${breadcrumb}`}><div>{breadcrumb}</div></li>))
|
||||
}
|
||||
</ul>
|
||||
) : null}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@openedx/paragon';
|
||||
import { ManageSearch } from '@openedx/paragon/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -7,8 +7,9 @@ import messages from './messages';
|
||||
import { useCoursewareSearchFeatureFlag, useCoursewareSearchParams } from './hooks';
|
||||
import { setShowSearch } from '../data/slice';
|
||||
|
||||
const CoursewareSearchToggle = () => {
|
||||
const intl = useIntl();
|
||||
const CoursewareSearchToggle = ({
|
||||
intl,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const enabled = useCoursewareSearchFeatureFlag();
|
||||
const { query } = useCoursewareSearchParams();
|
||||
@@ -40,4 +41,8 @@ const CoursewareSearchToggle = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CoursewareSearchToggle;
|
||||
CoursewareSearchToggle.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CoursewareSearchToggle);
|
||||
|
||||
@@ -38,24 +38,29 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Demo Course Overview
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Introduction, then Demo Course Overview."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Introduction
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Demo Course Overview
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Introduction
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Demo Course Overview
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -91,32 +96,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Passing a Course
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
1
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: About Exams and Certificates, then edX Exams, then Passing a Course."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Passing a Course
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Passing a Course
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -152,29 +164,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Passing a Course
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: About Exams and Certificates, then edX Exams, then Passing a Course."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Passing a Course
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Passing a Course
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -210,29 +227,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Text Input
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Text input."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Text input
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Text input
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -268,29 +290,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Pointing on a Picture
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Pointing on a Picture."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Pointing on a Picture
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Pointing on a Picture
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -326,29 +353,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Getting Answers
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: About Exams and Certificates, then edX Exams, then Getting Answers."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Getting Answers
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Getting Answers
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -384,32 +416,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Welcome!
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
30
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Introduction, then Demo Course Overview, then Introduction: Video and Sequences."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Introduction
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Demo Course Overview
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Introduction: Video and Sequences
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Introduction
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Demo Course Overview
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Introduction: Video and Sequences
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -445,29 +484,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Multiple Choice Questions
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Multiple Choice Questions."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Multiple Choice Questions
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Multiple Choice Questions
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -503,29 +547,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Numerical Input
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Numerical Input."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Numerical Input
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Numerical Input
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -561,32 +610,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Connecting a Circuit and a Circuit Diagram
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
3
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Lesson 1 - Getting Started, then Video Presentation Styles."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 1 - Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Video Presentation Styles
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 1 - Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Video Presentation Styles
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -622,29 +678,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
CAPA
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 2: Get Interactive, then Homework - Labs and Demos, then Code Grader."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 2: Get Interactive
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Labs and Demos
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Code Grader
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 2: Get Interactive
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Labs and Demos
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Code Grader
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -680,29 +741,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Interactive Questions
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Lesson 1 - Getting Started, then Interactive Questions."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 1 - Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Interactive Questions
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 1 - Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Interactive Questions
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -738,32 +804,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Blank HTML Page
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
6
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Introduction, then Demo Course Overview, then Introduction: Video and Sequences."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Introduction
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Demo Course Overview
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Introduction: Video and Sequences
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Introduction
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Demo Course Overview
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Introduction: Video and Sequences
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -799,32 +872,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Discussion Forums
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
5
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 3: Be Social, then Lesson 3 - Be Social, then Discussion Forums."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 3 - Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Discussion Forums
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 3 - Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Discussion Forums
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -860,32 +940,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Overall Grade
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
7
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: About Exams and Certificates, then edX Exams, then Overall Grade Performance."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Overall Grade Performance
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Overall Grade Performance
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -921,32 +1008,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Blank HTML Page
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
3
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 3: Be Social, then Lesson 3 - Be Social, then Homework - Find Your Study Buddy."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 3 - Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Find Your Study Buddy
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 3 - Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Find Your Study Buddy
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -982,32 +1076,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Find Your Study Buddy
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
3
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 3: Be Social, then Homework - Find Your Study Buddy, then Homework - Find Your Study Buddy."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Find Your Study Buddy
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Find Your Study Buddy
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Find Your Study Buddy
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Find Your Study Buddy
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -1043,32 +1144,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Be Social
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
4
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 3: Be Social, then Lesson 3 - Be Social, then Be Social."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 3 - Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Be Social
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 3 - Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Be Social
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -1104,32 +1212,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
EdX Exams
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
4
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: About Exams and Certificates, then edX Exams, then EdX Exams."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
EdX Exams
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
EdX Exams
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -1165,32 +1280,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
When Are Your Exams?
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
2
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Lesson 1 - Getting Started, then When Are Your Exams? ."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 1 - Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
When Are Your Exams?
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 1 - Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
When Are Your Exams?
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -1228,10 +1350,13 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
External Course Link Test
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
aria-label=""
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -101,6 +101,11 @@
|
||||
font-size: 0.875rem;
|
||||
color: $black;
|
||||
|
||||
> h3 {
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { fetchCoursewareSearchSettings } from '../data/thunks';
|
||||
@@ -38,13 +38,13 @@ describe('CoursewareSearch Hooks', () => {
|
||||
|
||||
it('should return true if feature is enabled', async () => {
|
||||
const hook = await renderTestHook();
|
||||
await waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
|
||||
await hook.waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
|
||||
expect(hook.result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if feature is disabled', async () => {
|
||||
const hook = await renderTestHook(false);
|
||||
await waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
|
||||
await hook.waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
|
||||
expect(hook.result.current).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -125,7 +125,7 @@ describe('CoursewareSearch Hooks', () => {
|
||||
it('should return the element bounding box', async () => {
|
||||
const hook = await renderTestHook({ elementId: 'test', mockedInfo });
|
||||
|
||||
await waitFor(() => expect(getBoundingClientRectSpy).toHaveBeenCalled());
|
||||
hook.waitFor(() => expect(getBoundingClientRectSpy).toHaveBeenCalled());
|
||||
|
||||
expect(hook.result.current).toEqual(mockedInfo);
|
||||
});
|
||||
|
||||
@@ -56,7 +56,21 @@ const messages = defineMessages({
|
||||
defaultMessage: 'There was an error on the search process. Please try again in a few minutes. If the problem persists, please contact the support team.',
|
||||
description: 'Error message to show to the users when there\'s an error with the endpoint or the returned payload format.',
|
||||
},
|
||||
|
||||
searchResultsFilterDescription: {
|
||||
id: 'learn.coursewareSearch.searchResultsFilterDescription',
|
||||
defaultMessage: 'Search result filters',
|
||||
description: 'Screen Reader text to describe the filter options.',
|
||||
},
|
||||
searchResultsBreadcrumb: {
|
||||
id: 'learn.coursewareSearch.searchResultsBreadcrumb',
|
||||
defaultMessage: 'Location: {path}.',
|
||||
description: 'Screen Reader text to describe the search result breadcrumbs.',
|
||||
},
|
||||
searchResultsBreadcrumbSeparator: {
|
||||
id: 'learn.searchResultsBreadcrumbSeparator',
|
||||
defaultMessage: ', then ',
|
||||
description: 'Screen Reader text to connect breadcrumb sections. i.e.: "Introduction, then Register, then Something else.',
|
||||
},
|
||||
// These are translations for labeling the filters
|
||||
'filter:all': {
|
||||
id: 'learn.coursewareSearch.filter:all',
|
||||
|
||||
@@ -31,6 +31,7 @@ Factory.define('outlineTabData')
|
||||
course_access_redirect: false,
|
||||
has_scheduled_content: null,
|
||||
access_expiration: null,
|
||||
can_show_upgrade_sock: false,
|
||||
cert_data: {
|
||||
cert_status: null,
|
||||
cert_web_view_url: null,
|
||||
|
||||
@@ -489,6 +489,7 @@ exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normali
|
||||
"outline": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"accessExpiration": null,
|
||||
"canShowUpgradeSock": false,
|
||||
"certData": {
|
||||
"certStatus": null,
|
||||
"certWebViewUrl": null,
|
||||
|
||||
@@ -367,6 +367,7 @@ export async function getOutlineTabData(courseId) {
|
||||
} = tabData;
|
||||
|
||||
const accessExpiration = camelCaseObject(data.access_expiration);
|
||||
const canShowUpgradeSock = data.can_show_upgrade_sock;
|
||||
const certData = camelCaseObject(data.cert_data);
|
||||
const courseBlocks = data.course_blocks ? normalizeOutlineBlocks(courseId, data.course_blocks.blocks) : {};
|
||||
const courseGoals = camelCaseObject(data.course_goals);
|
||||
@@ -388,6 +389,7 @@ export async function getOutlineTabData(courseId) {
|
||||
|
||||
return {
|
||||
accessExpiration,
|
||||
canShowUpgradeSock,
|
||||
certData,
|
||||
courseBlocks,
|
||||
courseGoals,
|
||||
|
||||
@@ -46,6 +46,7 @@ describe('Course Home Service', () => {
|
||||
willRespondWith: {
|
||||
status: 200,
|
||||
body: {
|
||||
can_show_upgrade_sock: boolean(false),
|
||||
verified_mode: like({
|
||||
access_expiration_date: null,
|
||||
currency: 'USD',
|
||||
@@ -93,6 +94,7 @@ describe('Course Home Service', () => {
|
||||
},
|
||||
});
|
||||
const normalizedTabData = {
|
||||
canShowUpgradeSock: false,
|
||||
verifiedMode: {
|
||||
accessExpirationDate: null,
|
||||
currency: 'USD',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import Timeline from './timeline/Timeline';
|
||||
@@ -14,8 +14,7 @@ import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
|
||||
import UpgradeToCompleteAlert from '../suggested-schedule-messaging/UpgradeToCompleteAlert';
|
||||
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
|
||||
|
||||
const DatesTab = () => {
|
||||
const intl = useIntl();
|
||||
const DatesTab = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -60,4 +59,8 @@ const DatesTab = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DatesTab;
|
||||
DatesTab.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(DatesTab);
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useSelector } from 'react-redux';
|
||||
import {
|
||||
FormattedDate,
|
||||
FormattedTime,
|
||||
useIntl,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Tooltip, OverlayTrigger } from '@openedx/paragon';
|
||||
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
@@ -19,10 +20,10 @@ import { isLearnerAssignment } from '../utils';
|
||||
const Day = ({
|
||||
date,
|
||||
first,
|
||||
intl,
|
||||
items,
|
||||
last,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -107,6 +108,7 @@ const Day = ({
|
||||
Day.propTypes = {
|
||||
date: PropTypes.objectOf(Date).isRequired,
|
||||
first: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
date: PropTypes.string,
|
||||
dateType: PropTypes.string,
|
||||
@@ -124,4 +126,4 @@ Day.defaultProps = {
|
||||
last: false,
|
||||
};
|
||||
|
||||
export default Day;
|
||||
export default injectIntl(Day);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useParams, generatePath, useNavigate } from 'react-router-dom';
|
||||
@@ -29,4 +30,6 @@ const DiscussionTab = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DiscussionTab;
|
||||
DiscussionTab.propTypes = {};
|
||||
|
||||
export default injectIntl(DiscussionTab);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import HeaderSlot from '../../plugin-slots/HeaderSlot';
|
||||
import PageLoading from '../../generic/PageLoading';
|
||||
@@ -10,8 +10,7 @@ import { unsubscribeFromCourseGoal } from '../data/api';
|
||||
import messages from './messages';
|
||||
import ResultPage from './ResultPage';
|
||||
|
||||
const GoalUnsubscribe = () => {
|
||||
const intl = useIntl();
|
||||
const GoalUnsubscribe = ({ intl }) => {
|
||||
const { token } = useParams();
|
||||
const [error, setError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -52,4 +51,8 @@ const GoalUnsubscribe = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default GoalUnsubscribe;
|
||||
GoalUnsubscribe.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(GoalUnsubscribe);
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Hyperlink } from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
import { ReactComponent as UnsubscribeIcon } from './unsubscribe.svg';
|
||||
|
||||
const ResultPage = ({ courseTitle, error }) => {
|
||||
const intl = useIntl();
|
||||
const errorDescription = intl.formatMessage(
|
||||
messages.errorDescription,
|
||||
{
|
||||
contactSupport: (
|
||||
<Hyperlink
|
||||
className="text-reset"
|
||||
style={{ textDecoration: 'underline' }}
|
||||
destination={`${getConfig().CONTACT_URL}`}
|
||||
>
|
||||
{intl.formatMessage(messages.contactSupport)}
|
||||
</Hyperlink>
|
||||
),
|
||||
},
|
||||
const ResultPage = ({ courseTitle, error, intl }) => {
|
||||
const errorDescription = (
|
||||
<FormattedMessage
|
||||
id="learning.goals.unsubscribe.errorDescription"
|
||||
defaultMessage="We were unable to unsubscribe you from goal reminder emails. Please try again later or {contactSupport} for help."
|
||||
values={{
|
||||
contactSupport: (
|
||||
<Hyperlink
|
||||
className="text-reset"
|
||||
style={{ textDecoration: 'underline' }}
|
||||
destination={`${getConfig().CONTACT_URL}`}
|
||||
>
|
||||
{intl.formatMessage(messages.contactSupport)}
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const header = error
|
||||
@@ -52,6 +54,7 @@ ResultPage.defaultProps = {
|
||||
ResultPage.propTypes = {
|
||||
courseTitle: PropTypes.string,
|
||||
error: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default ResultPage;
|
||||
export default injectIntl(ResultPage);
|
||||
|
||||
@@ -16,11 +16,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Something went wrong',
|
||||
description: 'It indicate that the unsubscribing request has failed',
|
||||
},
|
||||
errorDescription: {
|
||||
id: 'learning.goals.unsubscribe.errorDescription',
|
||||
defaultMessage: 'We were unable to unsubscribe you from goal reminder emails. Please try again later or {contactSupport} for help.',
|
||||
description: 'Message that notifies user that unsubscribing failed and to try again',
|
||||
},
|
||||
goToDashboard: {
|
||||
id: 'learning.goals.unsubscribe.goToDashboard',
|
||||
defaultMessage: 'Go to dashboard',
|
||||
|
||||
@@ -5,7 +5,7 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@openedx/paragon';
|
||||
import { CourseOutlineTabNotificationsSlot } from '../../plugin-slots/CourseOutlineTabNotificationsSlot';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import { AlertList } from '../../generic/user-messages';
|
||||
|
||||
import CourseDates from './widgets/CourseDates';
|
||||
@@ -16,6 +16,7 @@ import CourseTools from './widgets/CourseTools';
|
||||
import { fetchOutlineTab } from '../data';
|
||||
import messages from './messages';
|
||||
import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
|
||||
import UpgradeNotification from '../../generic/upgrade-notification/UpgradeNotification';
|
||||
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
|
||||
import useCertificateAvailableAlert from './alerts/certificate-status-alert';
|
||||
import useCourseEndAlert from './alerts/course-end-alert';
|
||||
@@ -39,11 +40,13 @@ const OutlineTab = () => {
|
||||
isSelfPaced,
|
||||
org,
|
||||
title,
|
||||
userTimezone,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const expandButtonRef = useRef();
|
||||
|
||||
const {
|
||||
accessExpiration,
|
||||
courseBlocks: {
|
||||
courses,
|
||||
sections,
|
||||
@@ -52,12 +55,20 @@ const OutlineTab = () => {
|
||||
selectedGoal,
|
||||
weeklyLearningGoalEnabled,
|
||||
} = {},
|
||||
datesBannerInfo,
|
||||
datesWidget: {
|
||||
courseDateBlocks,
|
||||
},
|
||||
enableProctoredExams,
|
||||
offer,
|
||||
timeOffsetMillis,
|
||||
verifiedMode,
|
||||
} = useModel('outline', courseId);
|
||||
|
||||
const {
|
||||
marketingUrl,
|
||||
} = useModel('coursewareMeta', courseId);
|
||||
|
||||
const [expandAll, setExpandAll] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -181,7 +192,27 @@ const OutlineTab = () => {
|
||||
/>
|
||||
)}
|
||||
<CourseTools />
|
||||
<CourseOutlineTabNotificationsSlot courseId={courseId} />
|
||||
<PluginSlot
|
||||
id="outline_tab_notifications_slot"
|
||||
pluginProps={{
|
||||
courseId,
|
||||
model: 'outline',
|
||||
}}
|
||||
>
|
||||
<UpgradeNotification
|
||||
offer={offer}
|
||||
verifiedMode={verifiedMode}
|
||||
accessExpiration={accessExpiration}
|
||||
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
|
||||
marketingUrl={marketingUrl}
|
||||
upsellPageName="course_home"
|
||||
userTimezone={userTimezone}
|
||||
shouldDisplayBorder
|
||||
timeOffsetMillis={timeOffsetMillis}
|
||||
courseId={courseId}
|
||||
org={org}
|
||||
/>
|
||||
</PluginSlot>
|
||||
<CourseDates />
|
||||
<CourseHandouts />
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Factory } from 'rosie';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import Cookies from 'js-cookie';
|
||||
@@ -139,7 +139,7 @@ describe('Outline Tab', () => {
|
||||
});
|
||||
await fetchAndRender();
|
||||
|
||||
expect(screen.getByTestId('org.openedx.frontend.learning.course_outline_tab_notifications.v1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('outline_tab_notifications_slot')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles expand/collapse all button click', async () => {
|
||||
@@ -1190,6 +1190,80 @@ describe('Outline Tab', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Upgrade Card', () => {
|
||||
it('renders title when upgrade is available', async () => {
|
||||
await fetchAndRender();
|
||||
expect(screen.queryByRole('heading', { name: 'Pursue a verified certificate' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays link to upgrade', async () => {
|
||||
await fetchAndRender();
|
||||
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('viewing upgrade card sends analytics', async () => {
|
||||
sendTrackEvent.mockClear();
|
||||
sendTrackingLogEvent.mockClear();
|
||||
await fetchAndRender();
|
||||
|
||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('Promotion Viewed', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
creative: 'sidebarupsell',
|
||||
name: 'In-Course Verification Prompt',
|
||||
position: 'sidebar-message',
|
||||
promotion_id: 'courseware_verified_certificate_upsell',
|
||||
});
|
||||
|
||||
expect(sendTrackingLogEvent).toHaveBeenCalledTimes(1);
|
||||
expect(sendTrackingLogEvent).toHaveBeenCalledWith('edx.bi.course.upgrade.sidebarupsell.displayed', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
});
|
||||
});
|
||||
|
||||
it('clicking upgrade link sends analytics', async () => {
|
||||
await fetchAndRender();
|
||||
|
||||
// Clearing after render to remove any events sent on view (ex. 'Promotion Viewed')
|
||||
sendTrackEvent.mockClear();
|
||||
sendTrackingLogEvent.mockClear();
|
||||
const upgradeButton = screen.getByRole('link', { name: 'Upgrade for $149' });
|
||||
|
||||
fireEvent.click(upgradeButton);
|
||||
|
||||
expect(sendTrackEvent).toHaveBeenCalledTimes(2);
|
||||
expect(sendTrackEvent).toHaveBeenNthCalledWith(1, 'Promotion Clicked', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
creative: 'sidebarupsell',
|
||||
name: 'In-Course Verification Prompt',
|
||||
position: 'sidebar-message',
|
||||
promotion_id: 'courseware_verified_certificate_upsell',
|
||||
});
|
||||
expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.bi.ecommerce.upsell_links_clicked', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
linkCategory: 'green_upgrade',
|
||||
linkName: 'course_home_green',
|
||||
linkType: 'button',
|
||||
pageName: 'course_home',
|
||||
});
|
||||
|
||||
expect(sendTrackingLogEvent).toHaveBeenCalledTimes(2);
|
||||
expect(sendTrackingLogEvent).toHaveBeenNthCalledWith(1, 'edx.bi.course.upgrade.sidebarupsell.clicked', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
});
|
||||
expect(sendTrackingLogEvent).toHaveBeenNthCalledWith(2, 'edx.course.enrollment.upgrade.clicked', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
location: 'sidebar-message',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Account Activation Alert', () => {
|
||||
beforeEach(() => {
|
||||
const intersectionObserverMock = () => ({
|
||||
|
||||
@@ -3,7 +3,8 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
FormattedDate,
|
||||
FormattedMessage,
|
||||
useIntl,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Button } from '@openedx/paragon';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -24,8 +25,7 @@ export const CERT_STATUS_TYPE = {
|
||||
UNVERIFIED: 'unverified',
|
||||
};
|
||||
|
||||
const CertificateStatusAlert = ({ payload }) => {
|
||||
const intl = useIntl();
|
||||
const CertificateStatusAlert = ({ intl, payload }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
certificateAvailableDate,
|
||||
@@ -192,6 +192,7 @@ const CertificateStatusAlert = ({ payload }) => {
|
||||
};
|
||||
|
||||
CertificateStatusAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
payload: PropTypes.shape({
|
||||
certificateAvailableDate: PropTypes.string,
|
||||
certStatus: PropTypes.string,
|
||||
@@ -209,4 +210,4 @@ CertificateStatusAlert.propTypes = {
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default CertificateStatusAlert;
|
||||
export default injectIntl(CertificateStatusAlert);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||
import { Alert, Button, Hyperlink } from '@openedx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
@@ -14,8 +14,7 @@ import outlineMessages from '../../messages';
|
||||
import useEnrollClickHandler from '../../../../alerts/enrollment-alert/clickHook';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
const PrivateCourseAlert = ({ payload }) => {
|
||||
const intl = useIntl();
|
||||
const PrivateCourseAlert = ({ intl, payload }) => {
|
||||
const {
|
||||
anonymousUser,
|
||||
canEnroll,
|
||||
@@ -104,6 +103,7 @@ const PrivateCourseAlert = ({ payload }) => {
|
||||
};
|
||||
|
||||
PrivateCourseAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
payload: PropTypes.shape({
|
||||
anonymousUser: PropTypes.bool,
|
||||
canEnroll: PropTypes.bool,
|
||||
@@ -111,4 +111,4 @@ PrivateCourseAlert.propTypes = {
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default PrivateCourseAlert;
|
||||
export default injectIntl(PrivateCourseAlert);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import DateSummary from '../DateSummary';
|
||||
import messages from '../messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
const CourseDates = () => {
|
||||
const intl = useIntl();
|
||||
const CourseDates = ({
|
||||
intl,
|
||||
}) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -47,4 +48,8 @@ const CourseDates = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseDates;
|
||||
CourseDates.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseDates);
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import LmsHtmlFragment from '../LmsHtmlFragment';
|
||||
import messages from '../messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
const CourseHandouts = () => {
|
||||
const intl = useIntl();
|
||||
const CourseHandouts = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -32,4 +31,8 @@ const CourseHandouts = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseHandouts;
|
||||
CourseHandouts.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseHandouts);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
|
||||
|
||||
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faBookmark, faCertificate, faInfo, faCalendar, faStar,
|
||||
@@ -14,8 +14,7 @@ import messages from '../messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
import LaunchCourseHomeTourButton from '../../../product-tours/newUserCourseHomeTour/LaunchCourseHomeTourButton';
|
||||
|
||||
const CourseTools = () => {
|
||||
const intl = useIntl();
|
||||
const CourseTools = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -82,4 +81,8 @@ const CourseTools = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseTools;
|
||||
CourseTools.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseTools);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
// These flag svgs are derivatives of the Flag icon from paragon
|
||||
import { ReactComponent as FlagIntenseIcon } from './flag_black.svg';
|
||||
import { ReactComponent as FlagCasualIcon } from './flag_outline.svg';
|
||||
@@ -13,8 +13,8 @@ const LearningGoalButton = ({
|
||||
level,
|
||||
isSelected,
|
||||
handleSelect,
|
||||
intl,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const buttonDetails = {
|
||||
casual: {
|
||||
daysPerWeek: 1,
|
||||
@@ -53,6 +53,7 @@ LearningGoalButton.propTypes = {
|
||||
level: PropTypes.string.isRequired,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
handleSelect: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default LearningGoalButton;
|
||||
export default injectIntl(LearningGoalButton);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import camelCase from 'lodash.camelcase';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@openedx/paragon';
|
||||
|
||||
import messages from '../messages';
|
||||
@@ -10,8 +10,7 @@ import { getProctoringInfoData } from '../../data/api';
|
||||
import { fetchProctoringInfoResolved } from '../../data/slice';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
const ProctoringInfoPanel = () => {
|
||||
const intl = useIntl();
|
||||
const ProctoringInfoPanel = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -217,4 +216,8 @@ const ProctoringInfoPanel = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ProctoringInfoPanel;
|
||||
ProctoringInfoPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ProctoringInfoPanel);
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Button, Card } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import messages from '../messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
const StartOrResumeCourseCard = () => {
|
||||
const intl = useIntl();
|
||||
const StartOrResumeCourseCard = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -63,4 +62,8 @@ const StartOrResumeCourseCard = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default StartOrResumeCourseCard;
|
||||
StartOrResumeCourseCard.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(StartOrResumeCourseCard);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Form, Card, Icon } from '@openedx/paragon';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Email } from '@openedx/paragon/icons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import messages from '../messages';
|
||||
@@ -18,8 +18,8 @@ import './FlagButton.scss';
|
||||
const WeeklyLearningGoalCard = ({
|
||||
daysPerWeek,
|
||||
subscribedToReminders,
|
||||
intl,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -152,10 +152,11 @@ const WeeklyLearningGoalCard = ({
|
||||
WeeklyLearningGoalCard.propTypes = {
|
||||
daysPerWeek: PropTypes.number,
|
||||
subscribedToReminders: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
WeeklyLearningGoalCard.defaultProps = {
|
||||
daysPerWeek: null,
|
||||
subscribedToReminders: false,
|
||||
};
|
||||
export default WeeklyLearningGoalCard;
|
||||
export default injectIntl(WeeklyLearningGoalCard);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -14,8 +14,7 @@ import { resetDeadlines } from '../data';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import messages from './messages';
|
||||
|
||||
const ShiftDatesAlert = ({ fetch, model }) => {
|
||||
const intl = useIntl();
|
||||
const ShiftDatesAlert = ({ fetch, intl, model }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -60,7 +59,8 @@ const ShiftDatesAlert = ({ fetch, model }) => {
|
||||
|
||||
ShiftDatesAlert.propTypes = {
|
||||
fetch: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
model: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ShiftDatesAlert;
|
||||
export default injectIntl(ShiftDatesAlert);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const SuggestedScheduleHeader = () => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<p className="large">
|
||||
{intl.formatMessage(messages.suggestedSchedule)}
|
||||
</p>
|
||||
);
|
||||
const SuggestedScheduleHeader = ({ intl }) => (
|
||||
<p className="large">
|
||||
{intl.formatMessage(messages.suggestedSchedule)}
|
||||
</p>
|
||||
);
|
||||
|
||||
SuggestedScheduleHeader.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default SuggestedScheduleHeader;
|
||||
export default injectIntl(SuggestedScheduleHeader);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import messages from './messages';
|
||||
|
||||
const UpgradeToCompleteAlert = ({ logUpgradeLinkClick }) => {
|
||||
const intl = useIntl();
|
||||
const UpgradeToCompleteAlert = ({ intl, logUpgradeLinkClick }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -59,6 +58,7 @@ const UpgradeToCompleteAlert = ({ logUpgradeLinkClick }) => {
|
||||
};
|
||||
|
||||
UpgradeToCompleteAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
logUpgradeLinkClick: PropTypes.func,
|
||||
};
|
||||
|
||||
@@ -66,4 +66,4 @@ UpgradeToCompleteAlert.defaultProps = {
|
||||
logUpgradeLinkClick: () => {},
|
||||
};
|
||||
|
||||
export default UpgradeToCompleteAlert;
|
||||
export default injectIntl(UpgradeToCompleteAlert);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -13,8 +13,7 @@ import {
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import messages from './messages';
|
||||
|
||||
const UpgradeToShiftDatesAlert = ({ logUpgradeLinkClick, model }) => {
|
||||
const intl = useIntl();
|
||||
const UpgradeToShiftDatesAlert = ({ intl, logUpgradeLinkClick, model }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -61,6 +60,7 @@ const UpgradeToShiftDatesAlert = ({ logUpgradeLinkClick, model }) => {
|
||||
};
|
||||
|
||||
UpgradeToShiftDatesAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
logUpgradeLinkClick: PropTypes.func,
|
||||
model: PropTypes.string.isRequired,
|
||||
};
|
||||
@@ -69,4 +69,4 @@ UpgradeToShiftDatesAlert.defaultProps = {
|
||||
logUpgradeLinkClick: () => {},
|
||||
};
|
||||
|
||||
export default UpgradeToShiftDatesAlert;
|
||||
export default injectIntl(UpgradeToShiftDatesAlert);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
@@ -9,9 +9,8 @@ import { CoursewareSearch, CoursewareSearchToggle } from '../course-home/coursew
|
||||
import { useCoursewareSearchState } from '../course-home/courseware-search/hooks';
|
||||
|
||||
const CourseTabsNavigation = ({
|
||||
activeTabSlug, className, tabs,
|
||||
activeTabSlug, className, tabs, intl,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { show } = useCoursewareSearchState();
|
||||
|
||||
return (
|
||||
@@ -52,6 +51,7 @@ CourseTabsNavigation.propTypes = {
|
||||
slug: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
CourseTabsNavigation.defaultProps = {
|
||||
@@ -59,4 +59,4 @@ CourseTabsNavigation.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
|
||||
export default CourseTabsNavigation;
|
||||
export default injectIntl(CourseTabsNavigation);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getConfig, history } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { waitForElementToBeRemoved, fireEvent } from '@testing-library/dom';
|
||||
import '@testing-library/jest-dom';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import {
|
||||
@@ -221,7 +221,7 @@ describe('CoursewareContainer', () => {
|
||||
});
|
||||
|
||||
history.push(`/course/${courseId}`);
|
||||
const container = await loadContainer();
|
||||
const container = await waitFor(() => loadContainer());
|
||||
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container);
|
||||
@@ -244,7 +244,7 @@ describe('CoursewareContainer', () => {
|
||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/courseware/resume/${courseId}`).reply(200, {});
|
||||
|
||||
history.push(`/course/${courseId}`);
|
||||
const container = await loadContainer();
|
||||
const container = await waitFor(() => loadContainer());
|
||||
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container);
|
||||
@@ -294,7 +294,7 @@ describe('CoursewareContainer', () => {
|
||||
describe('when the URL does not contain a unit ID', () => {
|
||||
it('should choose a unit within the section\'s first sequence', async () => {
|
||||
setUrl(sectionTree[1].id);
|
||||
const container = await loadContainer();
|
||||
const container = await waitFor(() => loadContainer());
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container, 2);
|
||||
assertLocation(container, sequenceTree[1][0].id, unitTree[1][0][0].id);
|
||||
@@ -369,7 +369,7 @@ describe('CoursewareContainer', () => {
|
||||
|
||||
it('should pick the first unit if position was not defined (activeUnitIndex becomes 0)', async () => {
|
||||
history.push(`/course/${courseId}/${sequenceBlock.id}`);
|
||||
const container = await loadContainer();
|
||||
const container = await waitFor(() => loadContainer());
|
||||
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container);
|
||||
@@ -388,7 +388,7 @@ describe('CoursewareContainer', () => {
|
||||
setUpMockRequests({ sequenceMetadatas: [sequenceMetadata] });
|
||||
|
||||
history.push(`/course/${courseId}/${sequenceBlock.id}`);
|
||||
const container = await loadContainer();
|
||||
const container = await waitFor(() => loadContainer());
|
||||
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container);
|
||||
@@ -405,7 +405,7 @@ describe('CoursewareContainer', () => {
|
||||
|
||||
it('should load the specified unit', async () => {
|
||||
history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[2].id}`);
|
||||
const container = await loadContainer();
|
||||
const container = await waitFor(() => loadContainer());
|
||||
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container);
|
||||
@@ -421,7 +421,7 @@ describe('CoursewareContainer', () => {
|
||||
});
|
||||
|
||||
history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[0].id}`);
|
||||
const container = await loadContainer();
|
||||
const container = await waitFor(() => loadContainer());
|
||||
|
||||
const sequenceNavButtons = container.querySelectorAll('nav.sequence-navigation a, nav.sequence-navigation button');
|
||||
const sequenceNextButton = sequenceNavButtons[4];
|
||||
|
||||
@@ -50,11 +50,7 @@ describe('Course', () => {
|
||||
global.innerWidth = breakpoints.extraLarge.minWidth;
|
||||
});
|
||||
|
||||
// This was passing when it shouldn't have been because of improper
|
||||
// waitFor use. With the React 18 upgrade it no longer improperly passes
|
||||
// so we are skipping it. See https://github.com/openedx/frontend-app-learning/issues/1669
|
||||
// for details.
|
||||
it.skip('loads learning sequence', () => {
|
||||
it('loads learning sequence', async () => {
|
||||
render(<Course {...mockData} />, { wrapWithRouter: true });
|
||||
expect(screen.queryByRole('navigation', { name: 'breadcrumb' })).not.toBeInTheDocument();
|
||||
waitFor(() => {
|
||||
@@ -98,11 +94,7 @@ describe('Course', () => {
|
||||
expect(screen.queryByRole('navigation', { name: 'breadcrumb' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// This was passing when it shouldn't have been because of improper
|
||||
// waitFor use. With the React 18 upgrade it no longer improperly passes
|
||||
// so we are skipping it. See https://github.com/openedx/frontend-app-learning/issues/1669
|
||||
// for details.
|
||||
it.skip('displays first section celebration modal', async () => {
|
||||
it('displays first section celebration modal', async () => {
|
||||
const courseHomeMetadata = Factory.build('courseHomeMetadata', { celebrations: { firstSection: true } });
|
||||
const testStore = await initializeTestStore({ courseHomeMetadata }, false);
|
||||
const { courseware, models } = testStore.getState();
|
||||
@@ -124,11 +116,7 @@ describe('Course', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// This was passing when it shouldn't have been because of improper
|
||||
// waitFor use. With the React 18 upgrade it no longer improperly passes
|
||||
// so we are skipping it. See https://github.com/openedx/frontend-app-learning/issues/1669
|
||||
// for details.
|
||||
it.skip('displays weekly goal celebration modal', async () => {
|
||||
it('displays weekly goal celebration modal', async () => {
|
||||
const courseHomeMetadata = Factory.build('courseHomeMetadata', { celebrations: { weeklyGoal: true } });
|
||||
const testStore = await initializeTestStore({ courseHomeMetadata }, false);
|
||||
const { courseware, models } = testStore.getState();
|
||||
@@ -148,6 +136,18 @@ describe('Course', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('displays notification trigger and toggles active class on click', async () => {
|
||||
render(<Course {...mockData} />, { wrapWithRouter: true });
|
||||
|
||||
waitFor(() => {
|
||||
const notificationTrigger = screen.getByRole('button', { name: /Show notification tray/i });
|
||||
expect(notificationTrigger).toBeInTheDocument();
|
||||
expect(notificationTrigger.parentNode).not.toHaveClass('sidebar-active', { exact: true });
|
||||
fireEvent.click(notificationTrigger);
|
||||
expect(notificationTrigger.parentNode).toHaveClass('sidebar-active');
|
||||
});
|
||||
});
|
||||
|
||||
it('handles click to open/close discussions sidebar', async () => {
|
||||
await setupDiscussionSidebar();
|
||||
|
||||
|
||||
@@ -1,13 +1,107 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faHome } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useToggle, ModalPopup, Menu } from '@openedx/paragon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useModel, useModels } from '../../generic/model-store';
|
||||
import JumpNavMenuItem from './JumpNavMenuItem';
|
||||
|
||||
import { useModel, useModels } from '../../../generic/model-store';
|
||||
import BreadcrumbItem from './BreadcrumbItem';
|
||||
const CourseBreadcrumb = ({
|
||||
content,
|
||||
withSeparator,
|
||||
courseId,
|
||||
sequenceId,
|
||||
unitId,
|
||||
isStaff,
|
||||
}) => {
|
||||
const defaultContent = content.filter(
|
||||
(destination) => destination.default,
|
||||
)[0] || { id: courseId, label: '', sequences: [] };
|
||||
|
||||
const showRegularLink = getConfig().ENABLE_JUMPNAV !== 'true' || content.length < 2 || !isStaff;
|
||||
const [isOpen, open, close] = useToggle(false);
|
||||
const [target, setTarget] = useState(null);
|
||||
return (
|
||||
<>
|
||||
{withSeparator && (
|
||||
<li className="col-auto p-0 mx-2 text-primary-500 text-truncate text-nowrap" role="presentation" aria-hidden>/</li>
|
||||
)}
|
||||
|
||||
<li
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
data-testid="breadcrumb-item"
|
||||
>
|
||||
{showRegularLink ? (
|
||||
<Link
|
||||
className="text-primary-500"
|
||||
to={
|
||||
defaultContent.sequences.length
|
||||
? `/course/${courseId}/${defaultContent.sequences[0].id}`
|
||||
: `/course/${courseId}/${defaultContent.id}`
|
||||
}
|
||||
>
|
||||
{defaultContent.label}
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
{
|
||||
// eslint-disable-next-line
|
||||
<a className="text-primary-500" onClick={open} ref={setTarget}>
|
||||
{defaultContent.label}
|
||||
</a>
|
||||
}
|
||||
<ModalPopup positionRef={target} isOpen={isOpen} onClose={close}>
|
||||
<Menu>
|
||||
{content.map((item) => (
|
||||
<JumpNavMenuItem
|
||||
key={item.label}
|
||||
isDefault={item.default}
|
||||
sequences={item.sequences}
|
||||
courseId={courseId}
|
||||
title={item.label}
|
||||
currentSequence={sequenceId}
|
||||
currentUnit={unitId}
|
||||
onClick={close}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
</ModalPopup>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
};
|
||||
CourseBreadcrumb.propTypes = {
|
||||
content: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
default: PropTypes.bool,
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
}),
|
||||
).isRequired,
|
||||
sequenceId: PropTypes.string,
|
||||
unitId: PropTypes.string,
|
||||
withSeparator: PropTypes.bool,
|
||||
courseId: PropTypes.string,
|
||||
isStaff: PropTypes.bool,
|
||||
};
|
||||
|
||||
CourseBreadcrumb.defaultProps = {
|
||||
withSeparator: false,
|
||||
sequenceId: null,
|
||||
unitId: null,
|
||||
courseId: null,
|
||||
isStaff: null,
|
||||
};
|
||||
|
||||
const CourseBreadcrumbs = ({
|
||||
courseId,
|
||||
@@ -23,7 +117,7 @@ const CourseBreadcrumbs = ({
|
||||
);
|
||||
|
||||
const allSequencesInSections = Object.fromEntries(
|
||||
useModels('sections', course.sectionIds)?.map((section) => [
|
||||
useModels('sections', course.sectionIds).map((section) => [
|
||||
section.id,
|
||||
{
|
||||
default: section.id === sectionId,
|
||||
@@ -77,7 +171,7 @@ const CourseBreadcrumbs = ({
|
||||
</Link>
|
||||
</li>
|
||||
{links.map((content, i) => (
|
||||
<BreadcrumbItem
|
||||
<CourseBreadcrumb
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={i}
|
||||
courseId={courseId}
|
||||
@@ -85,7 +179,6 @@ const CourseBreadcrumbs = ({
|
||||
content={content}
|
||||
unitId={unitId}
|
||||
withSeparator
|
||||
separator="/"
|
||||
isStaff={isStaff}
|
||||
/>
|
||||
))}
|
||||
134
src/courseware/course/CourseBreadcrumbs.test.jsx
Normal file
134
src/courseware/course/CourseBreadcrumbs.test.jsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React from 'react';
|
||||
import { screen, render } from '@testing-library/react';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { useModel, useModels } from '../../generic/model-store';
|
||||
import CourseBreadcrumbs from './CourseBreadcrumbs';
|
||||
|
||||
jest.mock('@edx/frontend-platform');
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
|
||||
// Remove When Fully rolled out>>>
|
||||
jest.mock('../../generic/model-store');
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
getConfig.mockImplementation(() => ({ ENABLE_JUMPNAV: 'true' }));
|
||||
getAuthenticatedUser.mockImplementation(() => ({ administrator: true }));
|
||||
// ^^^^Remove When Fully rolled out
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
connect: (mapStateToProps, mapDispatchToProps) => (ReactComponent) => ({
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
ReactComponent,
|
||||
}),
|
||||
Provider: ({ children }) => children,
|
||||
useSelector: () => 'loaded',
|
||||
}));
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
Link: jest.fn().mockImplementation(({ to, children }) => (
|
||||
<a href={to}>{children}</a>
|
||||
)),
|
||||
}));
|
||||
|
||||
useModels.mockImplementation((name) => {
|
||||
if (name === 'sections') {
|
||||
return [
|
||||
{
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
|
||||
sequenceIds: ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction'],
|
||||
title: 'Introduction',
|
||||
},
|
||||
{
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
||||
sequenceIds: ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions'],
|
||||
title: 'Example Week 1: Getting Started',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
|
||||
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
||||
title: 'Lesson 1 - Getting Started',
|
||||
unitIds: [
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
|
||||
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
||||
title: 'Homework - Question Styles',
|
||||
unitIds: [
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@47dbd5f836544e61877a483c0b75606c',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@54bb9b142c6c4c22afc62bcb628f0e68',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0c92347a5c00',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_1fef54c2b23b',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2889db1677a549abb15eb4d886f95d1c',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e8a5cc2aed424838853defab7be45e42',
|
||||
],
|
||||
},
|
||||
];
|
||||
});
|
||||
useModel.mockImplementation(() => ({
|
||||
sectionIds: ['block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations'],
|
||||
}));
|
||||
|
||||
describe('CourseBreadcrumbs', () => {
|
||||
jest.spyOn(React, 'useMemo').mockImplementation(() => [
|
||||
[
|
||||
{
|
||||
default: false,
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
|
||||
label: 'Introduction',
|
||||
url: 'http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction',
|
||||
},
|
||||
{
|
||||
default: true,
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
||||
label: 'Example Week 1: Getting Started',
|
||||
url: 'http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course/block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@simulations', label: "Lesson 2 - Let's Get Interactive!", default: true, url: 'http://localhost:2000/course/course-v1:edX+DemoX+D…e@vertical+block@d0d804e8863c4a95a659c04d8a2b2bc0',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@175e76c4951144a29d46211361266e0e', label: 'Homework - Essays', default: false, url: 'http://localhost:2000/course/course-v1:edX+DemoX+D…e@vertical+block@fb79dcbad35b466a8c6364f8ffee9050',
|
||||
},
|
||||
],
|
||||
]);
|
||||
render(
|
||||
<IntlProvider>
|
||||
<BrowserRouter>
|
||||
<CourseBreadcrumbs
|
||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
||||
sectionId="block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations"
|
||||
sequenceId="block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions"
|
||||
isStaff
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
</IntlProvider>,
|
||||
);
|
||||
it('renders course breadcrumbs as expected', async () => {
|
||||
expect(screen.queryAllByRole('link')).toHaveLength(1);
|
||||
const courseHomeButtonDestination = screen.getAllByRole('link')[0].href;
|
||||
expect(courseHomeButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home');
|
||||
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
|
||||
expect(screen.queryAllByTestId('breadcrumb-item')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -1,104 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
useToggle,
|
||||
ModalPopup,
|
||||
Menu,
|
||||
Button,
|
||||
} from '@openedx/paragon';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import JumpNavMenuItem from '../JumpNavMenuItem';
|
||||
|
||||
interface Props {
|
||||
content: {
|
||||
default: boolean,
|
||||
id: string,
|
||||
label: string,
|
||||
sequences: {
|
||||
id: string,
|
||||
}[],
|
||||
} [];
|
||||
withSeparator: boolean | false,
|
||||
separator: string | '';
|
||||
courseId: string;
|
||||
sequenceId: string | '';
|
||||
unitId: string | '';
|
||||
isStaff: boolean | false;
|
||||
}
|
||||
|
||||
const BreadcrumbItem: React.FC<Props> = ({
|
||||
content,
|
||||
withSeparator,
|
||||
separator,
|
||||
courseId,
|
||||
sequenceId,
|
||||
unitId,
|
||||
isStaff,
|
||||
}) => {
|
||||
const defaultContent = content.filter(
|
||||
(destination: { default: boolean }) => destination.default,
|
||||
)[0] || { id: courseId, label: '', sequences: [] };
|
||||
|
||||
const showRegularLink = getConfig().ENABLE_JUMPNAV !== 'true' || content.length < 2 || !isStaff;
|
||||
const [isOpen, open, close] = useToggle(false);
|
||||
const [target, setTarget] = useState(null);
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const isPreview = pathname.startsWith('/preview');
|
||||
const baseUrl = defaultContent.sequences.length
|
||||
? `/course/${courseId}/${defaultContent.sequences[0].id}`
|
||||
: `/course/${courseId}/${defaultContent.id}`;
|
||||
const link = isPreview ? `/preview${baseUrl}` : baseUrl;
|
||||
return (
|
||||
<>
|
||||
{withSeparator && separator && (
|
||||
<li className="col-auto p-0 mx-2 text-primary-500 text-truncate text-nowrap" role="presentation" aria-hidden>{separator}</li>
|
||||
)}
|
||||
|
||||
<li
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
data-testid="breadcrumb-item"
|
||||
>
|
||||
{showRegularLink ? (
|
||||
<Link
|
||||
className="text-primary-500"
|
||||
to={link}
|
||||
>
|
||||
{defaultContent.label}
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
{
|
||||
// @ts-ignore
|
||||
<Button className="text-primary-500 m-0 p-0" variant="link" onClick={open} ref={setTarget}>
|
||||
{defaultContent.label}
|
||||
</Button>
|
||||
}
|
||||
<ModalPopup positionRef={target} isOpen={isOpen} onClose={close}>
|
||||
<Menu>
|
||||
{content.map((item) => (
|
||||
<JumpNavMenuItem
|
||||
key={item.label}
|
||||
isDefault={item.default}
|
||||
sequences={item.sequences}
|
||||
courseId={courseId}
|
||||
title={item.label}
|
||||
currentSequence={sequenceId}
|
||||
currentUnit={unitId}
|
||||
onClick={close}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
</ModalPopup>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BreadcrumbItem;
|
||||
@@ -1,101 +0,0 @@
|
||||
import { Factory } from 'rosie';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { initializeMockApp, initializeTestStore } from '@src/setupTest';
|
||||
import CourseBreadcrumbs from './CourseBreadcrumbs';
|
||||
|
||||
const props = {
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
||||
sequenceId: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
|
||||
isStaff: true,
|
||||
};
|
||||
|
||||
const courseMetadata = Factory.build('courseMetadata', { courseId: props.courseId, sectionIds: [props.sectionId] });
|
||||
const sequenceBlocks = [Factory.build(
|
||||
'block',
|
||||
{ type: 'sequential', id: props.sequenceId, title: 'Subsection' },
|
||||
{ courseId: props.courseId },
|
||||
)];
|
||||
const sectionBlocks = [Factory.build(
|
||||
'block',
|
||||
{
|
||||
type: 'chapter',
|
||||
id: props.sectionId,
|
||||
title: 'Section',
|
||||
children: [{ id: props.sequenceId }],
|
||||
},
|
||||
{ courseId: props.courseId },
|
||||
)];
|
||||
|
||||
initializeMockApp();
|
||||
|
||||
describe('CourseBreadcrumbs', () => {
|
||||
let store = {};
|
||||
|
||||
const initTestStore = async () => {
|
||||
store = await initializeTestStore({ courseMetadata, sectionBlocks, sequenceBlocks });
|
||||
};
|
||||
|
||||
function renderWithProvider(pathname = '/course') {
|
||||
const { container } = render(
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter initialEntries={[{ pathname }]}>
|
||||
<CourseBreadcrumbs {...props} />
|
||||
</MemoryRouter>
|
||||
</IntlProvider>
|
||||
</AppProvider>,
|
||||
);
|
||||
return container;
|
||||
}
|
||||
|
||||
describe('in live view', () => {
|
||||
it('renders course breadcrumbs as expected', async () => {
|
||||
await initTestStore();
|
||||
renderWithProvider();
|
||||
const courseHomeButtonDestination = screen.getAllByRole('link')[0].href;
|
||||
|
||||
expect(courseHomeButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home');
|
||||
|
||||
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryAllByTestId('breadcrumb-item')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('section link does not include /preview/', async () => {
|
||||
await initTestStore();
|
||||
renderWithProvider();
|
||||
const sectionBreadcrumb = screen.getByText(sectionBlocks[0].block_id);
|
||||
const sectionLink = sectionBreadcrumb.closest('a').href;
|
||||
|
||||
expect(sectionLink.includes('/preview/')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('in live view', () => {
|
||||
it('renders course breadcrumbs as expected', async () => {
|
||||
await initTestStore();
|
||||
renderWithProvider('/preview/courses');
|
||||
const courseHomeButtonDestination = screen.getAllByRole('link')[0].href;
|
||||
|
||||
expect(courseHomeButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home');
|
||||
|
||||
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryAllByTestId('breadcrumb-item')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('section link does includes /preview/', async () => {
|
||||
await initTestStore();
|
||||
renderWithProvider('/preview/courses');
|
||||
const sectionBreadcrumb = screen.getByText(sectionBlocks[0].block_id);
|
||||
const sectionLink = sectionBreadcrumb.closest('a').href;
|
||||
|
||||
expect(sectionLink.includes('/preview/')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
import CourseBreadcrumbs from './CourseBreadcrumbs';
|
||||
|
||||
export default CourseBreadcrumbs;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
breakpoints,
|
||||
@@ -18,9 +18,8 @@ import { recordFirstSectionCelebration } from './utils';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
const CelebrationModal = ({
|
||||
courseId, isOpen, onClose, ...rest
|
||||
courseId, intl, isOpen, onClose, ...rest
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { org, celebrations } = useModel('courseHomeMeta', courseId);
|
||||
const dispatch = useDispatch();
|
||||
const wideScreen = useWindowSize().width >= breakpoints.small.minWidth;
|
||||
@@ -67,8 +66,9 @@ const CelebrationModal = ({
|
||||
|
||||
CelebrationModal.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CelebrationModal;
|
||||
export default injectIntl(CelebrationModal);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow, Button, Icon, StandardModal,
|
||||
} from '@openedx/paragon';
|
||||
@@ -12,9 +12,8 @@ import { recordWeeklyGoalCelebration } from './utils';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
const WeeklyGoalCelebrationModal = ({
|
||||
courseId, daysPerWeek, isOpen, onClose, ...rest
|
||||
courseId, daysPerWeek, intl, isOpen, onClose, ...rest
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { org } = useModel('courseHomeMeta', courseId);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -78,8 +77,9 @@ const WeeklyGoalCelebrationModal = ({
|
||||
WeeklyGoalCelebrationModal.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
daysPerWeek: PropTypes.number.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default WeeklyGoalCelebrationModal;
|
||||
export default injectIntl(WeeklyGoalCelebrationModal);
|
||||
|
||||
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { Xpert } from '@edx/frontend-lib-learning-assistant';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { ALLOW_UPSELL_MODES, VERIFIED_MODES } from '@src/constants';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
@@ -79,4 +80,4 @@ Chat.defaultProps = {
|
||||
enrollmentMode: null,
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
export default injectIntl(Chat);
|
||||
|
||||
@@ -1,389 +1,403 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { Collapsible } from '@openedx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
FormattedMessage, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faCalculator, faQuestionCircle, faTimesCircle, faEquals,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import messages from './messages';
|
||||
|
||||
const Calculator = () => {
|
||||
const intl = useIntl();
|
||||
const [equation, setEquation] = useState('');
|
||||
const [result, setResult] = useState('');
|
||||
class Calculator extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
equation: '',
|
||||
result: '',
|
||||
};
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
async handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const urlEncoded = new URLSearchParams();
|
||||
urlEncoded.append('equation', equation);
|
||||
urlEncoded.append('equation', this.state.equation);
|
||||
|
||||
const response = await getAuthenticatedHttpClient().get(
|
||||
`${getConfig().LMS_BASE_URL}/calculate?${urlEncoded.toString()}`,
|
||||
);
|
||||
setResult(response.data.result);
|
||||
};
|
||||
this.setState(() => ({ result: response.data.result }));
|
||||
}
|
||||
|
||||
const changeEquation = (value) => {
|
||||
setEquation(value);
|
||||
};
|
||||
changeEquation(value) {
|
||||
this.setState(() => ({ equation: value }));
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible.Advanced className="calculator">
|
||||
<div className="text-right">
|
||||
<Collapsible.Trigger tag="a" className="trigger btn">
|
||||
<Collapsible.Visible whenOpen>
|
||||
<FontAwesomeIcon icon={faTimesCircle} aria-hidden="true" className="mr-2" />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<FontAwesomeIcon icon={faCalculator} aria-hidden="true" className="mr-2" />
|
||||
</Collapsible.Visible>
|
||||
{intl.formatMessage(messages['calculator.button.label'])}
|
||||
</Collapsible.Trigger>
|
||||
</div>
|
||||
<Collapsible.Body className="calculator-content pt-4">
|
||||
<form onSubmit={handleSubmit} className="container-xl form-inline flex-nowrap">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={intl.formatMessage(messages['calculator.input.field.label'])}
|
||||
aria-label={intl.formatMessage(messages['calculator.input.field.label'])}
|
||||
className="form-control w-100"
|
||||
onChange={(event) => changeEquation(event.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-primary mx-3"
|
||||
aria-label={intl.formatMessage(messages['calculator.submit.button.label'])}
|
||||
type="submit"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEquals} aria-hidden="true" />
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
tabIndex="-1"
|
||||
readOnly
|
||||
aria-live="polite"
|
||||
placeholder={intl.formatMessage(messages['calculator.result.field.placeholder'])}
|
||||
aria-label={intl.formatMessage(messages['calculator.result.field.label'])}
|
||||
className="form-control w-50"
|
||||
value={result}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<Collapsible.Advanced>
|
||||
<div className="container-xl">
|
||||
<Collapsible.Trigger className="btn btn-link btn-sm px-0 d-inline-flex align-items-center">
|
||||
<Collapsible.Visible whenOpen>
|
||||
<FontAwesomeIcon icon={faTimesCircle} aria-hidden="true" className="mr-2" />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} aria-hidden="true" className="mr-2" />
|
||||
</Collapsible.Visible>
|
||||
<FormattedMessage
|
||||
id="calculator.instructions.button.label"
|
||||
defaultMessage="Calculator Instructions"
|
||||
/>
|
||||
</Collapsible.Trigger>
|
||||
</div>
|
||||
<Collapsible.Body className="container-xl pt-3" style={{ maxHeight: '50vh', overflow: 'auto' }}>
|
||||
<FormattedMessage
|
||||
tagName="h6"
|
||||
id="calculator.instructions"
|
||||
defaultMessage="For detailed information, see the {expressions_link}."
|
||||
description="Text that precedes the link which redirects to help page calculator"
|
||||
values={{
|
||||
expressions_link: (
|
||||
<a href={getConfig().SUPPORT_URL_CALCULATOR_MATH}>
|
||||
<FormattedMessage
|
||||
id="calculator.instructions.support.title"
|
||||
defaultMessage="Help Center"
|
||||
description="Anchor text for link which redirects to help page calculator"
|
||||
/>
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
render() {
|
||||
return (
|
||||
<Collapsible.Advanced className="calculator">
|
||||
<div className="text-right">
|
||||
<Collapsible.Trigger tag="a" className="trigger btn">
|
||||
<Collapsible.Visible whenOpen>
|
||||
<FontAwesomeIcon icon={faTimesCircle} aria-hidden="true" className="mr-2" />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<FontAwesomeIcon icon={faCalculator} aria-hidden="true" className="mr-2" />
|
||||
</Collapsible.Visible>
|
||||
{this.props.intl.formatMessage(messages['calculator.button.label'])}
|
||||
</Collapsible.Trigger>
|
||||
</div>
|
||||
<Collapsible.Body className="calculator-content pt-4">
|
||||
<form onSubmit={this.handleSubmit} className="container-xl form-inline flex-nowrap">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={this.props.intl.formatMessage(messages['calculator.input.field.label'])}
|
||||
aria-label={this.props.intl.formatMessage(messages['calculator.input.field.label'])}
|
||||
className="form-control w-100"
|
||||
onChange={(event) => this.changeEquation(event.target.value)}
|
||||
/>
|
||||
<p>
|
||||
<strong>
|
||||
<button
|
||||
className="btn btn-primary mx-3"
|
||||
aria-label={this.props.intl.formatMessage(messages['calculator.submit.button.label'])}
|
||||
type="submit"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEquals} aria-hidden="true" />
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
tabIndex="-1"
|
||||
readOnly
|
||||
aria-live="polite"
|
||||
placeholder={this.props.intl.formatMessage(messages['calculator.result.field.placeholder'])}
|
||||
aria-label={this.props.intl.formatMessage(messages['calculator.result.field.label'])}
|
||||
className="form-control w-50"
|
||||
value={this.state.result}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<Collapsible.Advanced>
|
||||
<div className="container-xl">
|
||||
<Collapsible.Trigger className="btn btn-link btn-sm px-0 d-inline-flex align-items-center">
|
||||
<Collapsible.Visible whenOpen>
|
||||
<FontAwesomeIcon icon={faTimesCircle} aria-hidden="true" className="mr-2" />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} aria-hidden="true" className="mr-2" />
|
||||
</Collapsible.Visible>
|
||||
<FormattedMessage
|
||||
id="calculator.instructions.useful.tips"
|
||||
defaultMessage="Useful tips:"
|
||||
description="Headline for the (list of tips) about using the calculator"
|
||||
id="calculator.instructions.button.label"
|
||||
defaultMessage="Calculator Instructions"
|
||||
/>
|
||||
</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li className="hint-item" id="hint-paren">
|
||||
<FormattedMessage
|
||||
id="calculator.hint1"
|
||||
defaultMessage="Use parentheses () to make expressions clear. You can use parentheses inside other parentheses."
|
||||
description="The text indicate that the calculator supports parentheses"
|
||||
/>
|
||||
</li>
|
||||
<li className="hint-item" id="hint-spaces">
|
||||
<FormattedMessage
|
||||
id="calculator.hint2"
|
||||
defaultMessage="Do not use spaces in expressions."
|
||||
description="It indicate that using a space might cause un expected behavior"
|
||||
/>
|
||||
</li>
|
||||
<li className="hint-item" id="hint-howto-constants">
|
||||
<FormattedMessage
|
||||
id="calculator.hint3"
|
||||
defaultMessage="For constants, indicate multiplication explicitly (example: 5*c)."
|
||||
description="It indicate the style of math notation"
|
||||
/>
|
||||
</li>
|
||||
<li className="hint-item" id="hint-howto-maffixes">
|
||||
<FormattedMessage
|
||||
id="calculator.hint4"
|
||||
defaultMessage="For affixes, type the number and affix without a space (example: 5c)."
|
||||
/>
|
||||
</li>
|
||||
<li className="hint-item" id="hint-howto-functions">
|
||||
<FormattedMessage
|
||||
id="calculator.hint5"
|
||||
defaultMessage="For functions, type the name of the function, then the expression in parentheses."
|
||||
description="It indicate how to use a math function, e.g. exp(4)."
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<table className="table small">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.heading"
|
||||
defaultMessage="To Use"
|
||||
description="Column header which indicate calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
<th scope="col">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.type.heading"
|
||||
defaultMessage="Type"
|
||||
description="Column header which indicate the supported type(s) of a the calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
<th scope="col">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.examples.heading"
|
||||
defaultMessage="Examples"
|
||||
description="Column header which list examples of calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.numbers"
|
||||
defaultMessage="Numbers"
|
||||
description="A calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
<td>
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.numbers.type1"
|
||||
defaultMessage="Integers"
|
||||
description="Type of numbers that is supported the calculator"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.numbers.type2"
|
||||
defaultMessage="Fractions"
|
||||
description="Type of numbers that is supported by the calculator"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.numbers.type3"
|
||||
defaultMessage="Decimals"
|
||||
description="Type of numbers that is supported by the calculator"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>2520</li>
|
||||
<li>2/3</li>
|
||||
<li>3.14, .98</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.operators"
|
||||
defaultMessage="Operators"
|
||||
description="A calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>
|
||||
{' + - * / '}
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.operators.type1"
|
||||
defaultMessage="(add, subtract, multiply, divide)"
|
||||
description="Type of opprators that are supported by the calculator"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
{'^ '}
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.operators.type2"
|
||||
defaultMessage="(raise to a power)"
|
||||
description="It indicate that symbol (^) is being used to raise power, e.g. 2^2 = 4"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
{'|| '}
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.operators.type3"
|
||||
defaultMessage="(parallel resistors)"
|
||||
description="It indicate that the sympol (||) is being used to calculate (parallel resistor), it is a concept in electrical/electronic engineering"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>x+(2*y)/x-1</li>
|
||||
<li>x^(n+1)</li>
|
||||
<li>v_IN+v_OUT</li>
|
||||
<li>1||2</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.constants"
|
||||
defaultMessage="Constants"
|
||||
description="It indicate that the calculator support constants, e.g. the speed of light"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">e, pi</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>20*e</li>
|
||||
<li>418*pi</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.affixes"
|
||||
defaultMessage="Affixes"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.affixes.type"
|
||||
defaultMessage="Percent sign (%)"
|
||||
/>
|
||||
</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>20%</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.basic.functions"
|
||||
defaultMessage="Basic functions"
|
||||
description="It indicate that calculator supports mathematical function"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">abs, exp, fact, factorial, ln, log2, log10, sqrt</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>abs(x+y)</li>
|
||||
<li>sqrt(x^2-y)</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.trig.functions"
|
||||
defaultMessage="Trigonometric functions"
|
||||
description="Type of mathematical function that is supported by the calculator"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>sin, cos, tan, sec, csc, cot</li>
|
||||
<li>arcsin, sinh, arcsinh</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>sin(4x+y)</li>
|
||||
<li>arccsch(4x+y)</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation"
|
||||
defaultMessage="Scientific notation"
|
||||
description="It indicate that calculator supports scientific notation"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation.type1"
|
||||
defaultMessage="{exponentSyntax} and the exponent"
|
||||
description="Type of scientific notation that is supported by the calculator"
|
||||
values={{
|
||||
exponentSyntax: '10^',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td dir="auto">10^-9</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation.type2"
|
||||
defaultMessage="{notationSyntax} notation"
|
||||
description="It indicate that calculator supports (e) to be used in notation"
|
||||
values={{
|
||||
notationSyntax: 'e',
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation.type3"
|
||||
defaultMessage="{notationSyntax} and the exponent"
|
||||
description="An example for using (e) in notation"
|
||||
values={{
|
||||
notationSyntax: '1e',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td dir="auto">1e-9</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
</Collapsible.Trigger>
|
||||
</div>
|
||||
<Collapsible.Body className="container-xl pt-3" style={{ maxHeight: '50vh', overflow: 'auto' }}>
|
||||
<FormattedMessage
|
||||
tagName="h6"
|
||||
id="calculator.instructions"
|
||||
defaultMessage="For detailed information, see the {expressions_link}."
|
||||
description="Text that precedes the link which redirects to help page calculator"
|
||||
values={{
|
||||
expressions_link: (
|
||||
<a href={getConfig().SUPPORT_URL_CALCULATOR_MATH}>
|
||||
<FormattedMessage
|
||||
id="calculator.instructions.support.title"
|
||||
defaultMessage="Help Center"
|
||||
description="Anchor text for link which redirects to help page calculator"
|
||||
/>
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<p>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="calculator.instructions.useful.tips"
|
||||
defaultMessage="Useful tips:"
|
||||
description="Headline for the (list of tips) about using the calculator"
|
||||
/>
|
||||
</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li className="hint-item" id="hint-paren">
|
||||
<FormattedMessage
|
||||
id="calculator.hint1"
|
||||
defaultMessage="Use parentheses () to make expressions clear. You can use parentheses inside other parentheses."
|
||||
description="The text indicate that the calculator supports parentheses"
|
||||
/>
|
||||
</li>
|
||||
<li className="hint-item" id="hint-spaces">
|
||||
<FormattedMessage
|
||||
id="calculator.hint2"
|
||||
defaultMessage="Do not use spaces in expressions."
|
||||
description="It indicate that using a space might cause un expected behavior"
|
||||
/>
|
||||
</li>
|
||||
<li className="hint-item" id="hint-howto-constants">
|
||||
<FormattedMessage
|
||||
id="calculator.hint3"
|
||||
defaultMessage="For constants, indicate multiplication explicitly (example: 5*c)."
|
||||
description="It indicate the style of math notation"
|
||||
/>
|
||||
</li>
|
||||
<li className="hint-item" id="hint-howto-maffixes">
|
||||
<FormattedMessage
|
||||
id="calculator.hint4"
|
||||
defaultMessage="For affixes, type the number and affix without a space (example: 5c)."
|
||||
/>
|
||||
</li>
|
||||
<li className="hint-item" id="hint-howto-functions">
|
||||
<FormattedMessage
|
||||
id="calculator.hint5"
|
||||
defaultMessage="For functions, type the name of the function, then the expression in parentheses."
|
||||
description="It indicate how to use a math function, e.g. exp(4)."
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<table className="table small">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.heading"
|
||||
defaultMessage="To Use"
|
||||
description="Column header which indicate calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
<th scope="col">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.type.heading"
|
||||
defaultMessage="Type"
|
||||
description="Column header which indicate the supported type(s) of a the calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
<th scope="col">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.examples.heading"
|
||||
defaultMessage="Examples"
|
||||
description="Column header which list examples of calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.numbers"
|
||||
defaultMessage="Numbers"
|
||||
description="A calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
<td>
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.numbers.type1"
|
||||
defaultMessage="Integers"
|
||||
description="Type of numbers that is supported the calculator"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.numbers.type2"
|
||||
defaultMessage="Fractions"
|
||||
description="Type of numbers that is supported by the calculator"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.numbers.type3"
|
||||
defaultMessage="Decimals"
|
||||
description="Type of numbers that is supported by the calculator"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>2520</li>
|
||||
<li>2/3</li>
|
||||
<li>3.14, .98</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.operators"
|
||||
defaultMessage="Operators"
|
||||
description="A calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>
|
||||
{' + - * / '}
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.operators.type1"
|
||||
defaultMessage="(add, subtract, multiply, divide)"
|
||||
description="Type of opprators that are supported by the calculator"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
{'^ '}
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.operators.type2"
|
||||
defaultMessage="(raise to a power)"
|
||||
description="It indicate that symbol (^) is being used to raise power, e.g. 2^2 = 4"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
{'|| '}
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.operators.type3"
|
||||
defaultMessage="(parallel resistors)"
|
||||
description="It indicate that the sympol (||) is being used to calculate (parallel resistor), it is a concept in electrical/electronic engineering"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>x+(2*y)/x-1</li>
|
||||
<li>x^(n+1)</li>
|
||||
<li>v_IN+v_OUT</li>
|
||||
<li>1||2</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.constants"
|
||||
defaultMessage="Constants"
|
||||
description="It indicate that the calculator support constants, e.g. the speed of light"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">e, pi</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>20*e</li>
|
||||
<li>418*pi</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.affixes"
|
||||
defaultMessage="Affixes"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.affixes.type"
|
||||
defaultMessage="Percent sign (%)"
|
||||
/>
|
||||
</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>20%</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.basic.functions"
|
||||
defaultMessage="Basic functions"
|
||||
description="It indicate that calculator supports mathematical function"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">abs, exp, fact, factorial, ln, log2, log10, sqrt</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>abs(x+y)</li>
|
||||
<li>sqrt(x^2-y)</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.trig.functions"
|
||||
defaultMessage="Trigonometric functions"
|
||||
description="Type of mathematical function that is supported by the calculator"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>sin, cos, tan, sec, csc, cot</li>
|
||||
<li>arcsin, sinh, arcsinh</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>sin(4x+y)</li>
|
||||
<li>arccsch(4x+y)</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td dir="auto" />
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation"
|
||||
defaultMessage="Scientific notation"
|
||||
description="It indicate that calculator supports scientific notation"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation.type1"
|
||||
defaultMessage="{exponentSyntax} and the exponent"
|
||||
description="Type of scientific notation that is supported by the calculator"
|
||||
values={{
|
||||
exponentSyntax: '10^',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td dir="auto">10^-9</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation.type2"
|
||||
defaultMessage="{notationSyntax} notation"
|
||||
description="It indicate that calculator supports (e) to be used in notation"
|
||||
values={{
|
||||
notationSyntax: 'e',
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation.type3"
|
||||
defaultMessage="{notationSyntax} and the exponent"
|
||||
description="An example for using (e) in notation"
|
||||
values={{
|
||||
notationSyntax: '1e',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td dir="auto">1e-9</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Calculator.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default Calculator;
|
||||
export default injectIntl(Calculator);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import messages from './messages';
|
||||
@@ -13,41 +15,47 @@ function toggleNotes() {
|
||||
iframe.contentWindow.postMessage('tools.toggleNotes', getConfig().LMS_BASE_URL);
|
||||
}
|
||||
|
||||
const NotesVisibility = ({ course }) => {
|
||||
const intl = useIntl();
|
||||
const [visible, setVisible] = useState(course.notes.visible);
|
||||
const visibilityUrl = `${getConfig().LMS_BASE_URL}/courses/${course.id}/edxnotes/visibility/`;
|
||||
class NotesVisibility extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
visible: props.course.notes.visible,
|
||||
};
|
||||
this.visibilityUrl = `${getConfig().LMS_BASE_URL}/courses/${props.course.id}/edxnotes/visibility/`;
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
const data = { visibility: !visible };
|
||||
handleClick = () => {
|
||||
const data = { visibility: !this.state.visible };
|
||||
getAuthenticatedHttpClient().put(
|
||||
visibilityUrl,
|
||||
this.visibilityUrl,
|
||||
data,
|
||||
).then(() => {
|
||||
setVisible(!visible);
|
||||
this.setState((state) => ({ visible: !state.visible }));
|
||||
toggleNotes();
|
||||
});
|
||||
};
|
||||
|
||||
const message = visible ? 'notes.button.hide' : 'notes.button.show';
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`trigger btn ${visible ? 'text-secondary' : 'text-success'} mx-2 `}
|
||||
role="switch"
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleClick}
|
||||
tabIndex="-1"
|
||||
aria-checked={visible ? 'true' : 'false'}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencilAlt} aria-hidden="true" className="mr-2" />
|
||||
{intl.formatMessage(messages[message])}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
render() {
|
||||
const message = this.state.visible ? 'notes.button.hide' : 'notes.button.show';
|
||||
return (
|
||||
<button
|
||||
className={`trigger btn ${this.state.visible ? 'text-secondary' : 'text-success'} mx-2 `}
|
||||
role="switch"
|
||||
type="button"
|
||||
onClick={this.handleClick}
|
||||
onKeyDown={this.handleClick}
|
||||
tabIndex="-1"
|
||||
aria-checked={this.state.visible ? 'true' : 'false'}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencilAlt} aria-hidden="true" className="mr-2" />
|
||||
{this.props.intl.formatMessage(messages[message])}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NotesVisibility.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
course: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
notes: PropTypes.shape({
|
||||
@@ -56,4 +64,4 @@ NotesVisibility.propTypes = {
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default NotesVisibility;
|
||||
export default injectIntl(NotesVisibility);
|
||||
|
||||
@@ -4,7 +4,9 @@ import { useSelector } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
FormattedMessage, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
@@ -14,8 +16,7 @@ import { useModel } from '../../../generic/model-store';
|
||||
import messages from './messages';
|
||||
import { logClick } from './utils';
|
||||
|
||||
const CatalogSuggestion = ({ variant }) => {
|
||||
const intl = useIntl();
|
||||
const CatalogSuggestion = ({ intl, variant }) => {
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
const { org } = useModel('courseHomeMeta', courseId);
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
@@ -47,7 +48,8 @@ const CatalogSuggestion = ({ variant }) => {
|
||||
};
|
||||
|
||||
CatalogSuggestion.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
variant: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CatalogSuggestion;
|
||||
export default injectIntl(CatalogSuggestion);
|
||||
|
||||
@@ -2,7 +2,9 @@ import React, { useEffect } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faLinkedinIn } from '@fortawesome/free-brands-svg-icons';
|
||||
|
||||
import { FormattedDate, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
FormattedDate, FormattedMessage, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
@@ -18,8 +20,8 @@ import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
import CelebrationMobile from './assets/celebration_456x328.gif';
|
||||
import CelebrationDesktop from './assets/celebration_750x540.gif';
|
||||
import certificate from '../../../generic/assets/openedx_certificate.png';
|
||||
import certificateLocked from '../../../generic/assets/openedx_locked_certificate.png';
|
||||
import certificate from '../../../generic/assets/edX_certificate.png';
|
||||
import certificateLocked from '../../../generic/assets/edX_locked_certificate.png';
|
||||
import { FormattedPricing } from '../../../generic/upgrade-button';
|
||||
import messages from './messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
@@ -34,8 +36,7 @@ import CourseRecommendationsSlot from '../../../plugin-slots/CourseRecommendatio
|
||||
|
||||
const LINKEDIN_BLUE = '#2867B2';
|
||||
|
||||
const CourseCelebration = () => {
|
||||
const intl = useIntl();
|
||||
const CourseCelebration = ({ intl }) => {
|
||||
const wideScreen = useWindowSize().width >= breakpoints.medium.minWidth;
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
const dispatch = useDispatch();
|
||||
@@ -363,4 +364,8 @@ const CourseCelebration = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseCelebration;
|
||||
CourseCelebration.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseCelebration);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@openedx/paragon';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
@@ -15,8 +15,7 @@ import { unsubscribeFromGoalReminders } from './data/thunks';
|
||||
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
const CourseExit = () => {
|
||||
const intl = useIntl();
|
||||
const CourseExit = ({ intl }) => {
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
const {
|
||||
certificateData,
|
||||
@@ -77,4 +76,8 @@ const CourseExit = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseExit;
|
||||
CourseExit.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseExit);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Alert, Button } from '@openedx/paragon';
|
||||
@@ -14,8 +14,7 @@ import DashboardFootnote from './DashboardFootnote';
|
||||
import messages from './messages';
|
||||
import { logClick, logVisit } from './utils';
|
||||
|
||||
const CourseInProgress = () => {
|
||||
const intl = useIntl();
|
||||
const CourseInProgress = ({ intl }) => {
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
const {
|
||||
org,
|
||||
@@ -61,4 +60,8 @@ const CourseInProgress = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseInProgress;
|
||||
CourseInProgress.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseInProgress);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Alert, Button } from '@openedx/paragon';
|
||||
@@ -14,8 +14,7 @@ import DashboardFootnote from './DashboardFootnote';
|
||||
import messages from './messages';
|
||||
import { logClick, logVisit } from './utils';
|
||||
|
||||
const CourseNonPassing = () => {
|
||||
const intl = useIntl();
|
||||
const CourseNonPassing = ({ intl }) => {
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
const {
|
||||
org,
|
||||
@@ -61,4 +60,8 @@ const CourseNonPassing = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseNonPassing;
|
||||
CourseNonPassing.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseNonPassing);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import {
|
||||
FormattedMessage, useIntl, defineMessages,
|
||||
FormattedMessage, injectIntl, intlShape, defineMessages,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import {
|
||||
@@ -64,8 +64,8 @@ const CourseCard = ({
|
||||
marketingUrl,
|
||||
onClick,
|
||||
},
|
||||
intl,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const formatList = (items, style) => (
|
||||
items.join(intl.formatMessage(
|
||||
messages.listJoin,
|
||||
@@ -127,12 +127,12 @@ CourseCard.propTypes = {
|
||||
})),
|
||||
onClick: PropTypes.func,
|
||||
}).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
const IntlCard = CourseCard;
|
||||
const IntlCard = injectIntl(CourseCard);
|
||||
|
||||
const CourseRecommendations = ({ variant }) => {
|
||||
const intl = useIntl();
|
||||
const CourseRecommendations = ({ intl, variant }) => {
|
||||
const { courseId, recommendationsStatus } = useSelector(state => ({ ...state.recommendations, ...state.courseware }));
|
||||
const { recommendations } = useModel('coursewareMeta', courseId);
|
||||
const { org, number } = useModel('courseHomeMeta', courseId);
|
||||
@@ -205,7 +205,8 @@ const CourseRecommendations = ({ variant }) => {
|
||||
};
|
||||
|
||||
CourseRecommendations.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
variant: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CourseRecommendations;
|
||||
export default injectIntl(CourseRecommendations);
|
||||
|
||||
@@ -3,7 +3,9 @@ import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
FormattedMessage, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
@@ -14,8 +16,7 @@ import Footnote from './Footnote';
|
||||
import messages from './messages';
|
||||
import { logClick } from './utils';
|
||||
|
||||
const DashboardFootnote = ({ variant }) => {
|
||||
const intl = useIntl();
|
||||
const DashboardFootnote = ({ intl, variant }) => {
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
const { org } = useModel('courseHomeMeta', courseId);
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
@@ -47,7 +48,8 @@ const DashboardFootnote = ({ variant }) => {
|
||||
};
|
||||
|
||||
DashboardFootnote.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
variant: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default DashboardFootnote;
|
||||
export default injectIntl(DashboardFootnote);
|
||||
|
||||
@@ -2,9 +2,9 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Button, Hyperlink } from '@openedx/paragon';
|
||||
import certImage from '../../../generic/assets/openedx_certificate.png';
|
||||
import certImage from '../../../generic/assets/edX_certificate.png';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
@@ -20,12 +20,12 @@ import messages from './messages';
|
||||
const programTypes = ['microbachelors', 'micromasters', 'professional-certificate', 'xseries'];
|
||||
|
||||
const ProgramCompletion = ({
|
||||
intl,
|
||||
progress,
|
||||
title,
|
||||
type,
|
||||
url,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
if (!programTypes.includes(type) || progress.notStarted !== 0 || progress.inProgress !== 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -98,6 +98,7 @@ const ProgramCompletion = ({
|
||||
};
|
||||
|
||||
ProgramCompletion.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
progress: PropTypes.shape({
|
||||
completed: PropTypes.number.isRequired,
|
||||
inProgress: PropTypes.number.isRequired,
|
||||
@@ -108,4 +109,4 @@ ProgramCompletion.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ProgramCompletion;
|
||||
export default injectIntl(ProgramCompletion);
|
||||
|
||||
@@ -3,7 +3,9 @@ import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { FormattedDate, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
FormattedDate, FormattedMessage, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
@@ -12,8 +14,7 @@ import { logClick } from './utils';
|
||||
import messages from './messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
const UpgradeFootnote = ({ deadline, href }) => {
|
||||
const intl = useIntl();
|
||||
const UpgradeFootnote = ({ deadline, href, intl }) => {
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
const { org } = useModel('courseHomeMeta', courseId);
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
@@ -59,6 +60,7 @@ const UpgradeFootnote = ({ deadline, href }) => {
|
||||
UpgradeFootnote.propTypes = {
|
||||
deadline: PropTypes.instanceOf(Date).isRequired,
|
||||
href: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default UpgradeFootnote;
|
||||
export default injectIntl(UpgradeFootnote);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCopyright } from '@fortawesome/free-regular-svg-icons';
|
||||
@@ -105,8 +105,8 @@ function parseLicense(license) {
|
||||
|
||||
const CourseLicense = ({
|
||||
license,
|
||||
intl,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const renderAllRightsReservedLicense = () => (
|
||||
<div className="text-gray-500">
|
||||
<FontAwesomeIcon aria-hidden="true" className="mr-1" icon={faCopyright} />
|
||||
@@ -155,10 +155,11 @@ const CourseLicense = ({
|
||||
|
||||
CourseLicense.propTypes = {
|
||||
license: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
CourseLicense.defaultProps = {
|
||||
license: 'all-rights-reserved',
|
||||
};
|
||||
|
||||
export default CourseLicense;
|
||||
export default injectIntl(CourseLicense);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { breakpoints } from '@openedx/paragon';
|
||||
|
||||
import {
|
||||
initializeMockApp, render, screen, act, fireEvent, waitFor,
|
||||
initializeMockApp, render, screen, within, act, fireEvent, waitFor,
|
||||
} from '../../../../../../setupTest';
|
||||
import initializeStore from '../../../../../../store';
|
||||
import { appendBrowserTimezoneToUrl, executeThunk } from '../../../../../../utils';
|
||||
@@ -90,7 +90,28 @@ describe('NotificationsWidget', () => {
|
||||
<NotificationsWidget />
|
||||
</SidebarContext.Provider>,
|
||||
);
|
||||
expect(screen.getByTestId('org.openedx.frontend.learning.notification_widget.v1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('notification_widget_slot')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders upgrade card', async () => {
|
||||
const contextData: Partial<SidebarContextData> = {
|
||||
currentSidebar: ID,
|
||||
courseId,
|
||||
hideNotificationbar: false,
|
||||
isNotificationbarAvailable: true,
|
||||
};
|
||||
await fetchAndRender(
|
||||
<SidebarContext.Provider value={contextData as SidebarContextData}>
|
||||
<NotificationsWidget />
|
||||
</SidebarContext.Provider>,
|
||||
);
|
||||
|
||||
// The Upgrade Notification should be inside the PluginSlot.
|
||||
const UpgradeNotification = document.querySelector('.upgrade-notification');
|
||||
expect(UpgradeNotification).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
|
||||
expect(screen.queryByText('You have no new notifications at this time.')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders no notifications bar if no verified mode', async () => {
|
||||
@@ -109,6 +130,44 @@ describe('NotificationsWidget', () => {
|
||||
expect(screen.queryByText('Notifications')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
description: 'close the notification widget.',
|
||||
enabledInContext: true,
|
||||
testId: 'notification-widget',
|
||||
},
|
||||
{
|
||||
description: 'close the sidebar when the notification widget is closed, and the discussion widget is unavailable.',
|
||||
enabledInContext: false,
|
||||
testId: 'sidebar-DISCUSSIONS_NOTIFICATIONS',
|
||||
},
|
||||
])('successfully %s', async ({ enabledInContext, testId }) => {
|
||||
const userVerifiedMode = Factory.build('verifiedMode');
|
||||
|
||||
await setupDiscussionSidebar({
|
||||
verifiedMode: userVerifiedMode,
|
||||
enabledInContext,
|
||||
isNewDiscussionSidebarViewEnabled: true,
|
||||
});
|
||||
|
||||
const sidebarButton = screen.getByRole('button', { name: /Show sidebar tray/i });
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(sidebarButton);
|
||||
});
|
||||
|
||||
const notificationWidget = await waitFor(() => screen.getByTestId('notification-widget'));
|
||||
const closeNotificationButton = within(notificationWidget).getByRole('button', { name: /Close/i });
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(closeNotificationButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId(testId)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('marks notification as seen 3 seconds later', async () => {
|
||||
const onNotificationSeen = jest.fn();
|
||||
const contextData: Partial<SidebarContextData> = {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useContext, useEffect, useMemo } from 'react';
|
||||
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import { useModel } from '../../../../../../generic/model-store';
|
||||
import UpgradeNotification from '../../../../../../generic/upgrade-notification/UpgradeNotification';
|
||||
import { WIDGETS } from '../../../../../../constants';
|
||||
import SidebarContext from '../../../SidebarContext';
|
||||
import { NotificationWidgetSlot } from '../../../../../../plugin-slots/NotificationWidgetSlot';
|
||||
|
||||
const NotificationsWidget = () => {
|
||||
const {
|
||||
@@ -20,11 +21,17 @@ const NotificationsWidget = () => {
|
||||
const course = useModel('coursewareMeta', courseId);
|
||||
|
||||
const {
|
||||
accessExpiration,
|
||||
contentTypeGatingEnabled,
|
||||
end,
|
||||
enrollmentEnd,
|
||||
enrollmentMode,
|
||||
enrollmentStart,
|
||||
marketingUrl,
|
||||
offer,
|
||||
start,
|
||||
timeOffsetMillis,
|
||||
userTimezone,
|
||||
verificationStatus,
|
||||
} = course;
|
||||
|
||||
@@ -67,12 +74,32 @@ const NotificationsWidget = () => {
|
||||
|
||||
return (
|
||||
<div className="border border-light-400 rounded-sm" data-testid="notification-widget">
|
||||
<NotificationWidgetSlot
|
||||
courseId={courseId}
|
||||
notificationCurrentState={upgradeNotificationCurrentState}
|
||||
setNotificationCurrentState={setUpgradeNotificationCurrentState}
|
||||
toggleSidebar={onToggleSidebar}
|
||||
/>
|
||||
<PluginSlot
|
||||
id="notification_widget_slot"
|
||||
pluginProps={{
|
||||
courseId,
|
||||
model: 'coursewareMeta',
|
||||
notificationCurrentState: upgradeNotificationCurrentState,
|
||||
setNotificationCurrentState: setUpgradeNotificationCurrentState,
|
||||
toggleSidebar: onToggleSidebar,
|
||||
}}
|
||||
>
|
||||
<UpgradeNotification
|
||||
offer={offer}
|
||||
verifiedMode={verifiedMode}
|
||||
accessExpiration={accessExpiration}
|
||||
contentTypeGatingEnabled={contentTypeGatingEnabled}
|
||||
marketingUrl={marketingUrl}
|
||||
upsellPageName="in_course"
|
||||
userTimezone={userTimezone}
|
||||
shouldDisplayBorder={false}
|
||||
timeOffsetMillis={timeOffsetMillis}
|
||||
courseId={courseId}
|
||||
org={org}
|
||||
setupgradeNotificationCurrentState={setUpgradeNotificationCurrentState}
|
||||
toggleSidebar={onToggleSidebar}
|
||||
/>
|
||||
</PluginSlot>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -154,7 +154,7 @@ describe('Sequence', () => {
|
||||
testStore.dispatch(fetchSequenceFailure({ sequenceId: mockData.sequenceId }));
|
||||
render(<Sequence {...mockData} />, { store: testStore, wrapWithRouter: true });
|
||||
|
||||
await screen.findByText('There was an error loading this course.');
|
||||
expect(screen.getByText('There was an error loading this course.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles loading unit', async () => {
|
||||
|
||||
@@ -4,8 +4,9 @@ import React from 'react';
|
||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
||||
import { StrictDict } from '@edx/react-unit-test-utils';
|
||||
import { ModalDialog, Modal } from '@openedx/paragon';
|
||||
import { ContentIFrameLoaderSlot } from '../../../../plugin-slots/ContentIFrameLoaderSlot';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
import PageLoading from '@src/generic/PageLoading';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
/**
|
||||
@@ -83,7 +84,17 @@ const ContentIFrame = ({
|
||||
return (
|
||||
<>
|
||||
{(shouldShowContent && !hasLoaded) && (
|
||||
showError ? <ErrorPage /> : <ContentIFrameLoaderSlot courseId={courseId} loadingMessage={loadingMessage} />
|
||||
showError ? <ErrorPage /> : (
|
||||
<PluginSlot
|
||||
id="content_iframe_loader_slot"
|
||||
pluginProps={{
|
||||
defaultLoaderComponent: <PageLoading srMessage={loadingMessage} />,
|
||||
courseId,
|
||||
}}
|
||||
>
|
||||
<PageLoading srMessage={loadingMessage} />
|
||||
</PluginSlot>
|
||||
)
|
||||
)}
|
||||
{shouldShowContent && (
|
||||
<div className="unit-iframe-wrapper">
|
||||
|
||||
@@ -6,7 +6,6 @@ import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import PageLoading from '@src/generic/PageLoading';
|
||||
|
||||
import { ContentIFrameLoaderSlot } from '@src/plugin-slots/ContentIFrameLoaderSlot';
|
||||
import * as hooks from './hooks';
|
||||
import ContentIFrame, { IFRAME_FEATURE_POLICY, testIDs } from './ContentIFrame';
|
||||
|
||||
@@ -100,8 +99,8 @@ describe('ContentIFrame Component', () => {
|
||||
});
|
||||
it('displays PageLoading component if not showError', () => {
|
||||
el = shallow(<ContentIFrame {...props} />);
|
||||
[component] = el.instance.findByType(ContentIFrameLoaderSlot);
|
||||
expect(component.props.loadingMessage).toEqual(props.loadingMessage);
|
||||
[component] = el.instance.findByType(PageLoading);
|
||||
expect(component.props.srMessage).toEqual(props.loadingMessage);
|
||||
});
|
||||
});
|
||||
describe('hasLoaded', () => {
|
||||
|
||||
@@ -2,13 +2,14 @@ import React, { Suspense } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
import { useModel } from '@src/generic/model-store';
|
||||
import PageLoading from '@src/generic/PageLoading';
|
||||
import { GatedUnitContentMessageSlot } from '../../../../plugin-slots/GatedUnitContentMessageSlot';
|
||||
|
||||
import messages from '../messages';
|
||||
import HonorCode from '../honor-code';
|
||||
import LockPaywall from '../lock-paywall';
|
||||
import * as hooks from './hooks';
|
||||
import { modelKeys } from './constants';
|
||||
|
||||
@@ -28,7 +29,14 @@ const UnitSuspense = ({
|
||||
<>
|
||||
{shouldDisplayContentGating && (
|
||||
<Suspense fallback={<PageLoading srMessage={formatMessage(messages.loadingLockedContent)} />}>
|
||||
<GatedUnitContentMessageSlot courseId={courseId} />
|
||||
<PluginSlot
|
||||
id="gated_unit_content_message_slot"
|
||||
pluginProps={{
|
||||
courseId,
|
||||
}}
|
||||
>
|
||||
<LockPaywall courseId={courseId} />
|
||||
</PluginSlot>
|
||||
</Suspense>
|
||||
)}
|
||||
{shouldDisplayHonorCode && (
|
||||
|
||||
@@ -5,7 +5,6 @@ import { formatMessage, shallow } from '@edx/react-unit-test-utils';
|
||||
import { useModel } from '@src/generic/model-store';
|
||||
import PageLoading from '@src/generic/PageLoading';
|
||||
|
||||
import { GatedUnitContentMessageSlot } from '@src/plugin-slots/GatedUnitContentMessageSlot';
|
||||
import messages from '../messages';
|
||||
import HonorCode from '../honor-code';
|
||||
import LockPaywall from '../lock-paywall';
|
||||
@@ -79,9 +78,10 @@ describe('UnitSuspense component', () => {
|
||||
beforeEach(() => { mockModels(true, true); });
|
||||
it('displays LockPaywall in Suspense wrapper with PageLoading fallback', () => {
|
||||
el = shallow(<UnitSuspense {...props} />);
|
||||
const [component] = el.instance.findByType(GatedUnitContentMessageSlot);
|
||||
expect(component.parent.type).toEqual('Suspense');
|
||||
expect(component.parent.props.fallback)
|
||||
const [component] = el.instance.findByType(LockPaywall);
|
||||
expect(component.parent.type).toEqual('PluginSlot');
|
||||
expect(component.parent.parent.type).toEqual('Suspense');
|
||||
expect(component.parent.parent.props.fallback)
|
||||
.toEqual(<PageLoading srMessage={formatMessage(messages.loadingLockedContent)} />);
|
||||
expect(component.props.courseId).toEqual(props.courseId);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { useExamAccessToken, useFetchExamAccessToken, useIsExam } from '@edx/frontend-lib-special-exams';
|
||||
|
||||
import { initializeMockApp } from '../../../../../setupTest';
|
||||
@@ -64,27 +64,24 @@ describe('useExamAccess hook', () => {
|
||||
it('returns true for blockAccess if an exam but accessToken not yet fetched', async () => {
|
||||
useIsExam.mockImplementation(() => mockUseIsExam(true));
|
||||
|
||||
const { result } = renderHook(() => useExamAccess({ id }));
|
||||
const { result, waitForNextUpdate } = renderHook(() => useExamAccess({ id }));
|
||||
const { accessToken, blockAccess } = result.current;
|
||||
|
||||
expect(accessToken).toEqual('');
|
||||
expect(blockAccess).toBe(true);
|
||||
expect(mockFetchExamAccessToken).toHaveBeenCalled();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBeTruthy();
|
||||
expect(result.current?.isFetching).toBeFalsy();
|
||||
// This is to get rid of the act(...) warning.
|
||||
await act(async () => {
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
});
|
||||
it('returns false for blockAccess if an exam and accessToken fetch succeeds', async () => {
|
||||
useIsExam.mockImplementation(() => mockUseIsExam(true));
|
||||
const { result } = renderHook(() => useExamAccess({ id }));
|
||||
const { result, waitForNextUpdate } = renderHook(() => useExamAccess({ id }));
|
||||
|
||||
// We wait for the promise to resolve and for updates to state to complete so that blockAccess is updated.
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBeTruthy();
|
||||
expect(result.current?.isFetching).toBeFalsy();
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
const { accessToken, blockAccess } = result.current;
|
||||
|
||||
@@ -93,7 +90,7 @@ describe('useExamAccess hook', () => {
|
||||
expect(mockFetchExamAccessToken).toHaveBeenCalled();
|
||||
});
|
||||
it('in progress', async () => {
|
||||
const { result } = renderHook(() => useExamAccess({ id }));
|
||||
const { result, waitForNextUpdate } = renderHook(() => useExamAccess({ id }));
|
||||
|
||||
let { accessToken, blockAccess } = result.current;
|
||||
|
||||
@@ -107,10 +104,7 @@ describe('useExamAccess hook', () => {
|
||||
// wait for call to setBlockAccess in the finally clause of useEffect hook.
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBeTruthy();
|
||||
expect(result.current?.isFetching).toBeFalsy();
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
||||
({ accessToken, blockAccess } = result.current);
|
||||
@@ -125,22 +119,17 @@ describe('useExamAccess hook', () => {
|
||||
const testError = 'test-error';
|
||||
mockFetchExamAccessToken.mockImplementationOnce(() => Promise.reject(testError));
|
||||
|
||||
const { result } = renderHook(() => useExamAccess({ id }));
|
||||
const { result, waitForNextUpdate } = renderHook(() => useExamAccess({ id }));
|
||||
|
||||
// We wait for the promise to resolve and for updates to state to complete so that blockAccess is updated.
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBeTruthy();
|
||||
expect(result.current?.isFetching).toBeFalsy();
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
const { accessToken, blockAccess } = result.current;
|
||||
|
||||
expect(accessToken).toEqual(testAccessToken.curr);
|
||||
expect(blockAccess).toBe(false);
|
||||
expect(mockFetchExamAccessToken).toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(logError).toHaveBeenCalledWith(testError);
|
||||
});
|
||||
expect(logError).toHaveBeenCalledWith(testError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('<Unit />', () => {
|
||||
describe('unit title', () => {
|
||||
it('has two children', () => {
|
||||
renderComponent(defaultProps);
|
||||
const unitTitleWrapper = screen.getByTestId('org.openedx.frontend.learning.unit_title.v1').children[0];
|
||||
const unitTitleWrapper = screen.getByTestId('unit_title_slot').children[0];
|
||||
|
||||
expect(unitTitleWrapper.children).toHaveLength(3);
|
||||
});
|
||||
|
||||
@@ -3,15 +3,14 @@ import PropTypes from 'prop-types';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faLock } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const ContentLock = ({
|
||||
courseId, prereqSectionName, prereqId, sequenceTitle,
|
||||
intl, courseId, prereqSectionName, prereqId, sequenceTitle,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const handleClick = useCallback(() => {
|
||||
navigate(`/course/${courseId}/${prereqId}`);
|
||||
@@ -37,9 +36,10 @@ const ContentLock = ({
|
||||
);
|
||||
};
|
||||
ContentLock.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
prereqSectionName: PropTypes.string.isRequired,
|
||||
prereqId: PropTypes.string.isRequired,
|
||||
sequenceTitle: PropTypes.string.isRequired,
|
||||
};
|
||||
export default ContentLock;
|
||||
export default injectIntl(ContentLock);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
||||
import { Info } from '@openedx/paragon/icons';
|
||||
|
||||
@@ -8,8 +8,7 @@ import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const HiddenAfterDue = ({ courseId }) => {
|
||||
const intl = useIntl();
|
||||
const HiddenAfterDue = ({ courseId, intl }) => {
|
||||
const { tabs } = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const progressTab = tabs.find(tab => tab.slug === 'progress');
|
||||
@@ -45,7 +44,8 @@ const HiddenAfterDue = ({ courseId }) => {
|
||||
};
|
||||
|
||||
HiddenAfterDue.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default HiddenAfterDue;
|
||||
export default injectIntl(HiddenAfterDue);
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { ActionRow, Alert, Button } from '@openedx/paragon';
|
||||
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -11,8 +11,7 @@ import { useModel } from '../../../../generic/model-store';
|
||||
import { saveIntegritySignature } from '../../../data';
|
||||
import messages from './messages';
|
||||
|
||||
const HonorCode = ({ courseId }) => {
|
||||
const intl = useIntl();
|
||||
const HonorCode = ({ intl, courseId }) => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
@@ -69,7 +68,8 @@ const HonorCode = ({ courseId }) => {
|
||||
};
|
||||
|
||||
HonorCode.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default HonorCode;
|
||||
export default injectIntl(HonorCode);
|
||||
|
||||
@@ -2,14 +2,14 @@ import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Alert, Hyperlink, breakpoints, useWindowSize,
|
||||
} from '@openedx/paragon';
|
||||
import { Locked } from '@openedx/paragon/icons';
|
||||
import SidebarContext from '../../sidebar/SidebarContext';
|
||||
import messages from './messages';
|
||||
import certificateLocked from '../../../../generic/assets/openedx_locked_certificate.png';
|
||||
import certificateLocked from '../../../../generic/assets/edX_locked_certificate.png';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
import { UpgradeButton } from '../../../../generic/upgrade-button';
|
||||
import {
|
||||
@@ -20,9 +20,9 @@ import {
|
||||
} from '../../../../generic/upsell-bullets/UpsellBullets';
|
||||
|
||||
const LockPaywall = ({
|
||||
intl,
|
||||
courseId,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { notificationTrayVisible } = useContext(SidebarContext);
|
||||
const course = useModel('coursewareMeta', courseId);
|
||||
const {
|
||||
@@ -143,6 +143,7 @@ const LockPaywall = ({
|
||||
);
|
||||
};
|
||||
LockPaywall.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
export default LockPaywall;
|
||||
export default injectIntl(LockPaywall);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Factory } from 'rosie';
|
||||
import { getAllByRole } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { act, fireEvent, getAllByRole } from '@testing-library/react';
|
||||
|
||||
import { initializeTestStore, render, screen } from '../../../../setupTest';
|
||||
import SequenceNavigationTabs from './SequenceNavigationTabs';
|
||||
@@ -49,18 +48,17 @@ describe('Sequence Navigation Tabs', () => {
|
||||
|
||||
it('renders unit buttons and dropdown button', async () => {
|
||||
let container = null;
|
||||
await act(async () => {
|
||||
useIndexOfLastVisibleChild.mockReturnValue([-1, null, null]);
|
||||
const booyah = render(<SequenceNavigationTabs {...mockData} />, { wrapWithRouter: true });
|
||||
container = booyah.container;
|
||||
|
||||
useIndexOfLastVisibleChild.mockReturnValue([-1, null, null]);
|
||||
const booyah = render(<SequenceNavigationTabs {...mockData} />, { wrapWithRouter: true });
|
||||
|
||||
// wait for links to appear so we aren't testing an empty div
|
||||
await screen.findAllByRole('link');
|
||||
|
||||
container = booyah.container;
|
||||
|
||||
const dropdownToggle = container.querySelector('.dropdown-toggle');
|
||||
await userEvent.click(dropdownToggle);
|
||||
|
||||
const dropdownToggle = container.querySelector('.dropdown-toggle');
|
||||
// We need to await this click here, which requires us to await the `act` as well above.
|
||||
// https://github.com/testing-library/react-testing-library/issues/535
|
||||
// Without doing this, we get a warning about using `act` even though we are.
|
||||
await fireEvent.click(dropdownToggle);
|
||||
});
|
||||
const dropdownMenu = container.querySelector('.dropdown');
|
||||
const dropdownButtons = getAllByRole(dropdownMenu, 'link');
|
||||
expect(dropdownButtons).toHaveLength(unitBlocks.length);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import EffortEstimate from '../../../../shared/effort-estimate';
|
||||
import { sequenceIdsSelector } from '../../../data';
|
||||
@@ -24,10 +24,10 @@ import messages from './messages';
|
||||
|
||||
const UnitNavigationEffortEstimate = ({
|
||||
children,
|
||||
intl,
|
||||
sequenceId,
|
||||
unitId,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const sequenceIds = useSelector(sequenceIdsSelector);
|
||||
const sequenceIndex = sequenceIds.indexOf(sequenceId);
|
||||
const nextSequenceId = sequenceIndex < sequenceIds.length - 1 ? sequenceIds[sequenceIndex + 1] : null;
|
||||
@@ -59,6 +59,7 @@ const UnitNavigationEffortEstimate = ({
|
||||
|
||||
UnitNavigationEffortEstimate.propTypes = {
|
||||
children: PropTypes.node,
|
||||
intl: intlShape.isRequired,
|
||||
sequenceId: PropTypes.string.isRequired,
|
||||
unitId: PropTypes.string,
|
||||
};
|
||||
@@ -68,4 +69,4 @@ UnitNavigationEffortEstimate.defaultProps = {
|
||||
unitId: null,
|
||||
};
|
||||
|
||||
export default UnitNavigationEffortEstimate;
|
||||
export default injectIntl(UnitNavigationEffortEstimate);
|
||||
|
||||
@@ -47,7 +47,8 @@ const NextButton = ({
|
||||
if (isAtTop) {
|
||||
return (
|
||||
<IconButton
|
||||
className={`${buttonStyle} icon-hover`}
|
||||
variant="light"
|
||||
className={buttonStyle}
|
||||
onClick={onClick}
|
||||
src={nextArrow}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -40,7 +40,8 @@ const PreviousButton = ({
|
||||
if (isAtTop) {
|
||||
return (
|
||||
<IconButton
|
||||
className={`${buttonStyle} icon-hover`}
|
||||
variant="light"
|
||||
className={buttonStyle}
|
||||
onClick={onClickHandler}
|
||||
src={prevArrow}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, IconButton } from '@openedx/paragon';
|
||||
import { ArrowBackIos, Close } from '@openedx/paragon/icons';
|
||||
import classNames from 'classnames';
|
||||
@@ -9,6 +9,7 @@ import messages from '../../messages';
|
||||
import SidebarContext from '../SidebarContext';
|
||||
|
||||
const SidebarBase = ({
|
||||
intl,
|
||||
title,
|
||||
ariaLabel,
|
||||
sidebarId,
|
||||
@@ -17,7 +18,6 @@ const SidebarBase = ({
|
||||
showTitleBar,
|
||||
width,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
toggleSidebar,
|
||||
shouldDisplayFullScreen,
|
||||
@@ -87,6 +87,7 @@ const SidebarBase = ({
|
||||
};
|
||||
|
||||
SidebarBase.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
ariaLabel: PropTypes.string.isRequired,
|
||||
sidebarId: PropTypes.string.isRequired,
|
||||
@@ -101,4 +102,4 @@ SidebarBase.defaultProps = {
|
||||
showTitleBar: true,
|
||||
};
|
||||
|
||||
export default SidebarBase;
|
||||
export default injectIntl(SidebarBase);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
@@ -24,4 +25,4 @@ SidebarTriggerBase.propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
};
|
||||
|
||||
export default SidebarTriggerBase;
|
||||
export default injectIntl(SidebarTriggerBase);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Button, useToggle, IconButton } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
MenuOpen as MenuOpenIcon,
|
||||
ChevronLeft as ChevronLeftIcon,
|
||||
@@ -16,8 +16,7 @@ import { ID } from './constants';
|
||||
import { useCourseOutlineSidebar } from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
const CourseOutlineTray = () => {
|
||||
const intl = useIntl();
|
||||
const CourseOutlineTray = ({ intl }) => {
|
||||
const [selectedSection, setSelectedSection] = useState(null);
|
||||
const [isDisplaySequenceLevel, setDisplaySequenceLevel, setDisplaySectionLevel] = useToggle(true);
|
||||
|
||||
@@ -132,6 +131,10 @@ const CourseOutlineTray = () => {
|
||||
);
|
||||
};
|
||||
|
||||
CourseOutlineTray.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
CourseOutlineTray.ID = ID;
|
||||
|
||||
export default CourseOutlineTray;
|
||||
export default injectIntl(CourseOutlineTray);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { IconButton } from '@openedx/paragon';
|
||||
import { MenuOpen as MenuOpenIcon } from '@openedx/paragon/icons';
|
||||
|
||||
@@ -8,8 +8,7 @@ import { useCourseOutlineSidebar } from './hooks';
|
||||
import { ID } from './constants';
|
||||
import messages from './messages';
|
||||
|
||||
const CourseOutlineTrigger = ({ isMobileView }) => {
|
||||
const intl = useIntl();
|
||||
const CourseOutlineTrigger = ({ intl, isMobileView }) => {
|
||||
const {
|
||||
currentSidebar,
|
||||
shouldDisplayFullScreen,
|
||||
@@ -46,7 +45,8 @@ CourseOutlineTrigger.defaultProps = {
|
||||
};
|
||||
|
||||
CourseOutlineTrigger.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
isMobileView: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default CourseOutlineTrigger;
|
||||
export default injectIntl(CourseOutlineTrigger);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Icon } from '@openedx/paragon';
|
||||
import { ChevronRight as ChevronRightIcon } from '@openedx/paragon/icons';
|
||||
|
||||
@@ -8,8 +8,7 @@ import courseOutlineMessages from '@src/course-home/outline-tab/messages';
|
||||
import CompletionIcon from './CompletionIcon';
|
||||
import { useCourseOutlineSidebar } from '../hooks';
|
||||
|
||||
const SidebarSection = ({ section, handleSelectSection }) => {
|
||||
const intl = useIntl();
|
||||
const SidebarSection = ({ intl, section, handleSelectSection }) => {
|
||||
const {
|
||||
id,
|
||||
complete,
|
||||
@@ -55,6 +54,7 @@ const SidebarSection = ({ section, handleSelectSection }) => {
|
||||
};
|
||||
|
||||
SidebarSection.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
section: PropTypes.shape({
|
||||
complete: PropTypes.bool,
|
||||
id: PropTypes.string,
|
||||
@@ -68,4 +68,4 @@ SidebarSection.propTypes = {
|
||||
handleSelectSection: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SidebarSection;
|
||||
export default injectIntl(SidebarSection);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Collapsible } from '@openedx/paragon';
|
||||
|
||||
import courseOutlineMessages from '@src/course-home/outline-tab/messages';
|
||||
@@ -11,12 +11,12 @@ import SidebarUnit from './SidebarUnit';
|
||||
import { UNIT_ICON_TYPES } from './UnitIcon';
|
||||
|
||||
const SidebarSequence = ({
|
||||
intl,
|
||||
courseId,
|
||||
defaultOpen,
|
||||
sequence,
|
||||
activeUnitId,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
id,
|
||||
complete,
|
||||
@@ -78,6 +78,7 @@ const SidebarSequence = ({
|
||||
};
|
||||
|
||||
SidebarSequence.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
defaultOpen: PropTypes.bool.isRequired,
|
||||
sequence: PropTypes.shape({
|
||||
@@ -95,4 +96,4 @@ SidebarSequence.propTypes = {
|
||||
activeUnitId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default SidebarSequence;
|
||||
export default injectIntl(SidebarSequence);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from '../messages';
|
||||
import UnitIcon, { UNIT_ICON_TYPES } from './UnitIcon';
|
||||
@@ -8,6 +8,7 @@ import UnitLinkWrapper from './UnitLinkWrapper';
|
||||
|
||||
const SidebarUnit = ({
|
||||
id,
|
||||
intl,
|
||||
courseId,
|
||||
sequenceId,
|
||||
isFirst,
|
||||
@@ -16,7 +17,6 @@ const SidebarUnit = ({
|
||||
isLocked,
|
||||
activeUnitId,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
complete,
|
||||
title,
|
||||
@@ -52,6 +52,7 @@ const SidebarUnit = ({
|
||||
};
|
||||
|
||||
SidebarUnit.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
isFirst: PropTypes.bool.isRequired,
|
||||
unit: PropTypes.shape({
|
||||
@@ -68,4 +69,4 @@ SidebarUnit.propTypes = {
|
||||
activeUnitId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default SidebarUnit;
|
||||
export default injectIntl(SidebarUnit);
|
||||
|
||||
@@ -36,12 +36,12 @@ describe('<SidebarUnit />', () => {
|
||||
};
|
||||
};
|
||||
|
||||
function renderWithProvider(props = {}, sidebarContext = defaultSidebarContext, pathname = '/course') {
|
||||
function renderWithProvider(props = {}, sidebarContext = defaultSidebarContext) {
|
||||
const { container } = render(
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<IntlProvider locale="en">
|
||||
<SidebarContext.Provider value={{ ...sidebarContext }}>
|
||||
<MemoryRouter initialEntries={[{ pathname }]}>
|
||||
<MemoryRouter>
|
||||
<SidebarUnit
|
||||
isFirst
|
||||
id={unit.id}
|
||||
@@ -138,34 +138,4 @@ describe('<SidebarUnit />', () => {
|
||||
expect(window.sessionStorage.getItem('hideCourseOutlineSidebar')).toEqual('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UnitLinkWrapper', () => {
|
||||
describe('course in preview mode', () => {
|
||||
beforeEach(async () => {
|
||||
await initTestStore();
|
||||
renderWithProvider({ unit: { ...unit } }, { ...defaultSidebarContext, shouldDisplayFullScreen: true }, '/preview/course');
|
||||
});
|
||||
|
||||
it('href includes /preview', async () => {
|
||||
const unitLink = screen.getByText(unit.title).closest('a');
|
||||
const linkHref = unitLink.getAttribute('href');
|
||||
|
||||
expect(linkHref.includes('/preview/')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('course in live mode', () => {
|
||||
beforeEach(async () => {
|
||||
await initTestStore();
|
||||
renderWithProvider({ unit: { ...unit } }, { ...defaultSidebarContext, shouldDisplayFullScreen: true });
|
||||
});
|
||||
|
||||
it('href does not include /preview/', async () => {
|
||||
const unitLink = screen.getByText(unit.title).closest('a');
|
||||
const linkHref = unitLink.getAttribute('href');
|
||||
|
||||
expect(linkHref.includes('/preview/')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useCourseOutlineSidebar } from '../hooks';
|
||||
|
||||
@@ -27,14 +27,10 @@ const UnitLinkWrapper: React.FC<Props> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { handleUnitClick } = useCourseOutlineSidebar();
|
||||
const { pathname } = useLocation();
|
||||
const isPreview = pathname.startsWith('/preview');
|
||||
const baseUrl = `/course/${courseId}/${sequenceId}/${id}`;
|
||||
const link = isPreview ? `/preview${baseUrl}` : baseUrl;
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={link}
|
||||
to={`/course/${courseId}/${sequenceId}/${id}`}
|
||||
className="row w-100 m-0 d-flex align-items-center text-gray-700"
|
||||
onClick={() => handleUnitClick({ sequenceId, activeUnitId, id })}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ensureConfig, getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useModel } from '@src/generic/model-store';
|
||||
import SidebarBase from '../../common/SidebarBase';
|
||||
@@ -12,8 +12,7 @@ import messages from './messages';
|
||||
|
||||
ensureConfig(['DISCUSSIONS_MFE_BASE_URL']);
|
||||
|
||||
const DiscussionsSidebar = () => {
|
||||
const intl = useIntl();
|
||||
const DiscussionsSidebar = ({ intl }) => {
|
||||
const {
|
||||
unitId,
|
||||
courseId,
|
||||
@@ -48,7 +47,11 @@ const DiscussionsSidebar = () => {
|
||||
);
|
||||
};
|
||||
|
||||
DiscussionsSidebar.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
DiscussionsSidebar.Trigger = DiscussionsSidebar;
|
||||
DiscussionsSidebar.ID = ID;
|
||||
|
||||
export default DiscussionsSidebar;
|
||||
export default injectIntl(DiscussionsSidebar);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ensureConfig, getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import { QuestionAnswer } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -16,9 +16,9 @@ ensureConfig(['DISCUSSIONS_MFE_BASE_URL']);
|
||||
export const ID = WIDGETS.DISCUSSIONS;
|
||||
|
||||
const DiscussionsTrigger = ({
|
||||
intl,
|
||||
onClick,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
unitId,
|
||||
courseId,
|
||||
@@ -51,7 +51,8 @@ const DiscussionsTrigger = ({
|
||||
};
|
||||
|
||||
DiscussionsTrigger.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default DiscussionsTrigger;
|
||||
export default injectIntl(DiscussionsTrigger);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import { WatchOutline } from '@openedx/paragon/icons';
|
||||
import classNames from 'classnames';
|
||||
@@ -8,36 +8,35 @@ import React from 'react';
|
||||
import messages from '../../../messages';
|
||||
|
||||
const NotificationIcon = ({
|
||||
intl,
|
||||
status,
|
||||
notificationColor,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<>
|
||||
<Icon src={WatchOutline} className="m-0 m-auto" alt={intl.formatMessage(messages.openNotificationTrigger)} />
|
||||
{status === 'active'
|
||||
? (
|
||||
<span
|
||||
className={classNames(notificationColor, 'rounded-circle p-1 position-absolute')}
|
||||
data-testid="notification-dot"
|
||||
style={{
|
||||
top: '0.3rem',
|
||||
right: '0.55rem',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
}) => (
|
||||
<>
|
||||
<Icon src={WatchOutline} className="m-0 m-auto" alt={intl.formatMessage(messages.openNotificationTrigger)} />
|
||||
{status === 'active'
|
||||
? (
|
||||
<span
|
||||
className={classNames(notificationColor, 'rounded-circle p-1 position-absolute')}
|
||||
data-testid="notification-dot"
|
||||
style={{
|
||||
top: '0.3rem',
|
||||
right: '0.55rem',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
|
||||
NotificationIcon.defaultProps = {
|
||||
status: null,
|
||||
};
|
||||
|
||||
NotificationIcon.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
status: PropTypes.string,
|
||||
notificationColor: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default NotificationIcon;
|
||||
export default injectIntl(NotificationIcon);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { useContext, useEffect, useMemo } from 'react';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { useModel } from '@src/generic/model-store';
|
||||
import { NotificationTraySlot } from '../../../../../plugin-slots/NotificationTraySlot';
|
||||
import UpgradeNotification from '../../../../../generic/upgrade-notification/UpgradeNotification';
|
||||
|
||||
import messages from '../../../messages';
|
||||
import SidebarBase from '../../common/SidebarBase';
|
||||
import SidebarContext from '../../SidebarContext';
|
||||
import NotificationTrigger, { ID } from './NotificationTrigger';
|
||||
|
||||
const NotificationTray = () => {
|
||||
const intl = useIntl();
|
||||
const NotificationTray = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
onNotificationSeen,
|
||||
@@ -23,11 +23,17 @@ const NotificationTray = () => {
|
||||
const course = useModel('coursewareMeta', courseId);
|
||||
|
||||
const {
|
||||
accessExpiration,
|
||||
contentTypeGatingEnabled,
|
||||
end,
|
||||
enrollmentEnd,
|
||||
enrollmentMode,
|
||||
enrollmentStart,
|
||||
marketingUrl,
|
||||
offer,
|
||||
start,
|
||||
timeOffsetMillis,
|
||||
userTimezone,
|
||||
verificationStatus,
|
||||
} = course;
|
||||
|
||||
@@ -76,11 +82,31 @@ const NotificationTray = () => {
|
||||
>
|
||||
<div>{verifiedMode
|
||||
? (
|
||||
<NotificationTraySlot
|
||||
courseId={courseId}
|
||||
notificationCurrentState={upgradeNotificationCurrentState}
|
||||
setNotificationCurrentState={setUpgradeNotificationCurrentState}
|
||||
/>
|
||||
<PluginSlot
|
||||
id="notification_tray_slot"
|
||||
pluginProps={{
|
||||
courseId,
|
||||
model: 'coursewareMeta',
|
||||
notificationCurrentState: upgradeNotificationCurrentState,
|
||||
setNotificationCurrentState: setUpgradeNotificationCurrentState,
|
||||
}}
|
||||
>
|
||||
<UpgradeNotification
|
||||
offer={offer}
|
||||
verifiedMode={verifiedMode}
|
||||
accessExpiration={accessExpiration}
|
||||
contentTypeGatingEnabled={contentTypeGatingEnabled}
|
||||
marketingUrl={marketingUrl}
|
||||
upsellPageName="in_course"
|
||||
userTimezone={userTimezone}
|
||||
shouldDisplayBorder={false}
|
||||
timeOffsetMillis={timeOffsetMillis}
|
||||
courseId={courseId}
|
||||
org={org}
|
||||
upgradeNotificationCurrentState={upgradeNotificationCurrentState}
|
||||
setupgradeNotificationCurrentState={setUpgradeNotificationCurrentState}
|
||||
/>
|
||||
</PluginSlot>
|
||||
) : (
|
||||
<p className="p-3 small">{intl.formatMessage(messages.noNotificationsMessage)}</p>
|
||||
)}
|
||||
@@ -89,7 +115,11 @@ const NotificationTray = () => {
|
||||
);
|
||||
};
|
||||
|
||||
NotificationTray.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
NotificationTray.Trigger = NotificationTrigger;
|
||||
NotificationTray.ID = ID;
|
||||
|
||||
export default NotificationTray;
|
||||
export default injectIntl(NotificationTray);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user