@edx/frontend-platform 4.6.0 → 4.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.development +30 -0
- package/.env.test +30 -0
- package/.eslintignore +6 -0
- package/.eslintrc.js +28 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- package/.github/workflows/add-depr-ticket-to-depr-board.yml +19 -0
- package/.github/workflows/add-remove-label-on-comment.yml +20 -0
- package/.github/workflows/ci.yml +42 -0
- package/.github/workflows/commitlint.yml +10 -0
- package/.github/workflows/lockfileversion-check.yml +13 -0
- package/.github/workflows/manual-publish.yml +43 -0
- package/.github/workflows/npm-deprecate.yml +22 -0
- package/.github/workflows/release.yml +45 -0
- package/.github/workflows/self-assign-issue.yml +12 -0
- package/.github/workflows/update-browserslist-db.yml +12 -0
- package/.nvmrc +1 -0
- package/.releaserc +32 -0
- package/catalog-info.yaml +21 -0
- package/dist/LICENSE +661 -0
- package/dist/README.md +155 -0
- package/dist/package.json +86 -0
- package/docs/addTagsPlugin.js +10 -0
- package/docs/auth-API.md +114 -0
- package/docs/decisions/0001-record-architecture-decisions.rst +32 -0
- package/docs/decisions/0002-frontend-base-design-goals.rst +222 -0
- package/docs/decisions/0003-consolidation-into-frontend-platform.rst +71 -0
- package/docs/decisions/0004-axios-caching-implementation.rst +88 -0
- package/docs/decisions/0005-token-null-after-successful-refresh.rst +69 -0
- package/docs/decisions/0006-middleware-support-for-http-clients.rst +44 -0
- package/docs/decisions/0007-javascript-file-configuration.rst +143 -0
- package/docs/how_tos/automatic-case-conversion.rst +58 -0
- package/docs/how_tos/caching.rst +93 -0
- package/docs/how_tos/i18n.rst +305 -0
- package/docs/removeExport.js +24 -0
- package/docs/template/edx/README.md +12 -0
- package/docs/template/edx/publish.js +713 -0
- package/docs/template/edx/static/fonts/OpenSans-Bold-webfont.eot +0 -0
- package/docs/template/edx/static/fonts/OpenSans-Bold-webfont.svg +1830 -0
- package/docs/template/edx/static/fonts/OpenSans-Bold-webfont.woff +0 -0
- package/docs/template/edx/static/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
- package/docs/template/edx/static/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
- package/docs/template/edx/static/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
- package/docs/template/edx/static/fonts/OpenSans-Italic-webfont.eot +0 -0
- package/docs/template/edx/static/fonts/OpenSans-Italic-webfont.svg +1830 -0
- package/docs/template/edx/static/fonts/OpenSans-Italic-webfont.woff +0 -0
- package/docs/template/edx/static/fonts/OpenSans-Light-webfont.eot +0 -0
- package/docs/template/edx/static/fonts/OpenSans-Light-webfont.svg +1831 -0
- package/docs/template/edx/static/fonts/OpenSans-Light-webfont.woff +0 -0
- package/docs/template/edx/static/fonts/OpenSans-LightItalic-webfont.eot +0 -0
- package/docs/template/edx/static/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
- package/docs/template/edx/static/fonts/OpenSans-LightItalic-webfont.woff +0 -0
- package/docs/template/edx/static/fonts/OpenSans-Regular-webfont.eot +0 -0
- package/docs/template/edx/static/fonts/OpenSans-Regular-webfont.svg +1831 -0
- package/docs/template/edx/static/fonts/OpenSans-Regular-webfont.woff +0 -0
- package/docs/template/edx/static/scripts/linenumber.js +25 -0
- package/docs/template/edx/static/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/template/edx/static/scripts/prettify/lang-css.js +2 -0
- package/docs/template/edx/static/scripts/prettify/prettify.js +28 -0
- package/docs/template/edx/static/styles/jsdoc-default.css +356 -0
- package/docs/template/edx/static/styles/prettify-jsdoc.css +111 -0
- package/docs/template/edx/static/styles/prettify-tomorrow.css +132 -0
- package/docs/template/edx/tmpl/augments.tmpl +10 -0
- package/docs/template/edx/tmpl/container.tmpl +196 -0
- package/docs/template/edx/tmpl/details.tmpl +143 -0
- package/docs/template/edx/tmpl/example.tmpl +2 -0
- package/docs/template/edx/tmpl/examples.tmpl +13 -0
- package/docs/template/edx/tmpl/exceptions.tmpl +32 -0
- package/docs/template/edx/tmpl/layout.tmpl +39 -0
- package/docs/template/edx/tmpl/mainpage.tmpl +10 -0
- package/docs/template/edx/tmpl/members.tmpl +38 -0
- package/docs/template/edx/tmpl/method.tmpl +131 -0
- package/docs/template/edx/tmpl/modifies.tmpl +14 -0
- package/docs/template/edx/tmpl/params.tmpl +131 -0
- package/docs/template/edx/tmpl/properties.tmpl +108 -0
- package/docs/template/edx/tmpl/returns.tmpl +19 -0
- package/docs/template/edx/tmpl/source.tmpl +8 -0
- package/docs/template/edx/tmpl/tutorial.tmpl +19 -0
- package/docs/template/edx/tmpl/type.tmpl +7 -0
- package/env.config.js +8 -0
- package/jsdoc.json +36 -0
- package/openedx.yaml +12 -0
- package/package.json +6 -6
- package/service-interface.png +0 -0
- package/src/analytics/MockAnalyticsService.js +71 -0
- package/src/analytics/SegmentAnalyticsService.js +243 -0
- package/src/analytics/index.js +12 -0
- package/src/analytics/interface.js +142 -0
- package/src/auth/AxiosCsrfTokenService.js +60 -0
- package/src/auth/AxiosJwtAuthService.js +364 -0
- package/src/auth/AxiosJwtTokenService.js +134 -0
- package/src/auth/LocalForageCache.js +78 -0
- package/src/auth/MockAuthService.js +285 -0
- package/src/auth/index.js +19 -0
- package/src/auth/interceptors/createCsrfTokenProviderInterceptor.js +37 -0
- package/src/auth/interceptors/createJwtTokenProviderInterceptor.js +38 -0
- package/src/auth/interceptors/createProcessAxiosRequestErrorInterceptor.js +20 -0
- package/src/auth/interceptors/createRetryInterceptor.js +72 -0
- package/src/auth/interface.js +309 -0
- package/src/auth/utils.js +105 -0
- package/src/config.js +327 -0
- package/src/constants.js +66 -0
- package/src/i18n/countries.js +57 -0
- package/src/i18n/index.js +123 -0
- package/src/i18n/injectIntlWithShim.jsx +45 -0
- package/src/i18n/languages.js +60 -0
- package/src/i18n/lib.js +282 -0
- package/src/i18n/scripts/README.md +29 -0
- package/src/i18n/scripts/intl-imports.js +259 -0
- package/src/i18n/scripts/transifex-utils.js +75 -0
- package/src/index.js +42 -0
- package/src/initialize.js +357 -0
- package/src/logging/MockLoggingService.js +31 -0
- package/src/logging/NewRelicLoggingService.js +181 -0
- package/src/logging/index.js +9 -0
- package/src/logging/interface.js +110 -0
- package/src/pubSub.js +47 -0
- package/src/react/AppContext.jsx +24 -0
- package/src/react/AppProvider.jsx +93 -0
- package/src/react/AuthenticatedPageRoute.jsx +60 -0
- package/src/react/ErrorBoundary.jsx +44 -0
- package/src/react/ErrorPage.jsx +76 -0
- package/src/react/LoginRedirect.jsx +16 -0
- package/src/react/OptionalReduxProvider.jsx +28 -0
- package/src/react/PageRoute.jsx +31 -0
- package/src/react/hooks.js +50 -0
- package/src/react/index.js +16 -0
- package/src/scripts/GoogleAnalyticsLoader.js +53 -0
- package/src/scripts/index.js +2 -0
- package/src/testing/index.js +9 -0
- package/src/testing/initializeMockApp.js +77 -0
- package/src/testing/mockMessages.js +21 -0
- package/src/utils.js +167 -0
- /package/{analytics → dist/analytics}/MockAnalyticsService.js +0 -0
- /package/{analytics → dist/analytics}/MockAnalyticsService.js.map +0 -0
- /package/{analytics → dist/analytics}/SegmentAnalyticsService.js +0 -0
- /package/{analytics → dist/analytics}/SegmentAnalyticsService.js.map +0 -0
- /package/{analytics → dist/analytics}/index.js +0 -0
- /package/{analytics → dist/analytics}/index.js.map +0 -0
- /package/{analytics → dist/analytics}/interface.js +0 -0
- /package/{analytics → dist/analytics}/interface.js.map +0 -0
- /package/{auth → dist/auth}/AxiosCsrfTokenService.js +0 -0
- /package/{auth → dist/auth}/AxiosCsrfTokenService.js.map +0 -0
- /package/{auth → dist/auth}/AxiosJwtAuthService.js +0 -0
- /package/{auth → dist/auth}/AxiosJwtAuthService.js.map +0 -0
- /package/{auth → dist/auth}/AxiosJwtTokenService.js +0 -0
- /package/{auth → dist/auth}/AxiosJwtTokenService.js.map +0 -0
- /package/{auth → dist/auth}/LocalForageCache.js +0 -0
- /package/{auth → dist/auth}/LocalForageCache.js.map +0 -0
- /package/{auth → dist/auth}/MockAuthService.js +0 -0
- /package/{auth → dist/auth}/MockAuthService.js.map +0 -0
- /package/{auth → dist/auth}/index.js +0 -0
- /package/{auth → dist/auth}/index.js.map +0 -0
- /package/{auth → dist/auth}/interceptors/createCsrfTokenProviderInterceptor.js +0 -0
- /package/{auth → dist/auth}/interceptors/createCsrfTokenProviderInterceptor.js.map +0 -0
- /package/{auth → dist/auth}/interceptors/createJwtTokenProviderInterceptor.js +0 -0
- /package/{auth → dist/auth}/interceptors/createJwtTokenProviderInterceptor.js.map +0 -0
- /package/{auth → dist/auth}/interceptors/createProcessAxiosRequestErrorInterceptor.js +0 -0
- /package/{auth → dist/auth}/interceptors/createProcessAxiosRequestErrorInterceptor.js.map +0 -0
- /package/{auth → dist/auth}/interceptors/createRetryInterceptor.js +0 -0
- /package/{auth → dist/auth}/interceptors/createRetryInterceptor.js.map +0 -0
- /package/{auth → dist/auth}/interface.js +0 -0
- /package/{auth → dist/auth}/interface.js.map +0 -0
- /package/{auth → dist/auth}/utils.js +0 -0
- /package/{auth → dist/auth}/utils.js.map +0 -0
- /package/{config.js → dist/config.js} +0 -0
- /package/{config.js.map → dist/config.js.map} +0 -0
- /package/{constants.js → dist/constants.js} +0 -0
- /package/{constants.js.map → dist/constants.js.map} +0 -0
- /package/{i18n → dist/i18n}/countries.js +0 -0
- /package/{i18n → dist/i18n}/countries.js.map +0 -0
- /package/{i18n → dist/i18n}/index.js +0 -0
- /package/{i18n → dist/i18n}/index.js.map +0 -0
- /package/{i18n → dist/i18n}/injectIntlWithShim.js +0 -0
- /package/{i18n → dist/i18n}/injectIntlWithShim.js.map +0 -0
- /package/{i18n → dist/i18n}/languages.js +0 -0
- /package/{i18n → dist/i18n}/languages.js.map +0 -0
- /package/{i18n → dist/i18n}/lib.js +0 -0
- /package/{i18n → dist/i18n}/lib.js.map +0 -0
- /package/{i18n → dist/i18n}/scripts/README.md +0 -0
- /package/{i18n → dist/i18n}/scripts/intl-imports.js +0 -0
- /package/{i18n → dist/i18n}/scripts/intl-imports.js.map +0 -0
- /package/{i18n → dist/i18n}/scripts/transifex-utils.js +0 -0
- /package/{i18n → dist/i18n}/scripts/transifex-utils.js.map +0 -0
- /package/{index.js → dist/index.js} +0 -0
- /package/{index.js.map → dist/index.js.map} +0 -0
- /package/{initialize.js → dist/initialize.js} +0 -0
- /package/{initialize.js.map → dist/initialize.js.map} +0 -0
- /package/{logging → dist/logging}/MockLoggingService.js +0 -0
- /package/{logging → dist/logging}/MockLoggingService.js.map +0 -0
- /package/{logging → dist/logging}/NewRelicLoggingService.js +0 -0
- /package/{logging → dist/logging}/NewRelicLoggingService.js.map +0 -0
- /package/{logging → dist/logging}/index.js +0 -0
- /package/{logging → dist/logging}/index.js.map +0 -0
- /package/{logging → dist/logging}/interface.js +0 -0
- /package/{logging → dist/logging}/interface.js.map +0 -0
- /package/{pubSub.js → dist/pubSub.js} +0 -0
- /package/{pubSub.js.map → dist/pubSub.js.map} +0 -0
- /package/{react → dist/react}/AppContext.js +0 -0
- /package/{react → dist/react}/AppContext.js.map +0 -0
- /package/{react → dist/react}/AppProvider.js +0 -0
- /package/{react → dist/react}/AppProvider.js.map +0 -0
- /package/{react → dist/react}/AuthenticatedPageRoute.js +0 -0
- /package/{react → dist/react}/AuthenticatedPageRoute.js.map +0 -0
- /package/{react → dist/react}/ErrorBoundary.js +0 -0
- /package/{react → dist/react}/ErrorBoundary.js.map +0 -0
- /package/{react → dist/react}/ErrorPage.js +0 -0
- /package/{react → dist/react}/ErrorPage.js.map +0 -0
- /package/{react → dist/react}/LoginRedirect.js +0 -0
- /package/{react → dist/react}/LoginRedirect.js.map +0 -0
- /package/{react → dist/react}/OptionalReduxProvider.js +0 -0
- /package/{react → dist/react}/OptionalReduxProvider.js.map +0 -0
- /package/{react → dist/react}/PageRoute.js +0 -0
- /package/{react → dist/react}/PageRoute.js.map +0 -0
- /package/{react → dist/react}/hooks.js +0 -0
- /package/{react → dist/react}/hooks.js.map +0 -0
- /package/{react → dist/react}/index.js +0 -0
- /package/{react → dist/react}/index.js.map +0 -0
- /package/{scripts → dist/scripts}/GoogleAnalyticsLoader.js +0 -0
- /package/{scripts → dist/scripts}/GoogleAnalyticsLoader.js.map +0 -0
- /package/{scripts → dist/scripts}/index.js +0 -0
- /package/{scripts → dist/scripts}/index.js.map +0 -0
- /package/{testing → dist/testing}/index.js +0 -0
- /package/{testing → dist/testing}/index.js.map +0 -0
- /package/{testing → dist/testing}/initializeMockApp.js +0 -0
- /package/{testing → dist/testing}/initializeMockApp.js.map +0 -0
- /package/{testing → dist/testing}/mockMessages.js +0 -0
- /package/{testing → dist/testing}/mockMessages.js.map +0 -0
- /package/{utils.js → dist/utils.js} +0 -0
- /package/{utils.js.map → dist/utils.js.map} +0 -0
package/src/pubSub.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #### Import members from **@edx/frontend-platform**
|
|
3
|
+
*
|
|
4
|
+
* The PubSub module is a thin wrapper around the base functionality of
|
|
5
|
+
* [PubSubJS](https://github.com/mroderick/PubSubJS). For the sake of simplicity and not relying
|
|
6
|
+
* too heavily on implementation-specific features, it maintains a fairly simple API (subscribe,
|
|
7
|
+
* unsubscribe, and publish).
|
|
8
|
+
*
|
|
9
|
+
* Publish/Subscribe events should be used mindfully, especially in relation to application UI
|
|
10
|
+
* frameworks like React. Given React's unidirectional data flow and prop/state management
|
|
11
|
+
* capabilities, using a pub/sub mechanism is at odds with that framework's best practices.
|
|
12
|
+
*
|
|
13
|
+
* That said, we use pub/sub in our application initialization sequence to allow applications to
|
|
14
|
+
* hook into the initialization lifecycle, and we also use them to publish when the application
|
|
15
|
+
* state has changed, i.e., when the config document or user's authentication state have changed.
|
|
16
|
+
*
|
|
17
|
+
* @module PubSub
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import PubSub from 'pubsub-js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
*
|
|
24
|
+
* @param {string} type
|
|
25
|
+
* @param {function} callback
|
|
26
|
+
* @returns {string} A subscription token that can be passed to `unsubscribe`
|
|
27
|
+
*/
|
|
28
|
+
export function subscribe(type, callback) {
|
|
29
|
+
return PubSub.subscribe(type, callback);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
*
|
|
34
|
+
* @param {string} token A subscription token provided by `subscribe`
|
|
35
|
+
*/
|
|
36
|
+
export function unsubscribe(token) {
|
|
37
|
+
return PubSub.unsubscribe(token);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
*
|
|
42
|
+
* @param {string} type
|
|
43
|
+
* @param {Object} data
|
|
44
|
+
*/
|
|
45
|
+
export function publish(type, data) {
|
|
46
|
+
return PubSub.publish(type, data);
|
|
47
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `AppContext` provides data from `App` in a way that React components can readily consume, even
|
|
5
|
+
* if it's mutable data. `AppContext` contains the following data structure:
|
|
6
|
+
*
|
|
7
|
+
* ```
|
|
8
|
+
* {
|
|
9
|
+
* authenticatedUser: <THE App.authenticatedUser OBJECT>,
|
|
10
|
+
* config: <THE App.config OBJECT>
|
|
11
|
+
* }
|
|
12
|
+
* ```
|
|
13
|
+
* If the `App.authenticatedUser` or `App.config` data changes, `AppContext` will be updated
|
|
14
|
+
* accordingly and pass those changes onto React components using the context.
|
|
15
|
+
*
|
|
16
|
+
* `AppContext` is used in a React application like any other `[React Context](https://reactjs.org/docs/context.html)
|
|
17
|
+
* @memberof module:React
|
|
18
|
+
*/
|
|
19
|
+
const AppContext = React.createContext({
|
|
20
|
+
authenticatedUser: null,
|
|
21
|
+
config: {},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export default AppContext;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { Router } from 'react-router-dom';
|
|
4
|
+
|
|
5
|
+
import OptionalReduxProvider from './OptionalReduxProvider';
|
|
6
|
+
|
|
7
|
+
import ErrorBoundary from './ErrorBoundary';
|
|
8
|
+
import AppContext from './AppContext';
|
|
9
|
+
import { useAppEvent, useTrackColorSchemeChoice } from './hooks';
|
|
10
|
+
import { getAuthenticatedUser, AUTHENTICATED_USER_CHANGED } from '../auth';
|
|
11
|
+
import { getConfig } from '../config';
|
|
12
|
+
import { CONFIG_CHANGED } from '../constants';
|
|
13
|
+
import { history } from '../initialize';
|
|
14
|
+
import {
|
|
15
|
+
getLocale,
|
|
16
|
+
getMessages,
|
|
17
|
+
IntlProvider,
|
|
18
|
+
LOCALE_CHANGED,
|
|
19
|
+
} from '../i18n';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A wrapper component for React-based micro-frontends to initialize a number of common data/
|
|
23
|
+
* context providers.
|
|
24
|
+
*
|
|
25
|
+
* ```
|
|
26
|
+
* subscribe(APP_READY, () => {
|
|
27
|
+
* ReactDOM.render(
|
|
28
|
+
* <AppProvider>
|
|
29
|
+
* <HelloWorld />
|
|
30
|
+
* </AppProvider>
|
|
31
|
+
* )
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* This will provide the following to HelloWorld:
|
|
36
|
+
* - An error boundary as described above.
|
|
37
|
+
* - An `AppContext` provider for React context data.
|
|
38
|
+
* - IntlProvider for @edx/frontend-i18n internationalization
|
|
39
|
+
* - Optionally a redux `Provider`. Will only be included if a `store` property is passed to
|
|
40
|
+
* `AppProvider`.
|
|
41
|
+
* - A `Router` for react-router.
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} props
|
|
44
|
+
* @param {Object} [props.store] A redux store.
|
|
45
|
+
* @memberof module:React
|
|
46
|
+
*/
|
|
47
|
+
export default function AppProvider({ store, children }) {
|
|
48
|
+
const [config, setConfig] = useState(getConfig());
|
|
49
|
+
const [authenticatedUser, setAuthenticatedUser] = useState(getAuthenticatedUser());
|
|
50
|
+
const [locale, setLocale] = useState(getLocale());
|
|
51
|
+
|
|
52
|
+
useTrackColorSchemeChoice();
|
|
53
|
+
|
|
54
|
+
useAppEvent(AUTHENTICATED_USER_CHANGED, () => {
|
|
55
|
+
setAuthenticatedUser(getAuthenticatedUser());
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
useAppEvent(CONFIG_CHANGED, () => {
|
|
59
|
+
setConfig(getConfig());
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
useAppEvent(LOCALE_CHANGED, () => {
|
|
63
|
+
setLocale(getLocale());
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const appContextValue = useMemo(() => ({ authenticatedUser, config, locale }), [authenticatedUser, config, locale]);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<IntlProvider locale={locale} messages={getMessages()}>
|
|
70
|
+
<ErrorBoundary>
|
|
71
|
+
<AppContext.Provider
|
|
72
|
+
value={appContextValue}
|
|
73
|
+
>
|
|
74
|
+
<OptionalReduxProvider store={store}>
|
|
75
|
+
<Router history={history}>
|
|
76
|
+
{children}
|
|
77
|
+
</Router>
|
|
78
|
+
</OptionalReduxProvider>
|
|
79
|
+
</AppContext.Provider>
|
|
80
|
+
</ErrorBoundary>
|
|
81
|
+
</IntlProvider>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
AppProvider.propTypes = {
|
|
86
|
+
// eslint-disable-next-line react/forbid-prop-types
|
|
87
|
+
store: PropTypes.object,
|
|
88
|
+
children: PropTypes.node.isRequired,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
AppProvider.defaultProps = {
|
|
92
|
+
store: null,
|
|
93
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { useContext } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { useRouteMatch } from 'react-router-dom';
|
|
4
|
+
|
|
5
|
+
import AppContext from './AppContext';
|
|
6
|
+
import PageRoute from './PageRoute';
|
|
7
|
+
import { getLoginRedirectUrl } from '../auth';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A react-router route that redirects to the login page when the route becomes active and the user
|
|
11
|
+
* is not authenticated. If the application has been initialized with `requireAuthenticatedUser`
|
|
12
|
+
* false, an authenticatedPageRoute can be used to protect a subset of the application's routes,
|
|
13
|
+
* rather than the entire application.
|
|
14
|
+
*
|
|
15
|
+
* It can optionally accept an override URL to redirect to instead of the login page.
|
|
16
|
+
*
|
|
17
|
+
* Like a `PageRoute`, also calls `sendPageEvent` when the route becomes active.
|
|
18
|
+
*
|
|
19
|
+
* @see PageRoute
|
|
20
|
+
* @see {@link module:frontend-platform/analytics~sendPageEvent}
|
|
21
|
+
* @memberof module:React
|
|
22
|
+
* @param {Object} props
|
|
23
|
+
* @param {string} props.redirectUrl The URL anonymous users should be redirected to, rather than
|
|
24
|
+
* viewing the route's contents.
|
|
25
|
+
*/
|
|
26
|
+
export default function AuthenticatedPageRoute({ redirectUrl, ...props }) {
|
|
27
|
+
const { authenticatedUser } = useContext(AppContext);
|
|
28
|
+
|
|
29
|
+
const match = useRouteMatch({
|
|
30
|
+
// eslint-disable-next-line react/prop-types
|
|
31
|
+
path: props.path,
|
|
32
|
+
// eslint-disable-next-line react/prop-types
|
|
33
|
+
exact: props.exact,
|
|
34
|
+
// eslint-disable-next-line react/prop-types
|
|
35
|
+
strict: props.strict,
|
|
36
|
+
// eslint-disable-next-line react/prop-types
|
|
37
|
+
sensitive: props.sensitive,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (authenticatedUser === null) {
|
|
41
|
+
if (match) {
|
|
42
|
+
const destination = redirectUrl || getLoginRedirectUrl(global.location.href);
|
|
43
|
+
global.location.assign(destination);
|
|
44
|
+
}
|
|
45
|
+
// This emulates a Route's way of displaying nothing if the route's path doesn't match the
|
|
46
|
+
// current URL.
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return (
|
|
50
|
+
<PageRoute {...props} />
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
AuthenticatedPageRoute.propTypes = {
|
|
55
|
+
redirectUrl: PropTypes.string,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
AuthenticatedPageRoute.defaultProps = {
|
|
59
|
+
redirectUrl: null,
|
|
60
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React, { Component } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
|
|
4
|
+
import { logError } from '../logging';
|
|
5
|
+
|
|
6
|
+
import ErrorPage from './ErrorPage';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Error boundary component used to log caught errors and display the error page.
|
|
10
|
+
*
|
|
11
|
+
* @memberof module:React
|
|
12
|
+
* @extends {Component}
|
|
13
|
+
*/
|
|
14
|
+
export default class ErrorBoundary extends Component {
|
|
15
|
+
constructor(props) {
|
|
16
|
+
super(props);
|
|
17
|
+
this.state = { hasError: false };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static getDerivedStateFromError() {
|
|
21
|
+
// Update state so the next render will show the fallback UI.
|
|
22
|
+
return { hasError: true };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
componentDidCatch(error, info) {
|
|
26
|
+
logError(error, { stack: info.componentStack });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
render() {
|
|
30
|
+
if (this.state.hasError) {
|
|
31
|
+
return <ErrorPage />;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return this.props.children;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ErrorBoundary.propTypes = {
|
|
39
|
+
children: PropTypes.node,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
ErrorBoundary.defaultProps = {
|
|
43
|
+
children: null,
|
|
44
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import {
|
|
4
|
+
Button, Container, Row, Col,
|
|
5
|
+
} from '@edx/paragon';
|
|
6
|
+
|
|
7
|
+
import { useAppEvent } from './hooks';
|
|
8
|
+
import {
|
|
9
|
+
FormattedMessage,
|
|
10
|
+
IntlProvider,
|
|
11
|
+
getMessages,
|
|
12
|
+
getLocale,
|
|
13
|
+
LOCALE_CHANGED,
|
|
14
|
+
} from '../i18n';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* An error page that displays a generic message for unexpected errors. Also contains a "Try
|
|
18
|
+
* Again" button to refresh the page.
|
|
19
|
+
*
|
|
20
|
+
* @memberof module:React
|
|
21
|
+
* @extends {Component}
|
|
22
|
+
*/
|
|
23
|
+
function ErrorPage({
|
|
24
|
+
message,
|
|
25
|
+
}) {
|
|
26
|
+
const [locale, setLocale] = useState(getLocale());
|
|
27
|
+
|
|
28
|
+
useAppEvent(LOCALE_CHANGED, () => {
|
|
29
|
+
setLocale(getLocale());
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/* istanbul ignore next */
|
|
33
|
+
const reload = () => {
|
|
34
|
+
global.location.reload();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<IntlProvider locale={locale} messages={getMessages()}>
|
|
39
|
+
<Container fluid className="py-5 justify-content-center align-items-start text-center">
|
|
40
|
+
<Row>
|
|
41
|
+
<Col>
|
|
42
|
+
<p className="text-muted">
|
|
43
|
+
<FormattedMessage
|
|
44
|
+
id="unexpected.error.message.text"
|
|
45
|
+
defaultMessage="An unexpected error occurred. Please click the button below to refresh the page."
|
|
46
|
+
description="error message when an unexpected error occurs"
|
|
47
|
+
/>
|
|
48
|
+
</p>
|
|
49
|
+
{message && (
|
|
50
|
+
<div role="alert" className="my-4">
|
|
51
|
+
<p>{message}</p>
|
|
52
|
+
</div>
|
|
53
|
+
)}
|
|
54
|
+
<Button onClick={reload}>
|
|
55
|
+
<FormattedMessage
|
|
56
|
+
id="unexpected.error.button.text"
|
|
57
|
+
defaultMessage="Try again"
|
|
58
|
+
description="text for button that tries to reload the app by refreshing the page"
|
|
59
|
+
/>
|
|
60
|
+
</Button>
|
|
61
|
+
</Col>
|
|
62
|
+
</Row>
|
|
63
|
+
</Container>
|
|
64
|
+
</IntlProvider>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
ErrorPage.propTypes = {
|
|
69
|
+
message: PropTypes.string,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
ErrorPage.defaultProps = {
|
|
73
|
+
message: null,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default ErrorPage;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { redirectToLogin } from '../auth';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A React component that, when rendered, redirects to the login page as a side effect. Uses
|
|
6
|
+
* `redirectToLogin` to perform the redirect.
|
|
7
|
+
*
|
|
8
|
+
* @see {@link module:frontend-platform/auth~redirectToLogin}
|
|
9
|
+
* @memberof module:React
|
|
10
|
+
*/
|
|
11
|
+
export default function LoginRedirect() {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
redirectToLogin(global.location.href);
|
|
14
|
+
}, []);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { Provider } from 'react-redux';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @memberof module:React
|
|
7
|
+
* @param {Object} props
|
|
8
|
+
*/
|
|
9
|
+
export default function OptionalReduxProvider({ store, children }) {
|
|
10
|
+
if (store === null) {
|
|
11
|
+
return children;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Provider store={store}>
|
|
16
|
+
{children}
|
|
17
|
+
</Provider>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
OptionalReduxProvider.propTypes = {
|
|
22
|
+
store: PropTypes.object, // eslint-disable-line
|
|
23
|
+
children: PropTypes.node.isRequired,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
OptionalReduxProvider.defaultProps = {
|
|
27
|
+
store: null,
|
|
28
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* eslint-disable react/prop-types */
|
|
2
|
+
import React, { useEffect } from 'react';
|
|
3
|
+
import { Route, useRouteMatch } from 'react-router-dom';
|
|
4
|
+
import { sendPageEvent } from '../analytics';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A react-router Route component that calls `sendPageEvent` when it becomes active.
|
|
8
|
+
*
|
|
9
|
+
* @see {@link module:frontend-platform/analytics~sendPageEvent}
|
|
10
|
+
* @memberof module:React
|
|
11
|
+
* @param {Object} props
|
|
12
|
+
*/
|
|
13
|
+
export default function PageRoute(props) {
|
|
14
|
+
const match = useRouteMatch({
|
|
15
|
+
path: props.path,
|
|
16
|
+
exact: props.exact,
|
|
17
|
+
strict: props.strict,
|
|
18
|
+
sensitive: props.sensitive,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (match) {
|
|
23
|
+
sendPageEvent();
|
|
24
|
+
}
|
|
25
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
26
|
+
}, [JSON.stringify(match)]);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Route {...props} />
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* eslint-disable import/prefer-default-export */
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { subscribe, unsubscribe } from '../pubSub';
|
|
4
|
+
import { sendTrackEvent } from '../analytics';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A React hook that allows functional components to subscribe to application events. This should
|
|
8
|
+
* be used sparingly - for the most part, Context should be used higher-up in the application to
|
|
9
|
+
* provide necessary data to a given component, rather than utilizing a non-React-like Pub/Sub
|
|
10
|
+
* mechanism.
|
|
11
|
+
*
|
|
12
|
+
* @memberof module:React
|
|
13
|
+
* @param {string} type
|
|
14
|
+
* @param {function} callback
|
|
15
|
+
*/
|
|
16
|
+
export const useAppEvent = (type, callback) => {
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const subscriptionToken = subscribe(type, callback);
|
|
19
|
+
|
|
20
|
+
return function cleanup() {
|
|
21
|
+
unsubscribe(subscriptionToken);
|
|
22
|
+
};
|
|
23
|
+
}, [callback, type]);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A React hook that tracks user's preferred color scheme (light or dark) and sends respective
|
|
28
|
+
* event to the tracking service.
|
|
29
|
+
*
|
|
30
|
+
* @memberof module:React
|
|
31
|
+
*/
|
|
32
|
+
export const useTrackColorSchemeChoice = () => {
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const trackColorSchemeChoice = ({ matches }) => {
|
|
35
|
+
const preferredColorScheme = matches ? 'dark' : 'light';
|
|
36
|
+
sendTrackEvent('openedx.ui.frontend-platform.prefers-color-scheme.selected', { preferredColorScheme });
|
|
37
|
+
};
|
|
38
|
+
const colorSchemeQuery = window.matchMedia?.('(prefers-color-scheme: dark)');
|
|
39
|
+
if (colorSchemeQuery) {
|
|
40
|
+
// send user's initial choice
|
|
41
|
+
trackColorSchemeChoice(colorSchemeQuery);
|
|
42
|
+
colorSchemeQuery.addEventListener('change', trackColorSchemeChoice);
|
|
43
|
+
}
|
|
44
|
+
return () => {
|
|
45
|
+
if (colorSchemeQuery) {
|
|
46
|
+
colorSchemeQuery.removeEventListener('change', trackColorSchemeChoice);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}, []);
|
|
50
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #### Import members from **@edx/frontend-platform/react**
|
|
3
|
+
* The React module provides a variety of React components, hooks, and contexts for use in an
|
|
4
|
+
* application.
|
|
5
|
+
*
|
|
6
|
+
* @module React
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { default as AppContext } from './AppContext';
|
|
10
|
+
export { default as AppProvider } from './AppProvider';
|
|
11
|
+
export { default as AuthenticatedPageRoute } from './AuthenticatedPageRoute';
|
|
12
|
+
export { default as ErrorBoundary } from './ErrorBoundary';
|
|
13
|
+
export { default as ErrorPage } from './ErrorPage';
|
|
14
|
+
export { default as LoginRedirect } from './LoginRedirect';
|
|
15
|
+
export { default as PageRoute } from './PageRoute';
|
|
16
|
+
export { useAppEvent } from './hooks';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @implements {GoogleAnalyticsLoader}
|
|
3
|
+
* @memberof module:GoogleAnalytics
|
|
4
|
+
*/
|
|
5
|
+
class GoogleAnalyticsLoader {
|
|
6
|
+
constructor({ config }) {
|
|
7
|
+
this.analyticsId = config.GOOGLE_ANALYTICS_4_ID;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
loadScript() {
|
|
11
|
+
if (!this.analyticsId) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
global.googleAnalytics = global.googleAnalytics || [];
|
|
16
|
+
const { googleAnalytics } = global;
|
|
17
|
+
|
|
18
|
+
// If the snippet was invoked do nothing.
|
|
19
|
+
if (googleAnalytics.invoked) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Invoked flag, to make sure the snippet
|
|
24
|
+
// is never invoked twice.
|
|
25
|
+
googleAnalytics.invoked = true;
|
|
26
|
+
|
|
27
|
+
googleAnalytics.load = (key, options) => {
|
|
28
|
+
const scriptSrc = document.createElement('script');
|
|
29
|
+
scriptSrc.type = 'text/javascript';
|
|
30
|
+
scriptSrc.async = true;
|
|
31
|
+
scriptSrc.src = `https://www.googletagmanager.com/gtag/js?id=${key}`;
|
|
32
|
+
|
|
33
|
+
const scriptGtag = document.createElement('script');
|
|
34
|
+
scriptGtag.innerHTML = `
|
|
35
|
+
window.dataLayer = window.dataLayer || [];
|
|
36
|
+
function gtag(){dataLayer.push(arguments);}
|
|
37
|
+
gtag('js', new Date());
|
|
38
|
+
gtag('config', '${key}');
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
// Insert our scripts next to the first script element.
|
|
42
|
+
const first = document.getElementsByTagName('script')[0];
|
|
43
|
+
first.parentNode.insertBefore(scriptSrc, first);
|
|
44
|
+
first.parentNode.insertBefore(scriptGtag, first);
|
|
45
|
+
googleAnalytics._loadOptions = options; // eslint-disable-line no-underscore-dangle
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Load GoogleAnalytics with your key.
|
|
49
|
+
googleAnalytics.load(this.analyticsId);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default GoogleAnalyticsLoader;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #### Import members from **@edx/frontend-platform/testing**
|
|
3
|
+
* The testing module provides helpers for writing tests in Jest.
|
|
4
|
+
*
|
|
5
|
+
* @module Testing
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { default as initializeMockApp } from './initializeMockApp';
|
|
9
|
+
export { default as mockMessages } from './mockMessages';
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { configure as configureAnalytics, MockAnalyticsService } from '../analytics';
|
|
2
|
+
import { configure as configureI18n } from '../i18n';
|
|
3
|
+
import { configure as configureLogging, MockLoggingService } from '../logging';
|
|
4
|
+
import { configure as configureAuth, MockAuthService } from '../auth';
|
|
5
|
+
import { getConfig } from '../config';
|
|
6
|
+
import mockMessages from './mockMessages';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Initializes a mock application for component testing. The mock application includes
|
|
10
|
+
* mock analytics, auth, and logging services, and the real i18n service.
|
|
11
|
+
*
|
|
12
|
+
* See MockAnalyticsService, MockAuthService, and MockLoggingService for mock implementation
|
|
13
|
+
* details. For the most part, the analytics and logging services just implement their functions
|
|
14
|
+
* with jest.fn() and do nothing else, whereas the MockAuthService actually has some behavior
|
|
15
|
+
* implemented, it just doesn't make any HTTP calls.
|
|
16
|
+
*
|
|
17
|
+
* Note that this mock application is not sufficient for testing the full application lifecycle or
|
|
18
|
+
* initialization callbacks/custom handlers as described in the 'initialize' function's
|
|
19
|
+
* documentation. It exists merely to set up the mock services that components themselves tend to
|
|
20
|
+
* interact with most often. It could be extended to allow for setting up custom handlers fairly
|
|
21
|
+
* easily, as this functionality would be more-or-less identical to what the real initialize
|
|
22
|
+
* function does.
|
|
23
|
+
*
|
|
24
|
+
* Example:
|
|
25
|
+
*
|
|
26
|
+
* ```
|
|
27
|
+
* import { initializeMockApp } from '@edx/frontend-platform/testing';
|
|
28
|
+
* import { logInfo } from '@edx/frontend-platform/logging';
|
|
29
|
+
*
|
|
30
|
+
* describe('initializeMockApp', () => {
|
|
31
|
+
* it('mocks things correctly', () => {
|
|
32
|
+
* const { loggingService } = initializeMockApp();
|
|
33
|
+
* logInfo('test', {});
|
|
34
|
+
* expect(loggingService.logInfo).toHaveBeenCalledWith('test', {});
|
|
35
|
+
* });
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @param {Object} [options]
|
|
40
|
+
* @param {*} [options.messages] A i18n-compatible messages object, or an array of such objects. If
|
|
41
|
+
* an array is provided, duplicate keys are resolved with the last-one-in winning.
|
|
42
|
+
* @param {UserData|null} [options.authenticatedUser] A UserData object representing the
|
|
43
|
+
* authenticated user. This is passed directly to MockAuthService.
|
|
44
|
+
* @memberof module:Testing
|
|
45
|
+
*/
|
|
46
|
+
export default function initializeMockApp({
|
|
47
|
+
messages = mockMessages,
|
|
48
|
+
authenticatedUser = null,
|
|
49
|
+
} = {}) {
|
|
50
|
+
const loggingService = configureLogging(MockLoggingService, {
|
|
51
|
+
config: getConfig(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const authService = configureAuth(MockAuthService, {
|
|
55
|
+
config: { ...getConfig(), authenticatedUser },
|
|
56
|
+
loggingService,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const analyticsService = configureAnalytics(MockAnalyticsService, {
|
|
60
|
+
config: getConfig(),
|
|
61
|
+
httpClient: authService.getAuthenticatedHttpClient(),
|
|
62
|
+
loggingService,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// The i18n service configure function has no return value, since there isn't a service class.
|
|
66
|
+
configureI18n({
|
|
67
|
+
config: getConfig(),
|
|
68
|
+
loggingService,
|
|
69
|
+
messages,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
analyticsService,
|
|
74
|
+
authService,
|
|
75
|
+
loggingService,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An empty messages object suitable for fulfilling the i18n service's contract.
|
|
3
|
+
* @memberof module:Testing
|
|
4
|
+
*/
|
|
5
|
+
const messages = {
|
|
6
|
+
ar: {},
|
|
7
|
+
'es-419': {},
|
|
8
|
+
fr: {},
|
|
9
|
+
'zh-cn': {},
|
|
10
|
+
ca: {},
|
|
11
|
+
he: {},
|
|
12
|
+
id: {},
|
|
13
|
+
'ko-kr': {},
|
|
14
|
+
pl: {},
|
|
15
|
+
'pt-br': {},
|
|
16
|
+
ru: {},
|
|
17
|
+
th: {},
|
|
18
|
+
uk: {},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default messages;
|