mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-03 20:40:00 -05:00
* MM-66925 - improve user email and password modals
* adjust error modal styling
* adjust e2e tests
(cherry picked from commit 1a21d34aab)
Co-authored-by: Pablo Vélez <pablovv2012@gmail.com>
This commit is contained in:
parent
69b019d53e
commit
6145da452c
15 changed files with 567 additions and 947 deletions
|
|
@ -134,9 +134,9 @@ describe('Guest Account - Guest User Invitation Flow', () => {
|
|||
cy.findByText('Update email').should('be.visible').click();
|
||||
|
||||
// * Update email outside whitelisted domain and verify error message
|
||||
cy.findByTestId('resetEmailModal').should('be.visible').within(() => {
|
||||
cy.findByTestId('resetEmailForm').should('be.visible').get('input').type(email);
|
||||
cy.findByTestId('resetEmailButton').click();
|
||||
cy.get('#resetEmailModal').should('be.visible').within(() => {
|
||||
cy.get('input[type="email"]').type(email);
|
||||
cy.get('button.btn-primary.confirm').click();
|
||||
cy.get('.error').should('be.visible').and('have.text', 'The email you provided does not belong to an accepted domain for guest accounts. Please contact your administrator or sign up with a different email.');
|
||||
cy.get('.close').click();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -85,9 +85,9 @@ describe('Guest Account - Verify Manage Guest Users', () => {
|
|||
|
||||
// * Update email of Guest User
|
||||
const email = `temp-${getRandomId()}@mattermost.com`;
|
||||
cy.findByTestId('resetEmailModal').should('be.visible').within(() => {
|
||||
cy.findByTestId('resetEmailForm').should('be.visible').get('input').type(email);
|
||||
cy.findByTestId('resetEmailButton').click();
|
||||
cy.get('#resetEmailModal').should('be.visible').within(() => {
|
||||
cy.get('input[type="email"]').type(email);
|
||||
cy.get('button.btn-primary.confirm').click();
|
||||
});
|
||||
|
||||
// * Verify if Guest's email was updated
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ describe('System Console > User Management > Users', () => {
|
|||
|
||||
// # Type new password and submit.
|
||||
cy.get('input[type=password]').type('new' + testUser.password);
|
||||
cy.get('button[type=submit]').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('button.btn-primary.confirm').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Log out.
|
||||
cy.apiLogout();
|
||||
|
|
@ -137,10 +137,10 @@ describe('System Console > User Management > Users', () => {
|
|||
cy.get('input[type=password]').eq(1).type('new' + otherAdmin.password);
|
||||
|
||||
// # Click the 'Reset' button.
|
||||
cy.get('button[type=submit] span').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('button.btn-primary.confirm').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Verify the appropriate error is returned.
|
||||
cy.get('form.form-horizontal').find('.has-error p.error').should('be.visible').
|
||||
// * Verify the appropriate error is returned (current password error shows in modal header area).
|
||||
cy.get('.genericModalError .error').should('be.visible').
|
||||
and('contain', 'The "Current Password" you entered is incorrect. Please check that Caps Lock is off and try again.');
|
||||
});
|
||||
|
||||
|
|
@ -160,11 +160,10 @@ describe('System Console > User Management > Users', () => {
|
|||
cy.get('input[type=password]').eq(1).type('new');
|
||||
|
||||
// # Click the 'Reset' button.
|
||||
cy.get('button[type=submit] span').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('button.btn-primary.confirm').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Verify the appropriate error is returned.
|
||||
cy.get('form.form-horizontal').find('.has-error p.error').should('be.visible').
|
||||
and('contain', 'Your password must be 5-72 characters long.');
|
||||
// * Verify the appropriate error is returned (new password error shows under the input).
|
||||
cy.get('.Input___error').should('be.visible').and('contain', 'characters long');
|
||||
});
|
||||
|
||||
it('MM-T936 Users - System admin changes own password - Blank fields', () => {
|
||||
|
|
@ -179,21 +178,20 @@ describe('System Console > User Management > Users', () => {
|
|||
cy.findByText('Reset password').click();
|
||||
|
||||
// # Click the 'Reset' button.
|
||||
cy.get('button[type=submit] span').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('button.btn-primary.confirm').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Verify the appropriate error is returned.
|
||||
cy.get('form.form-horizontal').find('.has-error p.error').should('be.visible').
|
||||
// * Verify the appropriate error is returned (current password missing).
|
||||
cy.get('.genericModalError .error').should('be.visible').
|
||||
and('contain', 'Please enter your current password.');
|
||||
|
||||
// # Type current password, leave new password blank.
|
||||
cy.get('input[type=password]').eq(0).type(otherAdmin.password);
|
||||
|
||||
// # Click the 'Reset' button.
|
||||
cy.get('button[type=submit] span').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('button.btn-primary.confirm').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Verify the appropriate error is returned.
|
||||
cy.get('form.form-horizontal').find('.has-error p.error').should('be.visible').
|
||||
and('contain', 'Your password must be 5-72 characters long.');
|
||||
// * Verify the appropriate error is returned (new password error shows under the input).
|
||||
cy.get('.Input___error').should('be.visible').and('contain', 'characters long');
|
||||
});
|
||||
|
||||
it('MM-T937 Users - System admin changes own password - Successfully changed', () => {
|
||||
|
|
@ -212,7 +210,7 @@ describe('System Console > User Management > Users', () => {
|
|||
cy.get('input[type=password]').eq(1).type('new' + otherAdmin.password);
|
||||
|
||||
// # Click the 'Reset' button.
|
||||
cy.get('button[type=submit] span').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('button.btn-primary.confirm').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Log out.
|
||||
cy.apiLogout();
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ describe('User Management', () => {
|
|||
|
||||
// # Set new password.
|
||||
cy.get('input[type=password]').type('new' + testUser.password);
|
||||
cy.get('button[type=submit]').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('button.btn-primary.confirm').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Verify Update email option is visible.
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
|
@ -262,22 +262,22 @@ describe('User Management', () => {
|
|||
cy.findByText('Update email').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Verify the modal opened.
|
||||
cy.findByTestId('resetEmailModal').should('exist');
|
||||
cy.get('#resetEmailModal').should('exist');
|
||||
|
||||
// # Type the new e-mail address.
|
||||
if (newEmail.length > 0) {
|
||||
cy.get('input[type=email]').eq(0).clear().type(newEmail);
|
||||
}
|
||||
|
||||
// # Click the "Reset" button.
|
||||
cy.findByTestId('resetEmailButton').click();
|
||||
// # Click the "Update" button.
|
||||
cy.get('button.btn-primary.confirm').click();
|
||||
|
||||
// * Check for the error messages, if any.
|
||||
if (errorMsg.length > 0) {
|
||||
cy.get('form.form-horizontal').find('.has-error p.error').should('be.visible').and('contain', errorMsg);
|
||||
cy.get('.Input___error').should('be.visible').and('contain', errorMsg);
|
||||
|
||||
// # Close the modal.
|
||||
cy.findByLabelText('Close').click();
|
||||
cy.get('button.close').click();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,4 +85,8 @@ export default class SystemConsolePage {
|
|||
async clickResetButton() {
|
||||
await this.saveChangesModal.container.locator('button.btn-primary:has-text("Reset")').click();
|
||||
}
|
||||
|
||||
async clickUpdateEmailButton() {
|
||||
await this.saveChangesModal.container.locator('button.btn-primary:has-text("Update")').click();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,10 +172,10 @@ test('MM-T5520-5 should change the users email', async ({pw}) => {
|
|||
const updateEmail = await systemConsolePage.systemUsersActionMenus[0].getMenuItem('Update email');
|
||||
await updateEmail.click();
|
||||
|
||||
// # Enter a random password and click Save
|
||||
// # Enter new email and click Update
|
||||
const emailInput = systemConsolePage.page.locator('input[type="email"]');
|
||||
await emailInput.fill(newEmail);
|
||||
await systemConsolePage.clickResetButton();
|
||||
await systemConsolePage.clickUpdateEmailButton();
|
||||
|
||||
// * Verify that the modal closed
|
||||
await emailInput.waitFor({state: 'detached'});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Shared styles for admin console modals that use the Input component
|
||||
// Used by: ResetPasswordModal, ResetEmailModal
|
||||
|
||||
.ResetPasswordModal,
|
||||
.ResetEmailModal {
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
// Override Input component styles to work properly in admin console modal
|
||||
// The Input component uses a fieldset for the border, so we need to remove
|
||||
// the default form-control border styling that GenericModal applies
|
||||
.Input_container {
|
||||
.Input_fieldset {
|
||||
background-color: var(--center-channel-bg);
|
||||
}
|
||||
|
||||
.Input_legend {
|
||||
background-color: var(--center-channel-bg);
|
||||
}
|
||||
|
||||
.Input_wrapper {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
// Remove the GenericModal form-control border styling since Input uses fieldset for border
|
||||
// Also remove any background color that might be inherited
|
||||
.Input.form-control,
|
||||
.form-control,
|
||||
input.form-control,
|
||||
input {
|
||||
height: auto;
|
||||
border: none !important;
|
||||
background: none !important;
|
||||
background-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
&:focus {
|
||||
border: none !important;
|
||||
background: none !important;
|
||||
background-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: none !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`components/admin_console/reset_email_modal/reset_email_modal.tsx should match snapshot when not the current user 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="resetEmailModalLabel"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
data-testid="resetEmailModal"
|
||||
dialogClassName="a11y__modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={true}
|
||||
>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
closeButton={true}
|
||||
closeLabel="Close"
|
||||
>
|
||||
<ModalTitle
|
||||
bsClass="modal-title"
|
||||
componentClass="h1"
|
||||
id="resetEmailModalLabel"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Update Email"
|
||||
id="admin.reset_email.titleReset"
|
||||
/>
|
||||
</ModalTitle>
|
||||
</ModalHeader>
|
||||
<form
|
||||
className="form-horizontal"
|
||||
role="form"
|
||||
>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="col-sm-10"
|
||||
>
|
||||
<div
|
||||
className="input-group input-group--limit mb-5"
|
||||
data-testid="resetEmailForm"
|
||||
>
|
||||
<span
|
||||
className="input-group-addon email__group-addon"
|
||||
data-toggle="tooltip"
|
||||
title="New Email"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="New Email"
|
||||
id="admin.reset_email.newEmail"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
autoFocus={true}
|
||||
className="form-control"
|
||||
maxLength={128}
|
||||
type="email"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter
|
||||
bsClass="modal-footer"
|
||||
componentClass="div"
|
||||
>
|
||||
<button
|
||||
className="btn btn-tertiary"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
id="admin.reset_email.cancel"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
data-testid="resetEmailButton"
|
||||
onClick={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Reset"
|
||||
id="admin.reset_email.reset"
|
||||
/>
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Modal>
|
||||
`;
|
||||
|
||||
exports[`components/admin_console/reset_email_modal/reset_email_modal.tsx should match snapshot when the current user 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="resetEmailModalLabel"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
data-testid="resetEmailModal"
|
||||
dialogClassName="a11y__modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={true}
|
||||
>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
closeButton={true}
|
||||
closeLabel="Close"
|
||||
>
|
||||
<ModalTitle
|
||||
bsClass="modal-title"
|
||||
componentClass="h1"
|
||||
id="resetEmailModalLabel"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Update Email"
|
||||
id="admin.reset_email.titleReset"
|
||||
/>
|
||||
</ModalTitle>
|
||||
</ModalHeader>
|
||||
<form
|
||||
className="form-horizontal"
|
||||
role="form"
|
||||
>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="col-sm-10"
|
||||
>
|
||||
<div
|
||||
className="input-group input-group--limit mb-5"
|
||||
data-testid="resetEmailForm"
|
||||
>
|
||||
<span
|
||||
className="input-group-addon email__group-addon"
|
||||
data-toggle="tooltip"
|
||||
title="New Email"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="New Email"
|
||||
id="admin.reset_email.newEmail"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
autoFocus={true}
|
||||
className="form-control"
|
||||
maxLength={128}
|
||||
type="email"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="input-group input-group--limit mb-5"
|
||||
data-testid="resetEmailForm"
|
||||
>
|
||||
<span
|
||||
className="input-group-addon email__group-addon"
|
||||
data-toggle="tooltip"
|
||||
title="Current Password"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Current Password"
|
||||
id="admin.reset_email.currentPassword"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
className="form-control"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter
|
||||
bsClass="modal-footer"
|
||||
componentClass="div"
|
||||
>
|
||||
<button
|
||||
className="btn btn-tertiary"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
id="admin.reset_email.cancel"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
data-testid="resetEmailButton"
|
||||
onClick={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Reset"
|
||||
id="admin.reset_email.reset"
|
||||
/>
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Modal>
|
||||
`;
|
||||
|
||||
exports[`components/admin_console/reset_email_modal/reset_email_modal.tsx should match snapshot when there is no user 1`] = `<div />`;
|
||||
|
|
@ -1,113 +1,136 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import ResetEmailModal from './reset_email_modal';
|
||||
|
||||
describe('components/admin_console/reset_email_modal/reset_email_modal.tsx', () => {
|
||||
const user: UserProfile = TestHelper.getUserMock({
|
||||
id: 'user_id_1',
|
||||
email: 'arvin.darmawan@gmail.com',
|
||||
first_name: 'Arvin',
|
||||
last_name: 'Darmawan',
|
||||
});
|
||||
|
||||
const baseProps = {
|
||||
actions: {patchUser: jest.fn(() => Promise.resolve({}))},
|
||||
user,
|
||||
currentUserId: 'random_user_id',
|
||||
onHide: jest.fn(),
|
||||
onSuccess: jest.fn(),
|
||||
onExited: jest.fn(),
|
||||
};
|
||||
|
||||
test('should match snapshot when not the current user', () => {
|
||||
const wrapper = shallow(<ResetEmailModal {...baseProps}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should match snapshot when there is no user', () => {
|
||||
test('should render modal with user name in title', () => {
|
||||
renderWithContext(<ResetEmailModal {...baseProps}/>);
|
||||
|
||||
expect(screen.getByText(/Update email for Arvin Darmawan/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render null when there is no user', () => {
|
||||
const props = {...baseProps, user: undefined};
|
||||
const wrapper = shallow(<ResetEmailModal {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
const {container} = renderWithContext(<ResetEmailModal {...props}/>);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
test('should match snapshot when the current user', () => {
|
||||
test('should show password field when updating own email', () => {
|
||||
const props = {...baseProps, currentUserId: user.id};
|
||||
const wrapper = shallow(<ResetEmailModal {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
renderWithContext(<ResetEmailModal {...props}/>);
|
||||
|
||||
// Should have both email and password inputs
|
||||
const inputs = screen.getAllByRole('textbox');
|
||||
expect(inputs.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Password input won't have role="textbox", check by placeholder
|
||||
expect(screen.getByPlaceholderText(/Current password/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not update email since the email is empty', () => {
|
||||
const wrapper = mountWithIntl(<ResetEmailModal {...baseProps}/>);
|
||||
test('should not update email since the email is empty', async () => {
|
||||
renderWithContext(<ResetEmailModal {...baseProps}/>);
|
||||
|
||||
(wrapper.find('input[type=\'email\']').first().instance() as unknown as HTMLInputElement).value = '';
|
||||
wrapper.find('button[type=\'submit\']').first().simulate('click', {preventDefault: jest.fn()});
|
||||
// Click submit without entering email
|
||||
await userEvent.click(screen.getByRole('button', {name: /Update/i}));
|
||||
|
||||
expect(baseProps.actions.patchUser.mock.calls.length).toBe(0);
|
||||
expect(wrapper.state('error')).toStrictEqual(
|
||||
<FormattedMessage
|
||||
id='user.settings.general.validEmail'
|
||||
defaultMessage='Please enter a valid email address.'
|
||||
/>,
|
||||
);
|
||||
expect(baseProps.actions.patchUser).not.toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Please enter a valid email address/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should not update email since the email is invalid', () => {
|
||||
const wrapper = mountWithIntl(<ResetEmailModal {...baseProps}/>);
|
||||
test('should not update email since the email is invalid', async () => {
|
||||
renderWithContext(<ResetEmailModal {...baseProps}/>);
|
||||
|
||||
(wrapper.find('input[type=\'email\']').first().instance() as unknown as HTMLInputElement).value = 'invalid-email';
|
||||
wrapper.find('button[type=\'submit\']').first().simulate('click', {preventDefault: jest.fn()});
|
||||
const emailInput = screen.getByPlaceholderText(/Enter new email address/i);
|
||||
await userEvent.type(emailInput, 'invalid-email');
|
||||
await userEvent.click(screen.getByRole('button', {name: /Update/i}));
|
||||
|
||||
expect(baseProps.actions.patchUser.mock.calls.length).toBe(0);
|
||||
expect(wrapper.state('error')).toStrictEqual(
|
||||
<FormattedMessage
|
||||
id='user.settings.general.validEmail'
|
||||
defaultMessage='Please enter a valid email address.'
|
||||
/>,
|
||||
);
|
||||
expect(baseProps.actions.patchUser).not.toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Please enter a valid email address/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should require password when updating email of the current user', () => {
|
||||
test('should require password when updating email of the current user', async () => {
|
||||
const props = {...baseProps, currentUserId: user.id};
|
||||
const wrapper = mountWithIntl(<ResetEmailModal {...props}/>);
|
||||
renderWithContext(<ResetEmailModal {...props}/>);
|
||||
|
||||
(wrapper.find('input[type=\'email\']').first().instance() as unknown as HTMLInputElement).value = 'currentUser@test.com';
|
||||
wrapper.find('button[type=\'submit\']').first().simulate('click', {preventDefault: jest.fn()});
|
||||
const emailInput = screen.getByPlaceholderText(/Enter new email address/i);
|
||||
await userEvent.type(emailInput, 'currentUser@test.com');
|
||||
await userEvent.click(screen.getByRole('button', {name: /Update/i}));
|
||||
|
||||
expect(baseProps.actions.patchUser.mock.calls.length).toBe(0);
|
||||
expect(wrapper.state('error')).toStrictEqual(
|
||||
<FormattedMessage
|
||||
id='admin.reset_email.missing_current_password'
|
||||
defaultMessage='Please enter your current password.'
|
||||
/>,
|
||||
);
|
||||
expect(baseProps.actions.patchUser).not.toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Please enter your current password/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should update email since the email is valid of the another user', () => {
|
||||
const wrapper = mountWithIntl(<ResetEmailModal {...baseProps}/>);
|
||||
test('should update email since the email is valid for another user', async () => {
|
||||
const patchUser = jest.fn(() => Promise.resolve({}));
|
||||
const props = {...baseProps, actions: {patchUser}};
|
||||
renderWithContext(<ResetEmailModal {...props}/>);
|
||||
|
||||
(wrapper.find('input[type=\'email\']').first().instance() as unknown as HTMLInputElement).value = 'user@test.com';
|
||||
wrapper.find('button[type=\'submit\']').first().simulate('click', {preventDefault: jest.fn()});
|
||||
const emailInput = screen.getByPlaceholderText(/Enter new email address/i);
|
||||
await userEvent.type(emailInput, 'user@test.com');
|
||||
await userEvent.click(screen.getByRole('button', {name: /Update/i}));
|
||||
|
||||
expect(baseProps.actions.patchUser.mock.calls.length).toBe(1);
|
||||
expect(wrapper.state('error')).toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(patchUser).toHaveBeenCalledTimes(1);
|
||||
expect(patchUser).toHaveBeenCalledWith(expect.objectContaining({
|
||||
email: 'user@test.com',
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
test('should update email since the email is valid of the current user', () => {
|
||||
const props = {...baseProps, currentUserId: user.id};
|
||||
const wrapper = mountWithIntl(<ResetEmailModal {...props}/>);
|
||||
test('should update email since the email is valid for the current user', async () => {
|
||||
const patchUser = jest.fn(() => Promise.resolve({}));
|
||||
const props = {...baseProps, currentUserId: user.id, actions: {patchUser}};
|
||||
renderWithContext(<ResetEmailModal {...props}/>);
|
||||
|
||||
(wrapper.find('input[type=\'email\']').first().instance() as unknown as HTMLInputElement).value = 'currentUser@test.com';
|
||||
(wrapper.find('input[type=\'password\']').first().instance() as unknown as HTMLInputElement).value = 'password';
|
||||
wrapper.find('button[type=\'submit\']').first().simulate('click', {preventDefault: jest.fn()});
|
||||
const emailInput = screen.getByPlaceholderText(/Enter new email address/i);
|
||||
await userEvent.type(emailInput, 'currentUser@test.com');
|
||||
|
||||
expect(baseProps.actions.patchUser.mock.calls.length).toBe(1);
|
||||
expect(wrapper.state('error')).toBeNull();
|
||||
const passwordInput = screen.getByPlaceholderText(/Current password/i);
|
||||
await userEvent.type(passwordInput, 'password123');
|
||||
|
||||
await userEvent.click(screen.getByRole('button', {name: /Update/i}));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(patchUser).toHaveBeenCalledTimes(1);
|
||||
expect(patchUser).toHaveBeenCalledWith(expect.objectContaining({
|
||||
email: 'currentuser@test.com', // lowercase
|
||||
password: 'password123',
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,21 +1,20 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import React, {useCallback, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import {GenericModal} from '@mattermost/components';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import {isEmail} from 'mattermost-redux/utils/helpers';
|
||||
|
||||
type State = {
|
||||
show: boolean;
|
||||
error: JSX.Element|string|null;
|
||||
isEmailError: boolean;
|
||||
isCurrentPasswordError: boolean;
|
||||
}
|
||||
import Input from 'components/widgets/inputs/input/input';
|
||||
|
||||
import {getFullName} from 'utils/utils';
|
||||
|
||||
import '../admin_modal_with_input.scss';
|
||||
|
||||
type Props = {
|
||||
user?: UserProfile;
|
||||
|
|
@ -27,220 +26,159 @@ type Props = {
|
|||
};
|
||||
}
|
||||
|
||||
export default class ResetEmailModal extends React.PureComponent<Props, State> {
|
||||
private emailRef: React.RefObject<HTMLInputElement>;
|
||||
private currentPasswordRef: React.RefObject<HTMLInputElement>;
|
||||
export default function ResetEmailModal({
|
||||
user,
|
||||
currentUserId,
|
||||
onSuccess,
|
||||
onExited,
|
||||
actions,
|
||||
}: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
const [show, setShow] = useState(true);
|
||||
const [email, setEmail] = useState('');
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [error, setError] = useState<React.ReactNode>(null);
|
||||
const [emailError, setEmailError] = useState<React.ReactNode>(null);
|
||||
const [passwordError, setPasswordError] = useState<React.ReactNode>(null);
|
||||
|
||||
this.state = {
|
||||
show: true,
|
||||
error: null,
|
||||
isEmailError: false,
|
||||
isCurrentPasswordError: false,
|
||||
};
|
||||
const emailRef = useRef<HTMLInputElement>(null);
|
||||
const currentPasswordRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
this.emailRef = React.createRef();
|
||||
this.currentPasswordRef = React.createRef();
|
||||
}
|
||||
const isUpdatingOwnEmail = user?.id === currentUserId;
|
||||
|
||||
private isEmailValid = (): boolean => {
|
||||
if (!this.emailRef.current || !this.emailRef.current.value || !isEmail(this.emailRef.current.value)) {
|
||||
const errMsg = (
|
||||
<FormattedMessage
|
||||
id='user.settings.general.validEmail'
|
||||
defaultMessage='Please enter a valid email address.'
|
||||
/>
|
||||
);
|
||||
this.setState({error: errMsg, isEmailError: true});
|
||||
return false;
|
||||
}
|
||||
const handleCancel = useCallback(() => {
|
||||
setShow(false);
|
||||
}, []);
|
||||
|
||||
this.setState({error: null, isEmailError: false});
|
||||
return true;
|
||||
};
|
||||
|
||||
private isCurrentPasswordValid = (): boolean => {
|
||||
if (!this.currentPasswordRef.current || !this.currentPasswordRef.current.value) {
|
||||
const errMsg = (
|
||||
<FormattedMessage
|
||||
id='admin.reset_email.missing_current_password'
|
||||
defaultMessage='Please enter your current password.'
|
||||
/>
|
||||
);
|
||||
|
||||
this.setState({error: errMsg, isCurrentPasswordError: true});
|
||||
return false;
|
||||
}
|
||||
this.setState({error: null, isCurrentPasswordError: false});
|
||||
return true;
|
||||
};
|
||||
|
||||
private doSubmit = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
if (!this.props.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isEmailValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = {
|
||||
...this.props.user,
|
||||
email: (this.emailRef.current as HTMLInputElement).value.trim().toLowerCase(),
|
||||
};
|
||||
|
||||
if (this.props.user?.id === this.props.currentUserId) {
|
||||
if (!this.isCurrentPasswordValid()) {
|
||||
return;
|
||||
}
|
||||
user.password = (this.currentPasswordRef.current as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
const result = await this.props.actions.patchUser(user);
|
||||
if ('error' in result) {
|
||||
this.setState({
|
||||
error: result.error.message,
|
||||
isEmailError: result.error.server_error_id === 'app.user.save.email_exists.app_error',
|
||||
isCurrentPasswordError: result.error.server_error_id === 'api.user.check_user_password.invalid.app_error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onSuccess(user.email);
|
||||
this.setState({show: false});
|
||||
};
|
||||
|
||||
private doCancel = (): void => {
|
||||
this.setState({
|
||||
show: false,
|
||||
error: null,
|
||||
});
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const user = this.props.user;
|
||||
const handleConfirm = useCallback(async () => {
|
||||
if (!user) {
|
||||
return <div/>;
|
||||
return;
|
||||
}
|
||||
|
||||
const groupClass = 'input-group input-group--limit mb-5';
|
||||
// Clear previous errors
|
||||
setError(null);
|
||||
setEmailError(null);
|
||||
setPasswordError(null);
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='admin.reset_email.titleReset'
|
||||
defaultMessage='Update Email'
|
||||
/>
|
||||
);
|
||||
// Validate email
|
||||
if (!email || !isEmail(email)) {
|
||||
setEmailError(formatMessage({
|
||||
id: 'user.settings.general.validEmail',
|
||||
defaultMessage: 'Please enter a valid email address.',
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
dialogClassName='a11y__modal'
|
||||
show={this.state.show}
|
||||
onHide={this.doCancel}
|
||||
onExited={this.props.onExited}
|
||||
role='none'
|
||||
aria-labelledby='resetEmailModalLabel'
|
||||
data-testid='resetEmailModal'
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title
|
||||
componentClass='h1'
|
||||
id='resetEmailModalLabel'
|
||||
>
|
||||
{title}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<form
|
||||
role='form'
|
||||
className='form-horizontal'
|
||||
>
|
||||
<Modal.Body>
|
||||
<div className='form-group'>
|
||||
<div className='col-sm-10'>
|
||||
<div
|
||||
className={`${groupClass}${this.state.isEmailError ? ' has-error' : ''}`}
|
||||
data-testid='resetEmailForm'
|
||||
>
|
||||
<span
|
||||
data-toggle='tooltip'
|
||||
title='New Email'
|
||||
className='input-group-addon email__group-addon'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.reset_email.newEmail'
|
||||
defaultMessage='New Email'
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
type='email'
|
||||
ref={this.emailRef}
|
||||
className='form-control'
|
||||
maxLength={128}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
// Validate current password if updating own email
|
||||
if (isUpdatingOwnEmail && !currentPassword) {
|
||||
setPasswordError(formatMessage({
|
||||
id: 'admin.reset_email.missing_current_password',
|
||||
defaultMessage: 'Please enter your current password.',
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
{this.props.user?.id === this.props.currentUserId && (
|
||||
<div
|
||||
className={`${groupClass}${this.state.isCurrentPasswordError ? ' has-error' : ''}`}
|
||||
data-testid='resetEmailForm'
|
||||
>
|
||||
<span
|
||||
data-toggle='tooltip'
|
||||
title='Current Password'
|
||||
className='input-group-addon email__group-addon'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.reset_email.currentPassword'
|
||||
defaultMessage='Current Password'
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
type='password'
|
||||
ref={this.currentPasswordRef}
|
||||
className='form-control'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
const updatedUser: UserProfile = {
|
||||
...user,
|
||||
email: email.trim().toLowerCase(),
|
||||
};
|
||||
|
||||
{this.state.error && (
|
||||
<div className='has-error'>
|
||||
<p className='input__help error'>
|
||||
{this.state.error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-tertiary'
|
||||
onClick={this.doCancel}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.reset_email.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={this.doSubmit}
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
data-testid='resetEmailButton'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.reset_email.reset'
|
||||
defaultMessage='Reset'
|
||||
/>
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
if (isUpdatingOwnEmail) {
|
||||
updatedUser.password = currentPassword;
|
||||
}
|
||||
|
||||
const result = await actions.patchUser(updatedUser);
|
||||
|
||||
if ('error' in result) {
|
||||
const isEmailError = result.error.server_error_id === 'app.user.save.email_exists.app_error';
|
||||
const isPasswordError = result.error.server_error_id === 'api.user.check_user_password.invalid.app_error';
|
||||
|
||||
if (isEmailError) {
|
||||
setEmailError(result.error.message);
|
||||
} else if (isPasswordError) {
|
||||
setPasswordError(result.error.message);
|
||||
} else {
|
||||
setError(result.error.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
onSuccess(updatedUser.email);
|
||||
setShow(false);
|
||||
}, [user, email, currentPassword, isUpdatingOwnEmail, actions, onSuccess, formatMessage]);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayName = getFullName(user) || user.username;
|
||||
|
||||
const title = formatMessage({
|
||||
id: 'admin.reset_email.titleResetFor',
|
||||
defaultMessage: 'Update email for {name}',
|
||||
}, {name: displayName});
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
id='resetEmailModal'
|
||||
className='ResetEmailModal'
|
||||
modalHeaderText={title}
|
||||
show={show}
|
||||
onExited={onExited}
|
||||
onHide={handleCancel}
|
||||
handleCancel={handleCancel}
|
||||
handleConfirm={handleConfirm}
|
||||
handleEnterKeyPress={handleConfirm}
|
||||
confirmButtonText={formatMessage({
|
||||
id: 'admin.reset_email.update',
|
||||
defaultMessage: 'Update',
|
||||
})}
|
||||
compassDesign={true}
|
||||
autoCloseOnConfirmButton={false}
|
||||
errorText={error ? <span className='error'>{error}</span> : undefined}
|
||||
dataTestId='resetEmailModal'
|
||||
>
|
||||
<div className='ResetEmailModal__body'>
|
||||
<Input
|
||||
ref={emailRef as React.Ref<HTMLInputElement>}
|
||||
type='email'
|
||||
name='newEmail'
|
||||
autoComplete='off'
|
||||
label={formatMessage({
|
||||
id: 'admin.reset_email.newEmail',
|
||||
defaultMessage: 'New email',
|
||||
})}
|
||||
placeholder={formatMessage({
|
||||
id: 'admin.reset_email.enterNewEmail',
|
||||
defaultMessage: 'Enter new email address',
|
||||
})}
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoFocus={true}
|
||||
maxLength={128}
|
||||
customMessage={emailError ? {type: 'error', value: emailError} : undefined}
|
||||
/>
|
||||
{isUpdatingOwnEmail && (
|
||||
<Input
|
||||
ref={currentPasswordRef as React.Ref<HTMLInputElement>}
|
||||
type='password'
|
||||
name='currentPassword'
|
||||
autoComplete='current-password'
|
||||
label={formatMessage({
|
||||
id: 'admin.reset_email.currentPassword',
|
||||
defaultMessage: 'Current password',
|
||||
})}
|
||||
placeholder={formatMessage({
|
||||
id: 'admin.reset_email.enterCurrentPassword',
|
||||
defaultMessage: 'Enter current password',
|
||||
})}
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
customMessage={passwordError ? {type: 'error', value: passwordError} : undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</GenericModal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,137 +0,0 @@
|
|||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`components/admin_console/reset_password_modal/reset_password_modal.tsx should match snapshot 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="resetPasswordModalLabel"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={true}
|
||||
>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
closeButton={true}
|
||||
closeLabel="Close"
|
||||
>
|
||||
<ModalTitle
|
||||
bsClass="modal-title"
|
||||
componentClass="h1"
|
||||
id="resetPasswordModalLabel"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Switch Account to Email/Password"
|
||||
id="admin.reset_password.titleSwitch"
|
||||
/>
|
||||
</ModalTitle>
|
||||
</ModalHeader>
|
||||
<form
|
||||
className="form-horizontal"
|
||||
role="form"
|
||||
>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="col-sm-10 password__group-addon-space"
|
||||
>
|
||||
<div
|
||||
className="input-group input-group--limit"
|
||||
>
|
||||
<span
|
||||
className="input-group-addon password__group-addon"
|
||||
data-toggle="tooltip"
|
||||
title="Current Password"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Current Password"
|
||||
id="admin.reset_password.curentPassword"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
autoFocus={true}
|
||||
className="form-control"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="col-sm-10"
|
||||
>
|
||||
<div
|
||||
className="input-group input-group--limit"
|
||||
>
|
||||
<span
|
||||
className="input-group-addon password__group-addon"
|
||||
data-toggle="tooltip"
|
||||
title="New Password"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="New Password"
|
||||
id="admin.reset_password.newPassword"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
autoFocus={false}
|
||||
className="form-control"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter
|
||||
bsClass="modal-footer"
|
||||
componentClass="div"
|
||||
>
|
||||
<button
|
||||
className="btn btn-tertiary"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
id="admin.reset_password.cancel"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Reset"
|
||||
id="admin.reset_password.reset"
|
||||
/>
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Modal>
|
||||
`;
|
||||
|
||||
exports[`components/admin_console/reset_password_modal/reset_password_modal.tsx should match snapshot when there is no user 1`] = `<div />`;
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import type {UserNotifyProps, UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import ResetPasswordModal from './reset_password_modal';
|
||||
|
|
@ -27,16 +25,20 @@ describe('components/admin_console/reset_password_modal/reset_password_modal.tsx
|
|||
push: 'default',
|
||||
push_status: 'ooo',
|
||||
};
|
||||
|
||||
const user: UserProfile = TestHelper.getUserMock({
|
||||
auth_service: 'test',
|
||||
id: 'user_id_1',
|
||||
auth_service: '',
|
||||
notify_props: notifyProps,
|
||||
first_name: 'Test',
|
||||
last_name: 'User',
|
||||
username: 'testuser',
|
||||
});
|
||||
|
||||
const baseProps = {
|
||||
actions: {updateUserPassword: jest.fn(() => Promise.resolve({data: ''}))},
|
||||
currentUserId: user.id,
|
||||
user,
|
||||
onHide: jest.fn(),
|
||||
onExited: jest.fn(),
|
||||
passwordConfig: {
|
||||
minimumLength: 10,
|
||||
|
|
@ -47,65 +49,119 @@ describe('components/admin_console/reset_password_modal/reset_password_modal.tsx
|
|||
},
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<ResetPasswordModal {...baseProps}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should match snapshot when there is no user', () => {
|
||||
test('should render modal with user name in title', () => {
|
||||
renderWithContext(<ResetPasswordModal {...baseProps}/>);
|
||||
|
||||
expect(screen.getByText(/Reset password for Test User/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render null when there is no user', () => {
|
||||
const props = {...baseProps, user: undefined};
|
||||
const wrapper = shallow(
|
||||
<ResetPasswordModal {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
const {container} = renderWithContext(<ResetPasswordModal {...props}/>);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
test('should call updateUserPassword', () => {
|
||||
test('should show switch account title when user has auth_service', () => {
|
||||
const authUser = TestHelper.getUserMock({
|
||||
...user,
|
||||
auth_service: 'ldap',
|
||||
});
|
||||
const props = {...baseProps, user: authUser};
|
||||
renderWithContext(<ResetPasswordModal {...props}/>);
|
||||
|
||||
expect(screen.getByText(/Switch account to Email\/Password/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show current password field when resetting own password', () => {
|
||||
renderWithContext(<ResetPasswordModal {...baseProps}/>);
|
||||
|
||||
expect(screen.getByPlaceholderText(/Current password/i)).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText(/New password/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not show current password field when resetting another user password', () => {
|
||||
const props = {...baseProps, currentUserId: 'different_user_id'};
|
||||
renderWithContext(<ResetPasswordModal {...props}/>);
|
||||
|
||||
expect(screen.queryByPlaceholderText(/Current password/i)).not.toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText(/New password/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should call updateUserPassword with both passwords when resetting own password', async () => {
|
||||
const updateUserPassword = jest.fn(() => Promise.resolve({data: ''}));
|
||||
const oldPassword = 'oldPassword123!';
|
||||
const newPassword = 'newPassword123!';
|
||||
const props = {...baseProps, actions: {updateUserPassword}};
|
||||
const wrapper = mountWithIntl(<ResetPasswordModal {...props}/>);
|
||||
renderWithContext(<ResetPasswordModal {...props}/>);
|
||||
|
||||
(wrapper.find('input[type=\'password\']').first().instance() as unknown as HTMLInputElement).value = oldPassword;
|
||||
(wrapper.find('input[type=\'password\']').last().instance() as unknown as HTMLInputElement).value = newPassword;
|
||||
wrapper.find('button[type=\'submit\']').first().simulate('click', {preventDefault: jest.fn()});
|
||||
const currentPasswordInput = screen.getByPlaceholderText(/Current password/i);
|
||||
const newPasswordInput = screen.getByPlaceholderText(/New password/i);
|
||||
|
||||
expect(updateUserPassword.mock.calls.length).toBe(1);
|
||||
expect(wrapper.state('serverErrorCurrentPass')).toBeNull();
|
||||
expect(wrapper.state('serverErrorNewPass')).toBeNull();
|
||||
await userEvent.type(currentPasswordInput, 'oldPassword123!');
|
||||
await userEvent.type(newPasswordInput, 'newPassword123!');
|
||||
await userEvent.click(screen.getByRole('button', {name: /Reset/i}));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateUserPassword).toHaveBeenCalledTimes(1);
|
||||
expect(updateUserPassword).toHaveBeenCalledWith(
|
||||
user.id,
|
||||
'oldPassword123!',
|
||||
'newPassword123!',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should not call updateUserPassword when the old password is not provided', () => {
|
||||
test('should not call updateUserPassword when the current password is not provided', async () => {
|
||||
const updateUserPassword = jest.fn(() => Promise.resolve({data: ''}));
|
||||
const newPassword = 'newPassword123!';
|
||||
const props = {...baseProps, actions: {updateUserPassword}};
|
||||
const wrapper = mountWithIntl(<ResetPasswordModal {...props}/>);
|
||||
renderWithContext(<ResetPasswordModal {...props}/>);
|
||||
|
||||
(wrapper.find('input[type=\'password\']').last().instance() as unknown as HTMLInputElement).value = newPassword;
|
||||
wrapper.find('button[type=\'submit\']').first().simulate('click', {preventDefault: jest.fn()});
|
||||
const newPasswordInput = screen.getByPlaceholderText(/New password/i);
|
||||
await userEvent.type(newPasswordInput, 'newPassword123!');
|
||||
await userEvent.click(screen.getByRole('button', {name: /Reset/i}));
|
||||
|
||||
expect(updateUserPassword.mock.calls.length).toBe(0);
|
||||
expect(wrapper.state('serverErrorCurrentPass')).toStrictEqual(
|
||||
<FormattedMessage
|
||||
defaultMessage='Please enter your current password.'
|
||||
id='admin.reset_password.missing_current'
|
||||
/>);
|
||||
expect(wrapper.state('serverErrorNewPass')).toBeNull();
|
||||
expect(updateUserPassword).not.toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Please enter your current password/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should call updateUserPassword', () => {
|
||||
test('should call updateUserPassword without current password when resetting another user', async () => {
|
||||
const updateUserPassword = jest.fn(() => Promise.resolve({data: ''}));
|
||||
const password = 'Password123!';
|
||||
const props = {...baseProps, currentUserId: 'different_user_id', actions: {updateUserPassword}};
|
||||
renderWithContext(<ResetPasswordModal {...props}/>);
|
||||
|
||||
const props = {...baseProps, currentUserId: '2', actions: {updateUserPassword}};
|
||||
const wrapper = mountWithIntl(<ResetPasswordModal {...props}/>);
|
||||
const newPasswordInput = screen.getByPlaceholderText(/New password/i);
|
||||
await userEvent.type(newPasswordInput, 'Password123!');
|
||||
await userEvent.click(screen.getByRole('button', {name: /Reset/i}));
|
||||
|
||||
(wrapper.find('input[type=\'password\']').first().instance() as unknown as HTMLInputElement).value = password;
|
||||
wrapper.find('button[type=\'submit\']').first().simulate('click', {preventDefault: jest.fn()});
|
||||
await waitFor(() => {
|
||||
expect(updateUserPassword).toHaveBeenCalledTimes(1);
|
||||
expect(updateUserPassword).toHaveBeenCalledWith(
|
||||
user.id,
|
||||
'',
|
||||
'Password123!',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
expect(updateUserPassword.mock.calls.length).toBe(1);
|
||||
test('should show error when password does not meet requirements', async () => {
|
||||
const updateUserPassword = jest.fn(() => Promise.resolve({data: ''}));
|
||||
const props = {...baseProps, currentUserId: 'different_user_id', actions: {updateUserPassword}};
|
||||
renderWithContext(<ResetPasswordModal {...props}/>);
|
||||
|
||||
const newPasswordInput = screen.getByPlaceholderText(/New password/i);
|
||||
await userEvent.type(newPasswordInput, 'weak');
|
||||
await userEvent.click(screen.getByRole('button', {name: /Reset/i}));
|
||||
|
||||
expect(updateUserPassword).not.toHaveBeenCalled();
|
||||
|
||||
// Password validation error should appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Must be 10-72 characters long/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import React, {useCallback, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import {GenericModal} from '@mattermost/components';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import Input from 'components/widgets/inputs/input/input';
|
||||
|
||||
import {isValidPassword} from 'utils/password';
|
||||
import {getFullName} from 'utils/utils';
|
||||
|
||||
import '../admin_modal_with_input.scss';
|
||||
|
||||
interface PasswordConfig {
|
||||
minimumLength: number;
|
||||
|
|
@ -19,12 +24,6 @@ interface PasswordConfig {
|
|||
requireUppercase: boolean;
|
||||
}
|
||||
|
||||
type State = {
|
||||
show: boolean;
|
||||
serverErrorNewPass: React.ReactNode;
|
||||
serverErrorCurrentPass: React.ReactNode;
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
user?: UserProfile;
|
||||
currentUserId: string;
|
||||
|
|
@ -36,215 +35,146 @@ export type Props = {
|
|||
};
|
||||
}
|
||||
|
||||
export default class ResetPasswordModal extends React.PureComponent<Props, State> {
|
||||
private currentPasswordRef: React.RefObject<HTMLInputElement>;
|
||||
private passwordRef: React.RefObject<HTMLInputElement>;
|
||||
export default function ResetPasswordModal({
|
||||
user,
|
||||
currentUserId,
|
||||
onSuccess,
|
||||
onExited,
|
||||
passwordConfig,
|
||||
actions,
|
||||
}: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
const [show, setShow] = useState(true);
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [errorNewPass, setErrorNewPass] = useState<React.ReactNode>(null);
|
||||
const [errorCurrentPass, setErrorCurrentPass] = useState<React.ReactNode>(null);
|
||||
|
||||
this.state = {
|
||||
show: true,
|
||||
serverErrorNewPass: null,
|
||||
serverErrorCurrentPass: null,
|
||||
};
|
||||
const currentPasswordRef = useRef<HTMLInputElement>(null);
|
||||
const newPasswordRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
this.currentPasswordRef = React.createRef();
|
||||
this.passwordRef = React.createRef();
|
||||
}
|
||||
const isResettingOwnPassword = user?.id === currentUserId;
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.setState({
|
||||
serverErrorNewPass: null,
|
||||
serverErrorCurrentPass: null,
|
||||
});
|
||||
}
|
||||
const handleCancel = useCallback(() => {
|
||||
setShow(false);
|
||||
}, []);
|
||||
|
||||
private doSubmit = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
if (!this.props.user) {
|
||||
const handleConfirm = useCallback(async () => {
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
let currentPassword = '';
|
||||
if (this.currentPasswordRef.current) {
|
||||
currentPassword = (this.currentPasswordRef.current as HTMLInputElement).value;
|
||||
if (currentPassword === '') {
|
||||
const errorMsg = (
|
||||
<FormattedMessage
|
||||
id='admin.reset_password.missing_current'
|
||||
defaultMessage='Please enter your current password.'
|
||||
/>
|
||||
);
|
||||
this.setState({serverErrorCurrentPass: errorMsg});
|
||||
return;
|
||||
}
|
||||
// Clear previous errors
|
||||
setErrorNewPass(null);
|
||||
setErrorCurrentPass(null);
|
||||
|
||||
// Validate current password if resetting own password
|
||||
if (isResettingOwnPassword && currentPassword === '') {
|
||||
setErrorCurrentPass(formatMessage({
|
||||
id: 'admin.reset_password.missing_current',
|
||||
defaultMessage: 'Please enter your current password.',
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const password = (this.passwordRef.current as HTMLInputElement).value;
|
||||
|
||||
const {valid, error} = isValidPassword(password, this.props.passwordConfig);
|
||||
// Validate new password
|
||||
const {valid, error} = isValidPassword(newPassword, passwordConfig);
|
||||
if (!valid && error) {
|
||||
this.setState({serverErrorNewPass: error});
|
||||
setErrorNewPass(error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({serverErrorNewPass: null});
|
||||
|
||||
const result = await this.props.actions.updateUserPassword(this.props.user.id, currentPassword, password);
|
||||
if ('error' in result) {
|
||||
this.setState({serverErrorCurrentPass: result.error.message});
|
||||
return;
|
||||
}
|
||||
this.props.onSuccess?.();
|
||||
this.setState({show: false});
|
||||
};
|
||||
|
||||
private doCancel = (): void => {
|
||||
this.setState({
|
||||
show: false,
|
||||
serverErrorNewPass: null,
|
||||
serverErrorCurrentPass: null,
|
||||
});
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const user = this.props.user;
|
||||
if (user == null) {
|
||||
return <div/>;
|
||||
}
|
||||
|
||||
let urlClass = 'input-group input-group--limit';
|
||||
let serverErrorNewPass = null;
|
||||
|
||||
if (this.state.serverErrorNewPass) {
|
||||
urlClass += ' has-error';
|
||||
serverErrorNewPass = <div className='has-error'><p className='input__help error'>{this.state.serverErrorNewPass}</p></div>;
|
||||
}
|
||||
|
||||
let title;
|
||||
if (user.auth_service) {
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id='admin.reset_password.titleSwitch'
|
||||
defaultMessage='Switch Account to Email/Password'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id='admin.reset_password.titleReset'
|
||||
defaultMessage='Reset Password'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let currentPassword = null;
|
||||
let serverErrorCurrentPass = null;
|
||||
let newPasswordFocus = true;
|
||||
if (this.props.currentUserId === user.id) {
|
||||
newPasswordFocus = false;
|
||||
let urlClassCurrentPass = 'input-group input-group--limit';
|
||||
if (this.state.serverErrorCurrentPass) {
|
||||
urlClassCurrentPass += ' has-error';
|
||||
serverErrorCurrentPass = <div className='has-error'><p className='input__help error'>{this.state.serverErrorCurrentPass}</p></div>;
|
||||
}
|
||||
currentPassword = (
|
||||
<div className='col-sm-10 password__group-addon-space'>
|
||||
<div className={urlClassCurrentPass}>
|
||||
<span
|
||||
data-toggle='tooltip'
|
||||
title='Current Password'
|
||||
className='input-group-addon password__group-addon'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.reset_password.curentPassword'
|
||||
defaultMessage='Current Password'
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
type='password'
|
||||
ref={this.currentPasswordRef}
|
||||
className='form-control'
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
dialogClassName='a11y__modal'
|
||||
show={this.state.show}
|
||||
onHide={this.doCancel}
|
||||
onExited={this.props.onExited}
|
||||
role='none'
|
||||
aria-labelledby='resetPasswordModalLabel'
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title
|
||||
componentClass='h1'
|
||||
id='resetPasswordModalLabel'
|
||||
>
|
||||
{title}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<form
|
||||
role='form'
|
||||
className='form-horizontal'
|
||||
>
|
||||
<Modal.Body>
|
||||
<div className='form-group'>
|
||||
{currentPassword}
|
||||
<div className='col-sm-10'>
|
||||
<div className={urlClass}>
|
||||
<span
|
||||
data-toggle='tooltip'
|
||||
title='New Password'
|
||||
className='input-group-addon password__group-addon'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.reset_password.newPassword'
|
||||
defaultMessage='New Password'
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
type='password'
|
||||
ref={this.passwordRef}
|
||||
className='form-control'
|
||||
autoFocus={newPasswordFocus}
|
||||
/>
|
||||
</div>
|
||||
{serverErrorNewPass}
|
||||
{serverErrorCurrentPass}
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-tertiary'
|
||||
onClick={this.doCancel}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.reset_password.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={this.doSubmit}
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.reset_password.reset'
|
||||
defaultMessage='Reset'
|
||||
/>
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</Modal>
|
||||
const result = await actions.updateUserPassword(
|
||||
user.id,
|
||||
isResettingOwnPassword ? currentPassword : '',
|
||||
newPassword,
|
||||
);
|
||||
|
||||
if ('error' in result) {
|
||||
setErrorCurrentPass(result.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
onSuccess?.();
|
||||
setShow(false);
|
||||
}, [user, isResettingOwnPassword, currentPassword, newPassword, passwordConfig, actions, onSuccess, formatMessage]);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayName = getFullName(user) || user.username;
|
||||
const isAuthUser = Boolean(user.auth_service);
|
||||
|
||||
const title = isAuthUser ?
|
||||
formatMessage({
|
||||
id: 'admin.reset_password.titleSwitchFor',
|
||||
defaultMessage: 'Switch account to Email/Password for {name}',
|
||||
}, {name: displayName}) :
|
||||
formatMessage({
|
||||
id: 'admin.reset_password.titleResetFor',
|
||||
defaultMessage: 'Reset password for {name}',
|
||||
}, {name: displayName});
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
id='resetPasswordModal'
|
||||
className='ResetPasswordModal'
|
||||
modalHeaderText={title}
|
||||
show={show}
|
||||
onExited={onExited}
|
||||
onHide={handleCancel}
|
||||
handleCancel={handleCancel}
|
||||
handleConfirm={handleConfirm}
|
||||
handleEnterKeyPress={handleConfirm}
|
||||
confirmButtonText={formatMessage({
|
||||
id: 'admin.reset_password.reset',
|
||||
defaultMessage: 'Reset',
|
||||
})}
|
||||
compassDesign={true}
|
||||
autoCloseOnConfirmButton={false}
|
||||
errorText={errorCurrentPass ? <span className='error'>{errorCurrentPass}</span> : undefined}
|
||||
>
|
||||
<div className='ResetPasswordModal__body'>
|
||||
{isResettingOwnPassword && (
|
||||
<Input
|
||||
ref={currentPasswordRef as React.Ref<HTMLInputElement>}
|
||||
type='password'
|
||||
name='currentPassword'
|
||||
autoComplete='current-password'
|
||||
label={formatMessage({
|
||||
id: 'admin.reset_password.currentPassword',
|
||||
defaultMessage: 'Current password',
|
||||
})}
|
||||
placeholder={formatMessage({
|
||||
id: 'admin.reset_password.enterCurrentPassword',
|
||||
defaultMessage: 'Enter current password',
|
||||
})}
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
autoFocus={true}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
ref={newPasswordRef as React.Ref<HTMLInputElement>}
|
||||
type='password'
|
||||
name='newPassword'
|
||||
autoComplete='new-password'
|
||||
label={formatMessage({
|
||||
id: 'admin.reset_password.newPassword',
|
||||
defaultMessage: 'New password',
|
||||
})}
|
||||
placeholder={formatMessage({
|
||||
id: 'admin.reset_password.enterNewPassword',
|
||||
defaultMessage: 'Enter new password',
|
||||
})}
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
autoFocus={!isResettingOwnPassword}
|
||||
customMessage={errorNewPass ? {type: 'error', value: errorNewPass} : undefined}
|
||||
/>
|
||||
</div>
|
||||
</GenericModal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2454,19 +2454,21 @@
|
|||
"admin.requestButton.loading": " Loading...",
|
||||
"admin.requestButton.requestFailure": "Test Failure: {error}",
|
||||
"admin.requestButton.requestSuccess": "Test Successful",
|
||||
"admin.reset_email.cancel": "Cancel",
|
||||
"admin.reset_email.currentPassword": "Current Password",
|
||||
"admin.reset_email.enterCurrentPassword": "Enter current password",
|
||||
"admin.reset_email.enterNewEmail": "Enter new email address",
|
||||
"admin.reset_email.missing_current_password": "Please enter your current password.",
|
||||
"admin.reset_email.newEmail": "New Email",
|
||||
"admin.reset_email.reset": "Reset",
|
||||
"admin.reset_email.titleReset": "Update Email",
|
||||
"admin.reset_password.cancel": "Cancel",
|
||||
"admin.reset_password.curentPassword": "Current Password",
|
||||
"admin.reset_email.titleResetFor": "Update email for {name}",
|
||||
"admin.reset_email.update": "Update",
|
||||
"admin.reset_password.currentPassword": "Current password",
|
||||
"admin.reset_password.enterCurrentPassword": "Enter current password",
|
||||
"admin.reset_password.enterNewPassword": "Enter new password",
|
||||
"admin.reset_password.missing_current": "Please enter your current password.",
|
||||
"admin.reset_password.newPassword": "New Password",
|
||||
"admin.reset_password.reset": "Reset",
|
||||
"admin.reset_password.titleReset": "Reset Password",
|
||||
"admin.reset_password.titleSwitch": "Switch Account to Email/Password",
|
||||
"admin.reset_password.titleResetFor": "Reset password for {name}",
|
||||
"admin.reset_password.titleSwitchFor": "Switch account to Email/Password for {name}",
|
||||
"admin.revoke_token_button.delete": "Delete",
|
||||
"admin.s3.connectionS3Test": "Test Connection",
|
||||
"admin.s3.s3Fail": "Connection unsuccessful: {error}",
|
||||
|
|
|
|||
|
|
@ -126,7 +126,8 @@
|
|||
display: flex;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
padding: 14px 32px;
|
||||
margin: 0 32px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid rgba(var(--dnd-indicator-rgb), 0.16);
|
||||
margin-bottom: 24px;
|
||||
background: rgba(var(--dnd-indicator-rgb), 0.08);
|
||||
|
|
|
|||
Loading…
Reference in a new issue