From 71885b6fd21efbdc49772bee11b6778abe685452 Mon Sep 17 00:00:00 2001
From: Zainab Amir
Date: Fri, 27 Nov 2020 15:22:41 +0500
Subject: [PATCH] Add stateful buttons on forms (#37)
Add stateful button component from paragon to
- LoginPage
- RegistrationPage
- ForgetPasswordPage
- ResetPasswordPage
VAN-123
---
src/data/constants.js | 4 +
src/forgot-password/ForgotPasswordPage.jsx | 16 +-
.../tests/ForgotPasswordPage.test.jsx | 10 +
.../ForgotPasswordPage.test.jsx.snap | 161 +-
src/logistration/LoginPage.jsx | 86 +-
src/logistration/RegistrationPage.jsx | 28 +-
src/logistration/data/reducers.js | 11 +-
src/logistration/messages.jsx | 10 +
src/logistration/tests/LoginPage.test.jsx | 17 +
.../tests/RegistrationPage.test.jsx | 14 +
.../__snapshots__/LoginPage.test.jsx.snap | 304 +++-
.../RegistrationPage.test.jsx.snap | 1486 ++++++++++++++++-
src/reset-password/ResetPasswordPage.jsx | 13 +-
.../tests/ResetPasswordPage.test.jsx | 41 +-
.../ResetPasswordPage.test.jsx.snap | 140 +-
15 files changed, 2179 insertions(+), 162 deletions(-)
diff --git a/src/data/constants.js b/src/data/constants.js
index 6b58337e..e8b1f5ac 100644
--- a/src/data/constants.js
+++ b/src/data/constants.js
@@ -6,3 +6,7 @@ export const DEFAULT_REDIRECT_URL = '/dashboard';
// Constants
export const SUPPORTED_ICON_CLASSES = ['apple', 'facebook', 'google', 'microsoft'];
+
+// Stateful Submit Button States
+export const DEFAULT_STATE = 'default';
+export const PENDING_STATE = 'pending';
diff --git a/src/forgot-password/ForgotPasswordPage.jsx b/src/forgot-password/ForgotPasswordPage.jsx
index 611add2b..7b4d3980 100644
--- a/src/forgot-password/ForgotPasswordPage.jsx
+++ b/src/forgot-password/ForgotPasswordPage.jsx
@@ -2,13 +2,14 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
-import { Button, Input, ValidationFormGroup } from '@edx/paragon';
+import { Input, StatefulButton, ValidationFormGroup } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './messages';
import { forgotPassword } from './data/actions';
import { forgotPasswordResultSelector } from './data/selectors';
import RequestInProgressAlert from './RequestInProgressAlert';
+import { LOGIN_PAGE } from '../data/constants';
import LoginHelpLinks from '../logistration/LoginHelpLinks';
const ForgotPasswordPage = (props) => {
@@ -39,7 +40,7 @@ const ForgotPasswordPage = (props) => {
return (
<>
- {status === 'complete' ? : null}
+ {status === 'complete' ? : null}
-
+ />
diff --git a/src/forgot-password/tests/ForgotPasswordPage.test.jsx b/src/forgot-password/tests/ForgotPasswordPage.test.jsx
index 22561ee3..1cb46428 100644
--- a/src/forgot-password/tests/ForgotPasswordPage.test.jsx
+++ b/src/forgot-password/tests/ForgotPasswordPage.test.jsx
@@ -49,6 +49,16 @@ describe('ForgotPasswordPage', () => {
expect(tree).toMatchSnapshot();
});
+ it('should match pending section snapshot', () => {
+ props = {
+ ...props,
+ status: 'pending',
+ };
+ const tree = renderer.create(reduxWrapper())
+ .toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
it('should match success section snapshot', () => {
props = {
...props,
diff --git a/src/forgot-password/tests/__snapshots__/ForgotPasswordPage.test.jsx.snap b/src/forgot-password/tests/__snapshots__/ForgotPasswordPage.test.jsx.snap
index 9468f6ac..6c22eaef 100644
--- a/src/forgot-password/tests/__snapshots__/ForgotPasswordPage.test.jsx.snap
+++ b/src/forgot-password/tests/__snapshots__/ForgotPasswordPage.test.jsx.snap
@@ -110,12 +110,18 @@ exports[`ForgotPasswordPage should match default section snapshot 1`] = `
@@ -260,12 +266,155 @@ exports[`ForgotPasswordPage should match forbidden section snapshot 1`] = `
+
+
+
+`;
+
+exports[`ForgotPasswordPage should match pending section snapshot 1`] = `
+
+
diff --git a/src/logistration/LoginPage.jsx b/src/logistration/LoginPage.jsx
index 821349a2..e187abe2 100644
--- a/src/logistration/LoginPage.jsx
+++ b/src/logistration/LoginPage.jsx
@@ -1,22 +1,23 @@
import React from 'react';
-import { Button, Input, ValidationFormGroup } from '@edx/paragon';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { Input, StatefulButton, ValidationFormGroup } from '@edx/paragon';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import { forgotPasswordResultSelector } from '../forgot-password';
import ConfirmationAlert from './ConfirmationAlert';
import { getThirdPartyAuthContext, loginRequest } from './data/actions';
-import { DEFAULT_REDIRECT_URL, REGISTER_PAGE } from '../data/constants';
import { loginRequestSelector, thirdPartyAuthContextSelector } from './data/selectors';
+import InstitutionLogistration, { RenderInstitutionButton } from './InstitutionLogistration';
import LoginHelpLinks from './LoginHelpLinks';
import LoginFailureMessage from './LoginFailure';
+import messages from './messages';
import RedirectLogistration from './RedirectLogistration';
import SocialAuthProviders from './SocialAuthProviders';
import ThirdPartyAuthAlert from './ThirdPartyAuthAlert';
-import InstitutionLogistration, { RenderInstitutionButton } from './InstitutionLogistration';
-import messages from './messages';
+
+import { DEFAULT_REDIRECT_URL, DEFAULT_STATE, REGISTER_PAGE } from '../data/constants';
+import { forgotPasswordResultSelector } from '../forgot-password';
class LoginPage extends React.Component {
constructor(props, context) {
@@ -111,13 +112,13 @@ class LoginPage extends React.Component {
}
render() {
- const { intl } = this.props;
- const { currentProvider, finishAuthUrl, providers } = this.props.thirdPartyAuthContext;
+ const { intl, submitState, thirdPartyAuthContext } = this.props;
+
if (this.state.institutionLogin) {
return (
@@ -128,15 +129,15 @@ class LoginPage extends React.Component {
- {currentProvider
+ {thirdPartyAuthContext.currentProvider
&& (
)}
{this.props.loginError ?
: null}
@@ -146,15 +147,21 @@ class LoginPage extends React.Component {
First time here?
Create an Account.
-
{intl.formatMessage(messages['logistration.login.institution.login.sign.in'])}
-
-
-
{intl.formatMessage(messages['logistration.login.institution.login.sign.in.with'])}
-
+
+ {intl.formatMessage(messages['logistration.login.institution.login.sign.in'])}
+
+ {thirdPartyAuthContext.secondaryProviders.length ? (
+ <>
+
+
+
{intl.formatMessage(messages['logistration.login.institution.login.sign.in.with'])}
+
+ >
+ ) : null }
- {providers.length && !currentProvider ? (
+ {thirdPartyAuthContext.providers.length && !thirdPartyAuthContext.currentProvider ? (
<>
or sign in with
-
+
>
) : null}
@@ -221,28 +231,32 @@ class LoginPage extends React.Component {
}
LoginPage.defaultProps = {
- loginResult: null,
forgotPassword: null,
+ loginResult: null,
loginError: null,
+ submitState: DEFAULT_STATE,
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
providers: [],
+ secondaryProviders: [],
},
};
LoginPage.propTypes = {
- intl: intlShape.isRequired,
+ forgotPassword: PropTypes.shape({
+ email: PropTypes.string,
+ status: PropTypes.string,
+ }),
getThirdPartyAuthContext: PropTypes.func.isRequired,
+ intl: intlShape.isRequired,
+ loginError: PropTypes.string,
loginRequest: PropTypes.func.isRequired,
loginResult: PropTypes.shape({
redirectUrl: PropTypes.string,
success: PropTypes.bool,
}),
- forgotPassword: PropTypes.shape({
- email: PropTypes.string,
- status: PropTypes.string,
- }),
+ submitState: PropTypes.string,
thirdPartyAuthContext: PropTypes.shape({
currentProvider: PropTypes.string,
platformName: PropTypes.string,
@@ -250,7 +264,6 @@ LoginPage.propTypes = {
secondaryProviders: PropTypes.array,
finishAuthUrl: PropTypes.string,
}),
- loginError: PropTypes.string,
};
const mapStateToProps = state => {
@@ -258,10 +271,11 @@ const mapStateToProps = state => {
const loginResult = loginRequestSelector(state);
const thirdPartyAuthContext = thirdPartyAuthContextSelector(state);
return {
+ loginError: state.logistration.loginError,
+ submitState: state.logistration.submitState,
forgotPassword,
loginResult,
thirdPartyAuthContext,
- loginError: state.logistration.loginError,
};
};
diff --git a/src/logistration/RegistrationPage.jsx b/src/logistration/RegistrationPage.jsx
index 37116553..f8a351c1 100644
--- a/src/logistration/RegistrationPage.jsx
+++ b/src/logistration/RegistrationPage.jsx
@@ -1,9 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
-import {
- Button, Input, ValidationFormGroup,
-} from '@edx/paragon';
+import { Input, StatefulButton, ValidationFormGroup } from '@edx/paragon';
import {
getLocale, getCountryList, injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
@@ -12,7 +10,9 @@ import { getThirdPartyAuthContext, registerNewUser } from './data/actions';
import { registrationRequestSelector, thirdPartyAuthContextSelector } from './data/selectors';
import RedirectLogistration from './RedirectLogistration';
import RegistrationFailure from './RegistrationFailure';
-import { DEFAULT_REDIRECT_URL, LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
+import {
+ DEFAULT_REDIRECT_URL, DEFAULT_STATE, LOGIN_PAGE, REGISTER_PAGE,
+} from '../data/constants';
import SocialAuthProviders from './SocialAuthProviders';
import ThirdPartyAuthAlert from './ThirdPartyAuthAlert';
import InstitutionLogistration, { RenderInstitutionButton } from './InstitutionLogistration';
@@ -159,7 +159,7 @@ class RegistrationPage extends React.Component {
}
render() {
- const { intl } = this.props;
+ const { intl, submitState } = this.props;
const {
currentProvider, finishAuthUrl, providers, secondaryProviders,
} = this.props.thirdPartyAuthContext;
@@ -295,12 +295,15 @@ class RegistrationPage extends React.Component {
/>
By creating an account, you agree to the Terms of Service and Honor Code and you acknowledge that edX and each Member process your personal data in accordance with the Privacy Policy.
-
+ />
>
@@ -312,6 +315,7 @@ RegistrationPage.defaultProps = {
registrationResult: null,
registerNewUser: null,
registrationError: null,
+ submitState: DEFAULT_STATE,
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
@@ -333,6 +337,7 @@ RegistrationPage.propTypes = {
email: PropTypes.array,
username: PropTypes.array,
}),
+ submitState: PropTypes.string,
thirdPartyAuthContext: PropTypes.shape({
currentProvider: PropTypes.string,
platformName: PropTypes.string,
@@ -353,8 +358,9 @@ const mapStateToProps = state => {
const registrationResult = registrationRequestSelector(state);
const thirdPartyAuthContext = thirdPartyAuthContextSelector(state);
return {
- registrationResult,
registrationError: state.logistration.registrationError,
+ submitState: state.logistration.submitState,
+ registrationResult,
thirdPartyAuthContext,
};
};
diff --git a/src/logistration/data/reducers.js b/src/logistration/data/reducers.js
index 7abc5284..013b9dd3 100644
--- a/src/logistration/data/reducers.js
+++ b/src/logistration/data/reducers.js
@@ -4,11 +4,13 @@ import {
THIRD_PARTY_AUTH_CONTEXT,
} from './actions';
+import { DEFAULT_STATE, PENDING_STATE } from '../../data/constants';
+
export const defaultState = {
- registrationResult: {},
+ loginError: null,
loginResult: {},
registrationError: null,
- loginError: null,
+ registrationResult: {},
};
const reducer = (state = defaultState, action) => {
@@ -16,21 +18,23 @@ const reducer = (state = defaultState, action) => {
case REGISTER_NEW_USER.BEGIN:
return {
...state,
+ submitState: PENDING_STATE,
};
case REGISTER_NEW_USER.SUCCESS:
return {
...state,
-
registrationResult: action.payload,
};
case REGISTER_NEW_USER.FAILURE:
return {
...state,
registrationError: action.payload.error,
+ submitState: DEFAULT_STATE,
};
case LOGIN_REQUEST.BEGIN:
return {
...state,
+ submitState: PENDING_STATE,
};
case LOGIN_REQUEST.SUCCESS:
return {
@@ -41,6 +45,7 @@ const reducer = (state = defaultState, action) => {
return {
...state,
loginError: action.payload.loginError,
+ submitState: DEFAULT_STATE,
};
case THIRD_PARTY_AUTH_CONTEXT.BEGIN:
return {
diff --git a/src/logistration/messages.jsx b/src/logistration/messages.jsx
index eb4bd58f..3d442448 100644
--- a/src/logistration/messages.jsx
+++ b/src/logistration/messages.jsx
@@ -2,6 +2,16 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
+ 'logistration.sign.in.button': {
+ id: 'logistration.sign.in.button',
+ defaultMessage: 'Sign in',
+ description: 'Button label that appears on login page',
+ },
+ 'logistration.create.account.button': {
+ id: 'ogistration.create.account.button',
+ defaultMessage: 'Create Account',
+ description: 'Button label that appears on register page',
+ },
'logistration.need.help.signing.in.collapsible.menu': {
id: 'logistration.need.help.signing.in.collapsible.menu',
defaultMessage: 'Need help signing in?',
diff --git a/src/logistration/tests/LoginPage.test.jsx b/src/logistration/tests/LoginPage.test.jsx
index 30b26626..1a1c9031 100644
--- a/src/logistration/tests/LoginPage.test.jsx
+++ b/src/logistration/tests/LoginPage.test.jsx
@@ -8,6 +8,7 @@ import { getConfig } from '@edx/frontend-platform';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import LoginPage from '../LoginPage';
import { RenderInstitutionButton } from '../InstitutionLogistration';
+import { PENDING_STATE } from '../../data/constants';
const IntlLoginPage = injectIntl(LoginPage);
const mockStore = configureStore();
@@ -64,6 +65,20 @@ describe('LoginPage', () => {
expect(tree).toMatchSnapshot();
});
+ it('should match pending button state snapshot', () => {
+ store = mockStore({
+ ...initialState,
+ logistration: {
+ ...initialState.logistration,
+ submitState: PENDING_STATE,
+ },
+ });
+
+ const tree = renderer.create(reduxWrapper(
))
+ .toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
it('should match forget password alert message snapshot', () => {
props = {
...props,
@@ -79,6 +94,7 @@ describe('LoginPage', () => {
logistration: {
...initialState.logistration,
thirdPartyAuthContext: {
+ ...initialState.logistration.thirdPartyAuthContext,
providers: [appleProvider],
},
},
@@ -184,6 +200,7 @@ describe('LoginPage', () => {
logistration: {
...initialState.logistration,
thirdPartyAuthContext: {
+ ...initialState.logistration.thirdPartyAuthContext,
providers: [{
...appleProvider,
loginUrl,
diff --git a/src/logistration/tests/RegistrationPage.test.jsx b/src/logistration/tests/RegistrationPage.test.jsx
index 40716492..7d0a4c99 100644
--- a/src/logistration/tests/RegistrationPage.test.jsx
+++ b/src/logistration/tests/RegistrationPage.test.jsx
@@ -8,6 +8,7 @@ import { IntlProvider, injectIntl, configure } from '@edx/frontend-platform/i18n
import RegistrationPage from '../RegistrationPage';
import { RenderInstitutionButton } from '../InstitutionLogistration';
+import { PENDING_STATE } from '../../data/constants';
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -78,6 +79,19 @@ describe('./RegistrationPage.js', () => {
expect(tree.toJSON()).toMatchSnapshot();
});
+ it('should match pending button state snapshot', () => {
+ store = mockStore({
+ ...initialState,
+ logistration: {
+ ...initialState.logistration,
+ submitState: PENDING_STATE,
+ },
+ });
+
+ const tree = renderer.create(reduxWrapper(
));
+ expect(tree.toJSON()).toMatchSnapshot();
+ });
+
it('should match TPA provider snapshot', () => {
store = mockStore({
...initialState,
diff --git a/src/logistration/tests/__snapshots__/LoginPage.test.jsx.snap b/src/logistration/tests/__snapshots__/LoginPage.test.jsx.snap
index facf4090..0791e67e 100644
--- a/src/logistration/tests/__snapshots__/LoginPage.test.jsx.snap
+++ b/src/logistration/tests/__snapshots__/LoginPage.test.jsx.snap
@@ -25,18 +25,11 @@ exports[`LoginPage should match TPA provider snapshot 1`] = `
-
Sign In
-
-
-
- or sign in with
-
-
+
-
Sign In
-
-
-
- or sign in with
-
-
+
@@ -401,18 +399,11 @@ exports[`LoginPage should match forget password alert message snapshot 1`] = `
-
Sign In
-
-
-
- or sign in with
-
-
+
+
+
+`;
+
+exports[`LoginPage should match pending button state snapshot 1`] = `
+
+
@@ -595,18 +771,11 @@ exports[`LoginPage should show error message on 400 1`] = `
-
Sign In
-
-
-
- or sign in with
-
-
+
@@ -797,18 +972,11 @@ exports[`LoginPage should show error message on 400 on receiving link 1`] = `
-
Sign In
-
-
-
- or sign in with
-
-
+
diff --git a/src/logistration/tests/__snapshots__/RegistrationPage.test.jsx.snap b/src/logistration/tests/__snapshots__/RegistrationPage.test.jsx.snap
index 3e2d8911..18b12226 100644
--- a/src/logistration/tests/__snapshots__/RegistrationPage.test.jsx.snap
+++ b/src/logistration/tests/__snapshots__/RegistrationPage.test.jsx.snap
@@ -1464,12 +1464,18 @@ exports[`./RegistrationPage.js should match TPA provider snapshot 1`] = `
.
@@ -2899,12 +2905,1468 @@ exports[`./RegistrationPage.js should match default section snapshot 1`] = `
.
+
+
+`;
+
+exports[`./RegistrationPage.js should match pending button state snapshot 1`] = `
+
@@ -4372,12 +5834,18 @@ exports[`./RegistrationPage.js should show error message on 409 1`] = `
.
diff --git a/src/reset-password/ResetPasswordPage.jsx b/src/reset-password/ResetPasswordPage.jsx
index 511b4e5a..73b84927 100644
--- a/src/reset-password/ResetPasswordPage.jsx
+++ b/src/reset-password/ResetPasswordPage.jsx
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
-import { Button, Input, ValidationFormGroup } from '@edx/paragon';
+import { Input, StatefulButton, ValidationFormGroup } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getQueryParameters } from '@edx/frontend-platform';
@@ -123,12 +123,15 @@ const ResetPasswordPage = (props) => {
-
+ />
diff --git a/src/reset-password/tests/ResetPasswordPage.test.jsx b/src/reset-password/tests/ResetPasswordPage.test.jsx
index f6bb7b24..e761f0fd 100644
--- a/src/reset-password/tests/ResetPasswordPage.test.jsx
+++ b/src/reset-password/tests/ResetPasswordPage.test.jsx
@@ -53,6 +53,21 @@ describe('ResetPasswordPage', () => {
expect(tree).toMatchSnapshot();
});
+ it('show spinner component during token validation', () => {
+ props = {
+ ...props,
+ token_status: 'pending',
+ match: {
+ params: {
+ token: 'test-token',
+ },
+ },
+ };
+ const tree = renderer.create(reduxWrapper())
+ .toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
it('should match invalid token message section snapshot', () => {
props = {
...props,
@@ -63,6 +78,17 @@ describe('ResetPasswordPage', () => {
expect(tree).toMatchSnapshot();
});
+ it('should match pending reset message section snapshot', () => {
+ props = {
+ ...props,
+ token_status: 'valid',
+ status: 'pending',
+ };
+ const tree = renderer.create(reduxWrapper())
+ .toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
it('should match successful reset message section snapshot', () => {
props = {
...props,
@@ -139,19 +165,4 @@ describe('ResetPasswordPage', () => {
expect(store.dispatch).toHaveBeenCalledWith(resetPassword(formPayload, props.token, {}));
resetPage.unmount();
});
-
- it('show spinner component during token validation', () => {
- props = {
- ...props,
- token_status: 'pending',
- match: {
- params: {
- token: 'test-token',
- },
- },
- };
- const tree = renderer.create(reduxWrapper())
- .toJSON();
- expect(tree).toMatchSnapshot();
- });
});
diff --git a/src/reset-password/tests/__snapshots__/ResetPasswordPage.test.jsx.snap b/src/reset-password/tests/__snapshots__/ResetPasswordPage.test.jsx.snap
index cb5937d7..cc7980b2 100644
--- a/src/reset-password/tests/__snapshots__/ResetPasswordPage.test.jsx.snap
+++ b/src/reset-password/tests/__snapshots__/ResetPasswordPage.test.jsx.snap
@@ -58,6 +58,122 @@ exports[`ResetPasswordPage should match invalid token message section snapshot 1
`;
+exports[`ResetPasswordPage should match pending reset message section snapshot 1`] = `
+
+`;
+
exports[`ResetPasswordPage should match reset password default section snapshot 1`] = `
@@ -345,12 +467,18 @@ Array [