Compare commits

..

32 Commits

Author SHA1 Message Date
Jacobo Dominguez
db0a56566f docs: add comprehensive readme documentation for plugin slots (#1770) 2025-08-06 16:41:36 +05:30
Brian Smith
b78e78313b fix(docs): correct ProgressCertificateStatusSlot README title (#1689) 2025-07-21 13:11:29 -04:00
Brian Smith
29ec85ddbe fix(docs): correct CourseRecommendationsSlot README title (#1688) 2025-07-21 13:11:29 -04:00
Maxim Beder
241e188465 feat: update certificate icons
Old certificates icons contained edX trademark logos, which were not
suitable for the open source repos. Replaced with icons that contain
Open edX logos.
2025-05-05 20:43:01 +05:30
Brian Smith
24c9437e91 feat: import FooterSlot from component package instead of slot package (#1682) 2025-04-24 12:32:45 -04:00
Brian Smith
fb6f110732 feat: standardize slot ids (#1685) 2025-04-24 07:27:23 -04:00
renovate[bot]
1656b73a31 fix(deps): update dependency @edx/frontend-component-header to v6.4.0 (#1684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-23 20:19:28 +00:00
edX requirements bot
81671ad328 chore: update browserslist DB (#1681)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-04-21 00:41:04 +00:00
Ivo Branco
4cc716b20c chore(deps): update dependency @openedx/frontend-build (#1677)
This will extract more messages to be translated.
2025-04-16 15:36:46 -04:00
Braden MacDonald
756fbbac83 chore: remove 'patch-package' and its unused frontend-build patch 2025-04-15 11:56:18 -07:00
KristinAoki
903fe28ff6 refactor: change to useIntl 2025-04-15 10:45:26 -07:00
Adolfo R. Brandes
14c662dc53 feat: removes Upgrade Notification as default content
As a follow-up to
https://github.com/openedx/frontend-app-learning/pull/1368, remove the
UpgradeNotification component from the sidebar's default content.
2025-04-14 16:50:55 -03:00
Adolfo R. Brandes
af432eab27 chore: remove extraneous config file 2025-04-14 15:28:43 -03:00
edX requirements bot
dde640df33 chore: update browserslist DB (#1673)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-04-14 00:41:29 +00:00
KristinAoki
b827db800d fix: lint errors 2025-04-10 11:14:16 -04:00
KristinAoki
5b7f76b43d fix: breadcrumb preview link 2025-04-10 11:14:16 -04:00
KristinAoki
cf4bea3604 fix: unit link in preview mode 2025-04-10 11:14:16 -04:00
Feanil Patel
85e6e9266d feat: Drop canShowUpgradeSock course data.
DEPR: https://github.com/openedx/edx-platform/issues/36429

This piece of data is not being used anywhere but was still being
consumed so just drop the data so that the backend can be updated to no
longer provide the data.

The backend API is being updated in https://github.com/openedx/edx-platform/pull/36436
2025-04-09 10:13:24 -04:00
Brian Smith
360af1f0e9 feat: upgrade to react 18 (#1663) 2025-04-07 14:58:51 -04:00
edX requirements bot
26f4a90976 chore: update browserslist DB (#1670)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-04-07 00:39:51 +00:00
renovate[bot]
0d45c78ace fix(deps): update dependency @edx/frontend-platform to v8.3.4 (#1623)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-03 14:27:08 +00:00
renovate[bot]
c18214dc41 fix(deps): update dependency @openedx/paragon to v22.17.0 (#1666)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-03 07:34:37 +00:00
renovate[bot]
54611c1b4d fix(deps): update dependency @openedx/frontend-build to v14.4.2 (#1665)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-03 03:19:56 +00:00
renovate[bot]
7ca4b71ff7 fix(deps): update dependency @edx/frontend-lib-learning-assistant to v2.21.0 (#1664)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-02 22:52:12 +00:00
renovate[bot]
63a7ff83cf fix(deps): update dependency @edx/frontend-component-footer to v14.4.0 (#1662)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-02 19:16:07 +00:00
renovate[bot]
8ecaa018da fix(deps): update dependency @edx/react-unit-test-utils to v3.1.0 (#1652)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-02 13:56:25 +00:00
renovate[bot]
64ca156095 fix(deps): update dependency @openedx/frontend-slot-footer to v1.1.1 (#1661)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-02 13:56:12 +00:00
Brian Smith
c06f2c37ab chore(deps): update @openedx dependencies to versions that support React 18 (#1656) 2025-04-02 09:49:49 -04:00
Sarina Canelake
d5a092b220 Update edx.rtd.io links to docs.openedx.org (#1654)
* docs: Update edx.rtd links to their new homes

* docs: Update README to not prescribe a version of Node
2025-03-26 17:43:44 +05:30
Kristin Aoki
81b621195e fix: button hover background color (#1653)
This PR updates the hover background for the top navigation buttons that are shown when the left side navigation is enabled. The hover background is updated to match the hover background of other existing IconButton components, see DiscussionNotificationTrigger.jsx, on the page.
2025-03-25 17:03:29 +00:00
renovate[bot]
226c4cc1d7 fix(deps): update dependency @edx/frontend-lib-special-exams to v3.4.0 (#1651)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-25 01:51:16 +00:00
renovate[bot]
7f6a59b701 fix(deps): update dependency @openedx/paragon to v22.16.1 (#1650)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 23:57:56 +00:00
182 changed files with 7268 additions and 6599 deletions

View File

@@ -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://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_credit_courses.html#keep-track-of-credit-requirements'
CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-college-credit-or-credit-hours-for-my-course'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
DISCOVERY_API_BASE_URL='http://localhost:18381'
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'

View File

@@ -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://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_credit_courses.html#keep-track-of-credit-requirements'
CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-college-credit-or-credit-hours-for-my-course'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
DISCOVERY_API_BASE_URL='http://localhost:18381'
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'

View File

@@ -41,9 +41,8 @@ Cloning and Setup
git clone https://github.com/openedx/frontend-app-learning.git
2. Use node v20.x.
2. Use the version of Node specified in ``.nvmrc``.
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>`_.
@@ -131,7 +130,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://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html>`_.
`MFE applications <https://openedx.github.io/frontend-platform/>`_.
Plugins
=======
@@ -145,7 +144,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://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`_.
`Required Environment Variables <https://openedx.github.io/frontend-platform/>`_.
The learning micro-frontend also supports the following additional variables:

7954
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,6 @@
"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",
@@ -35,20 +34,20 @@
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "1.5.0",
"@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/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/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "3.0.0",
"@edx/react-unit-test-utils": "^4.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.2.0",
"@openedx/frontend-plugin-framework": "^1.2.1",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.3.0",
"@openedx/frontend-build": "^14.5.0",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^22.16.0",
"@popperjs/core": "2.11.8",
"@reduxjs/toolkit": "1.9.7",
"buffer": "^6.0.3",
@@ -58,12 +57,11 @@
"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": "17.0.2",
"react-dom": "17.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet": "6.1.0",
"react-redux": "7.2.9",
"react-router": "6.15.0",
@@ -79,9 +77,8 @@
"devDependencies": {
"@edx/reactifex": "2.2.0",
"@pact-foundation/pact": "^13.0.0",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "14.6.1",
"axios-mock-adapter": "2.1.0",
"bundlewatch": "^0.4.0",
@@ -95,7 +92,7 @@
"files": [
{
"path": "dist/*.js",
"maxSize": "1400kB"
"maxSize": "1450kB"
}
],
"normalizeFilenames": "^.+?(\\..+?)\\.\\w+$"

View File

@@ -1,36 +0,0 @@
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'),

View File

@@ -0,0 +1,224 @@
// 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>
`;

View File

@@ -1,14 +1,13 @@
import PropTypes from 'prop-types';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
FormattedMessage, FormattedDate, injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
import { FormattedMessage, FormattedDate, useIntl } from '@edx/frontend-platform/i18n';
import { Alert, Hyperlink } from '@openedx/paragon';
import { Info } from '@openedx/paragon/icons';
import messages from './messages';
const AccessExpirationAlert = ({ intl, payload }) => {
const AccessExpirationAlert = ({ payload }) => {
const intl = useIntl();
const {
accessExpiration,
courseId,
@@ -119,7 +118,6 @@ const AccessExpirationAlert = ({ intl, payload }) => {
};
AccessExpirationAlert.propTypes = {
intl: intlShape.isRequired,
payload: PropTypes.shape({
accessExpiration: PropTypes.shape({
expirationDate: PropTypes.string.isRequired,
@@ -134,4 +132,4 @@ AccessExpirationAlert.propTypes = {
}).isRequired,
};
export default injectIntl(AccessExpirationAlert);
export default AccessExpirationAlert;

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { Alert, Hyperlink } from '@openedx/paragon';
import { WarningFilled } from '@openedx/paragon/icons';
@@ -7,7 +7,8 @@ import { WarningFilled } from '@openedx/paragon/icons';
import { getConfig } from '@edx/frontend-platform';
import genericMessages from './messages';
const ActiveEnterpriseAlert = ({ intl, payload }) => {
const ActiveEnterpriseAlert = ({ payload }) => {
const intl = useIntl();
const { text, courseId } = payload;
const changeActiveEnterprise = (
<Hyperlink
@@ -38,11 +39,10 @@ const ActiveEnterpriseAlert = ({ intl, payload }) => {
};
ActiveEnterpriseAlert.propTypes = {
intl: intlShape.isRequired,
payload: PropTypes.shape({
text: PropTypes.string,
courseId: PropTypes.string,
}).isRequired,
};
export default injectIntl(ActiveEnterpriseAlert);
export default ActiveEnterpriseAlert;

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { Alert, Button } from '@openedx/paragon';
import { Info, WarningFilled } from '@openedx/paragon/icons';
@@ -11,7 +11,8 @@ import { useModel } from '../../generic/model-store';
import messages from './messages';
import useEnrollClickHandler from './clickHook';
const EnrollmentAlert = ({ intl, payload }) => {
const EnrollmentAlert = ({ payload }) => {
const intl = useIntl();
const {
canEnroll,
courseId,
@@ -58,7 +59,6 @@ const EnrollmentAlert = ({ intl, payload }) => {
};
EnrollmentAlert.propTypes = {
intl: intlShape.isRequired,
payload: PropTypes.shape({
canEnroll: PropTypes.bool,
courseId: PropTypes.string,
@@ -67,4 +67,4 @@ EnrollmentAlert.propTypes = {
}).isRequired,
};
export default injectIntl(EnrollmentAlert);
export default EnrollmentAlert;

View File

@@ -9,13 +9,12 @@ import {
Icon,
} from '@openedx/paragon';
import { Check, ArrowForward } from '@openedx/paragon/icons';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { sendActivationEmail } from '../../courseware/data';
import messages from './messages';
const AccountActivationAlert = ({
intl,
}) => {
const AccountActivationAlert = () => {
const intl = useIntl();
const [showModal, setShowModal] = useState(false);
const [showSpinner, setShowSpinner] = useState(false);
const [showCheck, setShowCheck] = useState(false);
@@ -125,8 +124,4 @@ const AccountActivationAlert = ({
);
};
AccountActivationAlert.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(AccountActivationAlert);
export default AccountActivationAlert;

View File

@@ -1,13 +1,14 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { useIntl, 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 = ({ intl }) => {
const LogistrationAlert = () => {
const intl = useIntl();
const signIn = (
<Hyperlink
style={{ textDecoration: 'underline' }}
@@ -43,8 +44,4 @@ const LogistrationAlert = ({ intl }) => {
);
};
LogistrationAlert.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(LogistrationAlert);
export default LogistrationAlert;

View File

@@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Tabs, Tab } from '@openedx/paragon';
import { useParams } from 'react-router';
@@ -13,7 +13,8 @@ const filterTypes = ['text', 'video', 'sequence'];
const filterOther = 'other';
const validFilters = [filterAll, ...filterTypes, filterOther];
export const CoursewareSearchResultsFilter = ({ intl }) => {
export const CoursewareSearchResultsFilter = () => {
const intl = useIntl();
const { courseId } = useParams();
const lastSearch = useModel('contentSearchResults', courseId);
const { filter: filterKeyword, setFilter } = useCoursewareSearchParams();
@@ -63,7 +64,6 @@ export const CoursewareSearchResultsFilter = ({ intl }) => {
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,8 +74,4 @@ export const CoursewareSearchResultsFilter = ({ intl }) => {
);
};
CoursewareSearchResultsFilter.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(CoursewareSearchResultsFilter);
export default CoursewareSearchResultsFilter;

View File

@@ -141,15 +141,7 @@ const CoursewareSearch = ({ ...sectionProps }) => {
</Button>
</div>
</div>
<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'}
>
<div className="courseware-search__results" aria-live="polite" data-testid="courseware-search-results">
{status === 'loading' ? (
<div className="courseware-search__spinner" data-testid="courseware-search-spinner">
<Spinner animation="border" variant="light" screenReaderText={formatMessage(messages.loading)} />
@@ -165,6 +157,8 @@ 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>

View File

@@ -244,7 +244,7 @@ describe('CoursewareSearch', () => {
expect(screen.queryByTestId('courseware-search-summary')).not.toBeInTheDocument();
});
it('should show a wrapper div with proper aria attributes', () => {
it('should show a summary for the results within a container with aria-live="polite"', () => {
mockModels({
searchKeyword: 'fubar',
total: 1,
@@ -253,9 +253,7 @@ describe('CoursewareSearch', () => {
const results = screen.queryByTestId('courseware-search-results');
expect(results).toHaveAttribute('aria-live', 'assertive');
expect(results).toHaveAttribute('aria-atomic', 'true');
expect(results).toHaveAttribute('role', 'alert');
expect(results).toHaveAttribute('aria-live', 'polite');
expect(within(results).queryByTestId('courseware-search-summary').textContent).toBe('Results for "fubar":');
});
});

View File

@@ -1,15 +1,14 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
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,
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>
);
};
export default injectIntl(CoursewareSearchEmpty);
export default CoursewareSearchEmpty;

View File

@@ -1,4 +1,4 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import React from 'react';
import {
Folder, TextFields, VideoCamera, Article,
} from '@openedx/paragon/icons';
@@ -6,7 +6,6 @@ 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,
@@ -22,8 +21,6 @@ const CoursewareSearchResults = ({ results = [] }) => {
return <CoursewareSearchEmpty />;
}
const { formatMessage } = useIntl();
const baseUrl = `${getConfig().LMS_BASE_URL}`;
return (
@@ -45,30 +42,24 @@ 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">
<h3>{title}</h3>
{contentHits ? (<em aria-hidden="true">{contentHits}</em>) : null }
<span>{title}</span>
{contentHits ? (<em>{contentHits}</em>) : null }
</div>
<div aria-label={ariaLocation}>
{location?.length ? (
<ul className="courseware-search-results__breadcrumbs" aria-hidden="true">
{
{location?.length ? (
<ul className="courseware-search-results__breadcrumbs">
{
// 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}
</div>
</ul>
) : null}
</div>
</a>
);

View File

@@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { ManageSearch } from '@openedx/paragon/icons';
import { useDispatch } from 'react-redux';
@@ -7,9 +7,8 @@ import messages from './messages';
import { useCoursewareSearchFeatureFlag, useCoursewareSearchParams } from './hooks';
import { setShowSearch } from '../data/slice';
const CoursewareSearchToggle = ({
intl,
}) => {
const CoursewareSearchToggle = () => {
const intl = useIntl();
const dispatch = useDispatch();
const enabled = useCoursewareSearchFeatureFlag();
const { query } = useCoursewareSearchParams();
@@ -41,8 +40,4 @@ const CoursewareSearchToggle = ({
);
};
CoursewareSearchToggle.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(CoursewareSearchToggle);
export default CoursewareSearchToggle;

View File

@@ -38,29 +38,24 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
Demo Course Overview
</h3>
</span>
</div>
<div
aria-label="Location: Introduction, then Demo Course Overview."
<ul
class="courseware-search-results__breadcrumbs"
>
<ul
aria-hidden="true"
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Introduction
</div>
</li>
<li>
<div>
Demo Course Overview
</div>
</li>
</ul>
</div>
<li>
<div>
Introduction
</div>
</li>
<li>
<div>
Demo Course Overview
</div>
</li>
</ul>
</div>
</a>
<a
@@ -96,39 +91,32 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
Passing a Course
</h3>
<em
aria-hidden="true"
>
</span>
<em>
1
</em>
</div>
<div
aria-label="Location: About Exams and Certificates, then edX Exams, then Passing a Course."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<li>
<div>
About Exams and Certificates
</div>
</li>
<li>
<div>
edX Exams
</div>
</li>
<li>
<div>
Passing a Course
</div>
</li>
</ul>
</div>
</a>
<a
@@ -164,34 +152,29 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
Passing a Course
</h3>
</span>
</div>
<div
aria-label="Location: About Exams and Certificates, then edX Exams, then Passing a Course."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<li>
<div>
About Exams and Certificates
</div>
</li>
<li>
<div>
edX Exams
</div>
</li>
<li>
<div>
Passing a Course
</div>
</li>
</ul>
</div>
</a>
<a
@@ -227,34 +210,29 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
Text Input
</h3>
</span>
</div>
<div
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Text input."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<li>
<div>
Example Week 1: Getting Started
</div>
</li>
<li>
<div>
Homework - Question Styles
</div>
</li>
<li>
<div>
Text input
</div>
</li>
</ul>
</div>
</a>
<a
@@ -290,34 +268,29 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
Pointing on a Picture
</h3>
</span>
</div>
<div
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Pointing on a Picture."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<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>
</a>
<a
@@ -353,34 +326,29 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
Getting Answers
</h3>
</span>
</div>
<div
aria-label="Location: About Exams and Certificates, then edX Exams, then Getting Answers."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<li>
<div>
About Exams and Certificates
</div>
</li>
<li>
<div>
edX Exams
</div>
</li>
<li>
<div>
Getting Answers
</div>
</li>
</ul>
</div>
</a>
<a
@@ -416,39 +384,32 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
Welcome!
</h3>
<em
aria-hidden="true"
>
</span>
<em>
30
</em>
</div>
<div
aria-label="Location: Introduction, then Demo Course Overview, then Introduction: Video and Sequences."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<li>
<div>
Introduction
</div>
</li>
<li>
<div>
Demo Course Overview
</div>
</li>
<li>
<div>
Introduction: Video and Sequences
</div>
</li>
</ul>
</div>
</a>
<a
@@ -484,34 +445,29 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
Multiple Choice Questions
</h3>
</span>
</div>
<div
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Multiple Choice Questions."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<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>
</a>
<a
@@ -547,34 +503,29 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
Numerical Input
</h3>
</span>
</div>
<div
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Numerical Input."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<li>
<div>
Example Week 1: Getting Started
</div>
</li>
<li>
<div>
Homework - Question Styles
</div>
</li>
<li>
<div>
Numerical Input
</div>
</li>
</ul>
</div>
</a>
<a
@@ -610,39 +561,32 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
Connecting a Circuit and a Circuit Diagram
</h3>
<em
aria-hidden="true"
>
</span>
<em>
3
</em>
</div>
<div
aria-label="Location: Example Week 1: Getting Started, then Lesson 1 - Getting Started, then Video Presentation Styles."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<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>
</a>
<a
@@ -678,34 +622,29 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
CAPA
</h3>
</span>
</div>
<div
aria-label="Location: Example Week 2: Get Interactive, then Homework - Labs and Demos, then Code Grader."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<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>
</a>
<a
@@ -741,34 +680,29 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
Interactive Questions
</h3>
</span>
</div>
<div
aria-label="Location: Example Week 1: Getting Started, then Lesson 1 - Getting Started, then Interactive Questions."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<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>
</a>
<a
@@ -804,39 +738,32 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
Blank HTML Page
</h3>
<em
aria-hidden="true"
>
</span>
<em>
6
</em>
</div>
<div
aria-label="Location: Introduction, then Demo Course Overview, then Introduction: Video and Sequences."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<li>
<div>
Introduction
</div>
</li>
<li>
<div>
Demo Course Overview
</div>
</li>
<li>
<div>
Introduction: Video and Sequences
</div>
</li>
</ul>
</div>
</a>
<a
@@ -872,39 +799,32 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
Discussion Forums
</h3>
<em
aria-hidden="true"
>
</span>
<em>
5
</em>
</div>
<div
aria-label="Location: Example Week 3: Be Social, then Lesson 3 - Be Social, then Discussion Forums."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<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>
</a>
<a
@@ -940,39 +860,32 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
Overall Grade
</h3>
<em
aria-hidden="true"
>
</span>
<em>
7
</em>
</div>
<div
aria-label="Location: About Exams and Certificates, then edX Exams, then Overall Grade Performance."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<li>
<div>
About Exams and Certificates
</div>
</li>
<li>
<div>
edX Exams
</div>
</li>
<li>
<div>
Overall Grade Performance
</div>
</li>
</ul>
</div>
</a>
<a
@@ -1008,39 +921,32 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
Blank HTML Page
</h3>
<em
aria-hidden="true"
>
</span>
<em>
3
</em>
</div>
<div
aria-label="Location: Example Week 3: Be Social, then Lesson 3 - Be Social, then Homework - Find Your Study Buddy."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<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>
</a>
<a
@@ -1076,39 +982,32 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
Find Your Study Buddy
</h3>
<em
aria-hidden="true"
>
</span>
<em>
3
</em>
</div>
<div
aria-label="Location: Example Week 3: Be Social, then Homework - Find Your Study Buddy, then Homework - Find Your Study Buddy."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<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>
</a>
<a
@@ -1144,39 +1043,32 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
Be Social
</h3>
<em
aria-hidden="true"
>
</span>
<em>
4
</em>
</div>
<div
aria-label="Location: Example Week 3: Be Social, then Lesson 3 - Be Social, then Be Social."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<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>
</a>
<a
@@ -1212,39 +1104,32 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
EdX Exams
</h3>
<em
aria-hidden="true"
>
</span>
<em>
4
</em>
</div>
<div
aria-label="Location: About Exams and Certificates, then edX Exams, then EdX Exams."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<li>
<div>
About Exams and Certificates
</div>
</li>
<li>
<div>
edX Exams
</div>
</li>
<li>
<div>
EdX Exams
</div>
</li>
</ul>
</div>
</a>
<a
@@ -1280,39 +1165,32 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
When Are Your Exams?
</h3>
<em
aria-hidden="true"
>
</span>
<em>
2
</em>
</div>
<div
aria-label="Location: Example Week 1: Getting Started, then Lesson 1 - Getting Started, then When Are Your Exams? ."
<ul
class="courseware-search-results__breadcrumbs"
>
<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>
<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>
</a>
<a
@@ -1350,13 +1228,10 @@ exports[`CoursewareSearchResults when list of results is provided should match t
<div
class="courseware-search-results__title"
>
<h3>
<span>
External Course Link Test
</h3>
</span>
</div>
<div
aria-label=""
/>
</div>
</a>
</div>

View File

@@ -101,11 +101,6 @@
font-size: 0.875rem;
color: $black;
> h3 {
font-size: inherit;
margin: 0;
}
> span {
display: block;
overflow: hidden;

View File

@@ -1,4 +1,4 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { renderHook, act, waitFor } from '@testing-library/react';
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 hook.waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
await 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 hook.waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
await 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 });
hook.waitFor(() => expect(getBoundingClientRectSpy).toHaveBeenCalled());
await waitFor(() => expect(getBoundingClientRectSpy).toHaveBeenCalled());
expect(hook.result.current).toEqual(mockedInfo);
});

View File

@@ -56,21 +56,7 @@ 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',

View File

@@ -31,7 +31,6 @@ 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,

View File

@@ -489,7 +489,6 @@ 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,

View File

@@ -367,7 +367,6 @@ 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);
@@ -389,7 +388,6 @@ export async function getOutlineTabData(courseId) {
return {
accessExpiration,
canShowUpgradeSock,
certData,
courseBlocks,
courseGoals,

View File

@@ -46,7 +46,6 @@ describe('Course Home Service', () => {
willRespondWith: {
status: 200,
body: {
can_show_upgrade_sock: boolean(false),
verified_mode: like({
access_expiration_date: null,
currency: 'USD',
@@ -94,7 +93,6 @@ describe('Course Home Service', () => {
},
});
const normalizedTabData = {
canShowUpgradeSock: false,
verifiedMode: {
accessExpirationDate: null,
currency: 'USD',

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
import Timeline from './timeline/Timeline';
@@ -14,7 +14,8 @@ import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
import UpgradeToCompleteAlert from '../suggested-schedule-messaging/UpgradeToCompleteAlert';
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
const DatesTab = ({ intl }) => {
const DatesTab = () => {
const intl = useIntl();
const {
courseId,
} = useSelector(state => state.courseHome);
@@ -59,8 +60,4 @@ const DatesTab = ({ intl }) => {
);
};
DatesTab.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(DatesTab);
export default DatesTab;

View File

@@ -5,8 +5,7 @@ import { useSelector } from 'react-redux';
import {
FormattedDate,
FormattedTime,
injectIntl,
intlShape,
useIntl,
} from '@edx/frontend-platform/i18n';
import { Tooltip, OverlayTrigger } from '@openedx/paragon';
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
@@ -20,10 +19,10 @@ import { isLearnerAssignment } from '../utils';
const Day = ({
date,
first,
intl,
items,
last,
}) => {
const intl = useIntl();
const {
courseId,
} = useSelector(state => state.courseHome);
@@ -108,7 +107,6 @@ 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,
@@ -126,4 +124,4 @@ Day.defaultProps = {
last: false,
};
export default injectIntl(Day);
export default Day;

View File

@@ -1,5 +1,4 @@
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';
@@ -30,6 +29,4 @@ const DiscussionTab = () => {
);
};
DiscussionTab.propTypes = {};
export default injectIntl(DiscussionTab);
export default DiscussionTab;

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import HeaderSlot from '../../plugin-slots/HeaderSlot';
import PageLoading from '../../generic/PageLoading';
@@ -10,7 +10,8 @@ import { unsubscribeFromCourseGoal } from '../data/api';
import messages from './messages';
import ResultPage from './ResultPage';
const GoalUnsubscribe = ({ intl }) => {
const GoalUnsubscribe = () => {
const intl = useIntl();
const { token } = useParams();
const [error, setError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
@@ -51,8 +52,4 @@ const GoalUnsubscribe = ({ intl }) => {
);
};
GoalUnsubscribe.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(GoalUnsubscribe);
export default GoalUnsubscribe;

View File

@@ -1,28 +1,26 @@
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } 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, 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 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 header = error
@@ -54,7 +52,6 @@ ResultPage.defaultProps = {
ResultPage.propTypes = {
courseTitle: PropTypes.string,
error: PropTypes.bool,
intl: intlShape.isRequired,
};
export default injectIntl(ResultPage);
export default ResultPage;

View File

@@ -16,6 +16,11 @@ 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',

View File

@@ -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 { PluginSlot } from '@openedx/frontend-plugin-framework';
import { CourseOutlineTabNotificationsSlot } from '../../plugin-slots/CourseOutlineTabNotificationsSlot';
import { AlertList } from '../../generic/user-messages';
import CourseDates from './widgets/CourseDates';
@@ -16,7 +16,6 @@ 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';
@@ -40,13 +39,11 @@ const OutlineTab = () => {
isSelfPaced,
org,
title,
userTimezone,
} = useModel('courseHomeMeta', courseId);
const expandButtonRef = useRef();
const {
accessExpiration,
courseBlocks: {
courses,
sections,
@@ -55,20 +52,12 @@ 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();
@@ -192,27 +181,7 @@ const OutlineTab = () => {
/>
)}
<CourseTools />
<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>
<CourseOutlineTabNotificationsSlot courseId={courseId} />
<CourseDates />
<CourseHandouts />
</div>

View File

@@ -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, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import { sendTrackEvent } 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('outline_tab_notifications_slot')).toBeInTheDocument();
expect(screen.getByTestId('org.openedx.frontend.learning.course_outline_tab_notifications.v1')).toBeInTheDocument();
});
it('handles expand/collapse all button click', async () => {
@@ -1190,80 +1190,6 @@ 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 = () => ({

View File

@@ -3,8 +3,7 @@ import PropTypes from 'prop-types';
import {
FormattedDate,
FormattedMessage,
injectIntl,
intlShape,
useIntl,
} from '@edx/frontend-platform/i18n';
import { Alert, Button } from '@openedx/paragon';
import { useDispatch } from 'react-redux';
@@ -25,7 +24,8 @@ export const CERT_STATUS_TYPE = {
UNVERIFIED: 'unverified',
};
const CertificateStatusAlert = ({ intl, payload }) => {
const CertificateStatusAlert = ({ payload }) => {
const intl = useIntl();
const dispatch = useDispatch();
const {
certificateAvailableDate,
@@ -192,7 +192,6 @@ const CertificateStatusAlert = ({ intl, payload }) => {
};
CertificateStatusAlert.propTypes = {
intl: intlShape.isRequired,
payload: PropTypes.shape({
certificateAvailableDate: PropTypes.string,
certStatus: PropTypes.string,
@@ -210,4 +209,4 @@ CertificateStatusAlert.propTypes = {
}).isRequired,
};
export default injectIntl(CertificateStatusAlert);
export default CertificateStatusAlert;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { useIntl, 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,7 +14,8 @@ import outlineMessages from '../../messages';
import useEnrollClickHandler from '../../../../alerts/enrollment-alert/clickHook';
import { useModel } from '../../../../generic/model-store';
const PrivateCourseAlert = ({ intl, payload }) => {
const PrivateCourseAlert = ({ payload }) => {
const intl = useIntl();
const {
anonymousUser,
canEnroll,
@@ -103,7 +104,6 @@ const PrivateCourseAlert = ({ intl, payload }) => {
};
PrivateCourseAlert.propTypes = {
intl: intlShape.isRequired,
payload: PropTypes.shape({
anonymousUser: PropTypes.bool,
canEnroll: PropTypes.bool,
@@ -111,4 +111,4 @@ PrivateCourseAlert.propTypes = {
}).isRequired,
};
export default injectIntl(PrivateCourseAlert);
export default PrivateCourseAlert;

View File

@@ -1,15 +1,14 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import DateSummary from '../DateSummary';
import messages from '../messages';
import { useModel } from '../../../generic/model-store';
const CourseDates = ({
intl,
}) => {
const CourseDates = () => {
const intl = useIntl();
const {
courseId,
} = useSelector(state => state.courseHome);
@@ -48,8 +47,4 @@ const CourseDates = ({
);
};
CourseDates.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(CourseDates);
export default CourseDates;

View File

@@ -1,13 +1,14 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import LmsHtmlFragment from '../LmsHtmlFragment';
import messages from '../messages';
import { useModel } from '../../../generic/model-store';
const CourseHandouts = ({ intl }) => {
const CourseHandouts = () => {
const intl = useIntl();
const {
courseId,
} = useSelector(state => state.courseHome);
@@ -31,8 +32,4 @@ const CourseHandouts = ({ intl }) => {
);
};
CourseHandouts.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(CourseHandouts);
export default CourseHandouts;

View File

@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faBookmark, faCertificate, faInfo, faCalendar, faStar,
@@ -14,7 +14,8 @@ import messages from '../messages';
import { useModel } from '../../../generic/model-store';
import LaunchCourseHomeTourButton from '../../../product-tours/newUserCourseHomeTour/LaunchCourseHomeTourButton';
const CourseTools = ({ intl }) => {
const CourseTools = () => {
const intl = useIntl();
const {
courseId,
} = useSelector(state => state.courseHome);
@@ -81,8 +82,4 @@ const CourseTools = ({ intl }) => {
);
};
CourseTools.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(CourseTools);
export default CourseTools;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } 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,7 +53,6 @@ LearningGoalButton.propTypes = {
level: PropTypes.string.isRequired,
isSelected: PropTypes.bool.isRequired,
handleSelect: PropTypes.func.isRequired,
intl: intlShape.isRequired,
};
export default injectIntl(LearningGoalButton);
export default LearningGoalButton;

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import camelCase from 'lodash.camelcase';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import messages from '../messages';
@@ -10,7 +10,8 @@ import { getProctoringInfoData } from '../../data/api';
import { fetchProctoringInfoResolved } from '../../data/slice';
import { useModel } from '../../../generic/model-store';
const ProctoringInfoPanel = ({ intl }) => {
const ProctoringInfoPanel = () => {
const intl = useIntl();
const {
courseId,
} = useSelector(state => state.courseHome);
@@ -216,8 +217,4 @@ const ProctoringInfoPanel = ({ intl }) => {
);
};
ProctoringInfoPanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(ProctoringInfoPanel);
export default ProctoringInfoPanel;

View File

@@ -1,13 +1,14 @@
import React from 'react';
import { Button, Card } from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } 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 = ({ intl }) => {
const StartOrResumeCourseCard = () => {
const intl = useIntl();
const {
courseId,
} = useSelector(state => state.courseHome);
@@ -62,8 +63,4 @@ const StartOrResumeCourseCard = ({ intl }) => {
);
};
StartOrResumeCourseCard.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(StartOrResumeCourseCard);
export default StartOrResumeCourseCard;

View File

@@ -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 { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } 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,11 +152,10 @@ const WeeklyLearningGoalCard = ({
WeeklyLearningGoalCard.propTypes = {
daysPerWeek: PropTypes.number,
subscribedToReminders: PropTypes.bool,
intl: intlShape.isRequired,
};
WeeklyLearningGoalCard.defaultProps = {
daysPerWeek: null,
subscribedToReminders: false,
};
export default injectIntl(WeeklyLearningGoalCard);
export default WeeklyLearningGoalCard;

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Alert,
Button,
@@ -14,7 +14,8 @@ import { resetDeadlines } from '../data';
import { useModel } from '../../generic/model-store';
import messages from './messages';
const ShiftDatesAlert = ({ fetch, intl, model }) => {
const ShiftDatesAlert = ({ fetch, model }) => {
const intl = useIntl();
const {
courseId,
} = useSelector(state => state.courseHome);
@@ -59,8 +60,7 @@ const ShiftDatesAlert = ({ fetch, intl, model }) => {
ShiftDatesAlert.propTypes = {
fetch: PropTypes.func.isRequired,
intl: intlShape.isRequired,
model: PropTypes.string.isRequired,
};
export default injectIntl(ShiftDatesAlert);
export default ShiftDatesAlert;

View File

@@ -1,16 +1,15 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
const SuggestedScheduleHeader = ({ intl }) => (
<p className="large">
{intl.formatMessage(messages.suggestedSchedule)}
</p>
);
SuggestedScheduleHeader.propTypes = {
intl: intlShape.isRequired,
const SuggestedScheduleHeader = () => {
const intl = useIntl();
return (
<p className="large">
{intl.formatMessage(messages.suggestedSchedule)}
</p>
);
};
export default injectIntl(SuggestedScheduleHeader);
export default SuggestedScheduleHeader;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Alert,
Button,
@@ -12,7 +12,8 @@ import {
import { useModel } from '../../generic/model-store';
import messages from './messages';
const UpgradeToCompleteAlert = ({ intl, logUpgradeLinkClick }) => {
const UpgradeToCompleteAlert = ({ logUpgradeLinkClick }) => {
const intl = useIntl();
const {
courseId,
} = useSelector(state => state.courseHome);
@@ -58,7 +59,6 @@ const UpgradeToCompleteAlert = ({ intl, logUpgradeLinkClick }) => {
};
UpgradeToCompleteAlert.propTypes = {
intl: intlShape.isRequired,
logUpgradeLinkClick: PropTypes.func,
};
@@ -66,4 +66,4 @@ UpgradeToCompleteAlert.defaultProps = {
logUpgradeLinkClick: () => {},
};
export default injectIntl(UpgradeToCompleteAlert);
export default UpgradeToCompleteAlert;

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Alert,
Button,
@@ -13,7 +13,8 @@ import {
import { useModel } from '../../generic/model-store';
import messages from './messages';
const UpgradeToShiftDatesAlert = ({ intl, logUpgradeLinkClick, model }) => {
const UpgradeToShiftDatesAlert = ({ logUpgradeLinkClick, model }) => {
const intl = useIntl();
const {
courseId,
} = useSelector(state => state.courseHome);
@@ -60,7 +61,6 @@ const UpgradeToShiftDatesAlert = ({ intl, logUpgradeLinkClick, model }) => {
};
UpgradeToShiftDatesAlert.propTypes = {
intl: intlShape.isRequired,
logUpgradeLinkClick: PropTypes.func,
model: PropTypes.string.isRequired,
};
@@ -69,4 +69,4 @@ UpgradeToShiftDatesAlert.defaultProps = {
logUpgradeLinkClick: () => {},
};
export default injectIntl(UpgradeToShiftDatesAlert);
export default UpgradeToShiftDatesAlert;

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import classNames from 'classnames';
import messages from './messages';
@@ -9,8 +9,9 @@ import { CoursewareSearch, CoursewareSearchToggle } from '../course-home/coursew
import { useCoursewareSearchState } from '../course-home/courseware-search/hooks';
const CourseTabsNavigation = ({
activeTabSlug, className, tabs, intl,
activeTabSlug, className, tabs,
}) => {
const intl = useIntl();
const { show } = useCoursewareSearchState();
return (
@@ -51,7 +52,6 @@ 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 injectIntl(CourseTabsNavigation);
export default CourseTabsNavigation;

View File

@@ -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/extend-expect';
import '@testing-library/jest-dom';
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 waitFor(() => loadContainer());
const container = await 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 waitFor(() => loadContainer());
const container = await 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 waitFor(() => loadContainer());
const container = await 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 waitFor(() => loadContainer());
const container = await loadContainer();
assertLoadedHeader(container);
assertSequenceNavigation(container);
@@ -388,7 +388,7 @@ describe('CoursewareContainer', () => {
setUpMockRequests({ sequenceMetadatas: [sequenceMetadata] });
history.push(`/course/${courseId}/${sequenceBlock.id}`);
const container = await waitFor(() => loadContainer());
const container = await 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 waitFor(() => loadContainer());
const container = await loadContainer();
assertLoadedHeader(container);
assertSequenceNavigation(container);
@@ -421,7 +421,7 @@ describe('CoursewareContainer', () => {
});
history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[0].id}`);
const container = await waitFor(() => loadContainer());
const container = await loadContainer();
const sequenceNavButtons = container.querySelectorAll('nav.sequence-navigation a, nav.sequence-navigation button');
const sequenceNextButton = sequenceNavButtons[4];

View File

@@ -50,7 +50,11 @@ describe('Course', () => {
global.innerWidth = breakpoints.extraLarge.minWidth;
});
it('loads learning sequence', async () => {
// 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', () => {
render(<Course {...mockData} />, { wrapWithRouter: true });
expect(screen.queryByRole('navigation', { name: 'breadcrumb' })).not.toBeInTheDocument();
waitFor(() => {
@@ -94,7 +98,11 @@ describe('Course', () => {
expect(screen.queryByRole('navigation', { name: 'breadcrumb' })).not.toBeInTheDocument();
});
it('displays first section celebration modal', async () => {
// 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 () => {
const courseHomeMetadata = Factory.build('courseHomeMetadata', { celebrations: { firstSection: true } });
const testStore = await initializeTestStore({ courseHomeMetadata }, false);
const { courseware, models } = testStore.getState();
@@ -116,7 +124,11 @@ describe('Course', () => {
});
});
it('displays weekly goal celebration modal', async () => {
// 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 () => {
const courseHomeMetadata = Factory.build('courseHomeMetadata', { celebrations: { weeklyGoal: true } });
const testStore = await initializeTestStore({ courseHomeMetadata }, false);
const { courseware, models } = testStore.getState();
@@ -136,18 +148,6 @@ 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();

View File

@@ -1,134 +0,0 @@
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);
});
});

View File

@@ -0,0 +1,104 @@
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;

View File

@@ -1,107 +1,13 @@
import React, { useMemo, useState } from 'react';
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
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';
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,
};
import { useModel, useModels } from '../../../generic/model-store';
import BreadcrumbItem from './BreadcrumbItem';
const CourseBreadcrumbs = ({
courseId,
@@ -117,7 +23,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,
@@ -171,7 +77,7 @@ const CourseBreadcrumbs = ({
</Link>
</li>
{links.map((content, i) => (
<CourseBreadcrumb
<BreadcrumbItem
// eslint-disable-next-line react/no-array-index-key
key={i}
courseId={courseId}
@@ -179,6 +85,7 @@ const CourseBreadcrumbs = ({
content={content}
unitId={unitId}
withSeparator
separator="/"
isStaff={isStaff}
/>
))}

View File

@@ -0,0 +1,101 @@
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();
});
});
});

View File

@@ -0,0 +1,3 @@
import CourseBreadcrumbs from './CourseBreadcrumbs';
export default CourseBreadcrumbs;

View File

@@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
ActionRow,
breakpoints,
@@ -18,8 +18,9 @@ import { recordFirstSectionCelebration } from './utils';
import { useModel } from '../../../generic/model-store';
const CelebrationModal = ({
courseId, intl, isOpen, onClose, ...rest
courseId, isOpen, onClose, ...rest
}) => {
const intl = useIntl();
const { org, celebrations } = useModel('courseHomeMeta', courseId);
const dispatch = useDispatch();
const wideScreen = useWindowSize().width >= breakpoints.small.minWidth;
@@ -66,9 +67,8 @@ const CelebrationModal = ({
CelebrationModal.propTypes = {
courseId: PropTypes.string.isRequired,
intl: intlShape.isRequired,
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
};
export default injectIntl(CelebrationModal);
export default CelebrationModal;

View File

@@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import {
ActionRow, Button, Icon, StandardModal,
} from '@openedx/paragon';
@@ -12,8 +12,9 @@ import { recordWeeklyGoalCelebration } from './utils';
import { useModel } from '../../../generic/model-store';
const WeeklyGoalCelebrationModal = ({
courseId, daysPerWeek, intl, isOpen, onClose, ...rest
courseId, daysPerWeek, isOpen, onClose, ...rest
}) => {
const intl = useIntl();
const { org } = useModel('courseHomeMeta', courseId);
useEffect(() => {
@@ -77,9 +78,8 @@ 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 injectIntl(WeeklyGoalCelebrationModal);
export default WeeklyGoalCelebrationModal;

View File

@@ -4,7 +4,6 @@ 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';
@@ -80,4 +79,4 @@ Chat.defaultProps = {
enrollmentMode: null,
};
export default injectIntl(Chat);
export default Chat;

View File

@@ -1,403 +1,389 @@
import React, { Component } from 'react';
import React, { useState } from 'react';
import { Collapsible } from '@openedx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import {
FormattedMessage, injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } 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';
class Calculator extends Component {
constructor(props) {
super(props);
this.state = {
equation: '',
result: '',
};
this.handleSubmit = this.handleSubmit.bind(this);
}
const Calculator = () => {
const intl = useIntl();
const [equation, setEquation] = useState('');
const [result, setResult] = useState('');
async handleSubmit(event) {
const handleSubmit = async (event) => {
event.preventDefault();
event.stopPropagation();
const urlEncoded = new URLSearchParams();
urlEncoded.append('equation', this.state.equation);
urlEncoded.append('equation', equation);
const response = await getAuthenticatedHttpClient().get(
`${getConfig().LMS_BASE_URL}/calculate?${urlEncoded.toString()}`,
);
this.setState(() => ({ result: response.data.result }));
}
setResult(response.data.result);
};
changeEquation(value) {
this.setState(() => ({ equation: value }));
}
const changeEquation = (value) => {
setEquation(value);
};
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)}
/>
<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>
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' }}>
<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
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>
),
}}
id="calculator.instructions.button.label"
defaultMessage="Calculator Instructions"
/>
<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,
</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>
</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>
);
};
export default injectIntl(Calculator);
export default Calculator;

View File

@@ -1,11 +1,9 @@
import React, { Component } from 'react';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import {
injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import messages from './messages';
@@ -15,47 +13,41 @@ function toggleNotes() {
iframe.contentWindow.postMessage('tools.toggleNotes', getConfig().LMS_BASE_URL);
}
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 NotesVisibility = ({ course }) => {
const intl = useIntl();
const [visible, setVisible] = useState(course.notes.visible);
const visibilityUrl = `${getConfig().LMS_BASE_URL}/courses/${course.id}/edxnotes/visibility/`;
handleClick = () => {
const data = { visibility: !this.state.visible };
const handleClick = () => {
const data = { visibility: !visible };
getAuthenticatedHttpClient().put(
this.visibilityUrl,
visibilityUrl,
data,
).then(() => {
this.setState((state) => ({ visible: !state.visible }));
setVisible(!visible);
toggleNotes();
});
};
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>
);
}
}
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>
);
};
NotesVisibility.propTypes = {
intl: intlShape.isRequired,
course: PropTypes.shape({
id: PropTypes.string.isRequired,
notes: PropTypes.shape({
@@ -64,4 +56,4 @@ NotesVisibility.propTypes = {
}).isRequired,
};
export default injectIntl(NotesVisibility);
export default NotesVisibility;

View File

@@ -4,9 +4,7 @@ import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import {
FormattedMessage, injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@openedx/paragon';
import { faSearch } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -16,7 +14,8 @@ import { useModel } from '../../../generic/model-store';
import messages from './messages';
import { logClick } from './utils';
const CatalogSuggestion = ({ intl, variant }) => {
const CatalogSuggestion = ({ variant }) => {
const intl = useIntl();
const { courseId } = useSelector(state => state.courseware);
const { org } = useModel('courseHomeMeta', courseId);
const { administrator } = getAuthenticatedUser();
@@ -48,8 +47,7 @@ const CatalogSuggestion = ({ intl, variant }) => {
};
CatalogSuggestion.propTypes = {
intl: intlShape.isRequired,
variant: PropTypes.string.isRequired,
};
export default injectIntl(CatalogSuggestion);
export default CatalogSuggestion;

View File

@@ -2,9 +2,7 @@ import React, { useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faLinkedinIn } from '@fortawesome/free-brands-svg-icons';
import {
FormattedDate, FormattedMessage, injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
import { FormattedDate, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Helmet } from 'react-helmet';
import { useDispatch, useSelector } from 'react-redux';
import {
@@ -20,8 +18,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/edX_certificate.png';
import certificateLocked from '../../../generic/assets/edX_locked_certificate.png';
import certificate from '../../../generic/assets/openedx_certificate.png';
import certificateLocked from '../../../generic/assets/openedx_locked_certificate.png';
import { FormattedPricing } from '../../../generic/upgrade-button';
import messages from './messages';
import { useModel } from '../../../generic/model-store';
@@ -36,7 +34,8 @@ import CourseRecommendationsSlot from '../../../plugin-slots/CourseRecommendatio
const LINKEDIN_BLUE = '#2867B2';
const CourseCelebration = ({ intl }) => {
const CourseCelebration = () => {
const intl = useIntl();
const wideScreen = useWindowSize().width >= breakpoints.medium.minWidth;
const { courseId } = useSelector(state => state.courseware);
const dispatch = useDispatch();
@@ -364,8 +363,4 @@ const CourseCelebration = ({ intl }) => {
);
};
CourseCelebration.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(CourseCelebration);
export default CourseCelebration;

View File

@@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { useSelector } from 'react-redux';
import { Navigate } from 'react-router-dom';
@@ -15,7 +15,8 @@ import { unsubscribeFromGoalReminders } from './data/thunks';
import { useModel } from '../../../generic/model-store';
const CourseExit = ({ intl }) => {
const CourseExit = () => {
const intl = useIntl();
const { courseId } = useSelector(state => state.courseware);
const {
certificateData,
@@ -76,8 +77,4 @@ const CourseExit = ({ intl }) => {
);
};
CourseExit.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(CourseExit);
export default CourseExit;

View File

@@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Helmet } from 'react-helmet';
import { useSelector } from 'react-redux';
import { Alert, Button } from '@openedx/paragon';
@@ -14,7 +14,8 @@ import DashboardFootnote from './DashboardFootnote';
import messages from './messages';
import { logClick, logVisit } from './utils';
const CourseInProgress = ({ intl }) => {
const CourseInProgress = () => {
const intl = useIntl();
const { courseId } = useSelector(state => state.courseware);
const {
org,
@@ -60,8 +61,4 @@ const CourseInProgress = ({ intl }) => {
);
};
CourseInProgress.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(CourseInProgress);
export default CourseInProgress;

View File

@@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Helmet } from 'react-helmet';
import { useSelector } from 'react-redux';
import { Alert, Button } from '@openedx/paragon';
@@ -14,7 +14,8 @@ import DashboardFootnote from './DashboardFootnote';
import messages from './messages';
import { logClick, logVisit } from './utils';
const CourseNonPassing = ({ intl }) => {
const CourseNonPassing = () => {
const intl = useIntl();
const { courseId } = useSelector(state => state.courseware);
const {
org,
@@ -60,8 +61,4 @@ const CourseNonPassing = ({ intl }) => {
);
};
CourseNonPassing.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(CourseNonPassing);
export default CourseNonPassing;

View File

@@ -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, injectIntl, intlShape, defineMessages,
FormattedMessage, useIntl, 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 = injectIntl(CourseCard);
const IntlCard = CourseCard;
const CourseRecommendations = ({ intl, variant }) => {
const CourseRecommendations = ({ variant }) => {
const intl = useIntl();
const { courseId, recommendationsStatus } = useSelector(state => ({ ...state.recommendations, ...state.courseware }));
const { recommendations } = useModel('coursewareMeta', courseId);
const { org, number } = useModel('courseHomeMeta', courseId);
@@ -205,8 +205,7 @@ const CourseRecommendations = ({ intl, variant }) => {
};
CourseRecommendations.propTypes = {
intl: intlShape.isRequired,
variant: PropTypes.string.isRequired,
};
export default injectIntl(CourseRecommendations);
export default CourseRecommendations;

View File

@@ -3,9 +3,7 @@ import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import {
FormattedMessage, injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@openedx/paragon';
import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons';
import { getConfig } from '@edx/frontend-platform';
@@ -16,7 +14,8 @@ import Footnote from './Footnote';
import messages from './messages';
import { logClick } from './utils';
const DashboardFootnote = ({ intl, variant }) => {
const DashboardFootnote = ({ variant }) => {
const intl = useIntl();
const { courseId } = useSelector(state => state.courseware);
const { org } = useModel('courseHomeMeta', courseId);
const { administrator } = getAuthenticatedUser();
@@ -48,8 +47,7 @@ const DashboardFootnote = ({ intl, variant }) => {
};
DashboardFootnote.propTypes = {
intl: intlShape.isRequired,
variant: PropTypes.string.isRequired,
};
export default injectIntl(DashboardFootnote);
export default DashboardFootnote;

View File

@@ -2,9 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Alert, Button, Hyperlink } from '@openedx/paragon';
import certImage from '../../../generic/assets/edX_certificate.png';
import certImage from '../../../generic/assets/openedx_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,7 +98,6 @@ const ProgramCompletion = ({
};
ProgramCompletion.propTypes = {
intl: intlShape.isRequired,
progress: PropTypes.shape({
completed: PropTypes.number.isRequired,
inProgress: PropTypes.number.isRequired,
@@ -109,4 +108,4 @@ ProgramCompletion.propTypes = {
url: PropTypes.string.isRequired,
};
export default injectIntl(ProgramCompletion);
export default ProgramCompletion;

View File

@@ -3,9 +3,7 @@ import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import {
FormattedDate, FormattedMessage, injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
import { FormattedDate, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@openedx/paragon';
import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons';
@@ -14,7 +12,8 @@ import { logClick } from './utils';
import messages from './messages';
import { useModel } from '../../../generic/model-store';
const UpgradeFootnote = ({ deadline, href, intl }) => {
const UpgradeFootnote = ({ deadline, href }) => {
const intl = useIntl();
const { courseId } = useSelector(state => state.courseware);
const { org } = useModel('courseHomeMeta', courseId);
const { administrator } = getAuthenticatedUser();
@@ -60,7 +59,6 @@ const UpgradeFootnote = ({ deadline, href, intl }) => {
UpgradeFootnote.propTypes = {
deadline: PropTypes.instanceOf(Date).isRequired,
href: PropTypes.string.isRequired,
intl: intlShape.isRequired,
};
export default injectIntl(UpgradeFootnote);
export default UpgradeFootnote;

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } 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,11 +155,10 @@ const CourseLicense = ({
CourseLicense.propTypes = {
license: PropTypes.string,
intl: intlShape.isRequired,
};
CourseLicense.defaultProps = {
license: 'all-rights-reserved',
};
export default injectIntl(CourseLicense);
export default CourseLicense;

View File

@@ -9,7 +9,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { breakpoints } from '@openedx/paragon';
import {
initializeMockApp, render, screen, within, act, fireEvent, waitFor,
initializeMockApp, render, screen, act, fireEvent, waitFor,
} from '../../../../../../setupTest';
import initializeStore from '../../../../../../store';
import { appendBrowserTimezoneToUrl, executeThunk } from '../../../../../../utils';
@@ -90,28 +90,7 @@ describe('NotificationsWidget', () => {
<NotificationsWidget />
</SidebarContext.Provider>,
);
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();
expect(screen.getByTestId('org.openedx.frontend.learning.notification_widget.v1')).toBeInTheDocument();
});
it('renders no notifications bar if no verified mode', async () => {
@@ -130,44 +109,6 @@ 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> = {

View File

@@ -1,11 +1,10 @@
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 {
@@ -21,17 +20,11 @@ const NotificationsWidget = () => {
const course = useModel('coursewareMeta', courseId);
const {
accessExpiration,
contentTypeGatingEnabled,
end,
enrollmentEnd,
enrollmentMode,
enrollmentStart,
marketingUrl,
offer,
start,
timeOffsetMillis,
userTimezone,
verificationStatus,
} = course;
@@ -74,32 +67,12 @@ const NotificationsWidget = () => {
return (
<div className="border border-light-400 rounded-sm" data-testid="notification-widget">
<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>
<NotificationWidgetSlot
courseId={courseId}
notificationCurrentState={upgradeNotificationCurrentState}
setNotificationCurrentState={setUpgradeNotificationCurrentState}
toggleSidebar={onToggleSidebar}
/>
</div>
);
};

View File

@@ -154,7 +154,7 @@ describe('Sequence', () => {
testStore.dispatch(fetchSequenceFailure({ sequenceId: mockData.sequenceId }));
render(<Sequence {...mockData} />, { store: testStore, wrapWithRouter: true });
expect(screen.getByText('There was an error loading this course.')).toBeInTheDocument();
await screen.findByText('There was an error loading this course.');
});
it('handles loading unit', async () => {

View File

@@ -4,9 +4,8 @@ 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 { PluginSlot } from '@openedx/frontend-plugin-framework';
import { ContentIFrameLoaderSlot } from '../../../../plugin-slots/ContentIFrameLoaderSlot';
import PageLoading from '@src/generic/PageLoading';
import * as hooks from './hooks';
/**
@@ -84,17 +83,7 @@ const ContentIFrame = ({
return (
<>
{(shouldShowContent && !hasLoaded) && (
showError ? <ErrorPage /> : (
<PluginSlot
id="content_iframe_loader_slot"
pluginProps={{
defaultLoaderComponent: <PageLoading srMessage={loadingMessage} />,
courseId,
}}
>
<PageLoading srMessage={loadingMessage} />
</PluginSlot>
)
showError ? <ErrorPage /> : <ContentIFrameLoaderSlot courseId={courseId} loadingMessage={loadingMessage} />
)}
{shouldShowContent && (
<div className="unit-iframe-wrapper">

View File

@@ -6,6 +6,7 @@ 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';
@@ -99,8 +100,8 @@ describe('ContentIFrame Component', () => {
});
it('displays PageLoading component if not showError', () => {
el = shallow(<ContentIFrame {...props} />);
[component] = el.instance.findByType(PageLoading);
expect(component.props.srMessage).toEqual(props.loadingMessage);
[component] = el.instance.findByType(ContentIFrameLoaderSlot);
expect(component.props.loadingMessage).toEqual(props.loadingMessage);
});
});
describe('hasLoaded', () => {

View File

@@ -2,14 +2,13 @@ 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';
@@ -29,14 +28,7 @@ const UnitSuspense = ({
<>
{shouldDisplayContentGating && (
<Suspense fallback={<PageLoading srMessage={formatMessage(messages.loadingLockedContent)} />}>
<PluginSlot
id="gated_unit_content_message_slot"
pluginProps={{
courseId,
}}
>
<LockPaywall courseId={courseId} />
</PluginSlot>
<GatedUnitContentMessageSlot courseId={courseId} />
</Suspense>
)}
{shouldDisplayHonorCode && (

View File

@@ -5,6 +5,7 @@ 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';
@@ -78,10 +79,9 @@ 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(LockPaywall);
expect(component.parent.type).toEqual('PluginSlot');
expect(component.parent.parent.type).toEqual('Suspense');
expect(component.parent.parent.props.fallback)
const [component] = el.instance.findByType(GatedUnitContentMessageSlot);
expect(component.parent.type).toEqual('Suspense');
expect(component.parent.props.fallback)
.toEqual(<PageLoading srMessage={formatMessage(messages.loadingLockedContent)} />);
expect(component.props.courseId).toEqual(props.courseId);
});

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import { logError } from '@edx/frontend-platform/logging';
import { act, renderHook } from '@testing-library/react-hooks';
import { act, renderHook, waitFor } from '@testing-library/react';
import { useExamAccessToken, useFetchExamAccessToken, useIsExam } from '@edx/frontend-lib-special-exams';
import { initializeMockApp } from '../../../../../setupTest';
@@ -64,24 +64,27 @@ describe('useExamAccess hook', () => {
it('returns true for blockAccess if an exam but accessToken not yet fetched', async () => {
useIsExam.mockImplementation(() => mockUseIsExam(true));
const { result, waitForNextUpdate } = renderHook(() => useExamAccess({ id }));
const { result } = renderHook(() => useExamAccess({ id }));
const { accessToken, blockAccess } = result.current;
expect(accessToken).toEqual('');
expect(blockAccess).toBe(true);
expect(mockFetchExamAccessToken).toHaveBeenCalled();
// This is to get rid of the act(...) warning.
await act(async () => {
await waitForNextUpdate();
await waitFor(() => {
expect(result.current).toBeTruthy();
expect(result.current?.isFetching).toBeFalsy();
});
});
it('returns false for blockAccess if an exam and accessToken fetch succeeds', async () => {
useIsExam.mockImplementation(() => mockUseIsExam(true));
const { result, waitForNextUpdate } = renderHook(() => useExamAccess({ id }));
const { result } = renderHook(() => useExamAccess({ id }));
// We wait for the promise to resolve and for updates to state to complete so that blockAccess is updated.
await waitForNextUpdate();
await waitFor(() => {
expect(result.current).toBeTruthy();
expect(result.current?.isFetching).toBeFalsy();
});
const { accessToken, blockAccess } = result.current;
@@ -90,7 +93,7 @@ describe('useExamAccess hook', () => {
expect(mockFetchExamAccessToken).toHaveBeenCalled();
});
it('in progress', async () => {
const { result, waitForNextUpdate } = renderHook(() => useExamAccess({ id }));
const { result } = renderHook(() => useExamAccess({ id }));
let { accessToken, blockAccess } = result.current;
@@ -104,7 +107,10 @@ describe('useExamAccess hook', () => {
// wait for call to setBlockAccess in the finally clause of useEffect hook.
await act(async () => {
jest.runAllTimers();
await waitForNextUpdate();
await waitFor(() => {
expect(result.current).toBeTruthy();
expect(result.current?.isFetching).toBeFalsy();
});
});
({ accessToken, blockAccess } = result.current);
@@ -119,17 +125,22 @@ describe('useExamAccess hook', () => {
const testError = 'test-error';
mockFetchExamAccessToken.mockImplementationOnce(() => Promise.reject(testError));
const { result, waitForNextUpdate } = renderHook(() => useExamAccess({ id }));
const { result } = renderHook(() => useExamAccess({ id }));
// We wait for the promise to resolve and for updates to state to complete so that blockAccess is updated.
await waitForNextUpdate();
await waitFor(() => {
expect(result.current).toBeTruthy();
expect(result.current?.isFetching).toBeFalsy();
});
const { accessToken, blockAccess } = result.current;
expect(accessToken).toEqual(testAccessToken.curr);
expect(blockAccess).toBe(false);
expect(mockFetchExamAccessToken).toHaveBeenCalled();
expect(logError).toHaveBeenCalledWith(testError);
await waitFor(() => {
expect(logError).toHaveBeenCalledWith(testError);
});
});
});
});

View File

@@ -57,7 +57,7 @@ describe('<Unit />', () => {
describe('unit title', () => {
it('has two children', () => {
renderComponent(defaultProps);
const unitTitleWrapper = screen.getByTestId('unit_title_slot').children[0];
const unitTitleWrapper = screen.getByTestId('org.openedx.frontend.learning.unit_title.v1').children[0];
expect(unitTitleWrapper.children).toHaveLength(3);
});

View File

@@ -3,14 +3,15 @@ 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 { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import messages from './messages';
const ContentLock = ({
intl, courseId, prereqSectionName, prereqId, sequenceTitle,
courseId, prereqSectionName, prereqId, sequenceTitle,
}) => {
const intl = useIntl();
const navigate = useNavigate();
const handleClick = useCallback(() => {
navigate(`/course/${courseId}/${prereqId}`);
@@ -36,10 +37,9 @@ 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 injectIntl(ContentLock);
export default ContentLock;

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Alert, Hyperlink } from '@openedx/paragon';
import { Info } from '@openedx/paragon/icons';
@@ -8,7 +8,8 @@ import { useModel } from '../../../../generic/model-store';
import messages from './messages';
const HiddenAfterDue = ({ courseId, intl }) => {
const HiddenAfterDue = ({ courseId }) => {
const intl = useIntl();
const { tabs } = useModel('courseHomeMeta', courseId);
const progressTab = tabs.find(tab => tab.slug === 'progress');
@@ -44,8 +45,7 @@ const HiddenAfterDue = ({ courseId, intl }) => {
};
HiddenAfterDue.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
};
export default injectIntl(HiddenAfterDue);
export default HiddenAfterDue;

View File

@@ -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, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { ActionRow, Alert, Button } from '@openedx/paragon';
import { useNavigate } from 'react-router-dom';
@@ -11,7 +11,8 @@ import { useModel } from '../../../../generic/model-store';
import { saveIntegritySignature } from '../../../data';
import messages from './messages';
const HonorCode = ({ intl, courseId }) => {
const HonorCode = ({ courseId }) => {
const intl = useIntl();
const navigate = useNavigate();
const dispatch = useDispatch();
const {
@@ -68,8 +69,7 @@ const HonorCode = ({ intl, courseId }) => {
};
HonorCode.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
};
export default injectIntl(HonorCode);
export default HonorCode;

View File

@@ -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 { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } 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/edX_locked_certificate.png';
import certificateLocked from '../../../../generic/assets/openedx_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,7 +143,6 @@ const LockPaywall = ({
);
};
LockPaywall.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
};
export default injectIntl(LockPaywall);
export default LockPaywall;

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Factory } from 'rosie';
import { act, fireEvent, getAllByRole } from '@testing-library/react';
import { getAllByRole } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { initializeTestStore, render, screen } from '../../../../setupTest';
import SequenceNavigationTabs from './SequenceNavigationTabs';
@@ -48,17 +49,18 @@ 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;
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);
});
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 dropdownMenu = container.querySelector('.dropdown');
const dropdownButtons = getAllByRole(dropdownMenu, 'link');
expect(dropdownButtons).toHaveLength(unitBlocks.length);

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } 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,7 +59,6 @@ const UnitNavigationEffortEstimate = ({
UnitNavigationEffortEstimate.propTypes = {
children: PropTypes.node,
intl: intlShape.isRequired,
sequenceId: PropTypes.string.isRequired,
unitId: PropTypes.string,
};
@@ -69,4 +68,4 @@ UnitNavigationEffortEstimate.defaultProps = {
unitId: null,
};
export default injectIntl(UnitNavigationEffortEstimate);
export default UnitNavigationEffortEstimate;

View File

@@ -47,8 +47,7 @@ const NextButton = ({
if (isAtTop) {
return (
<IconButton
variant="light"
className={buttonStyle}
className={`${buttonStyle} icon-hover`}
onClick={onClick}
src={nextArrow}
disabled={disabled}

View File

@@ -40,8 +40,7 @@ const PreviousButton = ({
if (isAtTop) {
return (
<IconButton
variant="light"
className={buttonStyle}
className={`${buttonStyle} icon-hover`}
onClick={onClickHandler}
src={prevArrow}
disabled={disabled}

View File

@@ -1,4 +1,4 @@
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon, IconButton } from '@openedx/paragon';
import { ArrowBackIos, Close } from '@openedx/paragon/icons';
import classNames from 'classnames';
@@ -9,7 +9,6 @@ import messages from '../../messages';
import SidebarContext from '../SidebarContext';
const SidebarBase = ({
intl,
title,
ariaLabel,
sidebarId,
@@ -18,6 +17,7 @@ const SidebarBase = ({
showTitleBar,
width,
}) => {
const intl = useIntl();
const {
toggleSidebar,
shouldDisplayFullScreen,
@@ -87,7 +87,6 @@ const SidebarBase = ({
};
SidebarBase.propTypes = {
intl: intlShape.isRequired,
title: PropTypes.string.isRequired,
ariaLabel: PropTypes.string.isRequired,
sidebarId: PropTypes.string.isRequired,
@@ -102,4 +101,4 @@ SidebarBase.defaultProps = {
showTitleBar: true,
};
export default injectIntl(SidebarBase);
export default SidebarBase;

View File

@@ -1,4 +1,3 @@
import { injectIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import React from 'react';
@@ -25,4 +24,4 @@ SidebarTriggerBase.propTypes = {
children: PropTypes.element.isRequired,
};
export default injectIntl(SidebarTriggerBase);
export default SidebarTriggerBase;

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import classNames from 'classnames';
import { Button, useToggle, IconButton } from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
MenuOpen as MenuOpenIcon,
ChevronLeft as ChevronLeftIcon,
@@ -16,7 +16,8 @@ import { ID } from './constants';
import { useCourseOutlineSidebar } from './hooks';
import messages from './messages';
const CourseOutlineTray = ({ intl }) => {
const CourseOutlineTray = () => {
const intl = useIntl();
const [selectedSection, setSelectedSection] = useState(null);
const [isDisplaySequenceLevel, setDisplaySequenceLevel, setDisplaySectionLevel] = useToggle(true);
@@ -131,10 +132,6 @@ const CourseOutlineTray = ({ intl }) => {
);
};
CourseOutlineTray.propTypes = {
intl: intlShape.isRequired,
};
CourseOutlineTray.ID = ID;
export default injectIntl(CourseOutlineTray);
export default CourseOutlineTray;

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { IconButton } from '@openedx/paragon';
import { MenuOpen as MenuOpenIcon } from '@openedx/paragon/icons';
@@ -8,7 +8,8 @@ import { useCourseOutlineSidebar } from './hooks';
import { ID } from './constants';
import messages from './messages';
const CourseOutlineTrigger = ({ intl, isMobileView }) => {
const CourseOutlineTrigger = ({ isMobileView }) => {
const intl = useIntl();
const {
currentSidebar,
shouldDisplayFullScreen,
@@ -45,8 +46,7 @@ CourseOutlineTrigger.defaultProps = {
};
CourseOutlineTrigger.propTypes = {
intl: intlShape.isRequired,
isMobileView: PropTypes.bool,
};
export default injectIntl(CourseOutlineTrigger);
export default CourseOutlineTrigger;

View File

@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Icon } from '@openedx/paragon';
import { ChevronRight as ChevronRightIcon } from '@openedx/paragon/icons';
@@ -8,7 +8,8 @@ import courseOutlineMessages from '@src/course-home/outline-tab/messages';
import CompletionIcon from './CompletionIcon';
import { useCourseOutlineSidebar } from '../hooks';
const SidebarSection = ({ intl, section, handleSelectSection }) => {
const SidebarSection = ({ section, handleSelectSection }) => {
const intl = useIntl();
const {
id,
complete,
@@ -54,7 +55,6 @@ const SidebarSection = ({ intl, 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 injectIntl(SidebarSection);
export default SidebarSection;

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } 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,7 +78,6 @@ const SidebarSequence = ({
};
SidebarSequence.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
defaultOpen: PropTypes.bool.isRequired,
sequence: PropTypes.shape({
@@ -96,4 +95,4 @@ SidebarSequence.propTypes = {
activeUnitId: PropTypes.string.isRequired,
};
export default injectIntl(SidebarSequence);
export default SidebarSequence;

View File

@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from '../messages';
import UnitIcon, { UNIT_ICON_TYPES } from './UnitIcon';
@@ -8,7 +8,6 @@ import UnitLinkWrapper from './UnitLinkWrapper';
const SidebarUnit = ({
id,
intl,
courseId,
sequenceId,
isFirst,
@@ -17,6 +16,7 @@ const SidebarUnit = ({
isLocked,
activeUnitId,
}) => {
const intl = useIntl();
const {
complete,
title,
@@ -52,7 +52,6 @@ const SidebarUnit = ({
};
SidebarUnit.propTypes = {
intl: intlShape.isRequired,
id: PropTypes.string.isRequired,
isFirst: PropTypes.bool.isRequired,
unit: PropTypes.shape({
@@ -69,4 +68,4 @@ SidebarUnit.propTypes = {
activeUnitId: PropTypes.string.isRequired,
};
export default injectIntl(SidebarUnit);
export default SidebarUnit;

View File

@@ -36,12 +36,12 @@ describe('<SidebarUnit />', () => {
};
};
function renderWithProvider(props = {}, sidebarContext = defaultSidebarContext) {
function renderWithProvider(props = {}, sidebarContext = defaultSidebarContext, pathname = '/course') {
const { container } = render(
<AppProvider store={store} wrapWithRouter={false}>
<IntlProvider locale="en">
<SidebarContext.Provider value={{ ...sidebarContext }}>
<MemoryRouter>
<MemoryRouter initialEntries={[{ pathname }]}>
<SidebarUnit
isFirst
id={unit.id}
@@ -138,4 +138,34 @@ 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();
});
});
});
});

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom';
import { useCourseOutlineSidebar } from '../hooks';
@@ -27,10 +27,14 @@ 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={`/course/${courseId}/${sequenceId}/${id}`}
to={link}
className="row w-100 m-0 d-flex align-items-center text-gray-700"
onClick={() => handleUnitClick({ sequenceId, activeUnitId, id })}
>

View File

@@ -1,7 +1,7 @@
import { useContext } from 'react';
import classNames from 'classnames';
import { ensureConfig, getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useModel } from '@src/generic/model-store';
import SidebarBase from '../../common/SidebarBase';
@@ -12,7 +12,8 @@ import messages from './messages';
ensureConfig(['DISCUSSIONS_MFE_BASE_URL']);
const DiscussionsSidebar = ({ intl }) => {
const DiscussionsSidebar = () => {
const intl = useIntl();
const {
unitId,
courseId,
@@ -47,11 +48,7 @@ const DiscussionsSidebar = ({ intl }) => {
);
};
DiscussionsSidebar.propTypes = {
intl: intlShape.isRequired,
};
DiscussionsSidebar.Trigger = DiscussionsSidebar;
DiscussionsSidebar.ID = ID;
export default injectIntl(DiscussionsSidebar);
export default DiscussionsSidebar;

View File

@@ -1,5 +1,5 @@
import { ensureConfig, getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } 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,8 +51,7 @@ const DiscussionsTrigger = ({
};
DiscussionsTrigger.propTypes = {
intl: intlShape.isRequired,
onClick: PropTypes.func.isRequired,
};
export default injectIntl(DiscussionsTrigger);
export default DiscussionsTrigger;

View File

@@ -1,4 +1,4 @@
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon } from '@openedx/paragon';
import { WatchOutline } from '@openedx/paragon/icons';
import classNames from 'classnames';
@@ -8,35 +8,36 @@ import React from 'react';
import messages from '../../../messages';
const NotificationIcon = ({
intl,
status,
notificationColor,
}) => (
<>
<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}
</>
);
}) => {
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}
</>
);
};
NotificationIcon.defaultProps = {
status: null,
};
NotificationIcon.propTypes = {
intl: intlShape.isRequired,
status: PropTypes.string,
notificationColor: PropTypes.string.isRequired,
};
export default injectIntl(NotificationIcon);
export default NotificationIcon;

View File

@@ -1,18 +1,18 @@
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } 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 UpgradeNotification from '../../../../../generic/upgrade-notification/UpgradeNotification';
import { NotificationTraySlot } from '../../../../../plugin-slots/NotificationTraySlot';
import messages from '../../../messages';
import SidebarBase from '../../common/SidebarBase';
import SidebarContext from '../../SidebarContext';
import NotificationTrigger, { ID } from './NotificationTrigger';
const NotificationTray = ({ intl }) => {
const NotificationTray = () => {
const intl = useIntl();
const {
courseId,
onNotificationSeen,
@@ -23,17 +23,11 @@ const NotificationTray = ({ intl }) => {
const course = useModel('coursewareMeta', courseId);
const {
accessExpiration,
contentTypeGatingEnabled,
end,
enrollmentEnd,
enrollmentMode,
enrollmentStart,
marketingUrl,
offer,
start,
timeOffsetMillis,
userTimezone,
verificationStatus,
} = course;
@@ -82,31 +76,11 @@ const NotificationTray = ({ intl }) => {
>
<div>{verifiedMode
? (
<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>
<NotificationTraySlot
courseId={courseId}
notificationCurrentState={upgradeNotificationCurrentState}
setNotificationCurrentState={setUpgradeNotificationCurrentState}
/>
) : (
<p className="p-3 small">{intl.formatMessage(messages.noNotificationsMessage)}</p>
)}
@@ -115,11 +89,7 @@ const NotificationTray = ({ intl }) => {
);
};
NotificationTray.propTypes = {
intl: intlShape.isRequired,
};
NotificationTray.Trigger = NotificationTrigger;
NotificationTray.ID = ID;
export default injectIntl(NotificationTray);
export default NotificationTray;

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