diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2021-04-27 15:51:18 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-04-29 20:03:27 +0000 |
commit | 3848fcffa478531ffb167049965122e7844c8175 (patch) | |
tree | 005d04910b65862815531cfa62ac9bcebef89254 /server/sonar-web | |
parent | 64cf912b1ce5e4b5db2ed6366d7c046daddebcb0 (diff) | |
download | sonarqube-3848fcffa478531ffb167049965122e7844c8175.tar.gz sonarqube-3848fcffa478531ffb167049965122e7844c8175.zip |
SONAR-14604 Improve reset password form layout
Diffstat (limited to 'server/sonar-web')
12 files changed, 259 insertions, 238 deletions
diff --git a/server/sonar-web/src/main/js/app/components/ResetPassword.css b/server/sonar-web/src/main/js/app/components/ResetPassword.css deleted file mode 100644 index e0a344a0a42..00000000000 --- a/server/sonar-web/src/main/js/app/components/ResetPassword.css +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2021 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -.reset-page { - padding-top: 10vh; -} - -.reset-page h1 { - line-height: 1.5; - font-size: 24px; - font-weight: 300; - text-align: center; -} - -.reset-form { - width: 300px; - margin-left: auto; - margin-right: auto; -} diff --git a/server/sonar-web/src/main/js/app/components/ResetPassword.tsx b/server/sonar-web/src/main/js/app/components/ResetPassword.tsx index 9a55f23a4d2..55ef8b4c88f 100644 --- a/server/sonar-web/src/main/js/app/components/ResetPassword.tsx +++ b/server/sonar-web/src/main/js/app/components/ResetPassword.tsx @@ -19,30 +19,37 @@ */ import * as React from 'react'; import { translate } from 'sonar-ui-common/helpers/l10n'; -import ResetPasswordForm from '../../components/common/ResetPassword'; +import ResetPasswordForm from '../../components/common/ResetPasswordForm'; import { whenLoggedIn } from '../../components/hoc/whenLoggedIn'; +import { getBaseUrl } from '../../helpers/system'; import GlobalMessagesContainer from './GlobalMessagesContainer'; -import './ResetPassword.css'; export interface ResetPasswordProps { currentUser: T.LoggedInUser; } -export function ResetPassword(props: ResetPasswordProps) { - const { currentUser } = props; - const redirect = () => { - window.location.href = `/`; // force a refresh for the backend to handle additional redirects - }; - +export function ResetPassword({ currentUser }: ResetPasswordProps) { return ( - <div className="reset-page"> - <h1 className="text-center spacer-bottom">{translate('my_account.reset_password')}</h1> - <h2 className="text-center huge-spacer-bottom"> - {translate('my_account.reset_password.explain')} - </h2> - <GlobalMessagesContainer /> - <div className="reset-form"> - <ResetPasswordForm user={currentUser} onPasswordChange={redirect} /> + <div className="page-wrapper-simple"> + <div className="page-simple"> + <GlobalMessagesContainer /> + + <h1 className="text-center huge">{translate('my_account.reset_password')}</h1> + <p className="text-center huge-spacer-top huge-spacer-bottom"> + {translate('my_account.reset_password.explain')} + </p> + + <div className="text-center"> + <h2 className="big-spacer-bottom big">{translate('my_profile.password.title')}</h2> + + <ResetPasswordForm + user={currentUser} + onPasswordChange={() => { + // Force a refresh for the backend to handle additional redirects. + window.location.href = getBaseUrl() + '/'; + }} + /> + </div> </div> </div> ); diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ResetPassword-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ResetPassword-test.tsx index 0f3093f3f51..56b33efcc57 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/ResetPassword-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/ResetPassword-test.tsx @@ -19,13 +19,50 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import ResetPasswordForm from '../../../components/common/ResetPasswordForm'; import { mockLoggedInUser } from '../../../helpers/testMocks'; import { ResetPassword, ResetPasswordProps } from '../ResetPassword'; +jest.mock('../../../helpers/system', () => ({ + getBaseUrl: jest.fn().mockReturnValue('/context') +})); + +const originalLocation = window.location; + +beforeAll(() => { + const location = { + ...window.location, + href: null + }; + Object.defineProperty(window, 'location', { + writable: true, + value: location + }); +}); + +afterAll(() => { + Object.defineProperty(window, 'location', { + writable: true, + value: originalLocation + }); +}); + it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot(); }); +it('should navigate to the homepage after submission', () => { + const wrapper = shallowRender(); + const form = wrapper.find(ResetPasswordForm); + const { onPasswordChange } = form.props(); + + if (onPasswordChange) { + onPasswordChange(); + } + + expect(window.location.href).toBe('/context/'); +}); + function shallowRender(props: Partial<ResetPasswordProps> = {}) { return shallow(<ResetPassword currentUser={mockLoggedInUser()} {...props} />); } diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap index ad2db130e85..cf445a7dcb5 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap @@ -2,34 +2,43 @@ exports[`should render correctly 1`] = ` <div - className="reset-page" + className="page-wrapper-simple" > - <h1 - className="text-center spacer-bottom" - > - my_account.reset_password - </h1> - <h2 - className="text-center huge-spacer-bottom" - > - my_account.reset_password.explain - </h2> - <Connect(GlobalMessages) /> <div - className="reset-form" + className="page-simple" > - <ResetPassword - onPasswordChange={[Function]} - user={ - Object { - "groups": Array [], - "isLoggedIn": true, - "login": "luke", - "name": "Skywalker", - "scmAccounts": Array [], + <Connect(GlobalMessages) /> + <h1 + className="text-center huge" + > + my_account.reset_password + </h1> + <p + className="text-center huge-spacer-top huge-spacer-bottom" + > + my_account.reset_password.explain + </p> + <div + className="text-center" + > + <h2 + className="big-spacer-bottom big" + > + my_profile.password.title + </h2> + <ResetPasswordForm + onPasswordChange={[Function]} + user={ + Object { + "groups": Array [], + "isLoggedIn": true, + "login": "luke", + "name": "Skywalker", + "scmAccounts": Array [], + } } - } - /> + /> + </div> </div> </div> `; diff --git a/server/sonar-web/src/main/js/apps/account/components/Security.tsx b/server/sonar-web/src/main/js/apps/account/components/Security.tsx index e6c0af20238..b5300960175 100644 --- a/server/sonar-web/src/main/js/apps/account/components/Security.tsx +++ b/server/sonar-web/src/main/js/apps/account/components/Security.tsx @@ -21,7 +21,7 @@ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import { connect } from 'react-redux'; import { translate } from 'sonar-ui-common/helpers/l10n'; -import ResetPassword from '../../../components/common/ResetPassword'; +import ResetPasswordForm from '../../../components/common/ResetPasswordForm'; import { getCurrentUser, Store } from '../../../store/rootReducer'; import Tokens from './Tokens'; @@ -34,7 +34,12 @@ export function Security({ user }: SecurityProps) { <div className="account-body account-container"> <Helmet defer={false} title={translate('my_account.security')} /> <Tokens login={user.login} /> - {user.local && <ResetPassword user={user} />} + {user.local && ( + <section className="boxed-group"> + <h2 className="spacer-bottom">{translate('my_profile.password.title')}</h2> + <ResetPasswordForm className="boxed-group-inner" user={user} /> + </section> + )} </div> ); } diff --git a/server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Security-test.tsx.snap b/server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Security-test.tsx.snap index 4adb1b2c1c2..c120e9b1c33 100644 --- a/server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Security-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Security-test.tsx.snap @@ -12,18 +12,28 @@ exports[`should render correctly: local user 1`] = ` <Tokens login="luke" /> - <ResetPassword - user={ - Object { - "groups": Array [], - "isLoggedIn": true, - "local": true, - "login": "luke", - "name": "Skywalker", - "scmAccounts": Array [], + <section + className="boxed-group" + > + <h2 + className="spacer-bottom" + > + my_profile.password.title + </h2> + <ResetPasswordForm + className="boxed-group-inner" + user={ + Object { + "groups": Array [], + "isLoggedIn": true, + "local": true, + "login": "luke", + "name": "Skywalker", + "scmAccounts": Array [], + } } - } - /> + /> + </section> </div> `; diff --git a/server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx b/server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx index 0f01456ccba..cee0dce669e 100644 --- a/server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx @@ -137,7 +137,7 @@ export default function ChangeAdminPasswordAppRenderer(props: ChangeAdminPasswor <div className="form-field"> <SubmitButton disabled={!canSubmit || submitting}> - {translate('change_verb')} + {translate('update_verb')} {submitting && <i className="spinner spacer-left" />} </SubmitButton> </div> diff --git a/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordAppRenderer-test.tsx.snap index 5c5895a5782..836b65c7375 100644 --- a/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordAppRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordAppRenderer-test.tsx.snap @@ -79,7 +79,7 @@ exports[`should render correctly: cannot submit 1`] = ` <SubmitButton disabled={true} > - change_verb + update_verb </SubmitButton> </div> </form> @@ -164,7 +164,7 @@ exports[`should render correctly: default 1`] = ` <SubmitButton disabled={false} > - change_verb + update_verb </SubmitButton> </div> </form> @@ -249,7 +249,7 @@ exports[`should render correctly: submitting 1`] = ` <SubmitButton disabled={true} > - change_verb + update_verb <i className="spinner spacer-left" /> @@ -368,7 +368,7 @@ exports[`should render correctly: trying to use default admin password 1`] = ` <SubmitButton disabled={false} > - change_verb + update_verb </SubmitButton> </div> </form> diff --git a/server/sonar-web/src/main/js/components/common/ResetPassword.tsx b/server/sonar-web/src/main/js/components/common/ResetPasswordForm.tsx index ff59d0a1173..8f46d2f8650 100644 --- a/server/sonar-web/src/main/js/components/common/ResetPassword.tsx +++ b/server/sonar-web/src/main/js/components/common/ResetPasswordForm.tsx @@ -26,6 +26,7 @@ import { translate } from 'sonar-ui-common/helpers/l10n'; import { changePassword } from '../../api/users'; interface Props { + className?: string; user: T.LoggedInUser; onPasswordChange?: () => void; } @@ -35,7 +36,7 @@ interface State { success: boolean; } -export default class ResetPassword extends React.Component<Props, State> { +export default class ResetPasswordForm extends React.Component<Props, State> { oldPassword: HTMLInputElement | null = null; password: HTMLInputElement | null = null; passwordConfirmation: HTMLInputElement | null = null; @@ -84,72 +85,69 @@ export default class ResetPassword extends React.Component<Props, State> { }; render() { + const { className } = this.props; const { success, errors } = this.state; return ( - <section className="boxed-group"> - <h2 className="spacer-bottom">{translate('my_profile.password.title')}</h2> + <form className={className} onSubmit={this.handleChangePassword}> + {success && <Alert variant="success">{translate('my_profile.password.changed')}</Alert>} - <form className="boxed-group-inner" onSubmit={this.handleChangePassword}> - {success && <Alert variant="success">{translate('my_profile.password.changed')}</Alert>} + {errors && + errors.map((e, i) => ( + /* eslint-disable-next-line react/no-array-index-key */ + <Alert key={i} variant="error"> + {e} + </Alert> + ))} - {errors && - errors.map((e, i) => ( - /* eslint-disable-next-line react/no-array-index-key */ - <Alert key={i} variant="error"> - {e} - </Alert> - ))} + <MandatoryFieldsExplanation className="form-field" /> - <MandatoryFieldsExplanation className="form-field" /> - - <div className="form-field"> - <label htmlFor="old_password"> - {translate('my_profile.password.old')} - <MandatoryFieldMarker /> - </label> - <input - autoComplete="off" - id="old_password" - name="old_password" - ref={elem => (this.oldPassword = elem)} - required={true} - type="password" - /> - </div> - <div className="form-field"> - <label htmlFor="password"> - {translate('my_profile.password.new')} - <MandatoryFieldMarker /> - </label> - <input - autoComplete="off" - id="password" - name="password" - ref={elem => (this.password = elem)} - required={true} - type="password" - /> - </div> - <div className="form-field"> - <label htmlFor="password_confirmation"> - {translate('my_profile.password.confirm')} - <MandatoryFieldMarker /> - </label> - <input - autoComplete="off" - id="password_confirmation" - name="password_confirmation" - ref={elem => (this.passwordConfirmation = elem)} - required={true} - type="password" - /> - </div> - <div className="form-field"> - <SubmitButton id="change-password">{translate('update_verb')}</SubmitButton> - </div> - </form> - </section> + <div className="form-field"> + <label htmlFor="old_password"> + {translate('my_profile.password.old')} + <MandatoryFieldMarker /> + </label> + <input + autoComplete="off" + id="old_password" + name="old_password" + ref={elem => (this.oldPassword = elem)} + required={true} + type="password" + /> + </div> + <div className="form-field"> + <label htmlFor="password"> + {translate('my_profile.password.new')} + <MandatoryFieldMarker /> + </label> + <input + autoComplete="off" + id="password" + name="password" + ref={elem => (this.password = elem)} + required={true} + type="password" + /> + </div> + <div className="form-field"> + <label htmlFor="password_confirmation"> + {translate('my_profile.password.confirm')} + <MandatoryFieldMarker /> + </label> + <input + autoComplete="off" + id="password_confirmation" + name="password_confirmation" + ref={elem => (this.passwordConfirmation = elem)} + required={true} + type="password" + /> + </div> + <div className="form-field"> + <SubmitButton id="change-password">{translate('update_verb')}</SubmitButton> + </div> + </form> ); } } diff --git a/server/sonar-web/src/main/js/components/common/__tests__/ResetPassword-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/ResetPasswordForm-test.tsx index 904f693ed69..e940f20395e 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/ResetPassword-test.tsx +++ b/server/sonar-web/src/main/js/components/common/__tests__/ResetPasswordForm-test.tsx @@ -22,7 +22,7 @@ import * as React from 'react'; import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; import { changePassword } from '../../../api/users'; import { mockEvent, mockLoggedInUser } from '../../../helpers/testMocks'; -import ResetPassword from '../ResetPassword'; +import ResetPasswordForm from '../ResetPasswordForm'; jest.mock('../../../api/users', () => ({ changePassword: jest.fn().mockResolvedValue({}) @@ -74,6 +74,6 @@ it('renders correctly', () => { expect(shallowRender()).toMatchSnapshot(); }); -function shallowRender(props?: Partial<ResetPassword['props']>) { - return shallow<ResetPassword>(<ResetPassword user={mockLoggedInUser()} {...props} />); +function shallowRender(props?: Partial<ResetPasswordForm['props']>) { + return shallow<ResetPasswordForm>(<ResetPasswordForm user={mockLoggedInUser()} {...props} />); } diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ResetPassword-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ResetPassword-test.tsx.snap deleted file mode 100644 index f2fbe03c093..00000000000 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ResetPassword-test.tsx.snap +++ /dev/null @@ -1,81 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders correctly 1`] = ` -<section - className="boxed-group" -> - <h2 - className="spacer-bottom" - > - my_profile.password.title - </h2> - <form - className="boxed-group-inner" - onSubmit={[Function]} - > - <MandatoryFieldsExplanation - className="form-field" - /> - <div - className="form-field" - > - <label - htmlFor="old_password" - > - my_profile.password.old - <MandatoryFieldMarker /> - </label> - <input - autoComplete="off" - id="old_password" - name="old_password" - required={true} - type="password" - /> - </div> - <div - className="form-field" - > - <label - htmlFor="password" - > - my_profile.password.new - <MandatoryFieldMarker /> - </label> - <input - autoComplete="off" - id="password" - name="password" - required={true} - type="password" - /> - </div> - <div - className="form-field" - > - <label - htmlFor="password_confirmation" - > - my_profile.password.confirm - <MandatoryFieldMarker /> - </label> - <input - autoComplete="off" - id="password_confirmation" - name="password_confirmation" - required={true} - type="password" - /> - </div> - <div - className="form-field" - > - <SubmitButton - id="change-password" - > - update_verb - </SubmitButton> - </div> - </form> -</section> -`; diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ResetPasswordForm-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ResetPasswordForm-test.tsx.snap new file mode 100644 index 00000000000..3f32833747d --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ResetPasswordForm-test.tsx.snap @@ -0,0 +1,71 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<form + onSubmit={[Function]} +> + <MandatoryFieldsExplanation + className="form-field" + /> + <div + className="form-field" + > + <label + htmlFor="old_password" + > + my_profile.password.old + <MandatoryFieldMarker /> + </label> + <input + autoComplete="off" + id="old_password" + name="old_password" + required={true} + type="password" + /> + </div> + <div + className="form-field" + > + <label + htmlFor="password" + > + my_profile.password.new + <MandatoryFieldMarker /> + </label> + <input + autoComplete="off" + id="password" + name="password" + required={true} + type="password" + /> + </div> + <div + className="form-field" + > + <label + htmlFor="password_confirmation" + > + my_profile.password.confirm + <MandatoryFieldMarker /> + </label> + <input + autoComplete="off" + id="password_confirmation" + name="password_confirmation" + required={true} + type="password" + /> + </div> + <div + className="form-field" + > + <SubmitButton + id="change-password" + > + update_verb + </SubmitButton> + </div> +</form> +`; |