Compare commits

...

48 Commits

Author SHA1 Message Date
Renovate Bot
106cba1785 fix(deps): update dependency @edx/frontend-component-footer to v10.0.5 2019-12-06 18:13:49 +00:00
Renovate Bot
820768367f fix(deps): update dependency @fortawesome/react-fontawesome to v0.1.8 2019-12-06 17:12:56 +00:00
Renovate Bot
18df696902 fix(deps): update dependency @edx/frontend-platform to v1.1.9 2019-12-05 23:14:59 +00:00
Renovate Bot
f76383f034 fix(deps): update dependency @edx/frontend-component-footer to v10.0.4 2019-12-05 00:27:48 +00:00
Renovate Bot
f0ddf3bf09 fix(deps): update dependency @edx/frontend-platform to v1.1.8 2019-12-04 22:13:44 +00:00
renovate[bot]
11ac45f6b8 chore(deps): update dependency codecov to v3.6.1 (#228) 2019-12-04 16:38:04 -05:00
edX Transifex Bot
67c23630b9 fix(i18n): update translations 2019-12-04 16:18:11 -05:00
Renovate Bot
0ef787b9ce fix(deps): update dependency @edx/frontend-platform to v1.1.6 2019-12-04 20:19:41 +00:00
Renovate Bot
c3e3024971 chore(deps): update dependency glob to v7.1.6 2019-12-04 17:17:28 +00:00
Adam Butterworth
a95bf5006b fix: remove unneeded commitlint dependencies (#339) 2019-12-04 11:21:36 -05:00
Renovate Bot
517f3f57bf fix(deps): update dependency @edx/frontend-component-header to v2.0.3 2019-12-03 23:15:06 +00:00
Renovate Bot
2e464498d8 fix(deps): update dependency @edx/frontend-component-footer to v10.0.3 2019-12-03 22:18:45 +00:00
Renovate Bot
3f519aa678 chore(deps): update dependency @edx/frontend-build to v2.0.4 2019-12-03 21:05:49 +00:00
Renovate Bot
5b7a29422b fix(deps): update dependency @edx/frontend-component-header to v2.0.2 2019-12-03 20:53:22 +00:00
Renovate Bot
f3d9f2ae06 fix(deps): update dependency @edx/frontend-component-footer to v10.0.2 2019-12-03 20:14:16 +00:00
edX Transifex Bot
b5607e9b83 fix(i18n): update translations 2019-12-03 13:55:04 -05:00
David Joy
fd9b393bc7 fix: updating frontend-build to fix translations (#332) 2019-12-03 13:28:28 -05:00
renovate[bot]
0fe5354047 chore(deps): update dependency enzyme-adapter-react-16 to v1.15.1 (#295) 2019-12-03 11:24:05 -05:00
Renovate Bot
71923503c4 fix(deps): update dependency @edx/frontend-component-footer to v10.0.1 2019-12-03 16:16:55 +00:00
Renovate Bot
f1a620a784 fix(deps): update dependency @edx/frontend-component-header to v2.0.1 2019-12-03 15:39:12 +00:00
David Joy
645f378600 fix: updating frontend-platform required small refactorings (#328) 2019-12-02 17:28:32 -05:00
David Joy
057a39085c Revert "build: add newrelic plugin back in as experiment (#326)" (#327)
This reverts commit 3f31aba5a7.
2019-12-02 13:32:30 -05:00
David Joy
3f31aba5a7 build: add newrelic plugin back in as experiment (#326) 2019-12-02 13:12:38 -05:00
David Joy
321448c674 fix: using latest published version of frontend-build again 2019-12-02 12:56:51 -05:00
David Joy
77c9984d34 fix: bumping frontend-build version
Debugging build process, so pushing to master to speed up workflow.  Yes, I know.
2019-12-02 11:28:30 -05:00
David Joy
c9d0a7f86f fix: temporarily pin frontend-build to github link to test stage build (#325) 2019-12-02 11:18:58 -05:00
edX Transifex Bot
04d51ffee1 fix(i18n): update translations 2019-12-01 15:40:22 -05:00
Renovate Bot
25f6b86e92 fix(deps): update font awesome 2019-11-25 16:20:53 +00:00
Renovate Bot
c700a47e40 fix(deps): update dependency react-redux to v7.1.3 2019-11-25 15:23:57 +00:00
Adam Butterworth
db4b082247 feat: use frontend-platform for runways (#318)
Move logging, analytics, auth, base, and i18n to implementations in frontend-platform.
2019-11-25 09:29:14 -05:00
Renovate Bot
919677b544 fix(deps): update dependency form-urlencoded to v3.0.2 2019-11-06 17:19:41 +00:00
David Joy
32f0f33975 build: automerge patch renovate PRs (#309) 2019-11-04 17:14:41 -05:00
renovate[bot]
0662177f40 chore(deps): pin dependencies (#304) 2019-11-04 17:03:26 -05:00
David Joy
f266bba04c fix: initialize redux in prod mode by default (#303)
We were accidentally initializing in dev mode in production.

Also cleaning up some other misc detritus.
2019-10-22 15:11:35 -04:00
renovate[bot]
20e7bb09bb chore(deps): update dependency @edx/frontend-build to v1.2.2 (#302) 2019-10-18 10:20:34 -04:00
David Joy
da7e055d77 fix: send analytics events (#301)
frontend-base wasn’t calling identifyAuthenticatedUser and sendPageEvent.  Version 4.1.0 does.
2019-10-16 12:14:42 -04:00
Adam Butterworth
bee2e54b87 Update README.rst 2019-10-15 11:20:44 -04:00
Adam Butterworth
1b179f55d1 Update README.rst (#294)
* Update README.rst

* Update README.rst

* Update README.rst

* Update README.rst

* Update README.rst

* doc: updating readme with feedback

* Update README.rst
2019-10-11 17:27:53 -04:00
David Joy
edef77f101 fix: bump frontend-build one more time (#299) 2019-10-11 14:28:32 -04:00
David Joy
e4f1a6100a feat: add a favicon to the distribution (#297) 2019-10-11 13:25:15 -04:00
David Joy
b3869b97f7 fix: remove unnecessary environment variable (#292)
* fix: remove unnecessary environment variable

* fix: bumping version of frontend-base for csrf env var change

* fix: get IDE linting to work again
2019-10-10 14:16:25 -04:00
Adam Butterworth
cd339d491d Create 0002-build-time-customization-with-npm-aliases.rst (#291) 2019-10-07 16:05:57 -04:00
David Joy
b66cbb3eb6 fix: frontend-base 4.0.0 and a few others (#288)
* fix: bump version of frontend-base to 4.0.0

* fix: bumping a few other versions

* fix: bump footer version and lock travis npm version

* fix: use dist not src for header/footer styless
2019-10-02 15:54:25 -04:00
Adam Butterworth
cd2fdae725 feat: use openedx footer by default (#282)
* feat: use openedx footer by default

* fix: delete footer directory and update package-lock
2019-09-30 16:53:31 -04:00
Adam Butterworth
51b4d9b42a fix: re-add semantic-release (#275)
* fix: re-add semantic-release

* fix: removing semantic release dep
2019-09-27 10:03:53 -04:00
Adam Butterworth
112857de00 fix: update frontend-build to allow production env to pass through (#278) 2019-09-25 15:26:44 -04:00
David Joy
3317d2b148 fix: bump frontend-base to 2.2.0 to initialize segment (#272)
frontend-base 2.1.x had a bug where segment wasn’t being initialized.  Now it is.

Also allowing frontend-base to use its default logging service, which is NewRelicLoggingService.
2019-09-25 13:40:33 -04:00
Adam Butterworth
be67c13914 feat: delegate build scripts to edx/frontend-build (#257) 2019-09-25 11:54:49 -04:00
73 changed files with 5650 additions and 15603 deletions

32
.env Normal file
View File

@@ -0,0 +1,32 @@
NODE_ENV=null
ACCESS_TOKEN_COOKIE_NAME=null
BASE_URL=null
CREDENTIALS_BASE_URL=null
CSRF_TOKEN_API_PATH=null
ECOMMERCE_BASE_URL=null
LANGUAGE_PREFERENCE_COOKIE_NAME=null
LMS_BASE_URL=null
LOGIN_URL=null
LOGOUT_URL=null
MARKETING_SITE_BASE_URL=null
ORDER_HISTORY_URL=null
REFRESH_ACCESS_TOKEN_ENDPOINT=null
SEGMENT_KEY=null
SITE_NAME=null
USER_INFO_COOKIE_NAME=null
APPLE_APP_STORE_URL=null
CONTACT_URL=null
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=null
ENTERPRISE_MARKETING_URL=null
ENTERPRISE_MARKETING_UTM_CAMPAIGN=null
ENTERPRISE_MARKETING_UTM_SOURCE=null
FACEBOOK_URL=null
GOOGLE_PLAY_URL=null
LINKED_IN_URL=null
OPEN_SOURCE_URL=null
PRIVACY_POLICY_URL=null
REDDIT_URL=null
SUPPORT_URL=null
TERMS_OF_SERVICE_URL=null
TWITTER_URL=null
YOU_TUBE_URL=null

33
.env.development Normal file
View File

@@ -0,0 +1,33 @@
NODE_ENV='development'
PORT=1995
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='localhost:1995'
CREDENTIALS_BASE_URL='http://localhost:18150'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
ECOMMERCE_BASE_URL='http://localhost:18130'
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/login'
MARKETING_SITE_BASE_URL='http://localhost:18000'
ORDER_HISTORY_URL='localhost:1996/orders'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=null
SITE_NAME='edX'
USER_INFO_COOKIE_NAME='edx-user-info'
APPLE_APP_STORE_URL='https://www.apple.com/ios/app-store/'
CONTACT_URL='http://localhost:18000/contact'
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
ENTERPRISE_MARKETING_URL='http://example.com'
ENTERPRISE_MARKETING_UTM_CAMPAIGN='my_campaign'
ENTERPRISE_MARKETING_UTM_SOURCE='edX profile'
FACEBOOK_URL='https://www.facebook.com'
GOOGLE_PLAY_URL='https://play.google.com/store'
LINKED_IN_URL='https://www.linkedin.com'
OPEN_SOURCE_URL='http://localhost:18000/openedx'
PRIVACY_POLICY_URL='http://localhost:18000/privacy-policy'
REDDIT_URL='https://www.reddit.com'
SUPPORT_URL='http://localhost:18000/support'
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
TWITTER_URL='https://twitter.com'
YOU_TUBE_URL='https://www.youtube.com'

15
.env.test Normal file
View File

@@ -0,0 +1,15 @@
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='localhost:1995'
CREDENTIALS_BASE_URL='http://localhost:18150'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
ECOMMERCE_BASE_URL='http://localhost:18130'
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/login'
MARKETING_SITE_BASE_URL='http://localhost:18000'
ORDER_HISTORY_URL='localhost:1996/orders'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=null
SITE_NAME='edX'
USER_INFO_COOKIE_NAME='edx-user-info'

View File

@@ -2,3 +2,4 @@ coverage/*
dist/
node_modules/
__mocks__/
__snapshots__/

View File

@@ -1,34 +0,0 @@
{
"extends": "eslint-config-edx",
"parser": "babel-eslint",
"rules": {
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": [
"webpack/*.js",
"**/*.test.jsx",
"**/*.test.js"
]
}
],
// https://github.com/evcohen/eslint-plugin-jsx-a11y/issues/340#issuecomment-338424908
"jsx-a11y/anchor-is-valid": [ "error", {
"components": [ "Link" ],
"specialLink": [ "to" ]
}],
"jsx-a11y/label-has-for": [ 2, {
"components": [ "label" ],
"required": {
"some": [ "nesting", "id" ]
},
"allowChildren": false
}]
},
"env": {
"jest": true
},
"globals": {
"newrelic": false
}
}

3
.eslintrc.js Normal file
View File

@@ -0,0 +1,3 @@
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('eslint');

View File

@@ -1,11 +1,9 @@
.tx
coverage
dist
footer
node_modules
public
src
webpack
.dockerignore
.eslintignore
.eslintrc

View File

@@ -1,7 +1,7 @@
language: node_js
node_js: 12
before_install:
- npm install -g npm@latest
- npm install -g npm@6
install:
- npm ci
script:
@@ -13,7 +13,7 @@ script:
- npm run npm-build
- npm run is-es5
after_success:
- npm run semantic-release
- npx semantic-release
- codecov
env:
global:

View File

@@ -51,7 +51,7 @@ validate-no-uncommitted-package-lock-changes:
npm-build:
rm -rf ./npm-dist
./node_modules/.bin/babel src/profile --out-dir npm-dist --source-maps --ignore **/*.test.jsx,**/*.test.js,**/setupTest.js --copy-files
./node_modules/.bin/fedx-scripts babel src/profile --out-dir npm-dist --source-maps --ignore **/*.test.jsx,**/*.test.js,**/setupTest.js --copy-files
@# --copy-files will bring in everything else that wasn't processed by babel. Remove what we don't want.
@find npm-dist -name '*.test.js*' -delete
@rm -rf ./npm-dist/__mocks__

View File

@@ -3,22 +3,58 @@
frontend-app-profile
====================
Please tag **@edx/arch-fed** on any PRs or issues.
This is a micro-frontend application responsible for the display and updating of user profiles. Please tag **@edx/arch-fed** on any PRs or issues.
Introduction
------------
When a user views their own profile, they're given fields to edit their full name, location, primary spoken language, education, social links, and bio. Each field also has a dropdown to select the visibility of that field - i.e., whether it can be viewed by other learners.
React app for user account management.
When a user views someone else's profile, they see all those fields that that user set as public.
Important Note
--------------
----------
The production Webpack configuration for this repo uses `Purgecss <https://www.purgecss.com/>`_
to remove unused CSS from the production css file. In webpack/webpack.prod.config.js the Purgecss
plugin is configured to scan directories to determine what css selectors should remain. Currently
the src/ directory is scanned along with all @edx/frontend-component* node modules and paragon.
If you add and use a component in this repo that relies on HTML classes or ids for styling you
must add it to the Purgecss configuration or it will be unstyled in the production build.
Development
-----------
Start Devstack
^^^^^^^^^^^^^^
To use this application `devstack <https://github.com/edx/devstack>`__ must be running and you must be logged into it.
- Start devstack
- Log in (http://localhost:18000/login)
Start the development server
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In this project, install requirements and start the development server by running:
.. code:: bash
npm install
npm start # The server will run on port 1995
Once the dev server is up visit http://localhost:1995/u/staff.
----------
Configuration and Deployment
----------------------------
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:
.. code:: bash
NODE_ENV=production ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' npm run build
For more information see the document: `Micro-frontend applications in Open
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/micro-frontends-in-open-edx.html>`__.
----------
Notes
-----
The production Webpack configuration for this repo uses `Purgecss <https://www.purgecss.com/>`__ to remove unused CSS from the production css file. In ``webpack.prod.config.js`` the Purgecss plugin is configured to scan directories to determine what css selectors should remain. Currently the src/ directory is scanned along with all ``@edx/frontend-component*`` node modules and ``@edx/paragon``. **If you add and use a component in this repo that relies on HTML classes or ids for styling you must add it to the Purgecss configuration or it will be unstyled in the production build.**
.. |Build Status| image:: https://api.travis-ci.org/edx/frontend-app-profile.svg?branch=master
:target: https://travis-ci.org/edx/frontend-app-profile

View File

@@ -1,39 +0,0 @@
/* eslint-disable */
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-class-properties',
'@babel/plugin-syntax-dynamic-import',
[
'transform-imports',
{
'@fortawesome/free-brands-svg-icons': {
transform: '@fortawesome/free-brands-svg-icons/${member}',
skipDefaultConversion: true,
},
'@fortawesome/free-regular-svg-icons': {
transform: '@fortawesome/free-regular-svg-icons/${member}',
skipDefaultConversion: true,
},
'@fortawesome/free-solid-svg-icons': {
transform: '@fortawesome/free-solid-svg-icons/${member}',
skipDefaultConversion: true,
},
},
],
],
env: {
i18n: {
plugins: [
[
'react-intl',
{
messagesDir: './temp/babel-plugin-react-intl',
moduleSourceName: '@edx/frontend-i18n',
},
],
],
},
},
};

3
commitlint.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
extends: ['@commitlint/config-angular'],
};

View File

@@ -0,0 +1,72 @@
2. Build time customization using NPM aliases
---------------------------------------------
Status
------
Accepted
Context
-------
Frontend applications created throughout FY2019 contain hardcoded edX brand specific elements
such as the site's header, footer, logo, visual style, and navigational links. This enabled
teams to move more quickly in efforts to adopt a micro-frontend architecture to enable more
rapid UI innovation in the future. There was no easy path for these new applications to be
incorporated into the Open edX platform where they need to be branded properly.
Decision
--------
In order to make frontend applications brand agnostic, we will split branded elements into
npm packages such as ``frontend-component-header``. Frontend applications will expect to
find components or other exports according to a defined interface. Package interfaces will
be defined in the README for each npm package repository.
.. code-block:: javascript
// exports React component as default and a 'messages' object for i18n
import Header, { messages } from '@edx/frontend-component-header';
To build a frontend application for a specific brand (edX or other Open edX implementation) we
will leverage npm aliases to override these npm packages with branded packages that implement the
same interface before build. For example, ``frontend-component-header`` will be overriden with
``frontend-component-header-edx``. This is done using the
`npm alias syntax introduced in version 6.9.0`_.
We install aliases using the syntax below:
.. code-block:: bash
# npm install <package-name>@<type>:<branded-package>
# npm package
npm install @edx/frontend-component-header@npm:@edx/frontend-component-header-edx@latest
# git repository
npm install @edx/frontend-component-header@git:https://github.com/edx/frontend-component-header-edx.git
# local folder
npm install @edx/frontend-component-header@file:../path/to/local/module/during/build
After installing overrides using npm aliases, we build the project normally:
.. code-block:: bash
npm run build
Using this mechanism branded packages are substituted for the default unbranded packages at build
and deployment time.
.. _npm alias syntax introduced in version 6.9.0: https://github.com/npm/rfcs/blob/latest/implemented/0001-package-aliases.md
Consequences
------------
One drawback of this process is the inability to automatically test substituted packages before
deployment. This is a risk we are willing to accept until it becomes an issue.
edX has built a deployment pipeline that will read configuration for npm packages to override
and alias. This code is open source but heavily catered to edX's specific deployment needs.
The Open edX community will need to collaborate on a simpler, less opinionated build and
deployment process for micro-frontend applications.

View File

@@ -1,99 +0,0 @@
import React from 'react';
import SiteFooter from '@edx/frontend-component-footer';
import { sendTrackEvent } from '@edx/frontend-analytics';
import { App, validateConfig } from '@edx/frontend-base';
import {
faFacebookSquare,
faTwitterSquare,
faYoutubeSquare,
faLinkedin,
faRedditSquare,
} from '@fortawesome/free-brands-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import FooterLogo from './edx-footer.png';
const config = {
APPLE_APP_STORE_URL: process.env.APPLE_APP_STORE_URL,
CONTACT_URL: process.env.CONTACT_URL,
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: process.env.ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM,
ENTERPRISE_MARKETING_URL: process.env.ENTERPRISE_MARKETING_URL,
ENTERPRISE_MARKETING_UTM_CAMPAIGN: process.env.ENTERPRISE_MARKETING_UTM_CAMPAIGN,
ENTERPRISE_MARKETING_UTM_SOURCE: process.env.ENTERPRISE_MARKETING_UTM_SOURCE,
FACEBOOK_URL: process.env.FACEBOOK_URL,
GOOGLE_PLAY_URL: process.env.GOOGLE_PLAY_URL,
LINKED_IN_URL: process.env.LINKED_IN_URL,
OPEN_SOURCE_URL: process.env.OPEN_SOURCE_URL,
PRIVACY_POLICY_URL: process.env.PRIVACY_POLICY_URL,
REDDIT_URL: process.env.REDDIT_URL,
SUPPORT_URL: process.env.SUPPORT_URL,
TERMS_OF_SERVICE_URL: process.env.TERMS_OF_SERVICE_URL,
TWITTER_URL: process.env.TWITTER_URL,
YOU_TUBE_URL: process.env.YOU_TUBE_URL,
};
App.requireConfig(['SITE_NAME', 'MARKETING_SITE_BASE_URL'], 'ProfileFooter');
validateConfig(config, 'ProfileFooter');
export default function ProfileFooter() {
const socialLinks = [
{
title: 'Facebook',
url: config.FACEBOOK_URL,
icon: <FontAwesomeIcon icon={faFacebookSquare} className="social-icon" size="2x" />,
screenReaderText: 'Like edX on Facebook',
},
{
title: 'Twitter',
url: config.TWITTER_URL,
icon: <FontAwesomeIcon icon={faTwitterSquare} className="social-icon" size="2x" />,
screenReaderText: 'Follow edX on Twitter',
},
{
title: 'Youtube',
url: config.YOU_TUBE_URL,
icon: <FontAwesomeIcon icon={faYoutubeSquare} className="social-icon" size="2x" />,
screenReaderText: 'Subscribe to the edX YouTube channel',
},
{
title: 'LinkedIn',
url: config.LINKED_IN_URL,
icon: <FontAwesomeIcon icon={faLinkedin} className="social-icon" size="2x" />,
screenReaderText: 'Follow edX on LinkedIn',
},
{
title: 'Reddit',
url: config.REDDIT_URL,
icon: <FontAwesomeIcon icon={faRedditSquare} className="social-icon" size="2x" />,
screenReaderText: 'Subscribe to the edX subreddit',
},
];
const enterpriseMarketingLinkData = {
url: config.ENTERPRISE_MARKETING_URL,
queryParams: {
utm_campaign: config.ENTERPRISE_MARKETING_UTM_CAMPAIGN,
utm_source: config.ENTERPRISE_MARKETING_UTM_SOURCE,
utm_medium: config.ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM,
},
};
return (
<SiteFooter
appleAppStoreUrl={config.APPLE_APP_STORE_URL}
contactUrl={config.CONTACT_URL}
enterpriseMarketingLink={enterpriseMarketingLinkData}
googlePlayUrl={config.GOOGLE_PLAY_URL}
handleAllTrackEvents={sendTrackEvent}
marketingSiteBaseUrl={App.config.MARKETING_SITE_BASE_URL}
openSourceUrl={config.OPEN_SOURCE_URL}
privacyPolicyUrl={config.PRIVACY_POLICY_URL}
siteLogo={FooterLogo}
siteName={App.config.SITE_NAME}
socialLinks={socialLinks}
supportUrl={config.SUPPORT_URL}
termsOfServiceUrl={config.TERMS_OF_SERVICE_URL}
/>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

7
jest.config.js Normal file
View File

@@ -0,0 +1,7 @@
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('jest', {
setupFiles: [
'<rootDir>/src/setupTest.js',
],
});

View File

@@ -1,6 +1,6 @@
# This file describes this Open edX repo, as described in OEP-2:
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
nick: acct
nick: prof
oeps: {}
owner: edx/arch-team

19768
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,23 @@
{
"name": "@edx/frontend-app-profile",
"version": "1.0.0-semantically-released",
"description": "User profile micro-frontend for edX",
"description": "User profile micro-frontend for Open edX",
"author": "edX",
"license": "AGPL-3.0",
"main": "npm-dist/index.js",
"publishConfig": {
"access": "public"
"repository": {
"type": "git",
"url": "git+https://github.com/edx/frontend-app-profile.git"
},
"browserslist": [
"last 2 versions",
"ie 11"
],
"scripts": {
"build": "NODE_ENV=production BABEL_ENV=production webpack --config=webpack/webpack.prod.config.js",
"build": "fedx-scripts webpack",
"npm-build": "make npm-build",
"lint": "eslint --ext .js --ext .jsx .",
"i18n_extract": "BABEL_ENV=i18n babel src --quiet > /dev/null",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"is-es5": "es-check es5 ./dist/*.js",
"semantic-release": "semantic-release",
"snapshot": "jest --updateSnapshot",
"start": "NODE_ENV=development BABEL_ENV=development webpack-dev-server --config=webpack/webpack.dev.config.js --progress",
"test": "jest --coverage --passWithNoTests"
"lint": "fedx-scripts eslint",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"test": "fedx-scripts jest --coverage --passWithNoTests"
},
"files": [
"/npm-dist"
@@ -30,35 +28,32 @@
"commit-msg": "commitlint -e $GIT_PARAMS"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/edx/frontend-app-profile.git"
},
"author": "edX",
"license": "AGPL-3.0",
"bugs": {
"url": "https://github.com/edx/frontend-app-profile/issues"
},
"homepage": "https://github.com/edx/frontend-app-profile#readme",
"publishConfig": {
"access": "public"
},
"browserslist": [
"last 2 versions",
"ie 11"
],
"dependencies": {
"@edx/frontend-analytics": "3.0.0",
"@edx/frontend-auth": "7.0.1",
"@edx/frontend-base": "2.1.4",
"@edx/frontend-component-footer": "6.0.2",
"@edx/frontend-component-header": "1.1.2",
"@edx/frontend-i18n": "3.0.2",
"@edx/frontend-logging": "3.0.1",
"@edx/frontend-component-footer": "10.0.5",
"@edx/frontend-component-header": "2.0.3",
"@edx/frontend-platform": "1.1.9",
"@edx/paragon": "7.1.3",
"@fortawesome/fontawesome-svg-core": "1.2.14",
"@fortawesome/fontawesome-svg-core": "1.2.25",
"@fortawesome/free-brands-svg-icons": "5.7.2",
"@fortawesome/free-regular-svg-icons": "5.7.1",
"@fortawesome/free-solid-svg-icons": "5.7.1",
"@fortawesome/react-fontawesome": "0.1.4",
"@fortawesome/free-regular-svg-icons": "5.7.2",
"@fortawesome/free-solid-svg-icons": "5.7.2",
"@fortawesome/react-fontawesome": "0.1.8",
"babel-polyfill": "6.26.0",
"classnames": "2.2.6",
"email-prop-type": "1.1.7",
"font-awesome": "4.7.0",
"form-urlencoded": "3.0.0",
"form-urlencoded": "3.0.2",
"history": "4.7.2",
"lodash.camelcase": "4.3.0",
"lodash.get": "4.4.2",
@@ -69,7 +64,7 @@
"prop-types": "15.7.2",
"react": "16.9.0",
"react-dom": "16.9.0",
"react-redux": "7.1.1",
"react-redux": "7.1.3",
"react-router": "5.0.1",
"react-router-dom": "5.0.1",
"react-transition-group": "4.3.0",
@@ -82,82 +77,18 @@
"universal-cookie": "3.1.0"
},
"devDependencies": {
"@babel/cli": "7.6.0",
"@babel/core": "7.6.0",
"@babel/plugin-proposal-class-properties": "7.5.5",
"@babel/plugin-proposal-object-rest-spread": "7.5.5",
"@babel/plugin-syntax-dynamic-import": "7.2.0",
"@babel/preset-env": "7.6.0",
"@babel/preset-react": "7.0.0",
"@svgr/webpack": "4.3.2",
"autoprefixer": "9.6.1",
"axios-mock-adapter": "1.16.0",
"babel-eslint": "10.0.3",
"babel-jest": "24.9.0",
"babel-loader": "8.0.6",
"babel-plugin-react-intl": "4.1.16",
"babel-plugin-transform-imports": "2.0.0",
"clean-webpack-plugin": "0.1.19",
"codecov": "3.1.0",
"copy-webpack-plugin": "4.6.0",
"css-loader": "3.2.0",
"cssnano": "4.1.10",
"@commitlint/cli": "8.2.0",
"@commitlint/config-angular": "8.2.0",
"@edx/frontend-build": "2.0.4",
"codecov": "3.6.1",
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.14.0",
"enzyme-adapter-react-16": "1.15.1",
"es-check": "5.0.0",
"eslint-config-edx": "4.0.4",
"fetch-mock": "6.5.2",
"file-loader": "1.1.11",
"glob": "7.1.3",
"html-webpack-harddisk-plugin": "0.2.0",
"html-webpack-new-relic-plugin": "1.1.0",
"html-webpack-plugin": "3.2.0",
"husky": "0.14.3",
"identity-obj-proxy": "3.0.0",
"image-webpack-loader": "4.6.0",
"jest": "24.9.0",
"mini-css-extract-plugin": "0.4.5",
"new-relic-source-map-webpack-plugin": "1.2.0",
"node-sass": "4.12.0",
"postcss-loader": "3.0.0",
"postcss-rtl": "1.3.3",
"purgecss-webpack-plugin": "1.5.0",
"react-dev-utils": "9.0.3",
"glob": "7.1.6",
"husky": "3.1.0",
"purgecss-webpack-plugin": "1.6.0",
"react-test-renderer": "16.9.0",
"reactifex": "1.1.1",
"redux-mock-store": "1.5.3",
"resolve-url-loader": "3.1.0",
"sass-loader": "6.0.7",
"semantic-release": "15.13.24",
"source-map-loader": "0.2.4",
"style-loader": "0.20.3",
"url-loader": "1.1.2",
"webpack": "4.29.2",
"webpack-bundle-analyzer": "3.3.2",
"webpack-cli": "3.2.3",
"webpack-dev-server": "3.1.14",
"webpack-merge": "4.2.1"
},
"jest": {
"rootDir": "./src",
"testURL": "http://localhost/",
"setupFiles": [
"./setupTest.js"
],
"moduleNameMapper": {
"\\.svg": "<rootDir>/__mocks__/svgrMock.js",
"\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|scss)$": "identity-obj-proxy"
},
"collectCoverageFrom": [
"**/*.{js,jsx}"
],
"coveragePathIgnorePatterns": [
"setupTest.js",
"index.jsx"
],
"transformIgnorePatterns": [
"/node_modules/(?!((@edx/paragon)|(@edx/frontend-base))/).*/"
]
"redux-mock-store": "1.5.3"
}
}

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin>
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700" rel="stylesheet">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
</head>
<body>
<div id="root"></div>

View File

@@ -1,5 +1,9 @@
{
"extends": [
"config:base"
]
],
"patch": {
"automerge": true
},
"rebaseStalePrs": true
}

View File

@@ -1 +0,0 @@
module.exports = 'test-file-stub';

View File

@@ -1 +0,0 @@
module.exports = { ReactComponent: 'IconMock' };

BIN
src/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -0,0 +1,33 @@
import { getConfig } from '@edx/frontend-platform';
import { applyMiddleware, createStore, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import createRootReducer from './reducers';
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
function composeMiddleware() {
if (getConfig().ENVIRONMENT === 'development') {
const loggerMiddleware = createLogger({
collapsed: true,
});
return composeWithDevTools(applyMiddleware(thunkMiddleware, sagaMiddleware, loggerMiddleware));
}
return compose(applyMiddleware(thunkMiddleware, sagaMiddleware));
}
export default function configureStore(initialState = {}) {
const store = createStore(
createRootReducer(),
initialState,
composeMiddleware(),
);
sagaMiddleware.run(rootSaga);
return store;
}

View File

@@ -1,11 +1,9 @@
import { combineReducers } from 'redux';
import { userAccount } from '@edx/frontend-auth';
import { reducer as profilePage } from './profile';
import { reducer as profilePage } from '../profile';
const createRootReducer = () =>
combineReducers({
userAccount,
profilePage,
});

View File

@@ -1,6 +1,6 @@
import { all } from 'redux-saga/effects';
import { saga as profileSaga } from './profile';
import { saga as profileSaga } from '../profile';
export default function* rootSaga() {
yield all([

View File

@@ -15,7 +15,7 @@
"profile.certificates.types.unknown": "Certificado",
"profile.country.label": "Ubicación",
"profile.country.empty": "Añade ubicación",
"profile.education.empty": "Añade Education",
"profile.education.empty": "Añade Educación",
"profile.education.education": "Educación",
"profile.education.levels.p": "Doctorado",
"profile.education.levels.m": "Master o magíster",

View File

@@ -1,51 +1,51 @@
{
"profile.age.headline": "Votre profil ne peut pas être partagé.",
"profile.age.details": "Pour partager votre profil avec d'autres apprenants edX, vous devez confirmer que vous avez plus de 13 ans.",
"profile.age.set.date": "Définissez votre date de naissance",
"profile.datejoined.member.since": "Membre depuis {year}",
"profile.bio.empty": "Ajouter une courte biographie",
"profile.bio.about.me": "À propos de moi",
"profile.certificate.organization.label": "De",
"profile.certificate.completion.date.label": "Terminé le {date}",
"profile.no.certificates": "Vous n'avez pas encore de certificats.",
"profile.certificates.my.certificates": "Mes certificats",
"profile.certificates.view.certificate": "Voir le certificat",
"profile.certificates.types.verified": "Certificat vérifié",
"profile.certificates.types.professional": "Certificat professionnel",
"profile.certificates.types.unknown": "Certificat",
"profile.country.label": "Localisation",
"profile.country.empty": "Ajouter localisation",
"profile.education.empty": "Ajouter une éducation",
"profile.age.headline": "Your profile cannot be shared.",
"profile.age.details": "To share your profile with other edX learners, you must confirm that you are over the age of 13.",
"profile.age.set.date": "Set your date of birth",
"profile.datejoined.member.since": "Member since {year}",
"profile.bio.empty": "Add a short bio",
"profile.bio.about.me": "About Me",
"profile.certificate.organization.label": "From",
"profile.certificate.completion.date.label": "Completed on {date}",
"profile.no.certificates": "You don't have any certificates yet.",
"profile.certificates.my.certificates": "My Certificates",
"profile.certificates.view.certificate": "View Certificate",
"profile.certificates.types.verified": "Verified Certificate",
"profile.certificates.types.professional": "Professional Certificate",
"profile.certificates.types.unknown": "Certificate",
"profile.country.label": "Location",
"profile.country.empty": "Add location",
"profile.education.empty": "Add education",
"profile.education.education": "Education",
"profile.education.levels.p": "Doctorat",
"profile.education.levels.m": "Master ou diplôme professionnel",
"profile.education.levels.b": "Diplôme de licence",
"profile.education.levels.a": "Grade de l'associé",
"profile.education.levels.hs": "Lycée / enseignement secondaire",
"profile.education.levels.jhs": "Collège / enseignement secondaire inférieur",
"profile.education.levels.el": "Enseignement primaire",
"profile.education.levels.none": "Sans diplôme",
"profile.education.levels.o": "Autre niveau d'étude",
"profile.editbutton.edit": "Modifier",
"profile.formcontrols.who.can.see": "Qui peut voir ça :",
"profile.formcontrols.button.cancel": "Annuler",
"profile.formcontrols.button.save": "Enregistrer",
"profile.formcontrols.button.saving": "Enregistrement",
"profile.formcontrols.button.saved": "Enregistré",
"profile.visibility.who.just.me": "Juste moi",
"profile.visibility.who.everyone": "Tout le monde sur edX",
"profile.name.full.name": "Nom complet",
"profile.name.details": "C'est le nom qui apparaît dans votre compte et sur vos certificats.",
"profile.name.empty": "Ajouter un nom",
"profile.preferredlanguage.empty": "Ajouter une langue",
"profile.preferredlanguage.label": "Langue principale parlée",
"profile.profileavatar.upload-button": "Envoyer la photo",
"profile.profileavatar.remove.button": "Supprimer",
"profile.image.alt.attribute": "Profil avatar",
"profile.profileavatar.change-button": "Modifier",
"profile.sociallinks.add": "Ajouter {network}",
"profile.sociallinks.social.links": "Liens vers les réseaux sociaux",
"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.education.levels.p": "Doctorate",
"profile.education.levels.m": "Master's or professional degree",
"profile.education.levels.b": "Bachelor's Degree",
"profile.education.levels.a": "Associate's degree",
"profile.education.levels.hs": "Secondary/high school",
"profile.education.levels.jhs": "Junior secondary/junior high/middle school",
"profile.education.levels.el": "Elementary/primary school",
"profile.education.levels.none": "No formal education",
"profile.education.levels.o": "Other education",
"profile.editbutton.edit": "Edit",
"profile.formcontrols.who.can.see": "Who can see this:",
"profile.formcontrols.button.cancel": "Cancel",
"profile.formcontrols.button.save": "Save",
"profile.formcontrols.button.saving": "Saving",
"profile.formcontrols.button.saved": "Saved",
"profile.visibility.who.just.me": "Just me",
"profile.visibility.who.everyone": "Everyone on edX",
"profile.name.full.name": "Full Name",
"profile.name.details": "This is the name that appears in your account and on your certificates.",
"profile.name.empty": "Add name",
"profile.preferredlanguage.empty": "Add language",
"profile.preferredlanguage.label": "Primary Language Spoken",
"profile.profileavatar.upload-button": "Upload Photo",
"profile.profileavatar.remove.button": "Remove",
"profile.image.alt.attribute": "profile avatar",
"profile.profileavatar.change-button": "Change",
"profile.sociallinks.add": "Add {network}",
"profile.sociallinks.social.links": "Social Links",
"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..."
}

View File

@@ -1,22 +1,22 @@
{
"profile.age.headline": "您的个人资料无法共享",
"profile.age.details": "您必须确认您已年满13岁才能其他edX学员分享您的个人资料",
"profile.age.set.date": "设置您的出生日期",
"profile.datejoined.member.since": "会员自{year}",
"profile.bio.empty": "添加简介",
"profile.bio.about.me": "个人资料",
"profile.certificate.organization.label": "",
"profile.age.headline": "Your profile cannot be shared.",
"profile.age.details": "To share your profile with other edX learners, you must confirm that you are over the age of 13.",
"profile.age.set.date": "Set your date of birth",
"profile.datejoined.member.since": "Member since {year}",
"profile.bio.empty": "Add a short bio",
"profile.bio.about.me": "About Me",
"profile.certificate.organization.label": "From",
"profile.certificate.completion.date.label": "Completed on {date}",
"profile.no.certificates": "你还没有任何证书。",
"profile.certificates.my.certificates": "我的证书",
"profile.certificates.view.certificate": "查看证书",
"profile.no.certificates": "You don't have any certificates yet.",
"profile.certificates.my.certificates": "My Certificates",
"profile.certificates.view.certificate": "View Certificate",
"profile.certificates.types.verified": "Verified Certificate",
"profile.certificates.types.professional": "Professional Certificate",
"profile.certificates.types.unknown": "Certificate",
"profile.country.label": "位置",
"profile.country.empty": "添加地址",
"profile.education.empty": "添加教育程度",
"profile.education.education": "教育程度",
"profile.country.label": "Location",
"profile.country.empty": "Add location",
"profile.education.empty": "Add education",
"profile.education.education": "Education",
"profile.education.levels.p": "Doctorate",
"profile.education.levels.m": "Master's or professional degree",
"profile.education.levels.b": "Bachelor's Degree",
@@ -26,26 +26,26 @@
"profile.education.levels.el": "Elementary/primary school",
"profile.education.levels.none": "No formal education",
"profile.education.levels.o": "Other education",
"profile.editbutton.edit": "编辑",
"profile.formcontrols.who.can.see": "对谁可见:",
"profile.formcontrols.button.cancel": "取消",
"profile.formcontrols.button.save": "保存",
"profile.formcontrols.button.saving": "正在保存",
"profile.formcontrols.button.saved": "已保存",
"profile.visibility.who.just.me": "仅自己",
"profile.visibility.who.everyone": "edX的所有人",
"profile.name.full.name": "全名",
"profile.name.details": "用于显示在账户和证书上的姓名。",
"profile.name.empty": "添加名字",
"profile.preferredlanguage.empty": "添加语言",
"profile.editbutton.edit": "Edit",
"profile.formcontrols.who.can.see": "Who can see this:",
"profile.formcontrols.button.cancel": "Cancel",
"profile.formcontrols.button.save": "Save",
"profile.formcontrols.button.saving": "Saving",
"profile.formcontrols.button.saved": "Saved",
"profile.visibility.who.just.me": "Just me",
"profile.visibility.who.everyone": "Everyone on edX",
"profile.name.full.name": "Full Name",
"profile.name.details": "This is the name that appears in your account and on your certificates.",
"profile.name.empty": "Add name",
"profile.preferredlanguage.empty": "Add language",
"profile.preferredlanguage.label": "Primary Language Spoken",
"profile.profileavatar.upload-button": "上传照片",
"profile.profileavatar.remove.button": "移除",
"profile.profileavatar.upload-button": "Upload Photo",
"profile.profileavatar.remove.button": "Remove",
"profile.image.alt.attribute": "profile avatar",
"profile.profileavatar.change-button": "更改",
"profile.sociallinks.add": "添加{network}",
"profile.sociallinks.social.links": "社交链接",
"profile.notfound.message": "您访问的地址不存在或有误。请检查URL后重新尝试访问。",
"profile.profileavatar.change-button": "Change",
"profile.sociallinks.add": "Add {network}",
"profile.sociallinks.social.links": "Social Links",
"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..."
}

View File

@@ -1,20 +1,23 @@
import 'babel-polyfill';
import { App, AppProvider, APP_ERROR, APP_READY, ErrorPage } from '@edx/frontend-base';
import { NewRelicLoggingService } from '@edx/frontend-logging';
import { APP_INIT_ERROR, APP_READY, initialize, subscribe } from '@edx/frontend-platform';
import { AppProvider, ErrorPage } from '@edx/frontend-platform/react';
import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Switch } from 'react-router-dom';
import Header, { messages as headerMessages } from '@edx/frontend-component-header';
import Footer from '../footer/Footer';
import Footer, { messages as footerMessages } from '@edx/frontend-component-footer';
import appMessages from './i18n';
import './index.scss';
import { ProfilePage, NotFoundPage } from './profile';
import configureStore from './store';
import configureStore from './data/configureStore';
App.subscribe(APP_READY, () => {
import './index.scss';
import './assets/favicon.ico';
subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider store={configureStore()}>
<Header />
@@ -31,8 +34,16 @@ App.subscribe(APP_READY, () => {
);
});
App.subscribe(APP_ERROR, (error) => {
subscribe(APP_INIT_ERROR, (error) => {
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
});
App.initialize({ messages: [appMessages, headerMessages], loggingService: NewRelicLoggingService });
initialize({
messages: [
appMessages,
headerMessages,
footerMessages,
],
requireAuthenticatedUser: true,
hydrateAuthenticatedUser: true,
});

View File

@@ -3,5 +3,5 @@
@import './profile/index.scss';
@import "~@edx/frontend-component-header/src/index";
@import "~@edx/frontend-component-footer/src/lib/scss/site-footer";
@import "~@edx/frontend-component-header/dist/index";
@import "~@edx/frontend-component-footer/dist/footer";

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StatusAlert } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-i18n';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
function AgeMessage({ accountSettingsUrl }) {
return (

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, FormattedDate } from '@edx/frontend-i18n';
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
function DateJoined({ date }) {
if (date == null) return null;

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-i18n';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
export default function NotFoundPage() {
return (

View File

@@ -2,9 +2,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { sendTrackingLogEvent } from '@edx/frontend-analytics';
import { App, AppContext, fetchUserAccount } from '@edx/frontend-base';
import { injectIntl, intlShape } from '@edx/frontend-i18n';
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import { ensureConfig } from '@edx/frontend-platform';
import { AppContext } from '@edx/frontend-platform/react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { StatusAlert, Hyperlink } from '@edx/paragon';
// Actions
@@ -38,7 +39,7 @@ import { profilePageSelector } from './data/selectors';
// i18n
import messages from './ProfilePage.messages';
App.requireConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL'], 'ProfilePage');
ensureConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL'], 'ProfilePage');
class ProfilePage extends React.Component {
constructor(props, context) {
@@ -58,7 +59,6 @@ class ProfilePage extends React.Component {
}
componentDidMount() {
this.props.fetchUserAccount(this.context.authenticatedUser.username);
this.props.fetchProfile(this.props.match.params.username);
sendTrackingLogEvent('edx.profile.viewed', {
username: this.props.match.params.username,
@@ -332,7 +332,6 @@ ProfilePage.propTypes = {
photoUploadError: PropTypes.objectOf(PropTypes.string),
// Actions
fetchUserAccount: PropTypes.func.isRequired,
fetchProfile: PropTypes.func.isRequired,
saveProfile: PropTypes.func.isRequired,
saveProfilePhoto: PropTypes.func.isRequired,
@@ -372,7 +371,6 @@ ProfilePage.defaultProps = {
export default connect(
profilePageSelector,
{
fetchUserAccount,
fetchProfile,
saveProfilePhoto,
deleteProfilePhoto,

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@edx/frontend-i18n';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.viewMyRecords': {

View File

@@ -1,7 +1,8 @@
/* eslint-disable global-require */
import * as analytics from '@edx/frontend-analytics';
import { App, AppContext } from '@edx/frontend-base';
import { configure as configureI18n, IntlProvider } from '@edx/frontend-i18n';
import { getConfig } from '@edx/frontend-platform';
import * as analytics from '@edx/frontend-platform/analytics';
import { AppContext } from '@edx/frontend-platform/react';
import { configure as configureI18n, IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme';
import React from 'react';
import { Provider } from 'react-redux';
@@ -33,21 +34,45 @@ const requiredProfilePageProps = {
// Mock language cookie
Object.defineProperty(global.document, 'cookie', {
writable: true,
value: `${App.config.LANGUAGE_PREFERENCE_COOKIE_NAME}=en`,
value: `${getConfig().LANGUAGE_PREFERENCE_COOKIE_NAME}=en`,
});
App.apiClient = jest.fn();
configureI18n(App.config, messages);
jest.mock('@edx/frontend-platform/auth', () => ({
configure: () => {},
getAuthenticatedUser: () => null,
fetchAuthenticatedUser: () => null,
getAuthenticatedHttpClient: jest.fn(),
AUTHENTICATED_USER_CHANGED: 'user_changed',
}));
jest.mock('@edx/frontend-platform/analytics', () => ({
configure: () => {},
identifyAnonymousUser: jest.fn(),
identifyAuthenticatedUser: jest.fn(),
sendTrackingLogEvent: jest.fn(),
}));
configureI18n({
loggingService: { logError: jest.fn() },
config: {
ENVIRONMENT: 'production',
LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum',
},
messages,
});
beforeEach(() => {
analytics.sendTrackingLogEvent.mockReset();
});
describe('<ProfilePage />', () => {
describe('Renders correctly in various states', () => {
it('app loading', () => {
analytics.sendTrackingLogEvent = jest.fn();
const component = (
<AppContext.Provider
value={{
authenticatedUser: { userId: null, username: null, administrator: false },
config: App.config,
config: getConfig(),
}}
>
<IntlProvider locale="en">
@@ -62,12 +87,11 @@ describe('<ProfilePage />', () => {
});
it('viewing own profile', () => {
analytics.sendTrackingLogEvent = jest.fn();
const component = (
<AppContext.Provider
value={{
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: App.config,
config: getConfig(),
}}
>
<IntlProvider locale="en">
@@ -82,12 +106,11 @@ describe('<ProfilePage />', () => {
});
it('viewing other profile', () => {
analytics.sendTrackingLogEvent = jest.fn();
const component = (
<AppContext.Provider
value={{
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: App.config,
config: getConfig(),
}}
>
<IntlProvider locale="en">
@@ -105,12 +128,11 @@ describe('<ProfilePage />', () => {
});
it('while saving an edited bio', () => {
analytics.sendTrackingLogEvent = jest.fn();
const component = (
<AppContext.Provider
value={{
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: App.config,
config: getConfig(),
}}
>
<IntlProvider locale="en">
@@ -127,12 +149,11 @@ describe('<ProfilePage />', () => {
describe('handles analytics', () => {
it('calls sendTrackingLogEvent when mounting', () => {
analytics.sendTrackingLogEvent = jest.fn();
const component = (
<AppContext.Provider
value={{
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: App.config,
config: getConfig(),
}}
>
<IntlProvider locale="en">
@@ -145,7 +166,8 @@ describe('<ProfilePage />', () => {
</IntlProvider>
</AppContext.Provider>
);
mount(component);
const wrapper = mount(component);
wrapper.update();
expect(analytics.sendTrackingLogEvent.mock.calls.length).toBe(1);
expect(analytics.sendTrackingLogEvent.mock.calls[0][0]).toEqual('edx.profile.viewed');

View File

@@ -1130,7 +1130,7 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
className="certificate-type-illustration"
style={
Object {
"backgroundImage": "url([object Object])",
"backgroundImage": "url(icon/mock/path)",
}
}
/>
@@ -2185,7 +2185,7 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
className="certificate-type-illustration"
style={
Object {
"backgroundImage": "url([object Object])",
"backgroundImage": "url(icon/mock/path)",
}
}
/>

View File

@@ -1,6 +1,5 @@
import { FETCH_USER_ACCOUNT_FAILURE } from '@edx/frontend-auth';
import { App } from '@edx/frontend-base';
import { logApiClientError } from '@edx/frontend-logging';
import { history } from '@edx/frontend-platform';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import pick from 'lodash.pick';
import { all, call, delay, put, select, takeEvery } from 'redux-saga/effects';
import {
@@ -31,7 +30,7 @@ import * as ProfileApiService from './services';
export function* handleFetchProfile(action) {
const { username } = action.payload;
const userAccount = yield select(userAccountSelector);
const isAuthenticatedUserProfile = username === App.authenticatedUser.username;
const isAuthenticatedUserProfile = username === getAuthenticatedUser().username;
// Default our data assuming the account is the current user's account.
let preferences = {};
let account = userAccount;
@@ -70,7 +69,7 @@ export function* handleFetchProfile(action) {
yield put(fetchProfileReset());
} catch (e) {
if (e.response.status === 404) {
App.history.push('/notfound');
history.push('/notfound');
} else {
throw e;
}
@@ -176,15 +175,9 @@ export function* handleDeleteProfilePhoto(action) {
}
}
export function handleFetchUserAccountFailure(action) {
logApiClientError(action.payload.error);
throw action.payload.error;
}
export default function* profileSaga() {
yield takeEvery(FETCH_PROFILE.BASE, handleFetchProfile);
yield takeEvery(SAVE_PROFILE.BASE, handleSaveProfile);
yield takeEvery(SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto);
yield takeEvery(DELETE_PROFILE_PHOTO.BASE, handleDeleteProfilePhoto);
yield takeEvery(FETCH_USER_ACCOUNT_FAILURE, handleFetchUserAccountFailure);
}

View File

@@ -1,5 +1,5 @@
import { takeEvery, put, call, delay, select, all } from 'redux-saga/effects';
import { FETCH_USER_ACCOUNT_FAILURE } from '@edx/frontend-auth';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import * as profileActions from './actions';
import { handleSaveProfileSelector, userAccountSelector } from './selectors';
@@ -14,6 +14,10 @@ jest.mock('./services', () => ({
getCourseCertificates: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(),
}));
// RootSaga and ProfileApiService must be imported AFTER the mock above.
/* eslint-disable import/first */
import profileSaga, {
@@ -21,10 +25,8 @@ import profileSaga, {
handleSaveProfile,
handleSaveProfilePhoto,
handleDeleteProfilePhoto,
handleFetchUserAccountFailure,
} from './sagas';
import * as ProfileApiService from './services';
import { App } from '@edx/frontend-base';
/* eslint-enable import/first */
describe('RootSaga', () => {
@@ -40,8 +42,6 @@ describe('RootSaga', () => {
.toEqual(takeEvery(profileActions.SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto));
expect(gen.next().value)
.toEqual(takeEvery(profileActions.DELETE_PROFILE_PHOTO.BASE, handleDeleteProfilePhoto));
expect(gen.next().value)
.toEqual(takeEvery(FETCH_USER_ACCOUNT_FAILURE, handleFetchUserAccountFailure));
expect(gen.next().value).toBeUndefined();
});
@@ -53,11 +53,11 @@ describe('RootSaga', () => {
username: 'gonzo',
other: 'data',
};
getAuthenticatedUser.mockReturnValue(userAccount);
const selectorData = {
userAccount,
};
App.authenticatedUser.username = 'gonzo';
const action = profileActions.fetchProfile('gonzo');
const gen = handleFetchProfile(action);
@@ -81,11 +81,11 @@ describe('RootSaga', () => {
username: 'gonzo',
other: 'data',
};
getAuthenticatedUser.mockReturnValue(userAccount);
const selectorData = {
userAccount,
};
App.authenticatedUser.username = 'gonzo';
const action = profileActions.fetchProfile('booyah');
const gen = handleFetchProfile(action);

View File

@@ -5,7 +5,7 @@ import {
getCountryList,
getCountryMessages,
getLanguageMessages,
} from '@edx/frontend-i18n'; // eslint-disable-line
} from '@edx/frontend-platform/i18n'; // eslint-disable-line
export const formIdSelector = (state, props) => props.formId;
export const userAccountSelector = state => state.userAccount;

View File

@@ -1,8 +1,9 @@
import { App } from '@edx/frontend-base';
import { logApiClientError } from '@edx/frontend-logging';
import { ensureConfig, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient as getHttpClient } from '@edx/frontend-platform/auth';
import { logError } from '@edx/frontend-platform/logging';
import { camelCaseObject, convertKeyNames, snakeCaseObject } from '../utils';
const { LMS_BASE_URL } = App.requireConfig(['LMS_BASE_URL'], 'Profile API service');
ensureConfig(['LMS_BASE_URL'], 'Profile API service');
function processAccountData(data) {
return camelCaseObject(data);
@@ -20,7 +21,7 @@ function processAndThrowError(error, errorDataProcessor) {
// GET ACCOUNT
export async function getAccount(username) {
const { data } = await App.apiClient.get(`${LMS_BASE_URL}/api/user/v1/accounts/${username}`);
const { data } = await getHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`);
// Process response data
return processAccountData(data);
@@ -30,8 +31,8 @@ export async function getAccount(username) {
export async function patchProfile(username, params) {
const processedParams = snakeCaseObject(params);
const { data } = await App.apiClient
.patch(`${LMS_BASE_URL}/api/user/v1/accounts/${username}`, processedParams, {
const { data } = await getHttpClient()
.patch(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`, processedParams, {
headers: {
'Content-Type': 'application/merge-patch+json',
},
@@ -48,8 +49,8 @@ export async function patchProfile(username, params) {
export async function postProfilePhoto(username, formData) {
// eslint-disable-next-line no-unused-vars
const { data } = await App.apiClient.post(
`${LMS_BASE_URL}/api/user/v1/accounts/${username}/image`,
const { data } = await getHttpClient().post(
`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}/image`,
formData,
{
headers: {
@@ -73,7 +74,7 @@ export async function postProfilePhoto(username, formData) {
export async function deleteProfilePhoto(username) {
// eslint-disable-next-line no-unused-vars
const { data } = await App.apiClient.delete(`${LMS_BASE_URL}/api/user/v1/accounts/${username}/image`);
const { data } = await getHttpClient().delete(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}/image`);
// TODO: Someday in the future the POST photo endpoint
// will return the new values. At that time we should
@@ -86,7 +87,7 @@ export async function deleteProfilePhoto(username) {
// GET PREFERENCES
export async function getPreferences(username) {
const { data } = await App.apiClient.get(`${LMS_BASE_URL}/api/user/v1/preferences/${username}`);
const { data } = await getHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`);
return camelCaseObject(data);
}
@@ -106,7 +107,7 @@ export async function patchPreferences(username, params) {
visibility_time_zone: 'visibility.time_zone',
});
await App.apiClient.patch(`${LMS_BASE_URL}/api/user/v1/preferences/${username}`, processedParams, {
await getHttpClient().patch(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`, processedParams, {
headers: { 'Content-Type': 'application/merge-patch+json' },
});
@@ -124,7 +125,7 @@ function transformCertificateData(data) {
cert.download_url.search(/http[s]?:\/\//) !== 0;
const downloadUrl = urlIsPath ?
`${LMS_BASE_URL}${cert.download_url}` :
`${getConfig().LMS_BASE_URL}${cert.download_url}` :
cert.download_url;
transformedData.push({
@@ -137,12 +138,12 @@ function transformCertificateData(data) {
}
export async function getCourseCertificates(username) {
const url = `${LMS_BASE_URL}/api/certificates/v0/certificates/${username}/`;
const url = `${getConfig().LMS_BASE_URL}/api/certificates/v0/certificates/${username}/`;
try {
const { data } = await App.apiClient.get(url);
const { data } = await getHttpClient().get(url);
return transformCertificateData(data);
} catch (e) {
logApiClientError(e);
logError(e);
return [];
}
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-i18n';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { ValidationFormGroup } from '@edx/paragon';
import messages from './Bio.messages';

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@edx/frontend-i18n';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.bio.about.me': {

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedDate, FormattedMessage, injectIntl, intlShape } from '@edx/frontend-i18n';
import { FormattedDate, FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@edx/paragon';
import { connect } from 'react-redux';
import get from 'lodash.get';

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@edx/frontend-i18n';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.certificates.my.certificates': {

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { ValidationFormGroup } from '@edx/paragon';
import messages from './Country.messages';

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@edx/frontend-i18n';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.country.label': {

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-i18n';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import get from 'lodash.get';
import { ValidationFormGroup } from '@edx/paragon';

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@edx/frontend-i18n';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.education.education': {

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './Name.messages';

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@edx/frontend-i18n';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.name.full.name': {

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { ValidationFormGroup } from '@edx/paragon';
import messages from './PreferredLanguage.messages';

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@edx/frontend-i18n';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.preferredlanguage.empty': {

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Dropdown } from '@edx/paragon';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-i18n';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { ReactComponent as DefaultAvatar } from '../assets/avatar.svg';

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@edx/frontend-i18n';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.image.alt.attribute': {

View File

@@ -4,7 +4,7 @@ import { StatusAlert } from '@edx/paragon';
import { connect } from 'react-redux';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTwitter, faFacebook, faLinkedin } from '@fortawesome/free-brands-svg-icons';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-i18n';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import classNames from 'classnames';
import messages from './SocialLinks.messages';

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@edx/frontend-i18n';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.sociallinks.social.links': {

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { injectIntl, intlShape } from '@edx/frontend-i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import messages from './EditButton.messages';

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@edx/frontend-i18n';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.editbutton.edit': {

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, StatefulButton } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './FormControls.messages';

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@edx/frontend-i18n';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.formcontrols.who.can.see': {

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faEyeSlash, faEye } from '@fortawesome/free-regular-svg-icons';

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@edx/frontend-i18n';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.visibility.who.just.me': {

View File

@@ -1,25 +1,4 @@
/* eslint-disable import/no-extraneous-dependencies */
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import 'babel-polyfill';
Enzyme.configure({ adapter: new Adapter() });
// These configuration values are usually set in webpack's EnvironmentPlugin however
// Jest does not use webpack so we need to set these so for testing
process.env.ACCESS_TOKEN_COOKIE_NAME = 'edx-jwt-cookie-header-payload';
process.env.BASE_URL = 'localhost:1995';
process.env.CREDENTIALS_BASE_URL = 'http://localhost:18150';
process.env.CSRF_COOKIE_NAME = 'csrftoken';
process.env.CSRF_TOKEN_API_PATH = '/csrf/api/v1/token';
process.env.ECOMMERCE_BASE_URL = 'http://localhost:18130';
process.env.LANGUAGE_PREFERENCE_COOKIE_NAME = 'openedx-language-preference';
process.env.LMS_BASE_URL = 'http://localhost:18000';
process.env.LOGIN_URL = 'http://localhost:18000/login';
process.env.LOGOUT_URL = 'http://localhost:18000/login';
process.env.MARKETING_SITE_BASE_URL = 'http://localhost:18000';
process.env.ORDER_HISTORY_URL = 'localhost:1996/orders';
process.env.REFRESH_ACCESS_TOKEN_ENDPOINT = 'http://localhost:18000/login_refresh';
process.env.SEGMENT_KEY = null;
process.env.SITE_NAME = 'edX';
process.env.USER_INFO_COOKIE_NAME = 'edx-user-info';

View File

@@ -1,25 +0,0 @@
import { applyMiddleware, createStore } from 'redux';
import createSagaMiddleware from 'redux-saga';
import thunkMiddleware from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
import { createLogger } from 'redux-logger';
import createRootReducer from '../reducers';
import rootSaga from '../sagas';
export default function configureStore(initialState = {}) {
const loggerMiddleware = createLogger({
collapsed: true,
});
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
createRootReducer(),
initialState,
composeWithDevTools(applyMiddleware(thunkMiddleware, sagaMiddleware, loggerMiddleware)), // eslint-disable-line
);
sagaMiddleware.run(rootSaga);
return store;
}

View File

@@ -1,20 +0,0 @@
import { applyMiddleware, createStore, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import thunkMiddleware from 'redux-thunk';
import createRootReducer from '../reducers';
import rootSaga from '../sagas';
export default function configureStore(initialState = {}) {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
createRootReducer(),
initialState,
compose(applyMiddleware(thunkMiddleware, sagaMiddleware)),
);
sagaMiddleware.run(rootSaga);
return store;
}

View File

@@ -1,9 +0,0 @@
import configureStoreProd from './configureStore.prod';
import configureStoreDev from './configureStore.dev';
export default function configureStore(state, env) {
if (env === 'production') {
return configureStoreProd(state);
}
return configureStoreDev(state);
}

21
webpack.prod.config.js Normal file
View File

@@ -0,0 +1,21 @@
const glob = require('glob');
const PurgecssPlugin = require('purgecss-webpack-plugin');
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('webpack-prod', {
plugins: [
// Scan files for class names and ids and remove unused css
new PurgecssPlugin({
paths: [].concat(
// Scan files in this app
glob.sync('src/**/*', { nodir: true }),
// Scan files in any edx frontend-component
glob.sync('node_modules/@edx/frontend-component*/**/*', { nodir: true }),
// Scan files in paragon
glob.sync('node_modules/@edx/paragon/**/*', { nodir: true }),
),
// Protect react-css-transition class names
whitelistPatterns: [/-enter/, /-appear/, /-exit/],
}),
],
});

View File

@@ -1,16 +0,0 @@
// This is the common Webpack config. The dev and prod Webpack configs both
// inherit config defined here.
const path = require('path');
module.exports = {
entry: {
app: path.resolve(__dirname, '../src/index.jsx'),
},
output: {
path: path.resolve(__dirname, '../dist'),
publicPath: '/',
},
resolve: {
extensions: ['.js', '.jsx'],
},
};

View File

@@ -1,173 +0,0 @@
// This is the dev Webpack config. All settings here should prefer a fast build
// time at the expense of creating larger, unoptimized bundles.
const Merge = require('webpack-merge');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const PostCssRtlPlugin = require('postcss-rtl');
const commonConfig = require('./webpack.common.config.js');
module.exports = Merge.smart(commonConfig, {
mode: 'development',
devtool: 'eval-source-map',
entry: {
// enable react's custom hot dev client so we get errors reported in the browser
hot: require.resolve('react-dev-utils/webpackHotDevClient'),
app: path.resolve(__dirname, '../src/index.jsx'),
},
module: {
// Specify file-by-file rules to Webpack. Some file-types need a particular kind of loader.
rules: [
// The babel-loader transforms newer ES2015+ syntax to older ES5 for older browsers.
// Babel is configured with the .babelrc file at the root of the project.
{
test: /\.(js|jsx)$/,
include: [
path.resolve(__dirname, '../src'),
path.resolve(__dirname, '../footer'),
path.resolve(__dirname, '../header'),
],
loader: 'babel-loader',
options: {
// Caches result of loader to the filesystem. Future builds will attempt to read from the
// cache to avoid needing to run the expensive recompilation process on each run.
cacheDirectory: true,
},
},
// We are not extracting CSS from the javascript bundles in development because extracting
// prevents hot-reloading from working, it increases build time, and we don't care about
// flash-of-unstyled-content issues in development.
{
test: /(.scss|.css)$/,
use: [
'style-loader', // creates style nodes from JS strings
{
loader: 'css-loader', // translates CSS into CommonJS
options: {
sourceMap: true,
},
},
{
loader: 'postcss-loader',
options: {
plugins: () => [PostCssRtlPlugin()],
},
},
{
loader: 'resolve-url-loader',
},
{
loader: 'sass-loader', // compiles Sass to CSS
options: {
sourceMap: true,
includePaths: [
path.join(__dirname, '../node_modules'),
path.join(__dirname, '../src'),
],
},
},
],
},
{
test: /.svg$/,
issuer: {
test: /\.jsx?$/,
},
loader: '@svgr/webpack',
},
// Webpack, by default, uses the url-loader for images and fonts that are required/included by
// files it processes, which just base64 encodes them and inlines them in the javascript
// bundles. This makes the javascript bundles ginormous and defeats caching so we will use the
// file-loader instead to copy the files directly to the output directory.
{
test: /\.(woff2?|ttf|svg|eot)(\?v=\d+\.\d+\.\d+)?$/,
loader: 'file-loader',
},
{
test: /\.(jpe?g|png|gif|ico)(\?v=\d+\.\d+\.\d+)?$/,
use: [
'file-loader',
{
loader: 'image-webpack-loader',
options: {
optimizationlevel: 7,
mozjpeg: {
progressive: true,
},
gifsicle: {
interlaced: false,
},
pngquant: {
quality: '65-90',
speed: 4,
},
},
},
],
},
],
},
// Specify additional processing or side-effects done on the Webpack output bundles as a whole.
plugins: [
// Generates an HTML file in the output directory.
new HtmlWebpackPlugin({
inject: true, // Appends script tags linking to the webpack bundles at the end of the body
template: path.resolve(__dirname, '../public/index.html'),
}),
new webpack.EnvironmentPlugin({
NODE_ENV: 'development',
// App configuration
ACCESS_TOKEN_COOKIE_NAME: 'edx-jwt-cookie-header-payload',
BASE_URL: 'localhost:1995',
CREDENTIALS_BASE_URL: 'http://localhost:18150',
CSRF_COOKIE_NAME: 'csrftoken',
CSRF_TOKEN_API_PATH: '/csrf/api/v1/token',
ECOMMERCE_BASE_URL: 'http://localhost:18130',
LANGUAGE_PREFERENCE_COOKIE_NAME: 'openedx-language-preference',
LMS_BASE_URL: 'http://localhost:18000',
LOGIN_URL: 'http://localhost:18000/login',
LOGOUT_URL: 'http://localhost:18000/login',
MARKETING_SITE_BASE_URL: 'http://localhost:18000',
ORDER_HISTORY_URL: 'localhost:1996/orders',
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://localhost:18000/login_refresh',
SEGMENT_KEY: null,
SITE_NAME: 'edX',
USER_INFO_COOKIE_NAME: 'edx-user-info',
// ProfileFooter configuration
APPLE_APP_STORE_URL: 'https://www.apple.com/ios/app-store/',
CONTACT_URL: 'http://localhost:18000/contact',
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: 'Footer',
ENTERPRISE_MARKETING_URL: 'http://example.com',
ENTERPRISE_MARKETING_UTM_CAMPAIGN: 'my_campaign',
ENTERPRISE_MARKETING_UTM_SOURCE: 'edX profile',
FACEBOOK_URL: 'https://www.facebook.com',
GOOGLE_PLAY_URL: 'https://play.google.com/store',
LINKED_IN_URL: 'https://www.linkedin.com',
OPEN_SOURCE_URL: 'http://localhost:18000/openedx',
PRIVACY_POLICY_URL: 'http://localhost:18000/privacy-policy',
REDDIT_URL: 'https://www.reddit.com',
SUPPORT_URL: 'http://localhost:18000/support',
TERMS_OF_SERVICE_URL: 'http://localhost:18000/terms-of-service',
TWITTER_URL: 'https://twitter.com',
YOU_TUBE_URL: 'https://www.youtube.com',
}),
// when the --hot option is not passed in as part of the command
// the HotModuleReplacementPlugin has to be specified in the Webpack configuration
// https://webpack.js.org/configuration/dev-server/#devserver-hot
new webpack.HotModuleReplacementPlugin(),
],
// This configures webpack-dev-server which serves bundles from memory and provides live
// reloading.
devServer: {
host: '0.0.0.0',
port: 1995,
historyApiFallback: true,
hot: true,
inline: true,
publicPath: '/',
},
});

View File

@@ -1,222 +0,0 @@
// This is the prod Webpack config. All settings here should prefer smaller,
// optimized bundles at the expense of a longer build time.
const Merge = require('webpack-merge');
const commonConfig = require('./webpack.common.config.js');
const path = require('path');
const glob = require('glob');
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackNewRelicPlugin = require('html-webpack-new-relic-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const PurgecssPlugin = require('purgecss-webpack-plugin');
const NewRelicSourceMapPlugin = require('new-relic-source-map-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; // eslint-disable-line prefer-destructuring
const PostCssRtlPlugin = require('postcss-rtl');
const PostCssAutoprefixerPlugin = require('autoprefixer');
const CssNano = require('cssnano');
module.exports = Merge.smart(commonConfig, {
mode: 'production',
devtool: 'source-map',
output: {
filename: '[name].[chunkhash].js',
path: path.resolve(__dirname, '../dist'),
},
module: {
// Specify file-by-file rules to Webpack. Some file-types need a particular kind of loader.
rules: [
// The babel-loader transforms newer ES2015+ syntax to older ES5 for older browsers.
// Babel is configured with the .babelrc file at the root of the project.
{
test: /\.(js|jsx)$/,
include: [
path.resolve(__dirname, '../src'),
path.resolve(__dirname, '../footer'),
path.resolve(__dirname, '../header'),
],
loader: 'babel-loader',
},
{
test: /\.js$/,
use: ['source-map-loader'],
enforce: 'pre',
},
// Webpack, by default, includes all CSS in the javascript bundles. Unfortunately, that means:
// a) The CSS won't be cached by browsers separately (a javascript change will force CSS
// re-download). b) Since CSS is applied asyncronously, it causes an ugly
// flash-of-unstyled-content.
//
// To avoid these problems, we extract the CSS from the bundles into separate CSS files that
// can be included as <link> tags in the HTML <head> manually.
//
// We will not do this in development because it prevents hot-reloading from working and it
// increases build time.
{
test: /(.scss|.css)$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader', // translates CSS into CommonJS
options: {
sourceMap: true,
},
},
{
loader: 'postcss-loader',
options: {
plugins: () => [
PostCssRtlPlugin(),
PostCssAutoprefixerPlugin({ grid: true }),
CssNano(),
],
},
},
{
loader: 'resolve-url-loader',
},
{
loader: 'sass-loader', // compiles Sass to CSS
options: {
sourceMap: true,
includePaths: [
path.join(__dirname, '../node_modules'),
path.join(__dirname, '../src'),
],
},
},
],
},
{
test: /.svg$/,
issuer: {
test: /\.jsx?$/,
},
loader: '@svgr/webpack',
},
// Webpack, by default, uses the url-loader for images and fonts that are required/included by
// files it processes, which just base64 encodes them and inlines them in the javascript
// bundles. This makes the javascript bundles ginormous and defeats caching so we will use the
// file-loader instead to copy the files directly to the output directory.
{
test: /\.(woff2?|ttf|svg|eot)(\?v=\d+\.\d+\.\d+)?$/,
loader: 'file-loader',
},
{
test: /\.(jpe?g|png|gif|ico)(\?v=\d+\.\d+\.\d+)?$/,
use: [
'file-loader',
{
loader: 'image-webpack-loader',
options: {
optimizationlevel: 7,
mozjpeg: {
progressive: true,
},
gifsicle: {
interlaced: false,
},
pngquant: {
quality: '65-90',
speed: 4,
},
},
},
],
},
],
},
// New in Webpack 4. Replaces CommonChunksPlugin. Extract common modules among all chunks to one
// common chunk and extract the Webpack runtime to a single runtime chunk.
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
},
},
// Specify additional processing or side-effects done on the Webpack output bundles as a whole.
plugins: [
// Cleans the dist directory before each build
new CleanWebpackPlugin(['dist'], {
root: path.join(__dirname, '../'),
}),
// Writes the extracted CSS from each entry to a file in the output directory.
new MiniCssExtractPlugin({
filename: '[name].[chunkhash].css',
}),
// Scan files for class names and ids and remove unused css
new PurgecssPlugin({
paths: [].concat(
// Scan files in this app
glob.sync(`${path.resolve(__dirname, '../src')}/**/*`, { nodir: true }),
// Scan files in any edx frontend-component
glob.sync(`${path.resolve(__dirname, '../node_modules/@edx/frontend-component')}*/**/*`, { nodir: true }),
// Scan files in paragon
glob.sync(`${path.resolve(__dirname, '../node_modules/@edx/paragon')}/**/*`, { nodir: true }),
),
// Protect react-css-transition class names
whitelistPatterns: [/-enter/, /-appear/, /-exit/],
}),
// Generates an HTML file in the output directory.
new HtmlWebpackPlugin({
inject: true, // Appends script tags linking to the webpack bundles at the end of the body
template: path.resolve(__dirname, '../public/index.html'),
}),
new webpack.EnvironmentPlugin({
NODE_ENV: 'production',
// App configuration
ACCESS_TOKEN_COOKIE_NAME: null,
BASE_URL: null,
CREDENTIALS_BASE_URL: null,
CSRF_COOKIE_NAME: 'csrftoken',
CSRF_TOKEN_API_PATH: null,
ECOMMERCE_BASE_URL: null,
LANGUAGE_PREFERENCE_COOKIE_NAME: null,
LMS_BASE_URL: null,
LOGIN_URL: null,
LOGOUT_URL: null,
MARKETING_SITE_BASE_URL: null,
ORDER_HISTORY_URL: null,
REFRESH_ACCESS_TOKEN_ENDPOINT: null,
SEGMENT_KEY: null,
SITE_NAME: null,
USER_INFO_COOKIE_NAME: null,
// ProfileFooter configuration
APPLE_APP_STORE_URL: null,
CONTACT_URL: null,
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: null,
ENTERPRISE_MARKETING_URL: null,
ENTERPRISE_MARKETING_UTM_CAMPAIGN: null,
ENTERPRISE_MARKETING_UTM_SOURCE: null,
FACEBOOK_URL: null,
GOOGLE_PLAY_URL: null,
LINKED_IN_URL: null,
OPEN_SOURCE_URL: null,
PRIVACY_POLICY_URL: null,
REDDIT_URL: null,
SUPPORT_URL: null,
TERMS_OF_SERVICE_URL: null,
TWITTER_URL: null,
YOU_TUBE_URL: null,
}),
new HtmlWebpackNewRelicPlugin({
// This plugin fixes an issue where the newrelic script will break if
// not added directly to the HTML.
// We use non empty strings as defaults here to prevent errors for empty configs
license: process.env.NEW_RELIC_LICENSE_KEY || 'fake_app',
applicationID: process.env.NEW_RELIC_APP_ID || 'fake_license',
}),
new NewRelicSourceMapPlugin({
applicationId: process.env.NEW_RELIC_APP_ID,
nrAdminKey: process.env.NEW_RELIC_ADMIN_KEY,
staticAssetUrl: process.env.BASE_URL,
noop: typeof process.env.NEW_RELIC_ADMIN_KEY === 'undefined', // upload source maps in prod builds only
}),
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
}),
],
});