Compare commits

..

1 Commits

Author SHA1 Message Date
Marcos
1a5ae3939b feat: Updated Courseware Search results for SR 2025-03-24 17:28:47 -03:00
177 changed files with 6596 additions and 6905 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://help.edx.org/edxlearner/s/article/Can-I-receive-college-credit-or-credit-hours-for-my-course'
CREDIT_HELP_LINK_URL='https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_credit_courses.html#keep-track-of-credit-requirements'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
DISCOVERY_API_BASE_URL='http://localhost:18381'
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'

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

View File

@@ -41,8 +41,9 @@ Cloning and Setup
git clone https://github.com/openedx/frontend-app-learning.git
2. Use the version of Node specified in ``.nvmrc``.
2. Use node v20.x.
The current version of the micro-frontend build scripts supports node 18.
Using other major versions of node *may* work, but this is unsupported. For
convenience, this repository includes an ``.nvmrc`` file to help in setting the
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
@@ -130,7 +131,7 @@ Deployment
The Learning MFE is similar to all the other Open edX MFEs. Read the Open
edX Developer Guide's section on
`MFE applications <https://openedx.github.io/frontend-platform/>`_.
`MFE applications <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html>`_.
Plugins
=======
@@ -144,7 +145,7 @@ Environment Variables
This MFE is configured via environment variables supplied at build time.
All micro-frontends have a shared set of required environment variables,
as documented in the Open edX Developer Guide under
`Required Environment Variables <https://openedx.github.io/frontend-platform/>`_.
`Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`_.
The learning micro-frontend also supports the following additional variables:

