Compare commits

...

25 Commits

Author SHA1 Message Date
Renovate Bot
00b2dee8a8 fix(deps): update dependency redux-saga to v1.1.3 2021-06-04 02:53:55 +00:00
Renovate Bot
bbb2224762 chore(deps): update dependency react-test-renderer to v16.14.0 2021-06-04 02:25:56 +00:00
dependabot[bot]
c78e8a608a Merge pull request #411 from edx/dependabot/npm_and_yarn/ssri-6.0.2 2021-06-04 00:44:52 +00:00
Renovate Bot
d6011917b3 fix(deps): update dependency newrelic to v5.13.1 2021-06-04 00:34:39 +00:00
Renovate Bot
7eb4d33037 chore(deps): update dependency es-check to v5.2.3 2021-06-04 00:02:12 +00:00
dependabot[bot]
808348bdce Merge pull request #419 from edx/dependabot/npm_and_yarn/dns-packet-1.3.4 2021-06-03 22:37:35 +00:00
dependabot[bot]
494787942d build(deps): bump ssri from 6.0.1 to 6.0.2
Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/npm/ssri/releases)
- [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md)
- [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-03 21:14:52 +00:00
dependabot[bot]
5d94d12670 build(deps): bump dns-packet from 1.3.1 to 1.3.4
Bumps [dns-packet](https://github.com/mafintosh/dns-packet) from 1.3.1 to 1.3.4.
- [Release notes](https://github.com/mafintosh/dns-packet/releases)
- [Changelog](https://github.com/mafintosh/dns-packet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mafintosh/dns-packet/compare/v1.3.1...v1.3.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-03 21:13:23 +00:00
dependabot[bot]
2b68a2cfe6 Merge pull request #416 from edx/dependabot/npm_and_yarn/hosted-git-info-2.8.9 2021-06-03 20:38:13 +00:00
Renovate Bot
3074dae40f chore(deps): update dependency codecov to v3.8.2 2021-06-03 19:50:58 +00:00
stvn
9ca7f57102 merge(#422): build/renovate
commits
=======
- build(renovate): be more liberal about what automerges
2021-06-03 11:15:04 -07:00
stvn
b33ed94fc6 build(renovate): be more liberal about what automerges
based on https://github.com/edx/frontend-app-account .
2021-06-03 10:42:01 -07:00
stvn
e1fa025eea merge: stvn/own/code 2021-05-26 13:42:18 -07:00
stvn
6bd5be71b8 build: add CODEOWNERS; edx/community-engineering
Background
==========
As part of our Squad-based ownership, we should stay on top of what
happens in these repositories. However, due to the number of
repositories (and subsequently pull requests) across the edX ecosystem,
it is challenging to stay on top of notifications, separating the
'signal' from the 'noise'. Email filters can go a long way to taming
Inbox notifications, but this is manual and requires maintenance as
Squad ownership changes. It also fails to account for Github-specific behavior.

Proposal
========
By leveraging Github support for `CODEOWNERS` files [1],
we can ensure that our team is at least CCed explicitly, here,
in the form a requested review. This request is just that, a request,
not a requirement; we are not enacting any new merge requirements
at this time.

- [1] https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners
2021-05-26 12:14:40 -07:00
David Joy
8cf271f434 chore: let renovate be more liberal about what it merges (#417)
We've adopted a looser renovate config that will auto-merge more things in other repositories, and it's worked out fine.  I just lifted this config from frontend-platform, which was itself based on prospectus (private).
2021-05-18 16:28:24 -04:00
dependabot[bot]
563bcc48c9 build(deps): bump hosted-git-info from 2.8.8 to 2.8.9
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-17 14:24:42 +00:00
renovate[bot]
283c6c143a chore(deps): update commitlint monorepo (#358)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-05-17 10:23:06 -04:00
David Joy
889b9a4482 fix: use the SITE_NAME env var in index.html (#413) 2021-05-04 14:06:04 -04:00
David Joy
fae8500223 build: add module.config.js to .gitignore (#412) 2021-05-04 09:22:12 -04:00
Renovate Bot
e4b218a47e fix(deps): update dependency @edx/frontend-component-header to v2.2.4 2021-02-08 20:35:27 +00:00
Renovate Bot
1f6cfefe18 fix(deps): update dependency @edx/frontend-platform to v1.8.4 2021-02-05 23:03:57 +00:00
Renovate Bot
efa68ef0be fix(deps): update dependency @edx/frontend-component-header to v2.2.3 2021-02-05 21:26:13 +00:00
Renovate Bot
aeebd4de33 fix(deps): update dependency @edx/frontend-component-footer to v10.1.4 2021-02-05 20:39:02 +00:00
Renovate Bot
849b0101e3 chore(deps): update dependency enzyme-adapter-react-16 to v1.15.6 2021-02-05 19:24:14 +00:00
David Joy
e3b692b9f2 Update Paragon to 13 and make the app brandable (#397)
* fix: fixing broken linter script and linting

The linter script in package.json didn’t specify which files to lint, so it never linted anything.  As soon as I fixed the script line, there was suddenly a ton of stuff that needed linting.

This fixes the script and cleans up all the things that needed linting.  The vast majority were formatting auto-fixes in VSCode for me.

* fix: setting NODE_ENV to production in .env

Without this change, the NODE_ENV comes through as “null” (a string!) in the app.  This causes a number of third party dependencies like React and Redux to potentially go into development mode, slowing them down, or to not realize they’re in production mode, causing them to throw some warnings.

* style: some additional linting

* feat: upgrading to modern paragon and a brand package

This commit updates the app from Paragon 7 to 12 and fixes the breaking changes in between.  Mostly small changes to Button and Dropdown, as well as using “container” instead of “container-fluid” to preserve the page’s width as closely as possible.

It also adds the brand package, which is why it’s a feature.  Using the brand package allows the MFE to be rebranded by using an npm alias to override the source of the brand.

* test: fixing test snapshots that failed when updating paragon

The test snapshots got a bit out of date when updating paragon.  Also removing an unncessary “type” from Dropdown.Toggle which does nothing.

- container has been replaced by container-fluid
- The Button component is a different implementation, which adds slightly different properties to the rendered button.  i.e., onKeyDown and disabled, but doesn’t add the id or onBlur.
- The Dropdown doesn’t render its contents until it’s opened, which is why “Upload Photo” and “Remove” are no longer in the snapshot.
-

* build: bumping paragon to 13

* fix: fixing broken test snapshot

btn-outline is definitely not a correct button type.

* fix: using the ‘size’ property on Button

* fix: updating dependencies

We needed to upgrade paragon to 13.1.2 to fix a transpilation issue that was causing ES6 code to be included in the build artifact.  All other upgrades here were attempts at fixing that, but they’re all also perfectly valid and good to update, so I left them.

babel-polyfill has been replaced by including core-js and regenerator-runtime.

Upgrading frontend-build fixed an issue with eslint configuration that emerged during the other upgrades.

* fix: switch back to container-fluid

We want to leave it as container-fluid and solve the max width problem through paragon.

* style: cleanup and formatting of SCSS

Also removing an unnecessary variant of primary on a button.

* test: fix broken snapshot test
2021-02-05 13:38:36 -05:00
40 changed files with 5095 additions and 6468 deletions

4
.env
View File

@@ -1,4 +1,4 @@
NODE_ENV=null
NODE_ENV='production'
ACCESS_TOKEN_COOKIE_NAME=null
BASE_URL=null
CREDENTIALS_BASE_URL=null
@@ -12,7 +12,7 @@ MARKETING_SITE_BASE_URL=null
ORDER_HISTORY_URL=null
REFRESH_ACCESS_TOKEN_ENDPOINT=null
SEGMENT_KEY=null
SITE_NAME=null
SITE_NAME=''
USER_INFO_COOKIE_NAME=null
APPLE_APP_STORE_URL=null
CONTACT_URL=null

View File

@@ -13,7 +13,7 @@ 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'
SITE_NAME=localhost
USER_INFO_COOKIE_NAME='edx-user-info'
APPLE_APP_STORE_URL='https://www.apple.com/ios/app-store/'
CONTACT_URL='http://localhost:18000/contact'

View File

@@ -11,7 +11,7 @@ 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'
SITE_NAME=localhost
USER_INFO_COOKIE_NAME='edx-user-info'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @edx/community-engineering

1
.gitignore vendored
View File

@@ -17,3 +17,4 @@ temp/babel-plugin-react-intl
/.vscode
/temp
/npm-dist
/module.config.js

11042
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@
"npm-build": "make npm-build",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"is-es5": "es-check es5 ./dist/*.js",
"lint": "fedx-scripts eslint",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"test": "fedx-scripts jest --coverage --passWithNoTests"
@@ -40,17 +40,18 @@
"ie 11"
],
"dependencies": {
"@edx/frontend-component-footer": "10.1.1",
"@edx/frontend-component-header": "2.2.1",
"@edx/frontend-platform": "1.8.1",
"@edx/paragon": "7.1.3",
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/frontend-component-footer": "10.1.4",
"@edx/frontend-component-header": "2.2.4",
"@edx/frontend-platform": "1.8.4",
"@edx/paragon": "13.1.2",
"@fortawesome/fontawesome-svg-core": "1.2.25",
"@fortawesome/free-brands-svg-icons": "5.7.2",
"@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",
"core-js": "3.8.3",
"email-prop-type": "1.1.7",
"font-awesome": "4.7.0",
"form-urlencoded": "3.0.2",
@@ -60,34 +61,35 @@
"lodash.memoize": "4.1.2",
"lodash.pick": "4.4.0",
"lodash.snakecase": "4.1.1",
"newrelic": "5.5.0",
"newrelic": "5.13.1",
"prop-types": "15.7.2",
"react": "16.9.0",
"react-dom": "16.9.0",
"react-redux": "7.1.3",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-redux": "7.2.2",
"react-router": "5.1.2",
"react-router-dom": "5.1.2",
"react-transition-group": "4.3.0",
"redux": "4.0.5",
"redux-devtools-extension": "2.13.8",
"redux-logger": "3.0.6",
"redux-saga": "1.0.5",
"redux-saga": "1.1.3",
"redux-thunk": "2.3.0",
"regenerator-runtime": "0.13.7",
"reselect": "4.0.0",
"universal-cookie": "3.1.0"
},
"devDependencies": {
"@commitlint/cli": "8.2.0",
"@commitlint/config-angular": "8.2.0",
"@edx/frontend-build": "5.3.2",
"codecov": "3.7.2",
"@commitlint/cli": "8.3.5",
"@commitlint/config-angular": "8.3.4",
"@edx/frontend-build": "5.6.8",
"codecov": "3.8.2",
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.15.5",
"es-check": "5.0.0",
"enzyme-adapter-react-16": "1.15.6",
"es-check": "5.2.3",
"glob": "7.1.6",
"husky": "3.1.0",
"purgecss-webpack-plugin": "1.6.0",
"react-test-renderer": "16.9.0",
"react-test-renderer": "16.14.0",
"reactifex": "1.1.1",
"redux-mock-store": "1.5.4"
}

View File

@@ -1,7 +1,7 @@
<!doctype html>
<html lang="en-us">
<head>
<title>Learner Profile | edX</title>
<title>Learner Profile | <%= process.env.SITE_NAME %></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin>

View File

@@ -1,9 +1,21 @@
{
"extends": [
"config:base"
"config:base",
"schedule:weekly",
":automergeMinor",
":enableVulnerabilityAlerts",
":rebaseStalePrs",
":semanticCommits",
":updateNotScheduled"
],
"patch": {
"automerge": true
},
"rebaseStalePrs": true
"packageRules": [
{
"matchDepTypes": [
"devDependencies",
"lockFileMaintenance"
],
"automerge": true
}
],
"timezone": "America/New_York"
}

View File

@@ -2,9 +2,8 @@ import { combineReducers } from 'redux';
import { reducer as profilePage } from '../profile';
const createRootReducer = () =>
combineReducers({
profilePage,
});
const createRootReducer = () => combineReducers({
profilePage,
});
export default createRootReducer;

View File

@@ -1,7 +1,16 @@
import 'babel-polyfill';
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import { APP_INIT_ERROR, APP_READY, initialize, subscribe } from '@edx/frontend-platform';
import { AppProvider, ErrorPage } from '@edx/frontend-platform/react';
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';

View File

@@ -1,7 +1,11 @@
@import '~@edx/paragon/scss/edx/theme.scss';
@import '~@edx/paragon/scss/edx/fonts.scss'; // Roboto
@import './profile/index.scss';
$fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome";
@import "~@edx/brand/paragon/fonts";
@import "~@edx/brand/paragon/variables";
@import "~@edx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
@import "~@edx/frontend-component-header/dist/index";
@import "~@edx/frontend-component-footer/dist/footer";
@import './profile/index';

View File

@@ -7,8 +7,8 @@ function AgeMessage({ accountSettingsUrl }) {
return (
<StatusAlert
alertType="info"
dialog={
<React.Fragment>
dialog={(
<>
<FormattedMessage
id="profile.age.headline"
defaultMessage="Your profile cannot be shared."
@@ -28,8 +28,8 @@ function AgeMessage({ accountSettingsUrl }) {
description="label on a link to set birthday"
/>
</a>
</React.Fragment>
}
</>
)}
dismissible={false}
open
/>

View File

@@ -3,7 +3,9 @@ import PropTypes from 'prop-types';
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
function DateJoined({ date }) {
if (date == null) return null;
if (date == null) {
return null;
}
return (
<p className="mb-0">

View File

@@ -111,13 +111,13 @@ class ProfilePage extends React.Component {
const { dateJoined } = this.props;
return (
<React.Fragment>
<>
<span data-hj-suppress>
<h1 className="h2 mb-0 font-weight-bold">{this.props.match.params.username}</h1>
<DateJoined date={dateJoined} />
<hr className="d-none d-md-block" />
</span>
</React.Fragment>
</>
);
}
@@ -179,7 +179,6 @@ class ProfilePage extends React.Component {
changeHandler: this.handleChange,
};
return (
<div className="container-fluid">
<div className="row align-items-center pt-4 mb-4 pt-md-0 mb-md-0">

View File

@@ -245,41 +245,18 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
>
<div
className="dropdown"
onKeyDown={[Function]}
>
<button
aria-expanded={false}
aria-haspopup={true}
className="dropdown-toggle btn btn-light"
id="pgn__dropdown-trigger-0"
className="dropdown-toggle btn btn-primary"
disabled={false}
onClick={[Function]}
type="btn-outline"
type="button"
>
Change
</button>
<div
aria-hidden={true}
aria-labelledby="pgn__dropdown-trigger-0"
className="dropdown-menu"
onKeyDown={[Function]}
role="menu"
>
<button
className="dropdown-item"
onClick={[Function]}
>
<span>
Upload Photo
</span>
</button>
<button
className="dropdown-item"
onClick={[Function]}
>
<span>
Remove
</span>
</button>
</div>
</div>
</div>
<img
@@ -441,10 +418,9 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
>
Full Name
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
className="float-right px-0 btn btn-link btn-sm"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
@@ -537,10 +513,9 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
>
Location
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
className="float-right px-0 btn btn-link btn-sm"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
@@ -628,10 +603,9 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
>
Primary Language Spoken
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
className="float-right px-0 btn btn-link btn-sm"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
@@ -719,10 +693,9 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
>
Education
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
className="float-right px-0 btn btn-link btn-sm"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
@@ -810,10 +783,9 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
>
Social Links
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
className="float-right px-0 btn btn-link btn-sm"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
@@ -935,6 +907,7 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
onClick={[Function]}
onKeyDown={[Function]}
tabIndex={0}
type="button"
>
<svg
aria-hidden="true"
@@ -989,10 +962,9 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
>
About Me
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
className="float-right px-0 btn btn-link btn-sm"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
@@ -1080,10 +1052,9 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
>
My Certificates
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
className="float-right px-0 btn btn-link btn-sm"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
@@ -1260,41 +1231,18 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
>
<div
className="dropdown"
onKeyDown={[Function]}
>
<button
aria-expanded={false}
aria-haspopup={true}
className="dropdown-toggle btn btn-light"
id="pgn__dropdown-trigger-1"
className="dropdown-toggle btn btn-primary"
disabled={false}
onClick={[Function]}
type="btn-outline"
type="button"
>
Change
</button>
<div
aria-hidden={true}
aria-labelledby="pgn__dropdown-trigger-1"
className="dropdown-menu"
onKeyDown={[Function]}
role="menu"
>
<button
className="dropdown-item"
onClick={[Function]}
>
<span>
Upload Photo
</span>
</button>
<button
className="dropdown-item"
onClick={[Function]}
>
<span>
Remove
</span>
</button>
</div>
</div>
</div>
<img
@@ -1456,10 +1404,9 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
>
Full Name
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
className="float-right px-0 btn btn-link btn-sm"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
@@ -1552,10 +1499,9 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
>
Location
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
className="float-right px-0 btn btn-link btn-sm"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
@@ -1643,10 +1589,9 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
>
Primary Language Spoken
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
className="float-right px-0 btn btn-link btn-sm"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
@@ -1734,10 +1679,9 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
>
Education
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
className="float-right px-0 btn btn-link btn-sm"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
@@ -1825,10 +1769,9 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
>
Social Links
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
className="float-right px-0 btn btn-link btn-sm"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
@@ -1950,6 +1893,7 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
onClick={[Function]}
onKeyDown={[Function]}
tabIndex={0}
type="button"
>
<svg
aria-hidden="true"
@@ -2088,10 +2032,9 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
<button
aria-disabled={false}
aria-live="assertive"
className="btn pgn__stateful-btn pgn__stateful-btn-state-pending btn-primary"
onBlur={[Function]}
className="pgn__stateful-btn pgn__stateful-btn-state-pending btn btn-primary"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="submit"
>
<span
@@ -2103,7 +2046,7 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
<span
aria-hidden={true}
className="icon fa fa-spinner fa-spin"
id="Icon2"
id="Icon1"
/>
</span>
Saving
@@ -2111,9 +2054,8 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
</button>
<button
className="btn btn-link"
onBlur={[Function]}
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Cancel
@@ -2148,10 +2090,9 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
>
My Certificates
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
className="float-right px-0 btn btn-link btn-sm"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",

View File

@@ -92,7 +92,6 @@ describe('SAVE profile actions', () => {
});
});
describe('SAVE profile photo actions', () => {
it('should create an action to signal the start of a profile photo save', () => {
const formData = 'multipart form data';
@@ -141,7 +140,6 @@ describe('SAVE profile photo actions', () => {
});
});
describe('DELETE profile photo actions', () => {
it('should create an action to signal the start of a profile photo deletion', () => {
const expectedAction = {
@@ -179,7 +177,6 @@ describe('DELETE profile photo actions', () => {
});
});
describe('Editable field opening and closing actions', () => {
const formId = 'name';

View File

@@ -57,13 +57,13 @@ const profilePage = (state = initialState, action) => {
// Account is always replaced completely.
account: action.payload.account !== null ? action.payload.account : state.account,
// Preferences changes get merged in.
preferences: Object.assign({}, state.preferences, action.payload.preferences),
preferences: { ...state.preferences, ...action.payload.preferences },
};
case SAVE_PROFILE.FAILURE:
return {
...state,
saveState: 'error',
errors: Object.assign({}, state.errors, action.payload.errors),
errors: { ...state.errors, ...action.payload.errors },
};
case SAVE_PROFILE.RESET:
return {
@@ -82,7 +82,7 @@ const profilePage = (state = initialState, action) => {
return {
...state,
// Merge in new profile image data
account: Object.assign({}, state.account, { profileImage: action.payload.profileImage }),
account: { ...state.account, profileImage: action.payload.profileImage },
savePhotoState: 'complete',
errors: {},
};
@@ -90,7 +90,7 @@ const profilePage = (state = initialState, action) => {
return {
...state,
savePhotoState: 'error',
errors: Object.assign({}, state.errors, { photo: action.payload.error }),
errors: { ...state.errors, photo: action.payload.error },
};
case SAVE_PROFILE_PHOTO.RESET:
return {
@@ -109,7 +109,7 @@ const profilePage = (state = initialState, action) => {
return {
...state,
// Merge in new profile image data (should be empty or default image)
account: Object.assign({}, state.account, { profileImage: action.payload.profileImage }),
account: { ...state.account, profileImage: action.payload.profileImage },
savePhotoState: 'complete',
errors: {},
};
@@ -117,7 +117,7 @@ const profilePage = (state = initialState, action) => {
return {
...state,
savePhotoState: 'error',
errors: Object.assign({}, state.errors, action.payload.errors),
errors: { ...state.errors, ...action.payload.errors },
};
case DELETE_PROFILE_PHOTO.RESET:
return {
@@ -129,9 +129,7 @@ const profilePage = (state = initialState, action) => {
case UPDATE_DRAFT:
return {
...state,
drafts: Object.assign({}, state.drafts, {
[action.payload.name]: action.payload.value,
}),
drafts: { ...state.drafts, [action.payload.name]: action.payload.value },
};
case RESET_DRAFTS:

View File

@@ -1,7 +1,14 @@
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 {
all,
call,
delay,
put,
select,
takeEvery,
} from 'redux-saga/effects';
import {
closeForm,
deleteProfilePhotoBegin,

View File

@@ -1,4 +1,11 @@
import { takeEvery, put, call, delay, select, all } from 'redux-saga/effects';
import {
takeEvery,
put,
call,
delay,
select,
all,
} from 'redux-saga/effects';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import * as profileActions from './actions';

View File

@@ -22,8 +22,7 @@ export const savePhotoStateSelector = state => state.profilePage.savePhotoState;
export const isLoadingProfileSelector = state => state.profilePage.isLoadingProfile;
export const currentlyEditingFieldSelector = state => state.profilePage.currentlyEditingField;
export const accountErrorsSelector = state => state.profilePage.errors;
export const isAuthenticatedUserProfileSelector = state =>
state.profilePage.isAuthenticatedUserProfile;
export const isAuthenticatedUserProfileSelector = state => state.profilePage.isAuthenticatedUserProfile;
export const editableFormModeSelector = createSelector(
profileAccountSelector,
@@ -147,13 +146,12 @@ export const certificatesSelector = createSelector(
export const profileImageSelector = createSelector(
profileAccountSelector,
account =>
(account.profileImage != null
? {
src: account.profileImage.imageUrlFull,
isDefault: !account.profileImage.hasImage,
}
: {}),
account => (account.profileImage != null
? {
src: account.profileImage.imageUrlFull,
isDefault: !account.profileImage.hasImage,
}
: {}),
);
/**

View File

@@ -121,12 +121,12 @@ function transformCertificateData(data) {
data.forEach((cert) => {
// download_url may be full url or absolute path.
// note: using the URL() api breaks in ie 11
const urlIsPath = typeof cert.download_url === 'string' &&
cert.download_url.search(/http[s]?:\/\//) !== 0;
const urlIsPath = typeof cert.download_url === 'string'
&& cert.download_url.search(/http[s]?:\/\//) !== 0;
const downloadUrl = urlIsPath ?
`${getConfig().LMS_BASE_URL}${cert.download_url}` :
cert.download_url;
const downloadUrl = urlIsPath
? `${getConfig().LMS_BASE_URL}${cert.download_url}`
: cert.download_url;
transformedData.push({
...camelCaseObject(cert),

View File

@@ -83,7 +83,7 @@ class Bio extends React.Component {
</div>
),
editable: (
<React.Fragment>
<>
<EditableItemHeader
content={intl.formatMessage(messages['profile.bio.about.me'])}
showEditButton
@@ -92,10 +92,10 @@ class Bio extends React.Component {
visibility={visibilityBio}
/>
<p data-hj-suppress className="lead">{bio}</p>
</React.Fragment>
</>
),
empty: (
<React.Fragment>
<>
<EditableItemHeader content={intl.formatMessage(messages['profile.bio.about.me'])} />
<EmptyContent onClick={this.handleOpen}>
<FormattedMessage
@@ -104,13 +104,13 @@ class Bio extends React.Component {
description="instructions when the user hasn't written an About Me"
/>
</EmptyContent>
</React.Fragment>
</>
),
static: (
<React.Fragment>
<>
<EditableItemHeader content={intl.formatMessage(messages['profile.bio.about.me'])} />
<p data-hj-suppress className="lead">{bio}</p>
</React.Fragment>
</>
),
}}
/>

View File

@@ -1,6 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedDate, FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/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';
@@ -113,11 +115,13 @@ class Certificates extends React.Component {
renderCertificates() {
if (this.props.certificates === null || this.props.certificates.length === 0) {
return (<FormattedMessage
id="profile.no.certificates"
defaultMessage="You don't have any certificates yet."
description="displays when user has no course completion certificates"
/>);
return (
<FormattedMessage
id="profile.no.certificates"
defaultMessage="You don't have any certificates yet."
description="displays when user has no course completion certificates"
/>
);
}
return (
@@ -154,7 +158,7 @@ class Certificates extends React.Component {
</div>
),
editable: (
<React.Fragment>
<>
<EditableItemHeader
content={intl.formatMessage(messages['profile.certificates.my.certificates'])}
showEditButton
@@ -163,10 +167,10 @@ class Certificates extends React.Component {
visibility={visibilityCourseCertificates}
/>
{this.renderCertificates()}
</React.Fragment>
</>
),
empty: (
<React.Fragment>
<>
<EditableItemHeader
content={intl.formatMessage(messages['profile.certificates.my.certificates'])}
showEditButton
@@ -175,13 +179,13 @@ class Certificates extends React.Component {
visibility={visibilityCourseCertificates}
/>
{this.renderCertificates()}
</React.Fragment>
</>
),
static: (
<React.Fragment>
<>
<EditableItemHeader content={intl.formatMessage(messages['profile.certificates.my.certificates'])} />
{this.renderCertificates()}
</React.Fragment>
</>
),
}}
/>

View File

@@ -84,7 +84,7 @@ class Country extends React.Component {
value={country}
onChange={this.handleChange}
>
<option value="" />
<option value="">&nbsp;</option>
{sortedCountries.map(({ code, name }) => (
<option key={code} value={code}>{name}</option>
))}
@@ -101,7 +101,7 @@ class Country extends React.Component {
</div>
),
editable: (
<React.Fragment>
<>
<EditableItemHeader
content={intl.formatMessage(messages['profile.country.label'])}
showEditButton
@@ -110,25 +110,25 @@ class Country extends React.Component {
visibility={visibilityCountry}
/>
<p data-hj-suppress className="h5">{countryMessages[country]}</p>
</React.Fragment>
</>
),
empty: (
<React.Fragment>
<>
<EditableItemHeader
content={intl.formatMessage(messages['profile.country.label'])}
/>
<EmptyContent onClick={this.handleOpen}>
{intl.formatMessage(messages['profile.country.empty'])}
</EmptyContent>
</React.Fragment>
</>
),
static: (
<React.Fragment>
<>
<EditableItemHeader
content={intl.formatMessage(messages['profile.country.label'])}
/>
<p data-hj-suppress className="h5">{countryMessages[country]}</p>
</React.Fragment>
</>
),
}}
/>

View File

@@ -79,7 +79,7 @@ class Education extends React.Component {
value={levelOfEducation}
onChange={this.handleChange}
>
<option value="" />
<option value="">&nbsp;</option>
{EDUCATION_LEVELS.map(level => (
<option key={level} value={level}>
{intl.formatMessage(get(
@@ -102,7 +102,7 @@ class Education extends React.Component {
</div>
),
editable: (
<React.Fragment>
<>
<EditableItemHeader
content={intl.formatMessage(messages['profile.education.education'])}
showEditButton
@@ -117,10 +117,10 @@ class Education extends React.Component {
messages['profile.education.levels.o'],
))}
</p>
</React.Fragment>
</>
),
empty: (
<React.Fragment>
<>
<EditableItemHeader content={intl.formatMessage(messages['profile.education.education'])} />
<EmptyContent onClick={this.handleOpen}>
<FormattedMessage
@@ -129,10 +129,10 @@ class Education extends React.Component {
description="instructions when the user doesn't have their level of education set"
/>
</EmptyContent>
</React.Fragment>
</>
),
static: (
<React.Fragment>
<>
<EditableItemHeader content={intl.formatMessage(messages['profile.education.education'])} />
<p data-hj-suppress className="h5">
{intl.formatMessage(get(
@@ -141,7 +141,7 @@ class Education extends React.Component {
messages['profile.education.levels.o'],
))}
</p>
</React.Fragment>
</>
),
}}
/>

View File

@@ -84,7 +84,7 @@ class Name extends React.Component {
</div>
),
editable: (
<React.Fragment>
<>
<EditableItemHeader
content={intl.formatMessage(messages['profile.name.full.name'])}
showEditButton
@@ -96,10 +96,10 @@ class Name extends React.Component {
<small className="form-text text-muted">
{intl.formatMessage(messages['profile.name.details'])}
</small>
</React.Fragment>
</>
),
empty: (
<React.Fragment>
<>
<EditableItemHeader content={intl.formatMessage(messages['profile.name.full.name'])} />
<EmptyContent onClick={this.handleOpen}>
{intl.formatMessage(messages['profile.name.empty'])}
@@ -107,13 +107,13 @@ class Name extends React.Component {
<small className="form-text text-muted">
{intl.formatMessage(messages['profile.name.details'])}
</small>
</React.Fragment>
</>
),
static: (
<React.Fragment>
<>
<EditableItemHeader content={intl.formatMessage(messages['profile.name.full.name'])} />
<p data-hj-suppress className="h5">{name}</p>
</React.Fragment>
</>
),
}}
/>

View File

@@ -93,7 +93,7 @@ class PreferredLanguage extends React.Component {
value={value}
onChange={this.handleChange}
>
<option value="" />
<option value="">&nbsp;</option>
{sortedLanguages.map(({ code, name }) => (
<option key={code} value={code}>{name}</option>
))}
@@ -110,7 +110,7 @@ class PreferredLanguage extends React.Component {
</div>
),
editable: (
<React.Fragment>
<>
<EditableItemHeader
content={intl.formatMessage(messages['profile.preferredlanguage.label'])}
showEditButton
@@ -119,25 +119,25 @@ class PreferredLanguage extends React.Component {
visibility={visibilityLanguageProficiencies}
/>
<p data-hj-suppress className="h5">{languageMessages[value]}</p>
</React.Fragment>
</>
),
empty: (
<React.Fragment>
<>
<EditableItemHeader
content={intl.formatMessage(messages['profile.preferredlanguage.label'])}
/>
<EmptyContent onClick={this.handleOpen}>
{intl.formatMessage(messages['profile.preferredlanguage.empty'])}
</EmptyContent>
</React.Fragment>
</>
),
static: (
<React.Fragment>
<>
<EditableItemHeader
content={intl.formatMessage(messages['profile.preferredlanguage.label'])}
/>
<p data-hj-suppress className="h5">{languageMessages[value]}</p>
</React.Fragment>
</>
),
}}
/>

View File

@@ -33,7 +33,9 @@ class ProfileAvatar extends React.Component {
}
onSubmit(e) {
if (e) e.preventDefault();
if (e) {
e.preventDefault();
}
this.props.onSave(new FormData(this.form.current));
this.form.current.reset();
}
@@ -55,7 +57,9 @@ class ProfileAvatar extends React.Component {
if (this.props.isDefault) {
return (
<Button
className="text-white btn-block btn-sm btn-link"
variant="link"
size="sm"
className="text-white btn-block"
onClick={this.onClickUpload}
>
<FormattedMessage
@@ -69,9 +73,9 @@ class ProfileAvatar extends React.Component {
return (
<Dropdown>
<Dropdown.Button type="btn-outline">
<Dropdown.Toggle>
{intl.formatMessage(messages['profile.profileavatar.change-button'])}
</Dropdown.Button>
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item type="button" onClick={this.onClickUpload}>
<FormattedMessage
@@ -93,7 +97,9 @@ class ProfileAvatar extends React.Component {
}
renderMenu() {
if (!this.props.isEditable) return null;
if (!this.props.isEditable) {
return null;
}
return (
<div className="profile-avatar-menu-container">

View File

@@ -102,7 +102,7 @@ class SocialLinks extends React.Component {
expression={editMode}
cases={{
empty: (
<React.Fragment>
<>
<EditableItemHeader content={intl.formatMessage(messages['profile.sociallinks.social.links'])} />
<ul className="list-unstyled">
{socialLinks.map(({ platform }) => (
@@ -113,10 +113,10 @@ class SocialLinks extends React.Component {
/>
))}
</ul>
</React.Fragment>
</>
),
static: (
<React.Fragment>
<>
<EditableItemHeader
content={intl.formatMessage(messages['profile.sociallinks.social.links'])}
/>
@@ -130,13 +130,12 @@ class SocialLinks extends React.Component {
url={socialLink}
platform={platform}
/>
))
}
))}
</ul>
</React.Fragment>
</>
),
editable: (
<React.Fragment>
<>
<EditableItemHeader
content={intl.formatMessage(messages['profile.sociallinks.social.links'])}
showEditButton
@@ -155,7 +154,7 @@ class SocialLinks extends React.Component {
/>
))}
</ul>
</React.Fragment>
</>
),
editing: (
<div role="dialog" aria-labelledby="social-links-label">

View File

@@ -1,6 +1,5 @@
import React from 'react';
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-platform/i18n';
@@ -13,7 +12,9 @@ function EditButton({
}) {
return (
<Button
className={classNames('btn-sm btn-link', className)}
variant="link"
size="sm"
className={className}
onClick={onClick}
style={style}
>

View File

@@ -13,7 +13,7 @@ function EditableItemHeader({
headingId,
}) {
return (
<React.Fragment>
<>
<div className="editable-item-header mb-2">
<h2 className="edit-section-header" id={headingId}>
{content}
@@ -21,7 +21,7 @@ function EditableItemHeader({
</h2>
{showVisibility ? <p className="mb-0"><Visibility to={visibility} /></p> : null}
</div>
</React.Fragment>
</>
);
}

View File

@@ -8,9 +8,10 @@ function EmptyContent({ children, onClick, showPlusIcon }) {
<div>
{onClick ? (
<button
type="button"
className="pl-0 text-left btn btn-link"
onClick={onClick}
onKeyDown={(e) => { if (e.key === 'Enter') onClick(); }}
onKeyDown={(e) => { if (e.key === 'Enter') { onClick(); } }}
tabIndex={0}
>
{showPlusIcon ? <FontAwesomeIcon size="xs" className="mr-2" icon={faPlus} /> : null}
@@ -21,7 +22,6 @@ function EmptyContent({ children, onClick, showPlusIcon }) {
);
}
export default EmptyContent;
EmptyContent.propTypes = {

View File

@@ -31,7 +31,6 @@ function FormControls({
<div className="form-group flex-shrink-0 flex-grow-1">
<StatefulButton
type="submit"
className="btn-primary"
state={buttonState}
labels={{
default: intl.formatMessage(messages['profile.formcontrols.button.save']),
@@ -46,11 +45,13 @@ function FormControls({
// Swallowing the onSubmit event on the form would be better, but
// we would have to add that logic for every field given our
// current structure of the application.
if (buttonState === 'pending') e.preventDefault();
if (buttonState === 'pending') {
e.preventDefault();
}
}}
disabledStates={[]}
/>
<Button className="btn-link" onClick={cancelHandler}>
<Button variant="link" onClick={cancelHandler}>
{intl.formatMessage(messages['profile.formcontrols.button.cancel'])}
</Button>
</div>

View File

@@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import { TransitionReplace } from '@edx/paragon';
const onChildExit = (htmlNode) => {
// If the leaving child has focus, take control and redirect it
if (htmlNode.contains(document.activeElement)) {
@@ -11,7 +10,9 @@ const onChildExit = (htmlNode) => {
const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling;
// There's no replacement, do nothing.
if (!enteringChild) return;
if (!enteringChild) {
return;
}
// Get all the focusable elements in the entering child and focus the first one
const focusableElements = enteringChild.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
@@ -21,7 +22,6 @@ const onChildExit = (htmlNode) => {
}
};
function SwitchContent({ expression, cases, className }) {
const getContent = (caseKey) => {
if (cases[caseKey]) {
@@ -29,7 +29,8 @@ function SwitchContent({ expression, cases, className }) {
return getContent(cases[caseKey]);
}
return React.cloneElement(cases[caseKey], { key: caseKey });
} else if (cases.default) {
}
if (cases.default) {
if (typeof cases.default === 'string') {
return getContent(cases.default);
}
@@ -49,7 +50,6 @@ function SwitchContent({ expression, cases, className }) {
);
}
SwitchContent.propTypes = {
expression: PropTypes.string,
cases: PropTypes.objectOf(PropTypes.node).isRequired,
@@ -61,5 +61,4 @@ SwitchContent.defaultProps = {
className: null,
};
export default SwitchContent;

View File

@@ -6,12 +6,11 @@ import { faEyeSlash, faEye } from '@fortawesome/free-regular-svg-icons';
import messages from './Visibility.messages';
function Visibility({ to, intl }) {
const icon = to === 'private' ? faEyeSlash : faEye;
const label = to === 'private' ?
intl.formatMessage(messages['profile.visibility.who.just.me']) :
intl.formatMessage(messages['profile.visibility.who.everyone']);
const label = to === 'private'
? intl.formatMessage(messages['profile.visibility.who.just.me'])
: intl.formatMessage(messages['profile.visibility.who.everyone']);
return (
<span className="ml-auto small text-muted">
@@ -30,7 +29,6 @@ Visibility.defaultProps = {
to: 'private',
};
function VisibilitySelect({ intl, className, ...props }) {
const { value } = props;
const icon = value === 'private' ? faEyeSlash : faEye;
@@ -73,7 +71,6 @@ VisibilitySelect.defaultProps = {
const intlVisibility = injectIntl(Visibility);
const intlVisibilitySelect = injectIntl(VisibilitySelect);
export {
intlVisibility as Visibility,
intlVisibilitySelect as VisibilitySelect,

View File

@@ -1,8 +1,3 @@
$fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome";
.word-break-all {
word-break: break-all !important;
}
@@ -36,9 +31,11 @@ $fa-font-path: "~font-awesome/fonts";
letter-spacing: 0;
margin: 0;
}
label.edit-section-header {
margin-bottom: $spacer * .5;
}
.profile-avatar-wrap {
@include media-breakpoint-up(md) {
max-width: 12rem;
@@ -57,10 +54,12 @@ $fa-font-path: "~font-awesome/fonts";
justify-content: center;
align-items: center;
border-radius: 50%;
@include media-breakpoint-up(md) {
background: linear-gradient(to top, rgba(0,0,0,.65) 4rem, rgba(0,0,0,0) 4rem);
align-items: flex-end;
}
.btn {
text-decoration: none;
@include media-breakpoint-up(md) {
@@ -72,6 +71,7 @@ $fa-font-path: "~font-awesome/fonts";
@include media-breakpoint-up(md) {
margin-bottom: 1.2rem;
}
.btn {
color: $white;
background: transparent;
@@ -118,10 +118,12 @@ $fa-font-path: "~font-awesome/fonts";
.certificate {
position: relative;
.certificate-title {
font-family: $font-family-serif;
font-weight: 400;
}
.certificate-type-illustration {
position: absolute;
top: 1rem;
@@ -133,9 +135,9 @@ $fa-font-path: "~font-awesome/fonts";
background-repeat: no-repeat;
background-position: right top;
}
.card-body {
position: relative;
}
}
}

View File

@@ -4,9 +4,9 @@ import snakeCase from 'lodash.snakecase';
export function modifyObjectKeys(object, modify) {
// If the passed in object is not an object, return it.
if (
object === undefined ||
object === null ||
(typeof object !== 'object' && !Array.isArray(object))
object === undefined
|| object === null
|| (typeof object !== 'object' && !Array.isArray(object))
) {
return object;
}

View File

@@ -1,4 +1,10 @@
import { AsyncActionType, modifyObjectKeys, camelCaseObject, snakeCaseObject, convertKeyNames } from './utils';
import {
AsyncActionType,
modifyObjectKeys,
camelCaseObject,
snakeCaseObject,
convertKeyNames,
} from './utils';
describe('modifyObjectKeys', () => {
it('should use the provided modify function to change all keys in and object and its children', () => {

View File

@@ -1,3 +1,6 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';