Compare commits
42 Commits
swayamrana
...
jwesson/in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d598cf1e4b | ||
|
|
d5949f55c2 | ||
|
|
cd28310937 | ||
|
|
b75347ad06 | ||
|
|
bd931338d8 | ||
|
|
f03d5afa0d | ||
|
|
910e17f75d | ||
|
|
2fa5cadf22 | ||
|
|
bd8221997e | ||
|
|
43f485d841 | ||
|
|
b892ba763e | ||
|
|
896905b457 | ||
|
|
095b91c8cb | ||
|
|
a6e63a8686 | ||
|
|
67c8d79aa2 | ||
|
|
f76185d57d | ||
|
|
f0678ca94c | ||
|
|
e73b646263 | ||
|
|
ddb8494471 | ||
|
|
a576bdf98b | ||
|
|
21dcadba5b | ||
|
|
e770101e4e | ||
|
|
8ca5ea5809 | ||
|
|
d687ea30cb | ||
|
|
ecda751786 | ||
|
|
e58b174c9e | ||
|
|
6c82805c7a | ||
|
|
d1d98794ab | ||
|
|
3c7baaa91b | ||
|
|
fe800f2ee9 | ||
|
|
e1d4e9b474 | ||
|
|
cf7568bcfb | ||
|
|
a0fd863bc4 | ||
|
|
b63341fe99 | ||
|
|
1887167d0e | ||
|
|
57de2b4156 | ||
|
|
aaf6935577 | ||
|
|
03b7859b20 | ||
|
|
2800e89f02 | ||
|
|
0cc03e5fec | ||
|
|
44f1a5f0cd | ||
|
|
2694f6f754 |
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
|
||||
|
||||
2
Makefile
2
Makefile
@@ -7,7 +7,7 @@ transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
transifex_temp = ./temp/babel-plugin-formatjs
|
||||
|
||||
NPM_TESTS=build i18n_extract lint test
|
||||
|
||||
|
||||
36
README.rst
36
README.rst
@@ -1,5 +1,6 @@
|
||||
#####################
|
||||
frontend-app-profile
|
||||
##########################
|
||||
#####################
|
||||
|
||||
|license-badge| |status-badge| |ci-badge| |codecov-badge|
|
||||
|
||||
@@ -17,8 +18,9 @@ frontend-app-profile
|
||||
:target: https://codecov.io/github/openedx/frontend-app-profile?branch=main
|
||||
:alt: Codecov
|
||||
|
||||
********
|
||||
Purpose
|
||||
=======
|
||||
********
|
||||
|
||||
This is a micro-frontend application responsible for the display and updating of user profiles.
|
||||
|
||||
@@ -26,11 +28,12 @@ When a user views their own profile, they're given fields to edit their full nam
|
||||
|
||||
When a user views someone else's profile, they see all those fields that that user set as public.
|
||||
|
||||
***************
|
||||
Getting Started
|
||||
===============
|
||||
***************
|
||||
|
||||
Devstack Installation
|
||||
---------------------
|
||||
Installation
|
||||
============
|
||||
|
||||
Follow these steps to provision, run, and enable an instance of the
|
||||
Profile MFE for local development via the `devstack`_.
|
||||
@@ -46,13 +49,30 @@ Profile MFE for local development via the `devstack`_.
|
||||
|
||||
.. code-block::
|
||||
|
||||
npm install
|
||||
npm start # The server will run on port 1995
|
||||
1. Clone your new repo:
|
||||
|
||||
``git clone https://github.com/openedx/frontend-app-profile.git``
|
||||
|
||||
2. Use node v18.x.
|
||||
|
||||
The current version of the micro-frontend build scripts support 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>`_.
|
||||
|
||||
3. Install npm dependencies:
|
||||
|
||||
``cd frontend-app-profile && npm ci``
|
||||
|
||||
4. Start the dev server:
|
||||
|
||||
``npm start``
|
||||
The server will run on port 1995
|
||||
|
||||
Once the dev server is up, visit http://localhost:1995/u/staff.
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
=============
|
||||
|
||||
This MFE is configured via node environment variables supplied at build time. See the .env file for the list of required environment variables. Example build syntax with a single environment variable:
|
||||
|
||||
|
||||
8335
package-lock.json
generated
8335
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
@@ -28,10 +28,11 @@
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
||||
"@edx/frontend-component-footer": "12.1.2",
|
||||
"@edx/frontend-component-header": "4.4.4",
|
||||
"@edx/frontend-platform": "4.6.0",
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-footer": "12.5.1",
|
||||
"@edx/frontend-component-header": "4.8.0",
|
||||
"@edx/frontend-platform": "5.6.1",
|
||||
"@edx/frontend-plugin-framework": "openedx/frontend-plugin-framework#jwesson/install-plugins",
|
||||
"@edx/paragon": "^20.44.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
@@ -40,7 +41,7 @@
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@pact-foundation/pact": "^11.0.2",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.32.0",
|
||||
"core-js": "3.33.1",
|
||||
"history": "5.3.0",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"lodash.get": "4.4.2",
|
||||
@@ -49,30 +50,31 @@
|
||||
"prop-types": "15.8.1",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-error-boundary": "^4.0.11",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-router": "5.3.4",
|
||||
"react-router-dom": "5.3.4",
|
||||
"react-router": "6.16.0",
|
||||
"react-router-dom": "6.16.0",
|
||||
"redux": "4.2.1",
|
||||
"redux-devtools-extension": "2.13.9",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-saga": "1.2.3",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "0.13.11",
|
||||
"regenerator-runtime": "0.14.0",
|
||||
"reselect": "4.1.8",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "17.6.7",
|
||||
"@commitlint/config-angular": "17.6.7",
|
||||
"@commitlint/cli": "17.8.1",
|
||||
"@commitlint/config-angular": "17.8.1",
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/frontend-build": "12.9.3",
|
||||
"@edx/frontend-build": "13.0.4",
|
||||
"@edx/reactifex": "2.2.0",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
|
||||
"codecov": "3.8.3",
|
||||
"enzyme": "3.11.0",
|
||||
"glob": "10.3.3",
|
||||
"glob": "10.3.10",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "1.5.4"
|
||||
|
||||
93
plugins/Plugin.jsx
Normal file
93
plugins/Plugin.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import React, {
|
||||
useEffect, useMemo, useState,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import {
|
||||
dispatchMountedEvent, dispatchReadyEvent, dispatchUnmountedEvent, useHostEvent,
|
||||
} from './data/hooks';
|
||||
import { PLUGIN_RESIZE } from './data/constants';
|
||||
|
||||
// see example-plugin-app/src/PluginOne.jsx for example of customizing errorFallback
|
||||
function errorFallbackDefault() {
|
||||
return (
|
||||
<div>
|
||||
<h2>
|
||||
Oops! An error occurred. Please refresh the screen to try again.
|
||||
</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function Plugin({
|
||||
children, className, style, ready, errorFallbackProp,
|
||||
}) {
|
||||
const [dimensions, setDimensions] = useState({
|
||||
width: null,
|
||||
height: null,
|
||||
});
|
||||
|
||||
const finalStyle = useMemo(() => ({
|
||||
...dimensions,
|
||||
...style,
|
||||
}), [dimensions, style]);
|
||||
|
||||
const errorFallback = errorFallbackProp || errorFallbackDefault;
|
||||
|
||||
// Error logging function
|
||||
// Need to confirm: When an error is caught here, the logging will be sent to the child MFE's logging service
|
||||
const logErrorToService = (error, info) => {
|
||||
logError(error, { stack: info.componentStack });
|
||||
};
|
||||
|
||||
useHostEvent(PLUGIN_RESIZE, ({ payload }) => {
|
||||
setDimensions({
|
||||
width: payload.width,
|
||||
height: payload.height,
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
dispatchMountedEvent();
|
||||
|
||||
return () => {
|
||||
dispatchUnmountedEvent();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (ready) {
|
||||
dispatchReadyEvent();
|
||||
}
|
||||
}, [ready]);
|
||||
|
||||
return (
|
||||
<div className={className} style={finalStyle}>
|
||||
<ErrorBoundary
|
||||
FallbackComponent={errorFallback}
|
||||
onError={logErrorToService}
|
||||
>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Plugin.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
errorFallbackProp: PropTypes.func,
|
||||
ready: PropTypes.bool,
|
||||
style: PropTypes.object, // eslint-disable-line
|
||||
};
|
||||
|
||||
Plugin.defaultProps = {
|
||||
className: null,
|
||||
errorFallbackProp: null,
|
||||
style: {},
|
||||
ready: true,
|
||||
};
|
||||
42
plugins/PluginContainer.jsx
Normal file
42
plugins/PluginContainer.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import PluginContainerIframe from './PluginContainerIframe';
|
||||
|
||||
import {
|
||||
IFRAME_PLUGIN,
|
||||
} from './data/constants';
|
||||
import { pluginConfigShape } from './data/shapes';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function PluginContainer({ config, ...props }) {
|
||||
if (config === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// this will allow for future plugin types to be inserted in the PluginErrorBoundary
|
||||
let renderer = null;
|
||||
switch (config.type) {
|
||||
case IFRAME_PLUGIN:
|
||||
renderer = (
|
||||
<PluginContainerIframe config={config} {...props} />
|
||||
);
|
||||
break;
|
||||
// istanbul ignore next: default isn't meaningful, just satisfying linter
|
||||
default:
|
||||
}
|
||||
|
||||
return (
|
||||
renderer
|
||||
);
|
||||
}
|
||||
|
||||
PluginContainer.propTypes = {
|
||||
config: pluginConfigShape,
|
||||
};
|
||||
|
||||
PluginContainer.defaultProps = {
|
||||
config: null,
|
||||
};
|
||||
99
plugins/PluginContainerIframe.jsx
Normal file
99
plugins/PluginContainerIframe.jsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import React, {
|
||||
useEffect, useState,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {
|
||||
PLUGIN_MOUNTED,
|
||||
PLUGIN_READY,
|
||||
PLUGIN_RESIZE,
|
||||
} from './data/constants';
|
||||
import {
|
||||
dispatchPluginEvent,
|
||||
useElementSize,
|
||||
usePluginEvent,
|
||||
} from './data/hooks';
|
||||
import { pluginConfigShape } from './data/shapes';
|
||||
|
||||
/**
|
||||
* Feature policy for iframe, allowing access to certain courseware-related media.
|
||||
*
|
||||
* We must use the wildcard (*) origin for each feature, as courseware content
|
||||
* may be embedded in external iframes. Notably, xblock-lti-consumer is a popular
|
||||
* block that iframes external course content.
|
||||
|
||||
* This policy was selected in conference with the edX Security Working Group.
|
||||
* Changes to it should be vetted by them (security@edx.org).
|
||||
*/
|
||||
export const IFRAME_FEATURE_POLICY = (
|
||||
'fullscreen; microphone *; camera *; midi *; geolocation *; encrypted-media *'
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function PluginContainerIframe({
|
||||
config, fallback, className, ...props
|
||||
}) {
|
||||
const { url } = config;
|
||||
const { title, scrolling } = props;
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
const [iframeRef, iframeElement, width, height] = useElementSize();
|
||||
|
||||
useEffect(() => {
|
||||
if (mounted) {
|
||||
dispatchPluginEvent(iframeElement, {
|
||||
type: PLUGIN_RESIZE,
|
||||
payload: {
|
||||
width,
|
||||
height,
|
||||
},
|
||||
}, url);
|
||||
}
|
||||
}, [iframeElement, mounted, width, height, url]);
|
||||
|
||||
usePluginEvent(iframeElement, PLUGIN_MOUNTED, () => {
|
||||
setMounted(true);
|
||||
});
|
||||
|
||||
usePluginEvent(iframeElement, PLUGIN_READY, () => {
|
||||
setReady(true);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
title={title}
|
||||
src={url}
|
||||
allow={IFRAME_FEATURE_POLICY}
|
||||
scrolling={scrolling}
|
||||
referrerPolicy="origin" // The sent referrer will be limited to the origin of the referring page: its scheme, host, and port.
|
||||
className={classNames(
|
||||
'border border-0',
|
||||
{ 'd-none': !ready },
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{!ready && fallback}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
PluginContainerIframe.propTypes = {
|
||||
config: pluginConfigShape,
|
||||
fallback: PropTypes.node,
|
||||
scrolling: PropTypes.oneOf(['auto', 'yes', 'no']),
|
||||
title: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
PluginContainerIframe.defaultProps = {
|
||||
config: null,
|
||||
fallback: null,
|
||||
scrolling: 'auto',
|
||||
title: null,
|
||||
className: null,
|
||||
};
|
||||
45
plugins/PluginErrorBoundary.jsx
Normal file
45
plugins/PluginErrorBoundary.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
export default class PluginErrorBoundary extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
// Update state so the next render will show the fallback UI.
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error, info) {
|
||||
logError(error, { stack: info.componentStack });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
// You can render any custom fallback UI
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="plugin.load.failure.text"
|
||||
defaultMessage="This content failed to load."
|
||||
description="error message when an unexpected error occurs"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
PluginErrorBoundary.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
PluginErrorBoundary.defaultProps = {
|
||||
children: null,
|
||||
};
|
||||
75
plugins/PluginSlot.jsx
Normal file
75
plugins/PluginSlot.jsx
Normal file
@@ -0,0 +1,75 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Spinner } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// import { usePluginSlot } from './data/hooks';
|
||||
import PluginContainer from './PluginContainer';
|
||||
|
||||
const PluginSlot = forwardRef(({
|
||||
as, id, intl, pluginProps, children, ...props
|
||||
}, ref) => {
|
||||
/* the plugins below are obtained by the id passed into PluginSlot by the Host MFE. See example/src/PluginsPage.jsx
|
||||
for an example of how PluginSlot is populated, and example/src/index.jsx for a dummy JS config that holds all plugins
|
||||
*/
|
||||
// const { plugins, keepDefault } = usePluginSlot(id);
|
||||
|
||||
const { fallback } = pluginProps;
|
||||
|
||||
// TODO: Add internationalization to the "Loading" text on the spinner.
|
||||
let finalFallback = (
|
||||
<div className={classNames(pluginProps.className, 'd-flex justify-content-center align-items-center')}>
|
||||
<Spinner animation="border" screenReaderText="Loading" />
|
||||
</div>
|
||||
);
|
||||
if (fallback !== undefined) {
|
||||
finalFallback = fallback;
|
||||
}
|
||||
|
||||
let finalChildren = [];
|
||||
// if (plugins.length > 0) {
|
||||
// if (keepDefault) {
|
||||
// finalChildren.push(children);
|
||||
// }
|
||||
// plugins.forEach((pluginConfig) => {
|
||||
// finalChildren.push(
|
||||
// <PluginContainer
|
||||
// key={pluginConfig.url}
|
||||
// config={pluginConfig}
|
||||
// fallback={finalFallback}
|
||||
// {...pluginProps}
|
||||
// />,
|
||||
// );
|
||||
// });
|
||||
// } else {
|
||||
finalChildren = children;
|
||||
// }
|
||||
|
||||
return React.createElement(
|
||||
as,
|
||||
{
|
||||
...props,
|
||||
ref,
|
||||
},
|
||||
finalChildren,
|
||||
);
|
||||
});
|
||||
|
||||
export default injectIntl(PluginSlot);
|
||||
|
||||
PluginSlot.propTypes = {
|
||||
as: PropTypes.elementType,
|
||||
children: PropTypes.node,
|
||||
id: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
pluginProps: PropTypes.object, // eslint-disable-line
|
||||
};
|
||||
|
||||
PluginSlot.defaultProps = {
|
||||
as: 'div',
|
||||
children: null,
|
||||
pluginProps: {},
|
||||
};
|
||||
8
plugins/data/constants.js
Normal file
8
plugins/data/constants.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// TODO: We expect other plugin types to be added here, such as LTI_PLUGIN and BUILD_TIME_PLUGIN.
|
||||
export const IFRAME_PLUGIN = 'IFRAME_PLUGIN'; // loads iframe at the URL, rather than loading a JS file.
|
||||
|
||||
// Plugin lifecycle events
|
||||
export const PLUGIN_MOUNTED = 'PLUGIN_MOUNTED';
|
||||
export const PLUGIN_READY = 'PLUGIN_READY';
|
||||
export const PLUGIN_UNMOUNTED = 'PLUGIN_UNMOUNTED';
|
||||
export const PLUGIN_RESIZE = 'PLUGIN_RESIZE';
|
||||
96
plugins/data/hooks.js
Normal file
96
plugins/data/hooks.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,
|
||||
} from 'react';
|
||||
import { PLUGIN_MOUNTED, PLUGIN_READY, PLUGIN_UNMOUNTED } from './constants';
|
||||
|
||||
export function useMessageEvent(srcWindow, type, callback) {
|
||||
useLayoutEffect(() => {
|
||||
const listener = (event) => {
|
||||
// Filter messages to those from our source window.
|
||||
if (event.source === srcWindow) {
|
||||
if (event.data.type === type) {
|
||||
callback({ type, payload: event.data.payload });
|
||||
}
|
||||
}
|
||||
};
|
||||
if (srcWindow !== null) {
|
||||
global.addEventListener('message', listener);
|
||||
}
|
||||
return () => {
|
||||
global.removeEventListener('message', listener);
|
||||
};
|
||||
}, [srcWindow, type, callback]);
|
||||
}
|
||||
|
||||
export function useHostEvent(type, callback) {
|
||||
useMessageEvent(global.parent, type, callback);
|
||||
}
|
||||
|
||||
export function usePluginEvent(iframeElement, type, callback) {
|
||||
const contentWindow = iframeElement ? iframeElement.contentWindow : null;
|
||||
useMessageEvent(contentWindow, type, callback);
|
||||
}
|
||||
|
||||
export function dispatchMessageEvent(targetWindow, message, targetOrigin) {
|
||||
// Checking targetOrigin falsiness here since '', null or undefined would all be reasons not to
|
||||
// try to post a message to the origin.
|
||||
if (targetOrigin) {
|
||||
targetWindow.postMessage(message, targetOrigin);
|
||||
}
|
||||
}
|
||||
|
||||
export function dispatchPluginEvent(iframeElement, message, targetOrigin) {
|
||||
dispatchMessageEvent(iframeElement.contentWindow, message, targetOrigin);
|
||||
}
|
||||
|
||||
export function dispatchHostEvent(message) {
|
||||
dispatchMessageEvent(global.parent, message, global.document.referrer);
|
||||
}
|
||||
|
||||
export function dispatchReadyEvent() {
|
||||
dispatchHostEvent({ type: PLUGIN_READY });
|
||||
}
|
||||
|
||||
export function dispatchMountedEvent() {
|
||||
dispatchHostEvent({ type: PLUGIN_MOUNTED });
|
||||
}
|
||||
|
||||
export function dispatchUnmountedEvent() {
|
||||
dispatchHostEvent({ type: PLUGIN_UNMOUNTED });
|
||||
}
|
||||
|
||||
export function useElementSize() {
|
||||
const observerRef = useRef();
|
||||
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
||||
|
||||
const [element, setElement] = useState(null);
|
||||
|
||||
const measuredRef = useCallback(_element => {
|
||||
setElement(_element);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
observerRef.current = new ResizeObserver(() => {
|
||||
if (element) {
|
||||
setDimensions({
|
||||
width: element.clientWidth,
|
||||
height: element.clientHeight,
|
||||
});
|
||||
setOffset({
|
||||
x: element.offsetLeft,
|
||||
y: element.offsetTop,
|
||||
});
|
||||
}
|
||||
});
|
||||
if (element) {
|
||||
observerRef.current.observe(element);
|
||||
}
|
||||
}, [element]);
|
||||
|
||||
return useMemo(
|
||||
() => ([measuredRef, element, dimensions.width, dimensions.height, offset.x, offset.y]),
|
||||
[measuredRef, element, dimensions, offset],
|
||||
);
|
||||
}
|
||||
10
plugins/data/shapes.js
Normal file
10
plugins/data/shapes.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import PropTypes from 'prop-types';
|
||||
import { IFRAME_PLUGIN } from './constants';
|
||||
|
||||
export const pluginConfigShape = PropTypes.shape({
|
||||
url: PropTypes.string.isRequired,
|
||||
type: PropTypes.oneOf([IFRAME_PLUGIN]).isRequired,
|
||||
// This is a place for us to put any generic props we want to pass to the component. We need it.
|
||||
props: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
});
|
||||
18
plugins/index.js
Normal file
18
plugins/index.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// export {
|
||||
// usePluginSlot,
|
||||
// } from './data/hooks';
|
||||
export {
|
||||
default as Plugin,
|
||||
} from './Plugin';
|
||||
export {
|
||||
default as PluginContainer,
|
||||
} from './PluginContainer';
|
||||
export {
|
||||
default as PluginSlot,
|
||||
} from './PluginSlot';
|
||||
export {
|
||||
IFRAME_PLUGIN,
|
||||
} from './data/constants';
|
||||
export {
|
||||
default as PluginErrorBoundary,
|
||||
} from './PluginErrorBoundary';
|
||||
@@ -24,7 +24,7 @@
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["@edx"],
|
||||
"matchPackagePatterns": ["@edx", "@openedx"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": true
|
||||
}
|
||||
|
||||
@@ -53,33 +53,5 @@
|
||||
"profile.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في العنوان. رجاءً تحقق من العنوان و حاول مجدّدًا.",
|
||||
"profile.viewMyRecords": "عرض سجلّاتي",
|
||||
"profile.loading": "يتم تحميل الملف الشخصي...",
|
||||
"profile.username.description": "معلومات ملفك الشخصي تظهر لك فقط. وحده اسم المستخدم الخاص بك يظهر للآخرين على {siteName}.",
|
||||
"skills.builder.header.title": "باني المهارات",
|
||||
"skills.builder.header.subheading": "دع (المنصة التعليمية أو edX) ان تكون دليلك",
|
||||
"skills.builder.header.title.is.medium": "edX Skills builder",
|
||||
"go.back.button": "العودة إلى الخلف",
|
||||
"next.step.button": "الخطوة التالية",
|
||||
"exit.button": "خروج",
|
||||
"select.preferences": "حدد التفضيلاتك",
|
||||
"review.results": "مراجعة النتائج",
|
||||
"skills.builder.description": "ابحث عن الدورات والبرامج المناسبة التي تساعدك في الوصول إلى أهدافك.",
|
||||
"learning.goal.prompt": "أولاً، أخبرنا بما تريد تحقيقه",
|
||||
"select.learning.goal": "اختر هدفًا",
|
||||
"learning.goal.start_career": "أريد أن أبدأ مسيرتي المهنية",
|
||||
"learning.goal.advance_career": "أريد أن ارتقي في مهنتي",
|
||||
"learning.goal.change_career": "اريد تغيير المهنتي",
|
||||
"learning.goal.something.new": "أريد أن أتعلم شيئًا جديدًا",
|
||||
"learning.goal.something.else": "شيء آخر",
|
||||
"job.title.prompt": "بعد ذلك، ابحث وحدد المسمى الوظيفي الحالي الخاص بك",
|
||||
"job.title.input.placeholder.text": "أبحث واختار مسمى وظيفي",
|
||||
"student.checkbox.prompt": "أنا طالب",
|
||||
"currently.looking.checkbox.prompt": "أنا حاليا أبحث عن عمل",
|
||||
"career.interest.prompt": "ما هي المهن التي تثير اهتمامك؟",
|
||||
"career.interest.input.placeholder.text": "حدد ما يصل إلى ثلاث عناوين وظيفية جديدة",
|
||||
"career.interest.remove.button.alt.text": "إزالة الاهتمام الوظيفي:",
|
||||
"matches.found.success.alert": "وجدنا المهارات والدورات التي تناسب تفضيلاتك!",
|
||||
"matches.not.found.danger.alert": "لم نتمكن من استرداد التوصيات في هذا الوقت. الرجاء معاودة المحاولة في وقت لاحق.",
|
||||
"related.skills.heading": "مهارات ذات الصلة",
|
||||
"related.skills.selectable.box.label.text": "مهارات ذات الصلة:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"profile.username.description": "معلومات ملفك الشخصي تظهر لك فقط. وحده اسم المستخدم الخاص بك يظهر للآخرين على {siteName}."
|
||||
}
|
||||
@@ -53,33 +53,5 @@
|
||||
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"profile.viewMyRecords": "View My Records",
|
||||
"profile.loading": "Profile loading...",
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"skills.builder.header.title.is.medium": "edX Skills builder",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit",
|
||||
"select.preferences": "Select preferences",
|
||||
"review.results": "Review results",
|
||||
"skills.builder.description": "Find the right courses and programs that help you reach your goals.",
|
||||
"learning.goal.prompt": "First, tell us what you want to achieve",
|
||||
"select.learning.goal": "Select a goal",
|
||||
"learning.goal.start_career": "I want to start my career",
|
||||
"learning.goal.advance_career": "I want to advance my career",
|
||||
"learning.goal.change_career": "I want to change careers",
|
||||
"learning.goal.something.new": "I want to learn something new",
|
||||
"learning.goal.something.else": "Something else",
|
||||
"job.title.prompt": "Next, search and select your current job title",
|
||||
"job.title.input.placeholder.text": "Search and select a job title",
|
||||
"student.checkbox.prompt": "I'm a student",
|
||||
"currently.looking.checkbox.prompt": "I'm currently looking for work",
|
||||
"career.interest.prompt": "What careers are you interested in?",
|
||||
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
|
||||
"career.interest.remove.button.alt.text": "Remove career interest: ",
|
||||
"matches.found.success.alert": "We found skills and courses that match your preferences!",
|
||||
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
|
||||
"related.skills.heading": "Related Skills",
|
||||
"related.skills.selectable.box.label.text": "Related skills:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"profile.page.title": "Profile | {siteName}",
|
||||
"profile.age.details": "To share your profile with other {siteName} learners, you must confirm that you are over the age of 13.",
|
||||
"profile.page.title": "Profil | {siteName}",
|
||||
"profile.age.details": "Um Ihr Profil mit anderen {siteName}-Lernern zu teilen, müssen Sie bestätigen, dass Sie über 13 Jahre alt sind.",
|
||||
"profile.age.set.date": "Legen Sie Ihr Geburtsdatum fest",
|
||||
"profile.datejoined.member.since": "Mitglied seit {year}",
|
||||
"profile.bio.empty": "Fügen Sie Ihre Kurzbiografie hinzu",
|
||||
@@ -33,12 +33,12 @@
|
||||
"profile.formcontrols.button.saving": "Speichert",
|
||||
"profile.formcontrols.button.saved": "Gespeichert",
|
||||
"profile.visibility.who.just.me": "Nur ich",
|
||||
"profile.visibility.who.everyone": "Everyone on {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Learning Goal",
|
||||
"profile.learningGoal.options.start_career": "I want to start my career",
|
||||
"profile.learningGoal.options.advance_career": "I want to advance my career",
|
||||
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
|
||||
"profile.learningGoal.options.something_else": "Something else",
|
||||
"profile.visibility.who.everyone": "Alle auf {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Lernziel",
|
||||
"profile.learningGoal.options.start_career": "Ich möchte meine Karriere starten",
|
||||
"profile.learningGoal.options.advance_career": "Ich möchte mich beruflich weiterentwickeln",
|
||||
"profile.learningGoal.options.learn_something_new": "Ich möchte etwas Neues lernen",
|
||||
"profile.learningGoal.options.something_else": "Etwas anderes",
|
||||
"profile.name.full.name": "Vollständiger Name",
|
||||
"profile.name.details": "Dies ist der Name, der in Ihrem Konto und auf Ihren Zertifikaten erscheint.",
|
||||
"profile.name.empty": "Name hinzufügen",
|
||||
@@ -53,9 +53,5 @@
|
||||
"profile.notfound.message": "Die gesuchte Seite ist nicht verfügbar oder es liegt ein Fehler in der URL vor. Bitte überprüfen Sie die URL und versuchen Sie es erneut.",
|
||||
"profile.viewMyRecords": "Meine Aufzeichnungen anzeigen",
|
||||
"profile.loading": "Profil lädt...",
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step"
|
||||
"profile.username.description": "Ihre Profilinformationen sind nur für Sie sichtbar. Nur Ihr Benutzername ist für andere auf {siteName} sichtbar."
|
||||
}
|
||||
@@ -53,33 +53,5 @@
|
||||
"profile.notfound.message": "La página que estas buscando no está disponible o hay un error en la URL. Por favor, comprueba la URL y vuelve a intentarlo.",
|
||||
"profile.viewMyRecords": "Ver mis registros",
|
||||
"profile.loading": "Cargando perfil...",
|
||||
"profile.username.description": "La información del perfil solo la visualiza usted. Solo el nombre de usuario es visible para los demás en {siteName}.",
|
||||
"skills.builder.header.title": "Constructor de habilidades",
|
||||
"skills.builder.header.subheading": "Dejanos ser tu guía",
|
||||
"skills.builder.header.title.is.medium": "edX Skills builder",
|
||||
"go.back.button": "Volver Atrás",
|
||||
"next.step.button": "Próximo paso",
|
||||
"exit.button": "Salida",
|
||||
"select.preferences": "Seleccionar preferencias",
|
||||
"review.results": "Revisar resultados",
|
||||
"skills.builder.description": "Encontrar los cursos y programas adecuados que lo ayuden a alcanzar sus metas.",
|
||||
"learning.goal.prompt": "Primero, contar qué quieres lograr",
|
||||
"select.learning.goal": "Seleccionar una meta",
|
||||
"learning.goal.start_career": "Quiero empezar mi carrera",
|
||||
"learning.goal.advance_career": "Quiero avanzar en mi carrera",
|
||||
"learning.goal.change_career": "Quiero cambiar de carrera",
|
||||
"learning.goal.something.new": "Quiero aprender algo nuevo",
|
||||
"learning.goal.something.else": "Algo más",
|
||||
"job.title.prompt": "A continuación, busque y seleccione su título de trabajo actual",
|
||||
"job.title.input.placeholder.text": "Search and select a job title",
|
||||
"student.checkbox.prompt": "Soy un estudiante",
|
||||
"currently.looking.checkbox.prompt": "Actualmente estoy buscando trabajo",
|
||||
"career.interest.prompt": "¿Qué carreras te interesan?",
|
||||
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
|
||||
"career.interest.remove.button.alt.text": "Eliminar interés profesional:",
|
||||
"matches.found.success.alert": "We found skills and courses that match your preferences!",
|
||||
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
|
||||
"related.skills.heading": "Related Skills",
|
||||
"related.skills.selectable.box.label.text": "Related skills:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"profile.username.description": "La información del perfil solo la visualiza usted. Solo el nombre de usuario es visible para los demás en {siteName}."
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"profile.page.title": "پرونده کاربری {siteName}",
|
||||
"profile.age.details": "برای اشتراکگذاری پرونده کاربری خود با سایر یادگیرندگان {siteName}، باید تأیید کنید که بیش از 13 سال سن دارید.",
|
||||
"profile.age.set.date": "تنظیم تاریخ تولد",
|
||||
"profile.datejoined.member.since": "عضو شده از {year}",
|
||||
@@ -33,6 +34,11 @@
|
||||
"profile.formcontrols.button.saved": "ذخیره شد",
|
||||
"profile.visibility.who.just.me": "فقط من",
|
||||
"profile.visibility.who.everyone": "هرکسی در {siteName}",
|
||||
"profile.learningGoal.learningGoal": "هدف یادگیری",
|
||||
"profile.learningGoal.options.start_career": "من می خواهم کارم را شروع کنم",
|
||||
"profile.learningGoal.options.advance_career": "من می خواهم حرفه ام را ارتقا دهم",
|
||||
"profile.learningGoal.options.learn_something_new": "میخواهم چیز جدیدی یاد بگیرم",
|
||||
"profile.learningGoal.options.something_else": "یک چیز دیگر",
|
||||
"profile.name.full.name": "نام و نام خانوادگی",
|
||||
"profile.name.details": "این همان نامی است که در حساب کاربری و گواهیهای شما درج میشود.",
|
||||
"profile.name.empty": "افزودن نام",
|
||||
|
||||
@@ -53,33 +53,5 @@
|
||||
"profile.notfound.message": "La page que vous recherchez n'est pas disponible ou il y a une erreur dans l'URL. Veuillez vérifier l'URL et réessayer.",
|
||||
"profile.viewMyRecords": "Voir mes succès",
|
||||
"profile.loading": "Chargement du profil....",
|
||||
"profile.username.description": "Les informations de votre profil ne sont visibles que par vous. Seul votre nom d'utilisateur est visible par les autres sur {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"skills.builder.header.title.is.medium": "edX Skills builder",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit",
|
||||
"select.preferences": "Select preferences",
|
||||
"review.results": "Review results",
|
||||
"skills.builder.description": "Find the right courses and programs that help you reach your goals.",
|
||||
"learning.goal.prompt": "First, tell us what you want to achieve",
|
||||
"select.learning.goal": "Select a goal",
|
||||
"learning.goal.start_career": "I want to start my career",
|
||||
"learning.goal.advance_career": "I want to advance my career",
|
||||
"learning.goal.change_career": "I want to change careers",
|
||||
"learning.goal.something.new": "I want to learn something new",
|
||||
"learning.goal.something.else": "Something else",
|
||||
"job.title.prompt": "Next, search and select your current job title",
|
||||
"job.title.input.placeholder.text": "Search and select a job title",
|
||||
"student.checkbox.prompt": "I'm a student",
|
||||
"currently.looking.checkbox.prompt": "I'm currently looking for work",
|
||||
"career.interest.prompt": "What careers are you interested in?",
|
||||
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
|
||||
"career.interest.remove.button.alt.text": "Remove career interest: ",
|
||||
"matches.found.success.alert": "We found skills and courses that match your preferences!",
|
||||
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
|
||||
"related.skills.heading": "Related Skills",
|
||||
"related.skills.selectable.box.label.text": "Related skills:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"profile.username.description": "Les informations de votre profil ne sont visibles que par vous. Seul votre nom d'utilisateur est visible par les autres sur {siteName}."
|
||||
}
|
||||
@@ -45,7 +45,7 @@
|
||||
"profile.preferredlanguage.empty": "Ajouter une langue",
|
||||
"profile.preferredlanguage.label": "Langue principale parlée",
|
||||
"profile.profileavatar.upload-button": "Téléverser une photo",
|
||||
"profile.profileavatar.remove.button": "Supprimer",
|
||||
"profile.profileavatar.remove.button": "Retirer",
|
||||
"profile.image.alt.attribute": "avatar de profil",
|
||||
"profile.profileavatar.change-button": "Modifier",
|
||||
"profile.sociallinks.add": "Ajouter {network}",
|
||||
@@ -53,33 +53,5 @@
|
||||
"profile.notfound.message": "La page que vous recherchez n'est pas disponible ou il y a une erreur dans l'URL. Veuillez vérifier l'URL et réessayer.",
|
||||
"profile.viewMyRecords": "Afficher mes dossiers",
|
||||
"profile.loading": "Chargement du profil...",
|
||||
"profile.username.description": "Les informations de votre profil ne sont visibles que par vous. Seul votre nom d'utilisateur est visible par les autres sur {siteName}.",
|
||||
"skills.builder.header.title": "Constructeur de compétences",
|
||||
"skills.builder.header.subheading": "Laissez EDUlib être votre guide",
|
||||
"skills.builder.header.title.is.medium": "Générateur de compétences edX",
|
||||
"go.back.button": "Retour",
|
||||
"next.step.button": "Prochaine étape",
|
||||
"exit.button": "Sortie",
|
||||
"select.preferences": "Sélectionnez les préférences",
|
||||
"review.results": "Examiner les résultats",
|
||||
"skills.builder.description": "Trouvez les bons cours et programmes qui vous aideront à atteindre vos objectifs.",
|
||||
"learning.goal.prompt": "Tout d'abord, dites-nous ce que vous voulez réaliser",
|
||||
"select.learning.goal": "Sélectionnez un objectif",
|
||||
"learning.goal.start_career": "Je veux commencer ma carrière",
|
||||
"learning.goal.advance_career": "Je veux faire progresser ma carrière",
|
||||
"learning.goal.change_career": "Je veux changer de métier",
|
||||
"learning.goal.something.new": "Je veux apprendre quelque chose de nouveau",
|
||||
"learning.goal.something.else": "Autre chose",
|
||||
"job.title.prompt": "Ensuite, recherchez et sélectionnez votre titre de poste actuel",
|
||||
"job.title.input.placeholder.text": "Rechercher et sélectionner un intitulé de poste",
|
||||
"student.checkbox.prompt": "Je suis étudiant.e",
|
||||
"currently.looking.checkbox.prompt": "Je suis actuellement à la recherche d'un emploi",
|
||||
"career.interest.prompt": "Quels métiers vous intéressent ?",
|
||||
"career.interest.input.placeholder.text": "Sélectionnez jusqu'à 3 nouveaux intitulés de poste",
|
||||
"career.interest.remove.button.alt.text": "Supprimer l'intérêt professionnel :",
|
||||
"matches.found.success.alert": "Nous avons trouvé des compétences et des cours qui correspondent à vos préférences !",
|
||||
"matches.not.found.danger.alert": "Nous n'avons pas pu récupérer les recommandations pour le moment. Veuillez réessayer plus tard.",
|
||||
"related.skills.heading": "Compétences connexes",
|
||||
"related.skills.selectable.box.label.text": "Compétences connexes:",
|
||||
"product.recommendations.header.text": "{productType} recommandations pour {jobName}"
|
||||
"profile.username.description": "Les informations de votre profil ne sont visibles que par vous. Seul votre nom d'utilisateur est visible par les autres sur {siteName}."
|
||||
}
|
||||
@@ -53,33 +53,5 @@
|
||||
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"profile.viewMyRecords": "View My Records",
|
||||
"profile.loading": "Profile loading...",
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"skills.builder.header.title.is.medium": "edX Skills builder",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit",
|
||||
"select.preferences": "Select preferences",
|
||||
"review.results": "Review results",
|
||||
"skills.builder.description": "Find the right courses and programs that help you reach your goals.",
|
||||
"learning.goal.prompt": "First, tell us what you want to achieve",
|
||||
"select.learning.goal": "Select a goal",
|
||||
"learning.goal.start_career": "I want to start my career",
|
||||
"learning.goal.advance_career": "I want to advance my career",
|
||||
"learning.goal.change_career": "I want to change careers",
|
||||
"learning.goal.something.new": "I want to learn something new",
|
||||
"learning.goal.something.else": "Something else",
|
||||
"job.title.prompt": "Next, search and select your current job title",
|
||||
"job.title.input.placeholder.text": "Search and select a job title",
|
||||
"student.checkbox.prompt": "I'm a student",
|
||||
"currently.looking.checkbox.prompt": "I'm currently looking for work",
|
||||
"career.interest.prompt": "What careers are you interested in?",
|
||||
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
|
||||
"career.interest.remove.button.alt.text": "Remove career interest: ",
|
||||
"matches.found.success.alert": "We found skills and courses that match your preferences!",
|
||||
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
|
||||
"related.skills.heading": "Related Skills",
|
||||
"related.skills.selectable.box.label.text": "Related skills:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
|
||||
}
|
||||
@@ -53,33 +53,5 @@
|
||||
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"profile.viewMyRecords": "View My Records",
|
||||
"profile.loading": "Profile loading...",
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"skills.builder.header.title.is.medium": "edX Skills builder",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit",
|
||||
"select.preferences": "Select preferences",
|
||||
"review.results": "Review results",
|
||||
"skills.builder.description": "Find the right courses and programs that help you reach your goals.",
|
||||
"learning.goal.prompt": "First, tell us what you want to achieve",
|
||||
"select.learning.goal": "Select a goal",
|
||||
"learning.goal.start_career": "I want to start my career",
|
||||
"learning.goal.advance_career": "I want to advance my career",
|
||||
"learning.goal.change_career": "I want to change careers",
|
||||
"learning.goal.something.new": "I want to learn something new",
|
||||
"learning.goal.something.else": "Something else",
|
||||
"job.title.prompt": "Next, search and select your current job title",
|
||||
"job.title.input.placeholder.text": "Search and select a job title",
|
||||
"student.checkbox.prompt": "I'm a student",
|
||||
"currently.looking.checkbox.prompt": "I'm currently looking for work",
|
||||
"career.interest.prompt": "What careers are you interested in?",
|
||||
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
|
||||
"career.interest.remove.button.alt.text": "Remove career interest: ",
|
||||
"matches.found.success.alert": "We found skills and courses that match your preferences!",
|
||||
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
|
||||
"related.skills.heading": "Related Skills",
|
||||
"related.skills.selectable.box.label.text": "Related skills:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"profile.page.title": "Profile | {siteName}",
|
||||
"profile.page.title": "Profilo | {siteName}",
|
||||
"profile.age.details": "Per condividere il tuo profilo con altri studenti di {siteName}, devi confermare di avere più di 13 anni.",
|
||||
"profile.age.set.date": "Imposta la data di nascita ",
|
||||
"profile.datejoined.member.since": "Membro da {year}",
|
||||
@@ -34,11 +34,11 @@
|
||||
"profile.formcontrols.button.saved": "Salvato",
|
||||
"profile.visibility.who.just.me": "Solo io ",
|
||||
"profile.visibility.who.everyone": "Tutti su {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Learning Goal",
|
||||
"profile.learningGoal.options.start_career": "I want to start my career",
|
||||
"profile.learningGoal.options.advance_career": "I want to advance my career",
|
||||
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
|
||||
"profile.learningGoal.options.something_else": "Something else",
|
||||
"profile.learningGoal.learningGoal": "Obiettivo di apprendimento",
|
||||
"profile.learningGoal.options.start_career": "Voglio iniziare il mio percorso",
|
||||
"profile.learningGoal.options.advance_career": "Voglio avanzare nel mio percorso",
|
||||
"profile.learningGoal.options.learn_something_new": "Voglio imparare qualcosa di nuovo",
|
||||
"profile.learningGoal.options.something_else": "Qualcos'altro",
|
||||
"profile.name.full.name": "Nome e Cognome",
|
||||
"profile.name.details": "Questo è il nome visualizzato nel proprio account e nei propri certificati. ",
|
||||
"profile.name.empty": "Aggiungi nome ",
|
||||
@@ -53,9 +53,5 @@
|
||||
"profile.notfound.message": "La pagina ricercata non è disponibile oppure è presente un errore nell'URL. Controllare l'URL e riprovare. ",
|
||||
"profile.viewMyRecords": "Visualizza record personali ",
|
||||
"profile.loading": "Caricamento del profilo... ",
|
||||
"profile.username.description": "Le informazioni del tuo profilo sono visibili solo a te. Solo il tuo nome utente è visibile agli altri su {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step"
|
||||
"profile.username.description": "Le informazioni del tuo profilo sono visibili solo a te. Solo il tuo nome utente è visibile agli altri su {siteName}."
|
||||
}
|
||||
@@ -53,33 +53,5 @@
|
||||
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"profile.viewMyRecords": "View My Records",
|
||||
"profile.loading": "Profile loading...",
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"skills.builder.header.title.is.medium": "edX Skills builder",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit",
|
||||
"select.preferences": "Select preferences",
|
||||
"review.results": "Review results",
|
||||
"skills.builder.description": "Find the right courses and programs that help you reach your goals.",
|
||||
"learning.goal.prompt": "First, tell us what you want to achieve",
|
||||
"select.learning.goal": "Select a goal",
|
||||
"learning.goal.start_career": "I want to start my career",
|
||||
"learning.goal.advance_career": "I want to advance my career",
|
||||
"learning.goal.change_career": "I want to change careers",
|
||||
"learning.goal.something.new": "I want to learn something new",
|
||||
"learning.goal.something.else": "Something else",
|
||||
"job.title.prompt": "Next, search and select your current job title",
|
||||
"job.title.input.placeholder.text": "Search and select a job title",
|
||||
"student.checkbox.prompt": "I'm a student",
|
||||
"currently.looking.checkbox.prompt": "I'm currently looking for work",
|
||||
"career.interest.prompt": "What careers are you interested in?",
|
||||
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
|
||||
"career.interest.remove.button.alt.text": "Remove career interest: ",
|
||||
"matches.found.success.alert": "We found skills and courses that match your preferences!",
|
||||
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
|
||||
"related.skills.heading": "Related Skills",
|
||||
"related.skills.selectable.box.label.text": "Related skills:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"profile.page.title": "Profile | {siteName}",
|
||||
"profile.age.details": "To share your profile with other {siteName} learners, you must confirm that you are over the age of 13.",
|
||||
"profile.page.title": "Perfil | {siteName}",
|
||||
"profile.age.details": "Para partilhar o seu perfil com outros estudantes da plataforma {siteName}, tem de confirmar que tem mais de 13 anos de idade.",
|
||||
"profile.age.set.date": "Indique a sua data de nascimento",
|
||||
"profile.datejoined.member.since": "Utilizador desde {year}",
|
||||
"profile.bio.empty": "Adicionar uma breve biografia",
|
||||
@@ -33,12 +33,12 @@
|
||||
"profile.formcontrols.button.saving": "A Guardar",
|
||||
"profile.formcontrols.button.saved": "Guardado",
|
||||
"profile.visibility.who.just.me": "Apenas eu",
|
||||
"profile.visibility.who.everyone": "Everyone on {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Learning Goal",
|
||||
"profile.learningGoal.options.start_career": "I want to start my career",
|
||||
"profile.learningGoal.options.advance_career": "I want to advance my career",
|
||||
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
|
||||
"profile.learningGoal.options.something_else": "Something else",
|
||||
"profile.visibility.who.everyone": "Toda a gente em {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Objectivo de aprendizagem",
|
||||
"profile.learningGoal.options.start_career": "Quero começar a minha carreira",
|
||||
"profile.learningGoal.options.advance_career": "Quero progredir na minha carreira",
|
||||
"profile.learningGoal.options.learn_something_new": "Quero aprender algo novo",
|
||||
"profile.learningGoal.options.something_else": "Outra coisa",
|
||||
"profile.name.full.name": "Nome Completo",
|
||||
"profile.name.details": "Este é o nome que aparece na sua conta e nos seus certificados.",
|
||||
"profile.name.empty": "Adicionar nome",
|
||||
@@ -53,9 +53,5 @@
|
||||
"profile.notfound.message": "A página que procura não está disponível ou há um erro no URL. Por favor, verifique o URL e tente novamente.",
|
||||
"profile.viewMyRecords": "Ver os Meus Registos",
|
||||
"profile.loading": "A carregar perfil...",
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step"
|
||||
"profile.username.description": "As informações do seu perfil só são visíveis para si. Apenas o seu nome de utilizador é visível para outros em {siteName}."
|
||||
}
|
||||
@@ -53,33 +53,5 @@
|
||||
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"profile.viewMyRecords": "View My Records",
|
||||
"profile.loading": "Profile loading...",
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"skills.builder.header.title.is.medium": "edX Skills builder",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit",
|
||||
"select.preferences": "Select preferences",
|
||||
"review.results": "Review results",
|
||||
"skills.builder.description": "Find the right courses and programs that help you reach your goals.",
|
||||
"learning.goal.prompt": "First, tell us what you want to achieve",
|
||||
"select.learning.goal": "Select a goal",
|
||||
"learning.goal.start_career": "I want to start my career",
|
||||
"learning.goal.advance_career": "I want to advance my career",
|
||||
"learning.goal.change_career": "I want to change careers",
|
||||
"learning.goal.something.new": "I want to learn something new",
|
||||
"learning.goal.something.else": "Something else",
|
||||
"job.title.prompt": "Next, search and select your current job title",
|
||||
"job.title.input.placeholder.text": "Search and select a job title",
|
||||
"student.checkbox.prompt": "I'm a student",
|
||||
"currently.looking.checkbox.prompt": "I'm currently looking for work",
|
||||
"career.interest.prompt": "What careers are you interested in?",
|
||||
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
|
||||
"career.interest.remove.button.alt.text": "Remove career interest: ",
|
||||
"matches.found.success.alert": "We found skills and courses that match your preferences!",
|
||||
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
|
||||
"related.skills.heading": "Related Skills",
|
||||
"related.skills.selectable.box.label.text": "Related skills:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
|
||||
}
|
||||
@@ -53,33 +53,5 @@
|
||||
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"profile.viewMyRecords": "View My Records",
|
||||
"profile.loading": "Profile loading...",
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"skills.builder.header.title.is.medium": "edX Skills builder",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit",
|
||||
"select.preferences": "Select preferences",
|
||||
"review.results": "Review results",
|
||||
"skills.builder.description": "Find the right courses and programs that help you reach your goals.",
|
||||
"learning.goal.prompt": "First, tell us what you want to achieve",
|
||||
"select.learning.goal": "Select a goal",
|
||||
"learning.goal.start_career": "I want to start my career",
|
||||
"learning.goal.advance_career": "I want to advance my career",
|
||||
"learning.goal.change_career": "I want to change careers",
|
||||
"learning.goal.something.new": "I want to learn something new",
|
||||
"learning.goal.something.else": "Something else",
|
||||
"job.title.prompt": "Next, search and select your current job title",
|
||||
"job.title.input.placeholder.text": "Search and select a job title",
|
||||
"student.checkbox.prompt": "I'm a student",
|
||||
"currently.looking.checkbox.prompt": "I'm currently looking for work",
|
||||
"career.interest.prompt": "What careers are you interested in?",
|
||||
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
|
||||
"career.interest.remove.button.alt.text": "Remove career interest: ",
|
||||
"matches.found.success.alert": "We found skills and courses that match your preferences!",
|
||||
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
|
||||
"related.skills.heading": "Related Skills",
|
||||
"related.skills.selectable.box.label.text": "Related skills:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
|
||||
}
|
||||
@@ -53,33 +53,5 @@
|
||||
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"profile.viewMyRecords": "View My Records",
|
||||
"profile.loading": "Profile loading...",
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"skills.builder.header.title.is.medium": "edX Skills builder",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit",
|
||||
"select.preferences": "Select preferences",
|
||||
"review.results": "Review results",
|
||||
"skills.builder.description": "Find the right courses and programs that help you reach your goals.",
|
||||
"learning.goal.prompt": "First, tell us what you want to achieve",
|
||||
"select.learning.goal": "Select a goal",
|
||||
"learning.goal.start_career": "I want to start my career",
|
||||
"learning.goal.advance_career": "I want to advance my career",
|
||||
"learning.goal.change_career": "I want to change careers",
|
||||
"learning.goal.something.new": "I want to learn something new",
|
||||
"learning.goal.something.else": "Something else",
|
||||
"job.title.prompt": "Next, search and select your current job title",
|
||||
"job.title.input.placeholder.text": "Search and select a job title",
|
||||
"student.checkbox.prompt": "I'm a student",
|
||||
"currently.looking.checkbox.prompt": "I'm currently looking for work",
|
||||
"career.interest.prompt": "What careers are you interested in?",
|
||||
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
|
||||
"career.interest.remove.button.alt.text": "Remove career interest: ",
|
||||
"matches.found.success.alert": "We found skills and courses that match your preferences!",
|
||||
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
|
||||
"related.skills.heading": "Related Skills",
|
||||
"related.skills.selectable.box.label.text": "Related skills:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import Header from '@edx/frontend-component-header';
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
@@ -27,15 +28,25 @@ import Head from './head/Head';
|
||||
|
||||
import AppRoutes from './routes/AppRoutes';
|
||||
|
||||
const RenderFooter = () => {
|
||||
const location = useLocation();
|
||||
return location.pathname.includes('/plugin') ? null : <Footer />;
|
||||
};
|
||||
|
||||
const RenderHeader = () => {
|
||||
const location = useLocation();
|
||||
return location.pathname.includes('/plugin') ? null : <Header />;
|
||||
};
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
<AppProvider store={configureStore()}>
|
||||
<Head />
|
||||
<Header />
|
||||
<main>
|
||||
<RenderHeader />
|
||||
<main id="main">
|
||||
<AppRoutes />
|
||||
</main>
|
||||
<Footer />
|
||||
<RenderFooter />
|
||||
</AppProvider>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
||||
@@ -6,7 +6,6 @@ const DateJoined = ({ date }) => {
|
||||
if (date == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="mb-0">
|
||||
<FormattedMessage
|
||||
|
||||
@@ -41,6 +41,8 @@ import { profilePageSelector } from './data/selectors';
|
||||
// i18n
|
||||
import messages from './ProfilePage.messages';
|
||||
|
||||
import withParams from '../utils/hoc';
|
||||
|
||||
ensureConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL'], 'ProfilePage');
|
||||
|
||||
class ProfilePage extends React.Component {
|
||||
@@ -63,9 +65,9 @@ class ProfilePage extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchProfile(this.props.match.params.username);
|
||||
this.props.fetchProfile(this.props.params.username);
|
||||
sendTrackingLogEvent('edx.profile.viewed', {
|
||||
username: this.props.match.params.username,
|
||||
username: this.props.params.username,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,7 +104,7 @@ class ProfilePage extends React.Component {
|
||||
}
|
||||
|
||||
isAuthenticatedUserProfile() {
|
||||
return this.props.match.params.username === this.context.authenticatedUser.username;
|
||||
return this.props.params.username === this.context.authenticatedUser.username;
|
||||
}
|
||||
|
||||
// Inserted into the DOM in two places (for responsive layout)
|
||||
@@ -124,7 +126,7 @@ class ProfilePage extends React.Component {
|
||||
|
||||
return (
|
||||
<span data-hj-suppress>
|
||||
<h1 className="h2 mb-0 font-weight-bold">{this.props.match.params.username}</h1>
|
||||
<h1 className="h2 mb-0 font-weight-bold">{this.props.params.username}</h1>
|
||||
<DateJoined date={dateJoined} />
|
||||
{this.isYOBDisabled() && <UsernameDescription />}
|
||||
<hr className="d-none d-md-block" />
|
||||
@@ -210,6 +212,7 @@ class ProfilePage extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>PluginPOC</div>
|
||||
<div className="col pl-0">
|
||||
<div className="d-md-none">
|
||||
{this.renderHeadingLockup()}
|
||||
@@ -370,10 +373,8 @@ ProfilePage.propTypes = {
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
|
||||
// Router
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.shape({
|
||||
username: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
params: PropTypes.shape({
|
||||
username: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
|
||||
// i18n
|
||||
@@ -410,4 +411,4 @@ export default connect(
|
||||
closeForm,
|
||||
updateDraft,
|
||||
},
|
||||
)(injectIntl(ProfilePage));
|
||||
)(injectIntl(withParams(ProfilePage)));
|
||||
|
||||
@@ -29,7 +29,7 @@ const requiredProfilePageProps = {
|
||||
deleteProfilePhoto: () => {},
|
||||
openField: () => {},
|
||||
closeField: () => {},
|
||||
match: { params: { username: 'staff' } },
|
||||
params: { username: 'staff' },
|
||||
};
|
||||
|
||||
// Mock language cookie
|
||||
@@ -67,29 +67,28 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
const ProfilePageWrapper = ({
|
||||
contextValue, store, match, requiresParentalConsent,
|
||||
contextValue, store, params, requiresParentalConsent,
|
||||
}) => (
|
||||
<AppContext.Provider
|
||||
value={contextValue}
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
<ProfilePage {...requiredProfilePageProps} match={match} requiresParentalConsent={requiresParentalConsent} />
|
||||
<ProfilePage {...requiredProfilePageProps} params={params} requiresParentalConsent={requiresParentalConsent} />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
|
||||
ProfilePageWrapper.defaultProps = {
|
||||
match: { params: { username: 'staff' } },
|
||||
params: { username: 'staff' },
|
||||
requiresParentalConsent: null,
|
||||
|
||||
};
|
||||
|
||||
ProfilePageWrapper.propTypes = {
|
||||
contextValue: PropTypes.shape({}).isRequired,
|
||||
store: PropTypes.shape({}).isRequired,
|
||||
match: PropTypes.shape({}),
|
||||
params: PropTypes.shape({}),
|
||||
requiresParentalConsent: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -124,7 +123,7 @@ describe('<ProfilePage />', () => {
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeMocks.viewOtherProfile)}
|
||||
match={{ params: { username: 'verified' } }} // Override default match
|
||||
params={{ username: 'verified' }} // Override default params
|
||||
/>
|
||||
);
|
||||
const tree = renderer.create(component).toJSON();
|
||||
@@ -279,7 +278,7 @@ describe('<ProfilePage />', () => {
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeMocks.loadingApp)}
|
||||
match={{ params: { username: 'test-username' } }}
|
||||
params={{ username: 'test-username' }}
|
||||
/>
|
||||
);
|
||||
const wrapper = mount(component);
|
||||
|
||||
219
src/profile/ProfilePluginPage.jsx
Normal file
219
src/profile/ProfilePluginPage.jsx
Normal file
@@ -0,0 +1,219 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { ensureConfig } from '@edx/frontend-platform';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { injectIntl, intlShape, FormattedDate } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faTwitter, faFacebook, faLinkedin } from '@fortawesome/free-brands-svg-icons';
|
||||
import {
|
||||
ActionRow, Avatar, Card, Hyperlink, Icon,
|
||||
} from '@edx/paragon';
|
||||
import { HistoryEdu, VerifiedUser } from '@edx/paragon/icons';
|
||||
|
||||
import get from 'lodash.get';
|
||||
|
||||
import { Plugin } from '@edx/frontend-plugin-framework/src/plugins';
|
||||
import PluginCountry from './forms/PluginCountry';
|
||||
|
||||
// Actions
|
||||
import {
|
||||
fetchProfile,
|
||||
} from './data/actions';
|
||||
|
||||
// Components
|
||||
import PageLoading from './PageLoading';
|
||||
|
||||
// Selectors
|
||||
import { profilePageSelector } from './data/selectors';
|
||||
|
||||
// i18n
|
||||
import messages from './ProfilePage.messages';
|
||||
import eduMessages from './forms/Education.messages';
|
||||
|
||||
import withParams from '../utils/hoc';
|
||||
|
||||
ensureConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL'], 'ProfilePage');
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
function Fallback() {
|
||||
return (
|
||||
<div>this is broken as all get</div>
|
||||
);
|
||||
}
|
||||
|
||||
const platformDisplayInfo = {
|
||||
facebook: {
|
||||
icon: faFacebook,
|
||||
name: '',
|
||||
},
|
||||
twitter: {
|
||||
icon: faTwitter,
|
||||
name: '',
|
||||
},
|
||||
linkedin: {
|
||||
icon: faLinkedin,
|
||||
name: '',
|
||||
},
|
||||
};
|
||||
|
||||
class ProfilePluginPage extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.fetchProfile(this.props.params.username);
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const {
|
||||
profileImage,
|
||||
country,
|
||||
levelOfEducation,
|
||||
socialLinks,
|
||||
isLoadingProfile,
|
||||
dateJoined,
|
||||
name,
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
if (isLoadingProfile) {
|
||||
return <PageLoading srMessage={this.props.intl.formatMessage(messages['profile.loading'])} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Plugin fallbackComponent={<Fallback />}>
|
||||
<Card className="mb-2">
|
||||
<Card.Header
|
||||
className="pb-5"
|
||||
subtitle={(
|
||||
<Hyperlink destination={`/u/${this.props.params.username}`}>
|
||||
View public profile
|
||||
</Hyperlink>
|
||||
)}
|
||||
actions={
|
||||
(
|
||||
<ActionRow className="mt-3">
|
||||
{socialLinks
|
||||
.filter(({ socialLink }) => Boolean(socialLink))
|
||||
.map(({ platform, socialLink }) => (
|
||||
<StaticListItem
|
||||
key={platform}
|
||||
name={platformDisplayInfo[platform].name}
|
||||
url={socialLink}
|
||||
platform={platform}
|
||||
/>
|
||||
))}
|
||||
</ActionRow>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Card.Section className="text-center" muted>
|
||||
<Avatar
|
||||
size="xl"
|
||||
className="profile-plugin-avatar"
|
||||
src={profileImage.src}
|
||||
alt="Profile image"
|
||||
/>
|
||||
<p className="h2 mb-0 font-weight-bold">{name}</p>
|
||||
<p className="h3 mb-0 font-weight-bold">{this.props.params.username}</p>
|
||||
<PluginCountry
|
||||
country={country}
|
||||
/>
|
||||
</Card.Section>
|
||||
<Card.Footer className="p-0">
|
||||
<Card.Section className="pgn-icons-cell-vertical">
|
||||
<Icon src={VerifiedUser} />
|
||||
<p>
|
||||
since <FormattedDate value={new Date(dateJoined)} year="numeric" />
|
||||
</p>
|
||||
</Card.Section>
|
||||
<Card.Section className="pgn-icons-cell-vertical">
|
||||
<Icon src={HistoryEdu} />
|
||||
<p>
|
||||
{intl.formatMessage(get(
|
||||
eduMessages,
|
||||
`profile.education.levels.${levelOfEducation}`,
|
||||
eduMessages['profile.education.levels.o'],
|
||||
))}
|
||||
</p>
|
||||
</Card.Section>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
</Plugin>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="profile-page">
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SocialLink = ({ url, name, platform }) => (
|
||||
<a href={url} className="font-weight-bold">
|
||||
<FontAwesomeIcon className="mr-2" icon={platformDisplayInfo[platform].icon} />
|
||||
{name}
|
||||
</a>
|
||||
);
|
||||
|
||||
const StaticListItem = ({ url, name, platform }) => (
|
||||
<ul className="list-inline">
|
||||
<SocialLink name={name} url={url} platform={platform} />
|
||||
</ul>
|
||||
);
|
||||
|
||||
ProfilePluginPage.contextType = AppContext;
|
||||
|
||||
ProfilePluginPage.propTypes = {
|
||||
// Account data
|
||||
dateJoined: PropTypes.string,
|
||||
|
||||
// Country form data
|
||||
country: PropTypes.string,
|
||||
|
||||
// Education form data
|
||||
levelOfEducation: PropTypes.string,
|
||||
|
||||
// Social links form data
|
||||
socialLinks: PropTypes.arrayOf(PropTypes.shape({
|
||||
platform: PropTypes.string,
|
||||
socialLink: PropTypes.string,
|
||||
})),
|
||||
|
||||
// Other data we need
|
||||
profileImage: PropTypes.shape({
|
||||
src: PropTypes.string,
|
||||
isDefault: PropTypes.bool,
|
||||
}),
|
||||
isLoadingProfile: PropTypes.bool.isRequired,
|
||||
|
||||
// Actions
|
||||
fetchProfile: PropTypes.func.isRequired,
|
||||
|
||||
// Router
|
||||
params: PropTypes.shape({
|
||||
username: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
|
||||
// i18n
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
ProfilePluginPage.defaultProps = {
|
||||
profileImage: {},
|
||||
levelOfEducation: null,
|
||||
country: null,
|
||||
socialLinks: [],
|
||||
dateJoined: null,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
profilePageSelector,
|
||||
{
|
||||
fetchProfile,
|
||||
},
|
||||
)(injectIntl(withParams(ProfilePluginPage)));
|
||||
@@ -103,6 +103,9 @@ exports[`<ProfilePage /> Renders correctly in various states test country edit w
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
PluginPOC
|
||||
</div>
|
||||
<div
|
||||
className="col pl-0"
|
||||
>
|
||||
@@ -2486,6 +2489,9 @@ exports[`<ProfilePage /> Renders correctly in various states test education edit
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
PluginPOC
|
||||
</div>
|
||||
<div
|
||||
className="col pl-0"
|
||||
>
|
||||
@@ -3663,6 +3669,9 @@ exports[`<ProfilePage /> Renders correctly in various states test preferreded la
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
PluginPOC
|
||||
</div>
|
||||
<div
|
||||
className="col pl-0"
|
||||
>
|
||||
@@ -5692,6 +5701,9 @@ exports[`<ProfilePage /> Renders correctly in various states viewing other profi
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
PluginPOC
|
||||
</div>
|
||||
<div
|
||||
className="col pl-0"
|
||||
>
|
||||
@@ -5947,6 +5959,9 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
PluginPOC
|
||||
</div>
|
||||
<div
|
||||
className="col pl-0"
|
||||
>
|
||||
@@ -6997,6 +7012,9 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
PluginPOC
|
||||
</div>
|
||||
<div
|
||||
className="col pl-0"
|
||||
>
|
||||
@@ -8114,6 +8132,9 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
PluginPOC
|
||||
</div>
|
||||
<div
|
||||
className="col pl-0"
|
||||
>
|
||||
@@ -9239,6 +9260,9 @@ exports[`<ProfilePage /> Renders correctly in various states without credentials
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
PluginPOC
|
||||
</div>
|
||||
<div
|
||||
className="col pl-0"
|
||||
>
|
||||
|
||||
40
src/profile/forms/PluginCountry.jsx
Normal file
40
src/profile/forms/PluginCountry.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@edx/paragon';
|
||||
import { LocationOn } from '@edx/paragon/icons';
|
||||
|
||||
// Selectors
|
||||
import { countrySelector } from '../data/selectors';
|
||||
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
class PluginCountry extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
country,
|
||||
countryMessages,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="pgn-icons-cell-horizontal">
|
||||
<Icon src={LocationOn} />
|
||||
<p className="h5 mt-1 ml-1">{countryMessages[country]}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PluginCountry.propTypes = {
|
||||
country: PropTypes.string,
|
||||
countryMessages: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||
};
|
||||
|
||||
PluginCountry.defaultProps = {
|
||||
country: null,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
countrySelector,
|
||||
{},
|
||||
)(injectIntl(PluginCountry));
|
||||
@@ -3,3 +3,4 @@ export { default as saga } from './data/sagas';
|
||||
export { default as ProfilePage } from './ProfilePage';
|
||||
export { default as NotFoundPage } from './NotFoundPage';
|
||||
export { default as messages } from './ProfilePage.messages';
|
||||
export { default as ProfilePluginPage } from './ProfilePluginPage';
|
||||
|
||||
@@ -162,4 +162,28 @@
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.pgn-icons-cell-vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin: 1px;
|
||||
}
|
||||
.pgn-icons-cell-horizontal {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
.profile-plugin-avatar {
|
||||
@include media-breakpoint-up(xs) {
|
||||
max-width: 12rem;
|
||||
margin-right: 0;
|
||||
margin-top: -4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
AuthenticatedPageRoute,
|
||||
PageRoute,
|
||||
PageWrap,
|
||||
} from '@edx/frontend-platform/react';
|
||||
import { Switch } from 'react-router-dom';
|
||||
import { ProfilePage, NotFoundPage } from '../profile';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { ProfilePage, NotFoundPage, ProfilePluginPage } from '../profile';
|
||||
|
||||
const AppRoutes = () => (
|
||||
<Switch>
|
||||
<AuthenticatedPageRoute path="/u/:username" component={ProfilePage} />
|
||||
<PageRoute path="/notfound" component={NotFoundPage} />
|
||||
<PageRoute path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
<Routes>
|
||||
<Route path="/u/:username" element={<AuthenticatedPageRoute><ProfilePage /></AuthenticatedPageRoute>} />
|
||||
<Route path="/u/:username/plugin" element={<AuthenticatedPageRoute><ProfilePluginPage /></AuthenticatedPageRoute>} />
|
||||
<Route path="/notfound" element={<PageWrap><NotFoundPage /></PageWrap>} />
|
||||
<Route path="*" element={<PageWrap><NotFoundPage /></PageWrap>} />
|
||||
</Routes>
|
||||
);
|
||||
|
||||
export default AppRoutes;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Router } from 'react-router';
|
||||
import { MemoryRouter as Router } from 'react-router-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||
import AppRoutes from './AppRoutes';
|
||||
|
||||
@@ -16,11 +15,12 @@ jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
jest.mock('../profile', () => ({
|
||||
ProfilePage: () => (<div>Profile page</div>),
|
||||
NotFoundPage: () => (<div>Not found page</div>),
|
||||
ProfilePluginPage: () => (<div>Plugin page</div>),
|
||||
}));
|
||||
|
||||
const RoutesWithProvider = (context, history) => (
|
||||
const RoutesWithProvider = (context, path) => (
|
||||
<AppContext.Provider value={context}>
|
||||
<Router history={history}>
|
||||
<Router initialEntries={[`${path}`]}>
|
||||
<AppRoutes />
|
||||
</Router>
|
||||
</AppContext.Provider>
|
||||
@@ -32,22 +32,14 @@ const unauthenticatedUser = {
|
||||
};
|
||||
|
||||
describe('routes', () => {
|
||||
let history;
|
||||
|
||||
beforeEach(() => {
|
||||
history = createMemoryHistory();
|
||||
});
|
||||
|
||||
test('Profile page should redirect for unauthenticated users', () => {
|
||||
history.push('/u/edx');
|
||||
render(
|
||||
RoutesWithProvider(unauthenticatedUser, history),
|
||||
RoutesWithProvider(unauthenticatedUser, '/u/edx'),
|
||||
);
|
||||
expect(getLoginRedirectUrl).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Profile page should be accessible for authenticated users', () => {
|
||||
history.push('/u/edx');
|
||||
render(
|
||||
RoutesWithProvider(
|
||||
{
|
||||
@@ -57,16 +49,31 @@ describe('routes', () => {
|
||||
},
|
||||
config: getConfig(),
|
||||
},
|
||||
history,
|
||||
'/u/edx',
|
||||
),
|
||||
);
|
||||
expect(screen.getByText('Profile page')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should show NotFound page for a bad route', () => {
|
||||
history.push('/nonMatchingRoute');
|
||||
test('Profile Plugin page should be accessible for authenticated users', () => {
|
||||
render(
|
||||
RoutesWithProvider(unauthenticatedUser, history),
|
||||
RoutesWithProvider(
|
||||
{
|
||||
authenticatedUser: {
|
||||
username: 'edx',
|
||||
email: 'edx@example.com',
|
||||
},
|
||||
config: getConfig(),
|
||||
},
|
||||
'/u/edx/plugin',
|
||||
),
|
||||
);
|
||||
expect(screen.getByText('Plugin page')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should show NotFound page for a bad route', () => {
|
||||
render(
|
||||
RoutesWithProvider(unauthenticatedUser, '/nonMatchingRoute'),
|
||||
);
|
||||
expect(screen.getByText('Not found page')).toBeTruthy();
|
||||
});
|
||||
|
||||
10
src/utils/hoc.jsx
Normal file
10
src/utils/hoc.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const withParams = (WrappedComponent) => {
|
||||
const WithParamsComponent = (props) => <WrappedComponent params={useParams()} {...props} />;
|
||||
return WithParamsComponent;
|
||||
};
|
||||
|
||||
export default withParams;
|
||||
Reference in New Issue
Block a user