7954
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,7 @@
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx .",
"postinstall": "patch-package",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/learning/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
@@ -34,20 +35,20 @@
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "1.5.0",
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^6.2.0",
"@edx/frontend-lib-learning-assistant": "^2.20.0",
"@edx/frontend-lib-special-exams": "^3.5.0",
"@edx/frontend-platform": "^8.3.1",
"@edx/frontend-component-header": "^5.8.0",
"@edx/frontend-lib-learning-assistant": "^2.19.2",
"@edx/frontend-lib-special-exams": "^3.1.3",
"@edx/frontend-platform": "^8.0.0",
"@edx/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "^4.0.0",
"@edx/react-unit-test-utils": "3.0.0",
"@fortawesome/free-brands-svg-icons": "5.15.4",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "^0.1.4",
"@openedx/frontend-build": "^14.5.0",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^22.16.0",
"@openedx/frontend-build": "^14.2.0",
"@openedx/frontend-plugin-framework": "^1.2.1",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.3.0",
"@popperjs/core": "2.11.8",
"@reduxjs/toolkit": "1.9.7",
"buffer": "^6.0.3",
@@ -57,11 +58,12 @@
"js-cookie": "3.0.5",
"lodash": "^4.17.21",
"lodash.camelcase": "4.3.0",
"patch-package": "^8.0.0",
"postcss-loader": "^8.1.1",
"prop-types": "15.8.1",
"query-string": "^7.1.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-helmet": "6.1.0",
"react-redux": "7.2.9",
"react-router": "6.15.0",
@@ -77,8 +79,9 @@
"devDependencies": {
"@edx/reactifex": "2.2.0",
"@pact-foundation/pact": "^13.0.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "14.6.1",
"axios-mock-adapter": "2.1.0",
"bundlewatch": "^0.4.0",
@@ -92,7 +95,7 @@
"files": [
{
"path": "dist/*.js",
"maxSize": "1450kB"
"maxSize": "1400kB"
}
],
"normalizeFilenames": "^.+?(\\..+?)\\.\\w+$"

View File

@@ -0,0 +1,36 @@
diff --git a/node_modules/@openedx/frontend-build/config/webpack.prod.config.js b/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
index 2879dd9..9efd0fc 100644
--- a/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
+++ b/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
@@ -12,6 +12,7 @@ const NewRelicSourceMapPlugin = require('@edx/new-relic-source-map-webpack-plugi
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');
+const fs = require('fs');
const PostCssAutoprefixerPlugin = require('autoprefixer');
const PostCssRTLCSS = require('postcss-rtlcss');
const PostCssCustomMediaCSS = require('postcss-custom-media');
@@ -23,6 +24,23 @@ const HtmlWebpackNewRelicPlugin = require('../lib/plugins/html-webpack-new-relic
const commonConfig = require('./webpack.common.config');
const presets = require('../lib/presets');
+/**
+ * This condition confirms whether the configuration for the MFE has switched to a JS-based configuration
+ * as previously implemented in frontend-build and frontend-platform. If the environment variable JS_CONFIG_FILEPATH
+ * exists, then an env.config.js(x) file will be copied from the location referenced by the environment variable to the
+ * root directory. Its env variables can be accessed with getConfig().
+ *
+ * https://github.com/openedx/frontend-build/blob/master/docs/0002-js-environment-config.md
+ * https://github.com/openedx/frontend-platform/blob/master/docs/decisions/0007-javascript-file-configuration.rst
+ */
+
+const envConfigPath = process.env.JS_CONFIG_FILEPATH;
+
+if (envConfigPath) {
+ const envConfigFilename = envConfigPath.slice(envConfigPath.indexOf('env.config'));
+ fs.copyFileSync(envConfigPath, envConfigFilename);
+}
+
// Add process env vars. Currently used only for setting the PUBLIC_PATH.
dotenv.config({
path: path.resolve(process.cwd(), '.env'),

View File

@@ -1,224 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
<React Strict Mode>
<ErrorPage
message="test-error-message"
/>
</React Strict Mode>
`;
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
<React Strict Mode>
<AppProvider
store={
{
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
}
}
>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
>
<link
href="favicon-url"
rel="shortcut icon"
type="image/x-icon"
/>
</HelmetWrapper>
<PathFixesProvider>
<NoticesProvider>
<UserMessagesProvider>
<Routes>
<Route
element={
<PageWrap>
<Page Not Found />
</PageWrap>
}
path="*"
/>
<Route
element={
<PageWrap>
<Goal Unsubscribe />
</PageWrap>
}
path="/goal-unsubscribe/:token"
/>
<Route
element={
<PageWrap>
<Courseware Redirect Landing Page />
</PageWrap>
}
path="/redirect/*"
/>
<Route
element={
<PageWrap>
<Preferences Unsubscribe />
</PageWrap>
}
path="/preferences-unsubscribe/:userToken/:updatePatch"
/>
<Route
element={
<DecodePageRoute>
<Course Access Error Page />
</DecodePageRoute>
}
path="/course/:courseId/access-denied"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseHome"
tab="outline"
>
<Outline Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/home"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseHome"
tab="lti_live"
>
<Live Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/live"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseHome"
tab="dates"
>
<Dates Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/dates"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseHome"
tab="discussion"
>
<Discussion Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/discussion/:path/*"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
isProgressTab={true}
slice="courseHome"
tab="progress"
>
<Progress Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/progress/:targetUserId/"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
isProgressTab={true}
slice="courseHome"
tab="progress"
>
<Progress Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/progress"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseware"
tab="courseware"
>
<Course Exit />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/course-end"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/course/:courseId/:sequenceId/:unitId"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/course/:courseId/:sequenceId"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/course/:courseId"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/preview/course/:courseId/:sequenceId/:unitId"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/preview/course/:courseId/:sequenceId"
/>
</Routes>
</UserMessagesProvider>
</NoticesProvider>
</PathFixesProvider>
</AppProvider>
</React Strict Mode>
`;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,13 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
import { Alert, Hyperlink } from '@openedx/paragon';
import { WarningFilled } from '@openedx/paragon/icons';
import genericMessages from '../../generic/messages';
const LogistrationAlert = () => {
const intl = useIntl();
const LogistrationAlert = ({ intl }) => {
const signIn = (
<Hyperlink
style={{ textDecoration: 'underline' }}
@@ -44,4 +43,8 @@ const LogistrationAlert = () => {
);
};
export default LogistrationAlert;
LogistrationAlert.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(LogistrationAlert);

View File

@@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Tabs, Tab } from '@openedx/paragon';
import { useParams } from 'react-router';
@@ -13,8 +13,7 @@ const filterTypes = ['text', 'video', 'sequence'];
const filterOther = 'other';
const validFilters = [filterAll, ...filterTypes, filterOther];
export const CoursewareSearchResultsFilter = () => {
const intl = useIntl();
export const CoursewareSearchResultsFilter = ({ intl }) => {
const { courseId } = useParams();
const lastSearch = useModel('contentSearchResults', courseId);
const { filter: filterKeyword, setFilter } = useCoursewareSearchParams();
@@ -64,6 +63,7 @@ export const CoursewareSearchResultsFilter = () => {
variant="tabs"
activeKey={activeKey}
onSelect={setFilter}
aria-label={intl.formatMessage(messages.searchResultsFilterDescription)}
>
{filters.filter(({ count }) => (count > 0)).map(({ key, label }) => (
<Tab key={key} eventKey={key} title={label} data-testid={`courseware-search-results-tabs-${key}`}>
@@ -74,4 +74,8 @@ export const CoursewareSearchResultsFilter = () => {
);
};
export default CoursewareSearchResultsFilter;
CoursewareSearchResultsFilter.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(CoursewareSearchResultsFilter);

View File

@@ -141,7 +141,15 @@ const CoursewareSearch = ({ ...sectionProps }) => {
</Button>
</div>
</div>
<div className="courseware-search__results" aria-live="polite" data-testid="courseware-search-results">
<div
key={status}
className="courseware-search__results"
aria-live="assertive"
aria-atomic="true"
aria-busy={status === 'loading'}
data-testid="courseware-search-results"
role={status === 'results' ? 'alert' : 'none'}
>
{status === 'loading' ? (
<div className="courseware-search__spinner" data-testid="courseware-search-spinner">
<Spinner animation="border" variant="light" screenReaderText={formatMessage(messages.loading)} />
@@ -157,8 +165,6 @@ const CoursewareSearch = ({ ...sectionProps }) => {
{total > 0 ? (
<div
className="courseware-search__results-summary"
aria-relevant="all"
aria-atomic="true"
data-testid="courseware-search-summary"
>{formatMessage(messages.searchResultsLabel, { total, keyword: lastSearchKeyword })}
</div>

View File

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

View File

@@ -1,14 +1,15 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './messages';
const CoursewareSearchEmpty = () => {
const intl = useIntl();
return (
<div className="courseware-search-results">
<p className="courseware-search-results__empty" data-testid="no-results">{intl.formatMessage(messages.searchResultsNone)}</p>
</div>
);
const CoursewareSearchEmpty = ({ intl }) => (
<div className="courseware-search-results">
<p className="courseware-search-results__empty" data-testid="no-results">{intl.formatMessage(messages.searchResultsNone)}</p>
</div>
);
CoursewareSearchEmpty.propTypes = {
intl: intlShape.isRequired,
};
export default CoursewareSearchEmpty;
export default injectIntl(CoursewareSearchEmpty);

View File

@@ -1,4 +1,4 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Folder, TextFields, VideoCamera, Article,
} from '@openedx/paragon/icons';
@@ -6,6 +6,7 @@ import { getConfig } from '@edx/frontend-platform';
import { Icon } from '@openedx/paragon';
import PropTypes from 'prop-types';
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
import messages from './messages';
const iconTypeMapping = {
text: TextFields,
@@ -21,6 +22,8 @@ const CoursewareSearchResults = ({ results = [] }) => {
return <CoursewareSearchEmpty />;
}
const { formatMessage } = useIntl();
const baseUrl = `${getConfig().LMS_BASE_URL}`;
return (
@@ -42,24 +45,30 @@ const CoursewareSearchResults = ({ results = [] }) => {
rel: 'nofollow',
} : { href: `${baseUrl}${url}` };
const ariaSeaparator = formatMessage(messages.searchResultsBreadcrumbSeparator);
const ariaLocation = location?.length ? formatMessage(messages.searchResultsBreadcrumb, { path: location.join(ariaSeaparator) }) : '';
return (
<a key={id} className="courseware-search-results__item" {...linkProps}>
<div className="courseware-search-results__icon"><Icon src={icon} /></div>
<div className="courseware-search-results__info">
<div className="courseware-search-results__title">
<span>{title}</span>
{contentHits ? (<em>{contentHits}</em>) : null }
<h3>{title}</h3>
{contentHits ? (<em aria-hidden="true">{contentHits}</em>) : null }
</div>
{location?.length ? (
<ul className="courseware-search-results__breadcrumbs">
{
<div aria-label={ariaLocation}>
{location?.length ? (
<ul className="courseware-search-results__breadcrumbs" aria-hidden="true">
{
// This ignore is necessary because the breadcrumb texts might have duplicates.
// The breadcrumbs are not expected to change.
// eslint-disable-next-line react/no-array-index-key
location.map((breadcrumb, i) => (<li key={`${i}:${breadcrumb}`}><div>{breadcrumb}</div></li>))
}
</ul>
) : null}
</ul>
) : null}
</div>
</div>
</a>
);

View File

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

View File

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

View File

@@ -101,6 +101,11 @@
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, waitFor } from '@testing-library/react';
import { renderHook, act } from '@testing-library/react-hooks';
import { useParams, useSearchParams } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { fetchCoursewareSearchSettings } from '../data/thunks';
@@ -38,13 +38,13 @@ describe('CoursewareSearch Hooks', () => {
it('should return true if feature is enabled', async () => {
const hook = await renderTestHook();
await waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
await hook.waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
expect(hook.result.current).toBe(true);
});
it('should return false if feature is disabled', async () => {
const hook = await renderTestHook(false);
await waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
await hook.waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
expect(hook.result.current).toBe(false);
});
});
@@ -125,7 +125,7 @@ describe('CoursewareSearch Hooks', () => {
it('should return the element bounding box', async () => {
const hook = await renderTestHook({ elementId: 'test', mockedInfo });
await waitFor(() => expect(getBoundingClientRectSpy).toHaveBeenCalled());
hook.waitFor(() => expect(getBoundingClientRectSpy).toHaveBeenCalled());
expect(hook.result.current).toEqual(mockedInfo);
});

View File

@@ -56,7 +56,21 @@ const messages = defineMessages({
defaultMessage: 'There was an error on the search process. Please try again in a few minutes. If the problem persists, please contact the support team.',
description: 'Error message to show to the users when there\'s an error with the endpoint or the returned payload format.',
},
searchResultsFilterDescription: {
id: 'learn.coursewareSearch.searchResultsFilterDescription',
defaultMessage: 'Search result filters',
description: 'Screen Reader text to describe the filter options.',
},
searchResultsBreadcrumb: {
id: 'learn.coursewareSearch.searchResultsBreadcrumb',
defaultMessage: 'Location: {path}.',
description: 'Screen Reader text to describe the search result breadcrumbs.',
},
searchResultsBreadcrumbSeparator: {
id: 'learn.searchResultsBreadcrumbSeparator',
defaultMessage: ', then ',
description: 'Screen Reader text to connect breadcrumb sections. i.e.: "Introduction, then Register, then Something else.',
},
// These are translations for labeling the filters
'filter:all': {
id: 'learn.coursewareSearch.filter:all',

View File

@@ -31,6 +31,7 @@ Factory.define('outlineTabData')
course_access_redirect: false,
has_scheduled_content: null,
access_expiration: null,
can_show_upgrade_sock: false,
cert_data: {
cert_status: null,
cert_web_view_url: null,

View File

@@ -489,6 +489,7 @@ exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normali
"outline": {
"course-v1:edX+DemoX+Demo_Course": {
"accessExpiration": null,
"canShowUpgradeSock": false,
"certData": {
"certStatus": null,
"certWebViewUrl": null,

View File

@@ -367,6 +367,7 @@ export async function getOutlineTabData(courseId) {
} = tabData;
const accessExpiration = camelCaseObject(data.access_expiration);
const canShowUpgradeSock = data.can_show_upgrade_sock;
const certData = camelCaseObject(data.cert_data);
const courseBlocks = data.course_blocks ? normalizeOutlineBlocks(courseId, data.course_blocks.blocks) : {};
const courseGoals = camelCaseObject(data.course_goals);
@@ -388,6 +389,7 @@ export async function getOutlineTabData(courseId) {
return {
accessExpiration,
canShowUpgradeSock,
certData,
courseBlocks,
courseGoals,

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import { getConfig } from '@edx/frontend-platform';
import { injectIntl } from '@edx/frontend-platform/i18n';
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { useParams, generatePath, useNavigate } from 'react-router-dom';
@@ -29,4 +30,6 @@ const DiscussionTab = () => {
);
};
export default DiscussionTab;
DiscussionTab.propTypes = {};
export default injectIntl(DiscussionTab);

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

View File

@@ -1,26 +1,28 @@
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Hyperlink } from '@openedx/paragon';
import messages from './messages';
import { ReactComponent as UnsubscribeIcon } from './unsubscribe.svg';
const ResultPage = ({ courseTitle, error }) => {
const intl = useIntl();
const errorDescription = intl.formatMessage(
messages.errorDescription,
{
contactSupport: (
<Hyperlink
className="text-reset"
style={{ textDecoration: 'underline' }}
destination={`${getConfig().CONTACT_URL}`}
>
{intl.formatMessage(messages.contactSupport)}
</Hyperlink>
),
},
const ResultPage = ({ courseTitle, error, intl }) => {
const errorDescription = (
<FormattedMessage
id="learning.goals.unsubscribe.errorDescription"
defaultMessage="We were unable to unsubscribe you from goal reminder emails. Please try again later or {contactSupport} for help."
values={{
contactSupport: (
<Hyperlink
className="text-reset"
style={{ textDecoration: 'underline' }}
destination={`${getConfig().CONTACT_URL}`}
>
{intl.formatMessage(messages.contactSupport)}
</Hyperlink>
),
}}
/>
);
const header = error
@@ -52,6 +54,7 @@ ResultPage.defaultProps = {
ResultPage.propTypes = {
courseTitle: PropTypes.string,
error: PropTypes.bool,
intl: intlShape.isRequired,
};
export default ResultPage;
export default injectIntl(ResultPage);

View File

@@ -16,11 +16,6 @@ const messages = defineMessages({
defaultMessage: 'Something went wrong',
description: 'It indicate that the unsubscribing request has failed',
},
errorDescription: {
id: 'learning.goals.unsubscribe.errorDescription',
defaultMessage: 'We were unable to unsubscribe you from goal reminder emails. Please try again later or {contactSupport} for help.',
description: 'Message that notifies user that unsubscribing failed and to try again',
},
goToDashboard: {
id: 'learning.goals.unsubscribe.goToDashboard',
defaultMessage: 'Go to dashboard',

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 { CourseOutlineTabNotificationsSlot } from '../../plugin-slots/CourseOutlineTabNotificationsSlot';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { AlertList } from '../../generic/user-messages';
import CourseDates from './widgets/CourseDates';
@@ -16,6 +16,7 @@ import CourseTools from './widgets/CourseTools';
import { fetchOutlineTab } from '../data';
import messages from './messages';
import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
import UpgradeNotification from '../../generic/upgrade-notification/UpgradeNotification';
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
import useCertificateAvailableAlert from './alerts/certificate-status-alert';
import useCourseEndAlert from './alerts/course-end-alert';
@@ -39,11 +40,13 @@ const OutlineTab = () => {
isSelfPaced,
org,
title,
userTimezone,
} = useModel('courseHomeMeta', courseId);
const expandButtonRef = useRef();
const {
accessExpiration,
courseBlocks: {
courses,
sections,
@@ -52,12 +55,20 @@ const OutlineTab = () => {
selectedGoal,
weeklyLearningGoalEnabled,
} = {},
datesBannerInfo,
datesWidget: {
courseDateBlocks,
},
enableProctoredExams,
offer,
timeOffsetMillis,
verifiedMode,
} = useModel('outline', courseId);
const {
marketingUrl,
} = useModel('coursewareMeta', courseId);
const [expandAll, setExpandAll] = useState(false);
const navigate = useNavigate();
@@ -181,7 +192,27 @@ const OutlineTab = () => {
/>
)}
<CourseTools />
<CourseOutlineTabNotificationsSlot courseId={courseId} />
<PluginSlot
id="outline_tab_notifications_slot"
pluginProps={{
courseId,
model: 'outline',
}}
>
<UpgradeNotification
offer={offer}
verifiedMode={verifiedMode}
accessExpiration={accessExpiration}
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
marketingUrl={marketingUrl}
upsellPageName="course_home"
userTimezone={userTimezone}
shouldDisplayBorder
timeOffsetMillis={timeOffsetMillis}
courseId={courseId}
org={org}
/>
</PluginSlot>
<CourseDates />
<CourseHandouts />
</div>

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 } from '@edx/frontend-platform/analytics';
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import MockAdapter from 'axios-mock-adapter';
import Cookies from 'js-cookie';
@@ -139,7 +139,7 @@ describe('Outline Tab', () => {
});
await fetchAndRender();
expect(screen.getByTestId('org.openedx.frontend.learning.course_outline_tab_notifications.v1')).toBeInTheDocument();
expect(screen.getByTestId('outline_tab_notifications_slot')).toBeInTheDocument();
});
it('handles expand/collapse all button click', async () => {
@@ -1190,6 +1190,80 @@ describe('Outline Tab', () => {
});
});
describe('Upgrade Card', () => {
it('renders title when upgrade is available', async () => {
await fetchAndRender();
expect(screen.queryByRole('heading', { name: 'Pursue a verified certificate' })).toBeInTheDocument();
});
it('displays link to upgrade', async () => {
await fetchAndRender();
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
});
it('viewing upgrade card sends analytics', async () => {
sendTrackEvent.mockClear();
sendTrackingLogEvent.mockClear();
await fetchAndRender();
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('Promotion Viewed', {
org_key: 'edX',
courserun_key: courseId,
creative: 'sidebarupsell',
name: 'In-Course Verification Prompt',
position: 'sidebar-message',
promotion_id: 'courseware_verified_certificate_upsell',
});
expect(sendTrackingLogEvent).toHaveBeenCalledTimes(1);
expect(sendTrackingLogEvent).toHaveBeenCalledWith('edx.bi.course.upgrade.sidebarupsell.displayed', {
org_key: 'edX',
courserun_key: courseId,
});
});
it('clicking upgrade link sends analytics', async () => {
await fetchAndRender();
// Clearing after render to remove any events sent on view (ex. 'Promotion Viewed')
sendTrackEvent.mockClear();
sendTrackingLogEvent.mockClear();
const upgradeButton = screen.getByRole('link', { name: 'Upgrade for $149' });
fireEvent.click(upgradeButton);
expect(sendTrackEvent).toHaveBeenCalledTimes(2);
expect(sendTrackEvent).toHaveBeenNthCalledWith(1, 'Promotion Clicked', {
org_key: 'edX',
courserun_key: courseId,
creative: 'sidebarupsell',
name: 'In-Course Verification Prompt',
position: 'sidebar-message',
promotion_id: 'courseware_verified_certificate_upsell',
});
expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.bi.ecommerce.upsell_links_clicked', {
org_key: 'edX',
courserun_key: courseId,
linkCategory: 'green_upgrade',
linkName: 'course_home_green',
linkType: 'button',
pageName: 'course_home',
});
expect(sendTrackingLogEvent).toHaveBeenCalledTimes(2);
expect(sendTrackingLogEvent).toHaveBeenNthCalledWith(1, 'edx.bi.course.upgrade.sidebarupsell.clicked', {
org_key: 'edX',
courserun_key: courseId,
});
expect(sendTrackingLogEvent).toHaveBeenNthCalledWith(2, 'edx.course.enrollment.upgrade.clicked', {
org_key: 'edX',
courserun_key: courseId,
location: 'sidebar-message',
});
});
});
describe('Account Activation Alert', () => {
beforeEach(() => {
const intersectionObserverMock = () => ({

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
// These flag svgs are derivatives of the Flag icon from paragon
import { ReactComponent as FlagIntenseIcon } from './flag_black.svg';
import { ReactComponent as FlagCasualIcon } from './flag_outline.svg';
@@ -13,8 +13,8 @@ const LearningGoalButton = ({
level,
isSelected,
handleSelect,
intl,
}) => {
const intl = useIntl();
const buttonDetails = {
casual: {
daysPerWeek: 1,
@@ -53,6 +53,7 @@ LearningGoalButton.propTypes = {
level: PropTypes.string.isRequired,
isSelected: PropTypes.bool.isRequired,
handleSelect: PropTypes.func.isRequired,
intl: intlShape.isRequired,
};
export default LearningGoalButton;
export default injectIntl(LearningGoalButton);

View File

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

View File

@@ -1,14 +1,13 @@
import React from 'react';
import { Button, Card } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useSelector } from 'react-redux';
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import messages from '../messages';
import { useModel } from '../../../generic/model-store';
const StartOrResumeCourseCard = () => {
const intl = useIntl();
const StartOrResumeCourseCard = ({ intl }) => {
const {
courseId,
} = useSelector(state => state.courseHome);
@@ -63,4 +62,8 @@ const StartOrResumeCourseCard = () => {
);
};
export default StartOrResumeCourseCard;
StartOrResumeCourseCard.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(StartOrResumeCourseCard);

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 { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Email } from '@openedx/paragon/icons';
import { useSelector } from 'react-redux';
import messages from '../messages';
@@ -18,8 +18,8 @@ import './FlagButton.scss';
const WeeklyLearningGoalCard = ({
daysPerWeek,
subscribedToReminders,
intl,
}) => {
const intl = useIntl();
const {
courseId,
} = useSelector(state => state.courseHome);
@@ -152,10 +152,11 @@ const WeeklyLearningGoalCard = ({
WeeklyLearningGoalCard.propTypes = {
daysPerWeek: PropTypes.number,
subscribedToReminders: PropTypes.bool,
intl: intlShape.isRequired,
};
WeeklyLearningGoalCard.defaultProps = {
daysPerWeek: null,
subscribedToReminders: false,
};
export default WeeklyLearningGoalCard;
export default injectIntl(WeeklyLearningGoalCard);

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import classNames from 'classnames';
import messages from './messages';
@@ -9,9 +9,8 @@ import { CoursewareSearch, CoursewareSearchToggle } from '../course-home/coursew
import { useCoursewareSearchState } from '../course-home/courseware-search/hooks';
const CourseTabsNavigation = ({
activeTabSlug, className, tabs,
activeTabSlug, className, tabs, intl,
}) => {
const intl = useIntl();
const { show } = useCoursewareSearchState();
return (
@@ -52,6 +51,7 @@ CourseTabsNavigation.propTypes = {
slug: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
})).isRequired,
intl: intlShape.isRequired,
};
CourseTabsNavigation.defaultProps = {
@@ -59,4 +59,4 @@ CourseTabsNavigation.defaultProps = {
className: null,
};
export default CourseTabsNavigation;
export default injectIntl(CourseTabsNavigation);

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';
import '@testing-library/jest-dom/extend-expect';
import { render, screen } from '@testing-library/react';
import React from 'react';
import {
@@ -221,7 +221,7 @@ describe('CoursewareContainer', () => {
});
history.push(`/course/${courseId}`);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
assertLoadedHeader(container);
assertSequenceNavigation(container);
@@ -244,7 +244,7 @@ describe('CoursewareContainer', () => {
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/courseware/resume/${courseId}`).reply(200, {});
history.push(`/course/${courseId}`);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
assertLoadedHeader(container);
assertSequenceNavigation(container);
@@ -294,7 +294,7 @@ describe('CoursewareContainer', () => {
describe('when the URL does not contain a unit ID', () => {
it('should choose a unit within the section\'s first sequence', async () => {
setUrl(sectionTree[1].id);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
assertLoadedHeader(container);
assertSequenceNavigation(container, 2);
assertLocation(container, sequenceTree[1][0].id, unitTree[1][0][0].id);
@@ -369,7 +369,7 @@ describe('CoursewareContainer', () => {
it('should pick the first unit if position was not defined (activeUnitIndex becomes 0)', async () => {
history.push(`/course/${courseId}/${sequenceBlock.id}`);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
assertLoadedHeader(container);
assertSequenceNavigation(container);
@@ -388,7 +388,7 @@ describe('CoursewareContainer', () => {
setUpMockRequests({ sequenceMetadatas: [sequenceMetadata] });
history.push(`/course/${courseId}/${sequenceBlock.id}`);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
assertLoadedHeader(container);
assertSequenceNavigation(container);
@@ -405,7 +405,7 @@ describe('CoursewareContainer', () => {
it('should load the specified unit', async () => {
history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[2].id}`);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
assertLoadedHeader(container);
assertSequenceNavigation(container);
@@ -421,7 +421,7 @@ describe('CoursewareContainer', () => {
});
history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[0].id}`);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
const sequenceNavButtons = container.querySelectorAll('nav.sequence-navigation a, nav.sequence-navigation button');
const sequenceNextButton = sequenceNavButtons[4];

View File

@@ -50,11 +50,7 @@ describe('Course', () => {
global.innerWidth = breakpoints.extraLarge.minWidth;
});
// This was passing when it shouldn't have been because of improper
// waitFor use. With the React 18 upgrade it no longer improperly passes
// so we are skipping it. See https://github.com/openedx/frontend-app-learning/issues/1669
// for details.
it.skip('loads learning sequence', () => {
it('loads learning sequence', async () => {
render(<Course {...mockData} />, { wrapWithRouter: true });
expect(screen.queryByRole('navigation', { name: 'breadcrumb' })).not.toBeInTheDocument();
waitFor(() => {
@@ -98,11 +94,7 @@ describe('Course', () => {
expect(screen.queryByRole('navigation', { name: 'breadcrumb' })).not.toBeInTheDocument();
});
// This was passing when it shouldn't have been because of improper
// waitFor use. With the React 18 upgrade it no longer improperly passes
// so we are skipping it. See https://github.com/openedx/frontend-app-learning/issues/1669
// for details.
it.skip('displays first section celebration modal', async () => {
it('displays first section celebration modal', async () => {
const courseHomeMetadata = Factory.build('courseHomeMetadata', { celebrations: { firstSection: true } });
const testStore = await initializeTestStore({ courseHomeMetadata }, false);
const { courseware, models } = testStore.getState();
@@ -124,11 +116,7 @@ describe('Course', () => {
});
});
// This was passing when it shouldn't have been because of improper
// waitFor use. With the React 18 upgrade it no longer improperly passes
// so we are skipping it. See https://github.com/openedx/frontend-app-learning/issues/1669
// for details.
it.skip('displays weekly goal celebration modal', async () => {
it('displays weekly goal celebration modal', async () => {
const courseHomeMetadata = Factory.build('courseHomeMetadata', { celebrations: { weeklyGoal: true } });
const testStore = await initializeTestStore({ courseHomeMetadata }, false);
const { courseware, models } = testStore.getState();
@@ -148,6 +136,18 @@ describe('Course', () => {
});
});
it('displays notification trigger and toggles active class on click', async () => {
render(<Course {...mockData} />, { wrapWithRouter: true });
waitFor(() => {
const notificationTrigger = screen.getByRole('button', { name: /Show notification tray/i });
expect(notificationTrigger).toBeInTheDocument();
expect(notificationTrigger.parentNode).not.toHaveClass('sidebar-active', { exact: true });
fireEvent.click(notificationTrigger);
expect(notificationTrigger.parentNode).toHaveClass('sidebar-active');
});
});
it('handles click to open/close discussions sidebar', async () => {
await setupDiscussionSidebar();

View File

@@ -1,13 +1,107 @@
import React, { useMemo } from 'react';
import React, { useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faHome } from '@fortawesome/free-solid-svg-icons';
import { useSelector } from 'react-redux';
import { useToggle, ModalPopup, Menu } from '@openedx/paragon';
import { Link } from 'react-router-dom';
import { useModel, useModels } from '../../generic/model-store';
import JumpNavMenuItem from './JumpNavMenuItem';
import { useModel, useModels } from '../../../generic/model-store';
import BreadcrumbItem from './BreadcrumbItem';
const CourseBreadcrumb = ({
content,
withSeparator,
courseId,
sequenceId,
unitId,
isStaff,
}) => {
const defaultContent = content.filter(
(destination) => destination.default,
)[0] || { id: courseId, label: '', sequences: [] };
const showRegularLink = getConfig().ENABLE_JUMPNAV !== 'true' || content.length < 2 || !isStaff;
const [isOpen, open, close] = useToggle(false);
const [target, setTarget] = useState(null);
return (
<>
{withSeparator && (
<li className="col-auto p-0 mx-2 text-primary-500 text-truncate text-nowrap" role="presentation" aria-hidden>/</li>
)}
<li
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
data-testid="breadcrumb-item"
>
{showRegularLink ? (
<Link
className="text-primary-500"
to={
defaultContent.sequences.length
? `/course/${courseId}/${defaultContent.sequences[0].id}`
: `/course/${courseId}/${defaultContent.id}`
}
>
{defaultContent.label}
</Link>
) : (
<>
{
// eslint-disable-next-line
<a className="text-primary-500" onClick={open} ref={setTarget}>
{defaultContent.label}
</a>
}
<ModalPopup positionRef={target} isOpen={isOpen} onClose={close}>
<Menu>
{content.map((item) => (
<JumpNavMenuItem
key={item.label}
isDefault={item.default}
sequences={item.sequences}
courseId={courseId}
title={item.label}
currentSequence={sequenceId}
currentUnit={unitId}
onClick={close}
/>
))}
</Menu>
</ModalPopup>
</>
)}
</li>
</>
);
};
CourseBreadcrumb.propTypes = {
content: PropTypes.arrayOf(
PropTypes.shape({
default: PropTypes.bool,
id: PropTypes.string,
label: PropTypes.string,
}),
).isRequired,
sequenceId: PropTypes.string,
unitId: PropTypes.string,
withSeparator: PropTypes.bool,
courseId: PropTypes.string,
isStaff: PropTypes.bool,
};
CourseBreadcrumb.defaultProps = {
withSeparator: false,
sequenceId: null,
unitId: null,
courseId: null,
isStaff: null,
};
const CourseBreadcrumbs = ({
courseId,
@@ -23,7 +117,7 @@ const CourseBreadcrumbs = ({
);
const allSequencesInSections = Object.fromEntries(
useModels('sections', course.sectionIds)?.map((section) => [
useModels('sections', course.sectionIds).map((section) => [
section.id,
{
default: section.id === sectionId,
@@ -77,7 +171,7 @@ const CourseBreadcrumbs = ({
</Link>
</li>
{links.map((content, i) => (
<BreadcrumbItem
<CourseBreadcrumb
// eslint-disable-next-line react/no-array-index-key
key={i}
courseId={courseId}
@@ -85,7 +179,6 @@ const CourseBreadcrumbs = ({
content={content}
unitId={unitId}
withSeparator
separator="/"
isStaff={isStaff}
/>
))}

View File

@@ -0,0 +1,134 @@
import React from 'react';
import { screen, render } from '@testing-library/react';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform';
import { BrowserRouter } from 'react-router-dom';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { useModel, useModels } from '../../generic/model-store';
import CourseBreadcrumbs from './CourseBreadcrumbs';
jest.mock('@edx/frontend-platform');
jest.mock('@edx/frontend-platform/analytics');
// Remove When Fully rolled out>>>
jest.mock('../../generic/model-store');
jest.mock('@edx/frontend-platform/auth');
getConfig.mockImplementation(() => ({ ENABLE_JUMPNAV: 'true' }));
getAuthenticatedUser.mockImplementation(() => ({ administrator: true }));
// ^^^^Remove When Fully rolled out
jest.mock('react-redux', () => ({
connect: (mapStateToProps, mapDispatchToProps) => (ReactComponent) => ({
mapStateToProps,
mapDispatchToProps,
ReactComponent,
}),
Provider: ({ children }) => children,
useSelector: () => 'loaded',
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
Link: jest.fn().mockImplementation(({ to, children }) => (
<a href={to}>{children}</a>
)),
}));
useModels.mockImplementation((name) => {
if (name === 'sections') {
return [
{
courseId: 'course-v1:edX+DemoX+Demo_Course',
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
sequenceIds: ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction'],
title: 'Introduction',
},
{
courseId: 'course-v1:edX+DemoX+Demo_Course',
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
sequenceIds: ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions'],
title: 'Example Week 1: Getting Started',
},
];
}
return [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
title: 'Lesson 1 - Getting Started',
unitIds: [
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
],
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
title: 'Homework - Question Styles',
unitIds: [
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@47dbd5f836544e61877a483c0b75606c',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@54bb9b142c6c4c22afc62bcb628f0e68',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0c92347a5c00',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_1fef54c2b23b',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2889db1677a549abb15eb4d886f95d1c',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e8a5cc2aed424838853defab7be45e42',
],
},
];
});
useModel.mockImplementation(() => ({
sectionIds: ['block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations'],
}));
describe('CourseBreadcrumbs', () => {
jest.spyOn(React, 'useMemo').mockImplementation(() => [
[
{
default: false,
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
label: 'Introduction',
url: 'http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction',
},
{
default: true,
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
label: 'Example Week 1: Getting Started',
url: 'http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course/block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
},
],
[
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@simulations', label: "Lesson 2 - Let's Get Interactive!", default: true, url: 'http://localhost:2000/course/course-v1:edX+DemoX+D…e@vertical+block@d0d804e8863c4a95a659c04d8a2b2bc0',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@175e76c4951144a29d46211361266e0e', label: 'Homework - Essays', default: false, url: 'http://localhost:2000/course/course-v1:edX+DemoX+D…e@vertical+block@fb79dcbad35b466a8c6364f8ffee9050',
},
],
]);
render(
<IntlProvider>
<BrowserRouter>
<CourseBreadcrumbs
courseId="course-v1:edX+DemoX+Demo_Course"
sectionId="block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations"
sequenceId="block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions"
isStaff
/>
</BrowserRouter>,
</IntlProvider>,
);
it('renders course breadcrumbs as expected', async () => {
expect(screen.queryAllByRole('link')).toHaveLength(1);
const courseHomeButtonDestination = screen.getAllByRole('link')[0].href;
expect(courseHomeButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home');
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
expect(screen.queryAllByTestId('breadcrumb-item')).toHaveLength(2);
});
});

View File

@@ -1,104 +0,0 @@
import React, { useState } from 'react';
import { getConfig } from '@edx/frontend-platform';
import {
useToggle,
ModalPopup,
Menu,
Button,
} from '@openedx/paragon';
import { Link, useLocation } from 'react-router-dom';
import JumpNavMenuItem from '../JumpNavMenuItem';
interface Props {
content: {
default: boolean,
id: string,
label: string,
sequences: {
id: string,
}[],
} [];
withSeparator: boolean | false,
separator: string | '';
courseId: string;
sequenceId: string | '';
unitId: string | '';
isStaff: boolean | false;
}
const BreadcrumbItem: React.FC<Props> = ({
content,
withSeparator,
separator,
courseId,
sequenceId,
unitId,
isStaff,
}) => {
const defaultContent = content.filter(
(destination: { default: boolean }) => destination.default,
)[0] || { id: courseId, label: '', sequences: [] };
const showRegularLink = getConfig().ENABLE_JUMPNAV !== 'true' || content.length < 2 || !isStaff;
const [isOpen, open, close] = useToggle(false);
const [target, setTarget] = useState(null);
const { pathname } = useLocation();
const isPreview = pathname.startsWith('/preview');
const baseUrl = defaultContent.sequences.length
? `/course/${courseId}/${defaultContent.sequences[0].id}`
: `/course/${courseId}/${defaultContent.id}`;
const link = isPreview ? `/preview${baseUrl}` : baseUrl;
return (
<>
{withSeparator && separator && (
<li className="col-auto p-0 mx-2 text-primary-500 text-truncate text-nowrap" role="presentation" aria-hidden>{separator}</li>
)}
<li
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
data-testid="breadcrumb-item"
>
{showRegularLink ? (
<Link
className="text-primary-500"
to={link}
>
{defaultContent.label}
</Link>
) : (
<>
{
// @ts-ignore
<Button className="text-primary-500 m-0 p-0" variant="link" onClick={open} ref={setTarget}>
{defaultContent.label}
</Button>
}
<ModalPopup positionRef={target} isOpen={isOpen} onClose={close}>
<Menu>
{content.map((item) => (
<JumpNavMenuItem
key={item.label}
isDefault={item.default}
sequences={item.sequences}
courseId={courseId}
title={item.label}
currentSequence={sequenceId}
currentUnit={unitId}
onClick={close}
/>
))}
</Menu>
</ModalPopup>
</>
)}
</li>
</>
);
};
export default BreadcrumbItem;

View File

@@ -1,101 +0,0 @@
import { Factory } from 'rosie';
import { AppProvider } from '@edx/frontend-platform/react';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { initializeMockApp, initializeTestStore } from '@src/setupTest';
import CourseBreadcrumbs from './CourseBreadcrumbs';
const props = {
courseId: 'course-v1:edX+DemoX+Demo_Course',
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
sequenceId: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
isStaff: true,
};
const courseMetadata = Factory.build('courseMetadata', { courseId: props.courseId, sectionIds: [props.sectionId] });
const sequenceBlocks = [Factory.build(
'block',
{ type: 'sequential', id: props.sequenceId, title: 'Subsection' },
{ courseId: props.courseId },
)];
const sectionBlocks = [Factory.build(
'block',
{
type: 'chapter',
id: props.sectionId,
title: 'Section',
children: [{ id: props.sequenceId }],
},
{ courseId: props.courseId },
)];
initializeMockApp();
describe('CourseBreadcrumbs', () => {
let store = {};
const initTestStore = async () => {
store = await initializeTestStore({ courseMetadata, sectionBlocks, sequenceBlocks });
};
function renderWithProvider(pathname = '/course') {
const { container } = render(
<AppProvider store={store} wrapWithRouter={false}>
<IntlProvider locale="en">
<MemoryRouter initialEntries={[{ pathname }]}>
<CourseBreadcrumbs {...props} />
</MemoryRouter>
</IntlProvider>
</AppProvider>,
);
return container;
}
describe('in live view', () => {
it('renders course breadcrumbs as expected', async () => {
await initTestStore();
renderWithProvider();
const courseHomeButtonDestination = screen.getAllByRole('link')[0].href;
expect(courseHomeButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home');
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
expect(screen.queryAllByTestId('breadcrumb-item')).toHaveLength(2);
});
it('section link does not include /preview/', async () => {
await initTestStore();
renderWithProvider();
const sectionBreadcrumb = screen.getByText(sectionBlocks[0].block_id);
const sectionLink = sectionBreadcrumb.closest('a').href;
expect(sectionLink.includes('/preview/')).toBeFalsy();
});
});
describe('in live view', () => {
it('renders course breadcrumbs as expected', async () => {
await initTestStore();
renderWithProvider('/preview/courses');
const courseHomeButtonDestination = screen.getAllByRole('link')[0].href;
expect(courseHomeButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home');
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
expect(screen.queryAllByTestId('breadcrumb-item')).toHaveLength(2);
});
it('section link does includes /preview/', async () => {
await initTestStore();
renderWithProvider('/preview/courses');
const sectionBreadcrumb = screen.getByText(sectionBlocks[0].block_id);
const sectionLink = sectionBreadcrumb.closest('a').href;
expect(sectionLink.includes('/preview/')).toBeTruthy();
});
});
});

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import { Xpert } from '@edx/frontend-lib-learning-assistant';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl } from '@edx/frontend-platform/i18n';
import { ALLOW_UPSELL_MODES, VERIFIED_MODES } from '@src/constants';
import { useModel } from '../../../generic/model-store';
@@ -79,4 +80,4 @@ Chat.defaultProps = {
enrollmentMode: null,
};
export default Chat;
export default injectIntl(Chat);

View File

@@ -1,389 +1,403 @@
import React, { useState } from 'react';
import React, { Component } from 'react';
import { Collapsible } from '@openedx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import {
FormattedMessage, injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faCalculator, faQuestionCircle, faTimesCircle, faEquals,
} from '@fortawesome/free-solid-svg-icons';
import messages from './messages';
const Calculator = () => {
const intl = useIntl();
const [equation, setEquation] = useState('');
const [result, setResult] = useState('');
class Calculator extends Component {
constructor(props) {
super(props);
this.state = {
equation: '',
result: '',
};
this.handleSubmit = this.handleSubmit.bind(this);
}
const handleSubmit = async (event) => {
async handleSubmit(event) {
event.preventDefault();
event.stopPropagation();
const urlEncoded = new URLSearchParams();
urlEncoded.append('equation', equation);
urlEncoded.append('equation', this.state.equation);
const response = await getAuthenticatedHttpClient().get(
`${getConfig().LMS_BASE_URL}/calculate?${urlEncoded.toString()}`,
);
setResult(response.data.result);
};
this.setState(() => ({ result: response.data.result }));
}
const changeEquation = (value) => {
setEquation(value);
};
changeEquation(value) {
this.setState(() => ({ equation: value }));
}
return (
<Collapsible.Advanced className="calculator">
<div className="text-right">
<Collapsible.Trigger tag="a" className="trigger btn">
<Collapsible.Visible whenOpen>
<FontAwesomeIcon icon={faTimesCircle} aria-hidden="true" className="mr-2" />
</Collapsible.Visible>
<Collapsible.Visible whenClosed>
<FontAwesomeIcon icon={faCalculator} aria-hidden="true" className="mr-2" />
</Collapsible.Visible>
{intl.formatMessage(messages['calculator.button.label'])}
</Collapsible.Trigger>
</div>
<Collapsible.Body className="calculator-content pt-4">
<form onSubmit={handleSubmit} className="container-xl form-inline flex-nowrap">
<input
type="text"
placeholder={intl.formatMessage(messages['calculator.input.field.label'])}
aria-label={intl.formatMessage(messages['calculator.input.field.label'])}
className="form-control w-100"
onChange={(event) => changeEquation(event.target.value)}
/>
<button
className="btn btn-primary mx-3"
aria-label={intl.formatMessage(messages['calculator.submit.button.label'])}
type="submit"
>
<FontAwesomeIcon icon={faEquals} aria-hidden="true" />
</button>
<input
type="text"
tabIndex="-1"
readOnly
aria-live="polite"
placeholder={intl.formatMessage(messages['calculator.result.field.placeholder'])}
aria-label={intl.formatMessage(messages['calculator.result.field.label'])}
className="form-control w-50"
value={result}
/>
</form>
<Collapsible.Advanced>
<div className="container-xl">
<Collapsible.Trigger className="btn btn-link btn-sm px-0 d-inline-flex align-items-center">
<Collapsible.Visible whenOpen>
<FontAwesomeIcon icon={faTimesCircle} aria-hidden="true" className="mr-2" />
</Collapsible.Visible>
<Collapsible.Visible whenClosed>
<FontAwesomeIcon icon={faQuestionCircle} aria-hidden="true" className="mr-2" />
</Collapsible.Visible>
<FormattedMessage
id="calculator.instructions.button.label"
defaultMessage="Calculator Instructions"
/>
</Collapsible.Trigger>
</div>
<Collapsible.Body className="container-xl pt-3" style={{ maxHeight: '50vh', overflow: 'auto' }}>
<FormattedMessage
tagName="h6"
id="calculator.instructions"
defaultMessage="For detailed information, see the {expressions_link}."
description="Text that precedes the link which redirects to help page calculator"
values={{
expressions_link: (
<a href={getConfig().SUPPORT_URL_CALCULATOR_MATH}>
<FormattedMessage
id="calculator.instructions.support.title"
defaultMessage="Help Center"
description="Anchor text for link which redirects to help page calculator"
/>
</a>
),
}}
render() {
return (
<Collapsible.Advanced className="calculator">
<div className="text-right">
<Collapsible.Trigger tag="a" className="trigger btn">
<Collapsible.Visible whenOpen>
<FontAwesomeIcon icon={faTimesCircle} aria-hidden="true" className="mr-2" />
</Collapsible.Visible>
<Collapsible.Visible whenClosed>
<FontAwesomeIcon icon={faCalculator} aria-hidden="true" className="mr-2" />
</Collapsible.Visible>
{this.props.intl.formatMessage(messages['calculator.button.label'])}
</Collapsible.Trigger>
</div>
<Collapsible.Body className="calculator-content pt-4">
<form onSubmit={this.handleSubmit} className="container-xl form-inline flex-nowrap">
<input
type="text"
placeholder={this.props.intl.formatMessage(messages['calculator.input.field.label'])}
aria-label={this.props.intl.formatMessage(messages['calculator.input.field.label'])}
className="form-control w-100"
onChange={(event) => this.changeEquation(event.target.value)}
/>
<p>
<strong>
<button
className="btn btn-primary mx-3"
aria-label={this.props.intl.formatMessage(messages['calculator.submit.button.label'])}
type="submit"
>
<FontAwesomeIcon icon={faEquals} aria-hidden="true" />
</button>
<input
type="text"
tabIndex="-1"
readOnly
aria-live="polite"
placeholder={this.props.intl.formatMessage(messages['calculator.result.field.placeholder'])}
aria-label={this.props.intl.formatMessage(messages['calculator.result.field.label'])}
className="form-control w-50"
value={this.state.result}
/>
</form>
<Collapsible.Advanced>
<div className="container-xl">
<Collapsible.Trigger className="btn btn-link btn-sm px-0 d-inline-flex align-items-center">
<Collapsible.Visible whenOpen>
<FontAwesomeIcon icon={faTimesCircle} aria-hidden="true" className="mr-2" />
</Collapsible.Visible>
<Collapsible.Visible whenClosed>
<FontAwesomeIcon icon={faQuestionCircle} aria-hidden="true" className="mr-2" />
</Collapsible.Visible>
<FormattedMessage
id="calculator.instructions.useful.tips"
defaultMessage="Useful tips:"
description="Headline for the (list of tips) about using the calculator"
id="calculator.instructions.button.label"
defaultMessage="Calculator Instructions"
/>
</strong>
</p>
<ul>
<li className="hint-item" id="hint-paren">
<FormattedMessage
id="calculator.hint1"
defaultMessage="Use parentheses () to make expressions clear. You can use parentheses inside other parentheses."
description="The text indicate that the calculator supports parentheses"
/>
</li>
<li className="hint-item" id="hint-spaces">
<FormattedMessage
id="calculator.hint2"
defaultMessage="Do not use spaces in expressions."
description="It indicate that using a space might cause un expected behavior"
/>
</li>
<li className="hint-item" id="hint-howto-constants">
<FormattedMessage
id="calculator.hint3"
defaultMessage="For constants, indicate multiplication explicitly (example: 5*c)."
description="It indicate the style of math notation"
/>
</li>
<li className="hint-item" id="hint-howto-maffixes">
<FormattedMessage
id="calculator.hint4"
defaultMessage="For affixes, type the number and affix without a space (example: 5c)."
/>
</li>
<li className="hint-item" id="hint-howto-functions">
<FormattedMessage
id="calculator.hint5"
defaultMessage="For functions, type the name of the function, then the expression in parentheses."
description="It indicate how to use a math function, e.g. exp(4)."
/>
</li>
</ul>
<table className="table small">
<thead>
<tr>
<th scope="col">
<FormattedMessage
id="calculator.instruction.table.to.use.heading"
defaultMessage="To Use"
description="Column header which indicate calculator functionality"
/>
</th>
<th scope="col">
<FormattedMessage
id="calculator.instruction.table.type.heading"
defaultMessage="Type"
description="Column header which indicate the supported type(s) of a the calculator functionality"
/>
</th>
<th scope="col">
<FormattedMessage
id="calculator.instruction.table.examples.heading"
defaultMessage="Examples"
description="Column header which list examples of calculator functionality"
/>
</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">
<FormattedMessage
id="calculator.instruction.table.to.use.numbers"
defaultMessage="Numbers"
description="A calculator functionality"
/>
</th>
<td>
<ul className="list-unstyled m-0">
<li>
<FormattedMessage
id="calculator.instruction.table.to.use.numbers.type1"
defaultMessage="Integers"
description="Type of numbers that is supported the calculator"
/>
</li>
<li>
<FormattedMessage
id="calculator.instruction.table.to.use.numbers.type2"
defaultMessage="Fractions"
description="Type of numbers that is supported by the calculator"
/>
</li>
<li>
<FormattedMessage
id="calculator.instruction.table.to.use.numbers.type3"
defaultMessage="Decimals"
description="Type of numbers that is supported by the calculator"
/>
</li>
</ul>
</td>
<td dir="auto">
<ul className="list-unstyled m-0">
<li>2520</li>
<li>2/3</li>
<li>3.14, .98</li>
</ul>
</td>
</tr>
<tr>
<th scope="row">
<FormattedMessage
id="calculator.instruction.table.to.use.operators"
defaultMessage="Operators"
description="A calculator functionality"
/>
</th>
<td dir="auto">
<ul className="list-unstyled m-0">
<li>
{' + - * / '}
<FormattedMessage
id="calculator.instruction.table.to.use.operators.type1"
defaultMessage="(add, subtract, multiply, divide)"
description="Type of opprators that are supported by the calculator"
/>
</li>
<li>
{'^ '}
<FormattedMessage
id="calculator.instruction.table.to.use.operators.type2"
defaultMessage="(raise to a power)"
description="It indicate that symbol (^) is being used to raise power, e.g. 2^2 = 4"
/>
</li>
<li>
{'|| '}
<FormattedMessage
id="calculator.instruction.table.to.use.operators.type3"
defaultMessage="(parallel resistors)"
description="It indicate that the sympol (||) is being used to calculate (parallel resistor), it is a concept in electrical/electronic engineering"
/>
</li>
</ul>
</td>
<td dir="auto">
<ul className="list-unstyled m-0">
<li>x+(2*y)/x-1</li>
<li>x^(n+1)</li>
<li>v_IN+v_OUT</li>
<li>1||2</li>
</ul>
</td>
</tr>
<tr>
<th scope="row">
<FormattedMessage
id="calculator.instruction.table.to.use.constants"
defaultMessage="Constants"
description="It indicate that the calculator support constants, e.g. the speed of light"
/>
</th>
<td dir="auto">e, pi</td>
<td dir="auto">
<ul className="list-unstyled m-0">
<li>20*e</li>
<li>418*pi</li>
</ul>
</td>
</tr>
<tr>
<th scope="row">
<FormattedMessage
id="calculator.instruction.table.to.use.affixes"
defaultMessage="Affixes"
/>
</th>
<td dir="auto">
<FormattedMessage
id="calculator.instruction.table.to.use.affixes.type"
defaultMessage="Percent sign (%)"
/>
</td>
<td dir="auto">
<ul className="list-unstyled m-0">
<li>20%</li>
</ul>
</td>
</tr>
<tr>
<th scope="row">
<FormattedMessage
id="calculator.instruction.table.to.use.basic.functions"
defaultMessage="Basic functions"
description="It indicate that calculator supports mathematical function"
/>
</th>
<td dir="auto">abs, exp, fact, factorial, ln, log2, log10, sqrt</td>
<td dir="auto">
<ul className="list-unstyled m-0">
<li>abs(x+y)</li>
<li>sqrt(x^2-y)</li>
</ul>
</td>
</tr>
<tr>
<th scope="row">
<FormattedMessage
id="calculator.instruction.table.to.use.trig.functions"
defaultMessage="Trigonometric functions"
description="Type of mathematical function that is supported by the calculator"
/>
</th>
<td dir="auto">
<ul className="list-unstyled m-0">
<li>sin, cos, tan, sec, csc, cot</li>
<li>arcsin, sinh, arcsinh</li>
</ul>
</td>
<td dir="auto">
<ul className="list-unstyled m-0">
<li>sin(4x+y)</li>
<li>arccsch(4x+y)</li>
</ul>
</td>
</tr>
<tr>
<th scope="row">
<FormattedMessage
id="calculator.instruction.table.to.use.scientific.notation"
defaultMessage="Scientific notation"
description="It indicate that calculator supports scientific notation"
/>
</th>
<td dir="auto">
<FormattedMessage
id="calculator.instruction.table.to.use.scientific.notation.type1"
defaultMessage="{exponentSyntax} and the exponent"
description="Type of scientific notation that is supported by the calculator"
values={{
exponentSyntax: '10^',
}}
/>
</td>
<td dir="auto">10^-9</td>
</tr>
<tr>
<th scope="row">
<FormattedMessage
id="calculator.instruction.table.to.use.scientific.notation.type2"
defaultMessage="{notationSyntax} notation"
description="It indicate that calculator supports (e) to be used in notation"
values={{
notationSyntax: 'e',
}}
/>
</th>
<td dir="auto">
<FormattedMessage
id="calculator.instruction.table.to.use.scientific.notation.type3"
defaultMessage="{notationSyntax} and the exponent"
description="An example for using (e) in notation"
values={{
notationSyntax: '1e',
}}
/>
</td>
<td dir="auto">1e-9</td>
</tr>
</tbody>
</table>
</Collapsible.Body>
</Collapsible.Advanced>
</Collapsible.Body>
</Collapsible.Advanced>
);
</Collapsible.Trigger>
</div>
<Collapsible.Body className="container-xl pt-3" style={{ maxHeight: '50vh', overflow: 'auto' }}>
<FormattedMessage
tagName="h6"
id="calculator.instructions"
defaultMessage="For detailed information, see the {expressions_link}."
description="Text that precedes the link which redirects to help page calculator"
values={{
expressions_link: (
<a href={getConfig().SUPPORT_URL_CALCULATOR_MATH}>
<FormattedMessage
id="calculator.instructions.support.title"
defaultMessage="Help Center"
description="Anchor text for link which redirects to help page calculator"
/>
</a>
),
}}
/>
<p>
<strong>
<FormattedMessage
id="calculator.instructions.useful.tips"
defaultMessage="Useful tips:"
description="Headline for the (list of tips) about using the calculator"
/>
</strong>
</p>
<ul>
<li className="hint-item" id="hint-paren">
<FormattedMessage
id="calculator.hint1"
defaultMessage="Use parentheses () to make expressions clear. You can use parentheses inside other parentheses."
description="The text indicate that the calculator supports parentheses"
/>
</li>
<li className="hint-item" id="hint-spaces">
<FormattedMessage
id="calculator.hint2"
defaultMessage="Do not use spaces in expressions."
description="It indicate that using a space might cause un expected behavior"
/>
</li>
<li className="hint-item" id="hint-howto-constants">
<FormattedMessage
id="calculator.hint3"
defaultMessage="For constants, indicate multiplication explicitly (example: 5*c)."
description="It indicate the style of math notation"
/>
</li>
<li className="hint-item" id="hint-howto-maffixes">
<FormattedMessage
id="calculator.hint4"
defaultMessage="For affixes, type the number and affix without a space (example: 5c)."
/>
</li>
<li className="hint-item" id="hint-howto-functions">
<FormattedMessage
id="calculator.hint5"
defaultMessage="For functions, type the name of the function, then the expression in parentheses."
description="It indicate how to use a math function, e.g. exp(4)."
/>
</li>
</ul>
<table className="table small">
<thead>
<tr>
<th scope="col">
<FormattedMessage
id="calculator.instruction.table.to.use.heading"
defaultMessage="To Use"
description="Column header which indicate calculator functionality"
/>
</th>
<th scope="col">
<FormattedMessage
id="calculator.instruction.table.type.heading"
defaultMessage="Type"
description="Column header which indicate the supported type(s) of a the calculator functionality"
/>
</th>
<th scope="col">
<FormattedMessage
id="calculator.instruction.table.examples.heading"
defaultMessage="Examples"
description="Column header which list examples of calculator functionality"
/>
</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">
<FormattedMessage
id="calculator.instruction.table.to.use.numbers"
defaultMessage="Numbers"
description="A calculator functionality"
/>
</th>
<td>
<ul className="list-unstyled m-0">
<li>
<FormattedMessage
id="calculator.instruction.table.to.use.numbers.type1"
defaultMessage="Integers"
description="Type of numbers that is supported the calculator"
/>
</li>
<li>
<FormattedMessage
id="calculator.instruction.table.to.use.numbers.type2"
defaultMessage="Fractions"
description="Type of numbers that is supported by the calculator"
/>
</li>
<li>
<FormattedMessage
id="calculator.instruction.table.to.use.numbers.type3"
defaultMessage="Decimals"
description="Type of numbers that is supported by the calculator"
/>
</li>
</ul>
</td>
<td dir="auto">
<ul className="list-unstyled m-0">
<li>2520</li>
<li>2/3</li>
<li>3.14, .98</li>
</ul>
</td>
</tr>
<tr>
<th scope="row">
<FormattedMessage
id="calculator.instruction.table.to.use.operators"
defaultMessage="Operators"
description="A calculator functionality"
/>
</th>
<td dir="auto">
<ul className="list-unstyled m-0">
<li>
{' + - * / '}
<FormattedMessage
id="calculator.instruction.table.to.use.operators.type1"
defaultMessage="(add, subtract, multiply, divide)"
description="Type of opprators that are supported by the calculator"
/>
</li>
<li>
{'^ '}
<FormattedMessage
id="calculator.instruction.table.to.use.operators.type2"
defaultMessage="(raise to a power)"
description="It indicate that symbol (^) is being used to raise power, e.g. 2^2 = 4"
/>
</li>
<li>
{'|| '}
<FormattedMessage
id="calculator.instruction.table.to.use.operators.type3"
defaultMessage="(parallel resistors)"
description="It indicate that the sympol (||) is being used to calculate (parallel resistor), it is a concept in electrical/electronic engineering"
/>
</li>
</ul>
</td>
<td dir="auto">
<ul className="list-unstyled m-0">
<li>x+(2*y)/x-1</li>
<li>x^(n+1)</li>
<li>v_IN+v_OUT</li>
<li>1||2</li>
</ul>
</td>
</tr>
<tr>
<th scope="row">
<FormattedMessage
id="calculator.instruction.table.to.use.constants"
defaultMessage="Constants"
description="It indicate that the calculator support constants, e.g. the speed of light"
/>
</th>
<td dir="auto">e, pi</td>
<td dir="auto">
<ul className="list-unstyled m-0">
<li>20*e</li>
<li>418*pi</li>
</ul>
</td>
</tr>
<tr>
<th scope="row">
<FormattedMessage
id="calculator.instruction.table.to.use.affixes"
defaultMessage="Affixes"
/>
</th>
<td dir="auto">
<FormattedMessage
id="calculator.instruction.table.to.use.affixes.type"
defaultMessage="Percent sign (%)"
/>
</td>
<td dir="auto">
<ul className="list-unstyled m-0">
<li>20%</li>
</ul>
</td>
</tr>
<tr>
<th scope="row">
<FormattedMessage
id="calculator.instruction.table.to.use.basic.functions"
defaultMessage="Basic functions"
description="It indicate that calculator supports mathematical function"
/>
</th>
<td dir="auto">abs, exp, fact, factorial, ln, log2, log10, sqrt</td>
<td dir="auto">
<ul className="list-unstyled m-0">
<li>abs(x+y)</li>
<li>sqrt(x^2-y)</li>
</ul>
</td>
</tr>
<tr>
<th scope="row">
<FormattedMessage
id="calculator.instruction.table.to.use.trig.functions"
defaultMessage="Trigonometric functions"
description="Type of mathematical function that is supported by the calculator"
/>
</th>
<td dir="auto">
<ul className="list-unstyled m-0">
<li>sin, cos, tan, sec, csc, cot</li>
<li>arcsin, sinh, arcsinh</li>
</ul>
</td>
<td dir="auto">
<ul className="list-unstyled m-0">
<li>sin(4x+y)</li>
<li>arccsch(4x+y)</li>
</ul>
</td>
<td dir="auto" />
</tr>
<tr>
<th scope="row">
<FormattedMessage
id="calculator.instruction.table.to.use.scientific.notation"
defaultMessage="Scientific notation"
description="It indicate that calculator supports scientific notation"
/>
</th>
<td dir="auto">
<FormattedMessage
id="calculator.instruction.table.to.use.scientific.notation.type1"
defaultMessage="{exponentSyntax} and the exponent"
description="Type of scientific notation that is supported by the calculator"
values={{
exponentSyntax: '10^',
}}
/>
</td>
<td dir="auto">10^-9</td>
</tr>
<tr>
<th scope="row">
<FormattedMessage
id="calculator.instruction.table.to.use.scientific.notation.type2"
defaultMessage="{notationSyntax} notation"
description="It indicate that calculator supports (e) to be used in notation"
values={{
notationSyntax: 'e',
}}
/>
</th>
<td dir="auto">
<FormattedMessage
id="calculator.instruction.table.to.use.scientific.notation.type3"
defaultMessage="{notationSyntax} and the exponent"
description="An example for using (e) in notation"
values={{
notationSyntax: '1e',
}}
/>
</td>
<td dir="auto">1e-9</td>
</tr>
</tbody>
</table>
</Collapsible.Body>
</Collapsible.Advanced>
</Collapsible.Body>
</Collapsible.Advanced>
);
}
}
Calculator.propTypes = {
intl: intlShape.isRequired,
};
export default Calculator;
export default injectIntl(Calculator);

View File

@@ -1,9 +1,11 @@
import React, { useState } from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import messages from './messages';
@@ -13,41 +15,47 @@ function toggleNotes() {
iframe.contentWindow.postMessage('tools.toggleNotes', getConfig().LMS_BASE_URL);
}
const NotesVisibility = ({ course }) => {
const intl = useIntl();
const [visible, setVisible] = useState(course.notes.visible);
const visibilityUrl = `${getConfig().LMS_BASE_URL}/courses/${course.id}/edxnotes/visibility/`;
class NotesVisibility extends Component {
constructor(props) {
super(props);
this.state = {
visible: props.course.notes.visible,
};
this.visibilityUrl = `${getConfig().LMS_BASE_URL}/courses/${props.course.id}/edxnotes/visibility/`;
}
const handleClick = () => {
const data = { visibility: !visible };
handleClick = () => {
const data = { visibility: !this.state.visible };
getAuthenticatedHttpClient().put(
visibilityUrl,
this.visibilityUrl,
data,
).then(() => {
setVisible(!visible);
this.setState((state) => ({ visible: !state.visible }));
toggleNotes();
});
};
const message = visible ? 'notes.button.hide' : 'notes.button.show';
return (
<button
className={`trigger btn ${visible ? 'text-secondary' : 'text-success'} mx-2 `}
role="switch"
type="button"
onClick={handleClick}
onKeyDown={handleClick}
tabIndex="-1"
aria-checked={visible ? 'true' : 'false'}
>
<FontAwesomeIcon icon={faPencilAlt} aria-hidden="true" className="mr-2" />
{intl.formatMessage(messages[message])}
</button>
);
};
render() {
const message = this.state.visible ? 'notes.button.hide' : 'notes.button.show';
return (
<button
className={`trigger btn ${this.state.visible ? 'text-secondary' : 'text-success'} mx-2 `}
role="switch"
type="button"
onClick={this.handleClick}
onKeyDown={this.handleClick}
tabIndex="-1"
aria-checked={this.state.visible ? 'true' : 'false'}
>
<FontAwesomeIcon icon={faPencilAlt} aria-hidden="true" className="mr-2" />
{this.props.intl.formatMessage(messages[message])}
</button>
);
}
}
NotesVisibility.propTypes = {
intl: intlShape.isRequired,
course: PropTypes.shape({
id: PropTypes.string.isRequired,
notes: PropTypes.shape({
@@ -56,4 +64,4 @@ NotesVisibility.propTypes = {
}).isRequired,
};
export default NotesVisibility;
export default injectIntl(NotesVisibility);

View File

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

View File

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

View File

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

View File

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

View File

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

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, useIntl, defineMessages,
FormattedMessage, injectIntl, intlShape, defineMessages,
} from '@edx/frontend-platform/i18n';
import { useSelector, useDispatch } from 'react-redux';
import {
@@ -64,8 +64,8 @@ const CourseCard = ({
marketingUrl,
onClick,
},
intl,
}) => {
const intl = useIntl();
const formatList = (items, style) => (
items.join(intl.formatMessage(
messages.listJoin,
@@ -127,12 +127,12 @@ CourseCard.propTypes = {
})),
onClick: PropTypes.func,
}).isRequired,
intl: intlShape.isRequired,
};
const IntlCard = CourseCard;
const IntlCard = injectIntl(CourseCard);
const CourseRecommendations = ({ variant }) => {
const intl = useIntl();
const CourseRecommendations = ({ intl, variant }) => {
const { courseId, recommendationsStatus } = useSelector(state => ({ ...state.recommendations, ...state.courseware }));
const { recommendations } = useModel('coursewareMeta', courseId);
const { org, number } = useModel('courseHomeMeta', courseId);
@@ -205,7 +205,8 @@ const CourseRecommendations = ({ variant }) => {
};
CourseRecommendations.propTypes = {
intl: intlShape.isRequired,
variant: PropTypes.string.isRequired,
};
export default CourseRecommendations;
export default injectIntl(CourseRecommendations);

View File

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

View File

@@ -2,9 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Alert, Button, Hyperlink } from '@openedx/paragon';
import certImage from '../../../generic/assets/openedx_certificate.png';
import certImage from '../../../generic/assets/edX_certificate.png';
import messages from './messages';
/**
@@ -20,12 +20,12 @@ import messages from './messages';
const programTypes = ['microbachelors', 'micromasters', 'professional-certificate', 'xseries'];
const ProgramCompletion = ({
intl,
progress,
title,
type,
url,
}) => {
const intl = useIntl();
if (!programTypes.includes(type) || progress.notStarted !== 0 || progress.inProgress !== 0) {
return null;
}
@@ -98,6 +98,7 @@ const ProgramCompletion = ({
};
ProgramCompletion.propTypes = {
intl: intlShape.isRequired,
progress: PropTypes.shape({
completed: PropTypes.number.isRequired,
inProgress: PropTypes.number.isRequired,
@@ -108,4 +109,4 @@ ProgramCompletion.propTypes = {
url: PropTypes.string.isRequired,
};
export default ProgramCompletion;
export default injectIntl(ProgramCompletion);

View File

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

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCopyright } from '@fortawesome/free-regular-svg-icons';
@@ -105,8 +105,8 @@ function parseLicense(license) {
const CourseLicense = ({
license,
intl,
}) => {
const intl = useIntl();
const renderAllRightsReservedLicense = () => (
<div className="text-gray-500">
<FontAwesomeIcon aria-hidden="true" className="mr-1" icon={faCopyright} />
@@ -155,10 +155,11 @@ const CourseLicense = ({
CourseLicense.propTypes = {
license: PropTypes.string,
intl: intlShape.isRequired,
};
CourseLicense.defaultProps = {
license: 'all-rights-reserved',
};
export default CourseLicense;
export default injectIntl(CourseLicense);

View File

@@ -9,7 +9,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { breakpoints } from '@openedx/paragon';
import {
initializeMockApp, render, screen, act, fireEvent, waitFor,
initializeMockApp, render, screen, within, act, fireEvent, waitFor,
} from '../../../../../../setupTest';
import initializeStore from '../../../../../../store';
import { appendBrowserTimezoneToUrl, executeThunk } from '../../../../../../utils';
@@ -90,7 +90,28 @@ describe('NotificationsWidget', () => {
<NotificationsWidget />
</SidebarContext.Provider>,
);
expect(screen.getByTestId('org.openedx.frontend.learning.notification_widget.v1')).toBeInTheDocument();
expect(screen.getByTestId('notification_widget_slot')).toBeInTheDocument();
});
it('renders upgrade card', async () => {
const contextData: Partial<SidebarContextData> = {
currentSidebar: ID,
courseId,
hideNotificationbar: false,
isNotificationbarAvailable: true,
};
await fetchAndRender(
<SidebarContext.Provider value={contextData as SidebarContextData}>
<NotificationsWidget />
</SidebarContext.Provider>,
);
// The Upgrade Notification should be inside the PluginSlot.
const UpgradeNotification = document.querySelector('.upgrade-notification');
expect(UpgradeNotification).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
expect(screen.queryByText('You have no new notifications at this time.')).not.toBeInTheDocument();
});
it('renders no notifications bar if no verified mode', async () => {
@@ -109,6 +130,44 @@ describe('NotificationsWidget', () => {
expect(screen.queryByText('Notifications')).not.toBeInTheDocument();
});
it.each([
{
description: 'close the notification widget.',
enabledInContext: true,
testId: 'notification-widget',
},
{
description: 'close the sidebar when the notification widget is closed, and the discussion widget is unavailable.',
enabledInContext: false,
testId: 'sidebar-DISCUSSIONS_NOTIFICATIONS',
},
])('successfully %s', async ({ enabledInContext, testId }) => {
const userVerifiedMode = Factory.build('verifiedMode');
await setupDiscussionSidebar({
verifiedMode: userVerifiedMode,
enabledInContext,
isNewDiscussionSidebarViewEnabled: true,
});
const sidebarButton = screen.getByRole('button', { name: /Show sidebar tray/i });
await act(async () => {
fireEvent.click(sidebarButton);
});
const notificationWidget = await waitFor(() => screen.getByTestId('notification-widget'));
const closeNotificationButton = within(notificationWidget).getByRole('button', { name: /Close/i });
await act(async () => {
fireEvent.click(closeNotificationButton);
});
await waitFor(() => {
expect(screen.queryByTestId(testId)).not.toBeInTheDocument();
});
});
it('marks notification as seen 3 seconds later', async () => {
const onNotificationSeen = jest.fn();
const contextData: Partial<SidebarContextData> = {

View File

@@ -1,10 +1,11 @@
import React, { useContext, useEffect, useMemo } from 'react';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { useModel } from '../../../../../../generic/model-store';
import UpgradeNotification from '../../../../../../generic/upgrade-notification/UpgradeNotification';
import { WIDGETS } from '../../../../../../constants';
import SidebarContext from '../../../SidebarContext';
import { NotificationWidgetSlot } from '../../../../../../plugin-slots/NotificationWidgetSlot';
const NotificationsWidget = () => {
const {
@@ -20,11 +21,17 @@ const NotificationsWidget = () => {
const course = useModel('coursewareMeta', courseId);
const {
accessExpiration,
contentTypeGatingEnabled,
end,
enrollmentEnd,
enrollmentMode,
enrollmentStart,
marketingUrl,
offer,
start,
timeOffsetMillis,
userTimezone,
verificationStatus,
} = course;
@@ -67,12 +74,32 @@ const NotificationsWidget = () => {
return (
<div className="border border-light-400 rounded-sm" data-testid="notification-widget">
<NotificationWidgetSlot
courseId={courseId}
notificationCurrentState={upgradeNotificationCurrentState}
setNotificationCurrentState={setUpgradeNotificationCurrentState}
toggleSidebar={onToggleSidebar}
/>
<PluginSlot
id="notification_widget_slot"
pluginProps={{
courseId,
model: 'coursewareMeta',
notificationCurrentState: upgradeNotificationCurrentState,
setNotificationCurrentState: setUpgradeNotificationCurrentState,
toggleSidebar: onToggleSidebar,
}}
>
<UpgradeNotification
offer={offer}
verifiedMode={verifiedMode}
accessExpiration={accessExpiration}
contentTypeGatingEnabled={contentTypeGatingEnabled}
marketingUrl={marketingUrl}
upsellPageName="in_course"
userTimezone={userTimezone}
shouldDisplayBorder={false}
timeOffsetMillis={timeOffsetMillis}
courseId={courseId}
org={org}
setupgradeNotificationCurrentState={setUpgradeNotificationCurrentState}
toggleSidebar={onToggleSidebar}
/>
</PluginSlot>
</div>
);
};

View File

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

View File

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

View File

@@ -6,7 +6,6 @@ import { shallow } from '@edx/react-unit-test-utils';
import PageLoading from '@src/generic/PageLoading';
import { ContentIFrameLoaderSlot } from '@src/plugin-slots/ContentIFrameLoaderSlot';
import * as hooks from './hooks';
import ContentIFrame, { IFRAME_FEATURE_POLICY, testIDs } from './ContentIFrame';
@@ -100,8 +99,8 @@ describe('ContentIFrame Component', () => {
});
it('displays PageLoading component if not showError', () => {
el = shallow(<ContentIFrame {...props} />);
[component] = el.instance.findByType(ContentIFrameLoaderSlot);
expect(component.props.loadingMessage).toEqual(props.loadingMessage);
[component] = el.instance.findByType(PageLoading);
expect(component.props.srMessage).toEqual(props.loadingMessage);
});
});
describe('hasLoaded', () => {

View File

@@ -2,13 +2,14 @@ import React, { Suspense } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { useModel } from '@src/generic/model-store';
import PageLoading from '@src/generic/PageLoading';
import { GatedUnitContentMessageSlot } from '../../../../plugin-slots/GatedUnitContentMessageSlot';
import messages from '../messages';
import HonorCode from '../honor-code';
import LockPaywall from '../lock-paywall';
import * as hooks from './hooks';
import { modelKeys } from './constants';
@@ -28,7 +29,14 @@ const UnitSuspense = ({
<>
{shouldDisplayContentGating && (
<Suspense fallback={<PageLoading srMessage={formatMessage(messages.loadingLockedContent)} />}>
<GatedUnitContentMessageSlot courseId={courseId} />
<PluginSlot
id="gated_unit_content_message_slot"
pluginProps={{
courseId,
}}
>
<LockPaywall courseId={courseId} />
</PluginSlot>
</Suspense>
)}
{shouldDisplayHonorCode && (

View File

@@ -5,7 +5,6 @@ import { formatMessage, shallow } from '@edx/react-unit-test-utils';
import { useModel } from '@src/generic/model-store';
import PageLoading from '@src/generic/PageLoading';
import { GatedUnitContentMessageSlot } from '@src/plugin-slots/GatedUnitContentMessageSlot';
import messages from '../messages';
import HonorCode from '../honor-code';
import LockPaywall from '../lock-paywall';
@@ -79,9 +78,10 @@ describe('UnitSuspense component', () => {
beforeEach(() => { mockModels(true, true); });
it('displays LockPaywall in Suspense wrapper with PageLoading fallback', () => {
el = shallow(<UnitSuspense {...props} />);
const [component] = el.instance.findByType(GatedUnitContentMessageSlot);
expect(component.parent.type).toEqual('Suspense');
expect(component.parent.props.fallback)
const [component] = el.instance.findByType(LockPaywall);
expect(component.parent.type).toEqual('PluginSlot');
expect(component.parent.parent.type).toEqual('Suspense');
expect(component.parent.parent.props.fallback)
.toEqual(<PageLoading srMessage={formatMessage(messages.loadingLockedContent)} />);
expect(component.props.courseId).toEqual(props.courseId);
});

View File

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

View File

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

View File

@@ -3,15 +3,14 @@ import PropTypes from 'prop-types';
import { useNavigate } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faLock } from '@fortawesome/free-solid-svg-icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import messages from './messages';
const ContentLock = ({
courseId, prereqSectionName, prereqId, sequenceTitle,
intl, courseId, prereqSectionName, prereqId, sequenceTitle,
}) => {
const intl = useIntl();
const navigate = useNavigate();
const handleClick = useCallback(() => {
navigate(`/course/${courseId}/${prereqId}`);
@@ -37,9 +36,10 @@ const ContentLock = ({
);
};
ContentLock.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
prereqSectionName: PropTypes.string.isRequired,
prereqId: PropTypes.string.isRequired,
sequenceTitle: PropTypes.string.isRequired,
};
export default ContentLock;
export default injectIntl(ContentLock);

View File

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

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

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 { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Alert, Hyperlink, breakpoints, useWindowSize,
} from '@openedx/paragon';
import { Locked } from '@openedx/paragon/icons';
import SidebarContext from '../../sidebar/SidebarContext';
import messages from './messages';
import certificateLocked from '../../../../generic/assets/openedx_locked_certificate.png';
import certificateLocked from '../../../../generic/assets/edX_locked_certificate.png';
import { useModel } from '../../../../generic/model-store';
import { UpgradeButton } from '../../../../generic/upgrade-button';
import {
@@ -20,9 +20,9 @@ import {
} from '../../../../generic/upsell-bullets/UpsellBullets';
const LockPaywall = ({
intl,
courseId,
}) => {
const intl = useIntl();
const { notificationTrayVisible } = useContext(SidebarContext);
const course = useModel('coursewareMeta', courseId);
const {
@@ -143,6 +143,7 @@ const LockPaywall = ({
);
};
LockPaywall.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
};
export default LockPaywall;
export default injectIntl(LockPaywall);

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { Factory } from 'rosie';
import { getAllByRole } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { act, fireEvent, getAllByRole } from '@testing-library/react';
import { initializeTestStore, render, screen } from '../../../../setupTest';
import SequenceNavigationTabs from './SequenceNavigationTabs';
@@ -49,18 +48,17 @@ describe('Sequence Navigation Tabs', () => {
it('renders unit buttons and dropdown button', async () => {
let container = null;
await act(async () => {
useIndexOfLastVisibleChild.mockReturnValue([-1, null, null]);
const booyah = render(<SequenceNavigationTabs {...mockData} />, { wrapWithRouter: true });
container = booyah.container;
useIndexOfLastVisibleChild.mockReturnValue([-1, null, null]);
const booyah = render(<SequenceNavigationTabs {...mockData} />, { wrapWithRouter: true });
// wait for links to appear so we aren't testing an empty div
await screen.findAllByRole('link');
container = booyah.container;
const dropdownToggle = container.querySelector('.dropdown-toggle');
await userEvent.click(dropdownToggle);
const dropdownToggle = container.querySelector('.dropdown-toggle');
// We need to await this click here, which requires us to await the `act` as well above.
// https://github.com/testing-library/react-testing-library/issues/535
// Without doing this, we get a warning about using `act` even though we are.
await fireEvent.click(dropdownToggle);
});
const dropdownMenu = container.querySelector('.dropdown');
const dropdownButtons = getAllByRole(dropdownMenu, 'link');
expect(dropdownButtons).toHaveLength(unitBlocks.length);

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import EffortEstimate from '../../../../shared/effort-estimate';
import { sequenceIdsSelector } from '../../../data';
@@ -24,10 +24,10 @@ import messages from './messages';
const UnitNavigationEffortEstimate = ({
children,
intl,
sequenceId,
unitId,
}) => {
const intl = useIntl();
const sequenceIds = useSelector(sequenceIdsSelector);
const sequenceIndex = sequenceIds.indexOf(sequenceId);
const nextSequenceId = sequenceIndex < sequenceIds.length - 1 ? sequenceIds[sequenceIndex + 1] : null;
@@ -59,6 +59,7 @@ const UnitNavigationEffortEstimate = ({
UnitNavigationEffortEstimate.propTypes = {
children: PropTypes.node,
intl: intlShape.isRequired,
sequenceId: PropTypes.string.isRequired,
unitId: PropTypes.string,
};
@@ -68,4 +69,4 @@ UnitNavigationEffortEstimate.defaultProps = {
unitId: null,
};
export default UnitNavigationEffortEstimate;
export default injectIntl(UnitNavigationEffortEstimate);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Collapsible } from '@openedx/paragon';
import courseOutlineMessages from '@src/course-home/outline-tab/messages';
@@ -11,12 +11,12 @@ import SidebarUnit from './SidebarUnit';
import { UNIT_ICON_TYPES } from './UnitIcon';
const SidebarSequence = ({
intl,
courseId,
defaultOpen,
sequence,
activeUnitId,
}) => {
const intl = useIntl();
const {
id,
complete,
@@ -78,6 +78,7 @@ const SidebarSequence = ({
};
SidebarSequence.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
defaultOpen: PropTypes.bool.isRequired,
sequence: PropTypes.shape({
@@ -95,4 +96,4 @@ SidebarSequence.propTypes = {
activeUnitId: PropTypes.string.isRequired,
};
export default SidebarSequence;
export default injectIntl(SidebarSequence);

View File

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

View File

@@ -36,12 +36,12 @@ describe('<SidebarUnit />', () => {
};
};
function renderWithProvider(props = {}, sidebarContext = defaultSidebarContext, pathname = '/course') {
function renderWithProvider(props = {}, sidebarContext = defaultSidebarContext) {
const { container } = render(
<AppProvider store={store} wrapWithRouter={false}>
<IntlProvider locale="en">
<SidebarContext.Provider value={{ ...sidebarContext }}>
<MemoryRouter initialEntries={[{ pathname }]}>
<MemoryRouter>
<SidebarUnit
isFirst
id={unit.id}
@@ -138,34 +138,4 @@ describe('<SidebarUnit />', () => {
expect(window.sessionStorage.getItem('hideCourseOutlineSidebar')).toEqual('true');
});
});
describe('UnitLinkWrapper', () => {
describe('course in preview mode', () => {
beforeEach(async () => {
await initTestStore();
renderWithProvider({ unit: { ...unit } }, { ...defaultSidebarContext, shouldDisplayFullScreen: true }, '/preview/course');
});
it('href includes /preview', async () => {
const unitLink = screen.getByText(unit.title).closest('a');
const linkHref = unitLink.getAttribute('href');
expect(linkHref.includes('/preview/')).toBeTruthy();
});
});
describe('course in live mode', () => {
beforeEach(async () => {
await initTestStore();
renderWithProvider({ unit: { ...unit } }, { ...defaultSidebarContext, shouldDisplayFullScreen: true });
});
it('href does not include /preview/', async () => {
const unitLink = screen.getByText(unit.title).closest('a');
const linkHref = unitLink.getAttribute('href');
expect(linkHref.includes('/preview/')).toBeFalsy();
});
});
});
});

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { useCourseOutlineSidebar } from '../hooks';
@@ -27,14 +27,10 @@ const UnitLinkWrapper: React.FC<Props> = ({
children,
}) => {
const { handleUnitClick } = useCourseOutlineSidebar();
const { pathname } = useLocation();
const isPreview = pathname.startsWith('/preview');
const baseUrl = `/course/${courseId}/${sequenceId}/${id}`;
const link = isPreview ? `/preview${baseUrl}` : baseUrl;
return (
<Link
to={link}
to={`/course/${courseId}/${sequenceId}/${id}`}
className="row w-100 m-0 d-flex align-items-center text-gray-700"
onClick={() => handleUnitClick({ sequenceId, activeUnitId, id })}
>

View File

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

View File

@@ -1,5 +1,5 @@
import { ensureConfig, getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Icon } from '@openedx/paragon';
import { QuestionAnswer } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
@@ -16,9 +16,9 @@ ensureConfig(['DISCUSSIONS_MFE_BASE_URL']);
export const ID = WIDGETS.DISCUSSIONS;
const DiscussionsTrigger = ({
intl,
onClick,
}) => {
const intl = useIntl();
const {
unitId,
courseId,
@@ -51,7 +51,8 @@ const DiscussionsTrigger = ({
};
DiscussionsTrigger.propTypes = {
intl: intlShape.isRequired,
onClick: PropTypes.func.isRequired,
};
export default DiscussionsTrigger;
export default injectIntl(DiscussionsTrigger);

View File

@@ -1,4 +1,4 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Icon } from '@openedx/paragon';
import { WatchOutline } from '@openedx/paragon/icons';
import classNames from 'classnames';
@@ -8,36 +8,35 @@ import React from 'react';
import messages from '../../../messages';
const NotificationIcon = ({
intl,
status,
notificationColor,
}) => {
const intl = useIntl();
return (
<>
<Icon src={WatchOutline} className="m-0 m-auto" alt={intl.formatMessage(messages.openNotificationTrigger)} />
{status === 'active'
? (
<span
className={classNames(notificationColor, 'rounded-circle p-1 position-absolute')}
data-testid="notification-dot"
style={{
top: '0.3rem',
right: '0.55rem',
}}
/>
)
: null}
</>
);
};
}) => (
<>
<Icon src={WatchOutline} className="m-0 m-auto" alt={intl.formatMessage(messages.openNotificationTrigger)} />
{status === 'active'
? (
<span
className={classNames(notificationColor, 'rounded-circle p-1 position-absolute')}
data-testid="notification-dot"
style={{
top: '0.3rem',
right: '0.55rem',
}}
/>
)
: null}
</>
);
NotificationIcon.defaultProps = {
status: null,
};
NotificationIcon.propTypes = {
intl: intlShape.isRequired,
status: PropTypes.string,
notificationColor: PropTypes.string.isRequired,
};
export default NotificationIcon;
export default injectIntl(NotificationIcon);

View File

@@ -1,18 +1,18 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import classNames from 'classnames';
import { useContext, useEffect, useMemo } from 'react';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { useModel } from '@src/generic/model-store';
import { NotificationTraySlot } from '../../../../../plugin-slots/NotificationTraySlot';
import UpgradeNotification from '../../../../../generic/upgrade-notification/UpgradeNotification';
import messages from '../../../messages';
import SidebarBase from '../../common/SidebarBase';
import SidebarContext from '../../SidebarContext';
import NotificationTrigger, { ID } from './NotificationTrigger';
const NotificationTray = () => {
const intl = useIntl();
const NotificationTray = ({ intl }) => {
const {
courseId,
onNotificationSeen,
@@ -23,11 +23,17 @@ const NotificationTray = () => {
const course = useModel('coursewareMeta', courseId);
const {
accessExpiration,
contentTypeGatingEnabled,
end,
enrollmentEnd,
enrollmentMode,
enrollmentStart,
marketingUrl,
offer,
start,
timeOffsetMillis,
userTimezone,
verificationStatus,
} = course;
@@ -76,11 +82,31 @@ const NotificationTray = () => {
>
<div>{verifiedMode
? (
<NotificationTraySlot
courseId={courseId}
notificationCurrentState={upgradeNotificationCurrentState}
setNotificationCurrentState={setUpgradeNotificationCurrentState}
/>
<PluginSlot
id="notification_tray_slot"
pluginProps={{
courseId,
model: 'coursewareMeta',
notificationCurrentState: upgradeNotificationCurrentState,
setNotificationCurrentState: setUpgradeNotificationCurrentState,
}}
>
<UpgradeNotification
offer={offer}
verifiedMode={verifiedMode}
accessExpiration={accessExpiration}
contentTypeGatingEnabled={contentTypeGatingEnabled}
marketingUrl={marketingUrl}
upsellPageName="in_course"
userTimezone={userTimezone}
shouldDisplayBorder={false}
timeOffsetMillis={timeOffsetMillis}
courseId={courseId}
org={org}
upgradeNotificationCurrentState={upgradeNotificationCurrentState}
setupgradeNotificationCurrentState={setUpgradeNotificationCurrentState}
/>
</PluginSlot>
) : (
<p className="p-3 small">{intl.formatMessage(messages.noNotificationsMessage)}</p>
)}
@@ -89,7 +115,11 @@ const NotificationTray = () => {
);
};
NotificationTray.propTypes = {
intl: intlShape.isRequired,
};
NotificationTray.Trigger = NotificationTrigger;
NotificationTray.ID = ID;
export default NotificationTray;
export default injectIntl(NotificationTray);

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