diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_invitation_ui_more_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_invitation_ui_more_spec.ts
index 069bd3be632..4de28ab1aef 100644
--- a/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_invitation_ui_more_spec.ts
+++ b/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_invitation_ui_more_spec.ts
@@ -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();
});
diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/system_console_manage_guest_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/system_console_manage_guest_spec.ts
index adc4adf47d3..b03d5a7251b 100644
--- a/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/system_console_manage_guest_spec.ts
+++ b/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/system_console_manage_guest_spec.ts
@@ -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
diff --git a/e2e-tests/cypress/tests/integration/channels/system_console/user_management/users_spec.js b/e2e-tests/cypress/tests/integration/channels/system_console/user_management/users_spec.js
index e6041c4f6d1..2263ccf3e91 100644
--- a/e2e-tests/cypress/tests/integration/channels/system_console/user_management/users_spec.js
+++ b/e2e-tests/cypress/tests/integration/channels/system_console/user_management/users_spec.js
@@ -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();
diff --git a/e2e-tests/cypress/tests/integration/channels/system_console/user_management_spec.js b/e2e-tests/cypress/tests/integration/channels/system_console/user_management_spec.js
index e7ea290bd29..23d6cee0a19 100644
--- a/e2e-tests/cypress/tests/integration/channels/system_console/user_management_spec.js
+++ b/e2e-tests/cypress/tests/integration/channels/system_console/user_management_spec.js
@@ -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();
}
}
diff --git a/e2e-tests/playwright/lib/src/ui/pages/system_console.ts b/e2e-tests/playwright/lib/src/ui/pages/system_console.ts
index 1ef638e2872..576e47532a8 100644
--- a/e2e-tests/playwright/lib/src/ui/pages/system_console.ts
+++ b/e2e-tests/playwright/lib/src/ui/pages/system_console.ts
@@ -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();
+ }
}
diff --git a/e2e-tests/playwright/specs/functional/system_console/system_users/actions.spec.ts b/e2e-tests/playwright/specs/functional/system_console/system_users/actions.spec.ts
index fb2d661e31c..de11901b08a 100644
--- a/e2e-tests/playwright/specs/functional/system_console/system_users/actions.spec.ts
+++ b/e2e-tests/playwright/specs/functional/system_console/system_users/actions.spec.ts
@@ -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'});
diff --git a/webapp/channels/src/components/admin_console/admin_modal_with_input.scss b/webapp/channels/src/components/admin_console/admin_modal_with_input.scss
new file mode 100644
index 00000000000..4aee7fd8a7b
--- /dev/null
+++ b/webapp/channels/src/components/admin_console/admin_modal_with_input.scss
@@ -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;
+ }
+ }
+ }
+}
+
diff --git a/webapp/channels/src/components/admin_console/reset_email_modal/__snapshots__/reset_email_modal.test.tsx.snap b/webapp/channels/src/components/admin_console/reset_email_modal/__snapshots__/reset_email_modal.test.tsx.snap
deleted file mode 100644
index dea75f5ceb8..00000000000
--- a/webapp/channels/src/components/admin_console/reset_email_modal/__snapshots__/reset_email_modal.test.tsx.snap
+++ /dev/null
@@ -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`] = `
-
-
-
-
-
-
-
-
-`;
-
-exports[`components/admin_console/reset_email_modal/reset_email_modal.tsx should match snapshot when the current user 1`] = `
-
-
-
-
-
-
-
-
-`;
-
-exports[`components/admin_console/reset_email_modal/reset_email_modal.tsx should match snapshot when there is no user 1`] = `
`;
diff --git a/webapp/channels/src/components/admin_console/reset_email_modal/reset_email_modal.test.tsx b/webapp/channels/src/components/admin_console/reset_email_modal/reset_email_modal.test.tsx
index b983d0ed619..0b3d4203bce 100644
--- a/webapp/channels/src/components/admin_console/reset_email_modal/reset_email_modal.test.tsx
+++ b/webapp/channels/src/components/admin_console/reset_email_modal/reset_email_modal.test.tsx
@@ -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();
- 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();
+
+ 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();
- expect(wrapper).toMatchSnapshot();
+ const {container} = renderWithContext();
+
+ 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();
- expect(wrapper).toMatchSnapshot();
+ renderWithContext();
+
+ // 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();
+ test('should not update email since the email is empty', async () => {
+ renderWithContext();
- (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(
- ,
- );
+ 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();
+ test('should not update email since the email is invalid', async () => {
+ renderWithContext();
- (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(
- ,
- );
+ 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();
+ renderWithContext();
- (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(
- ,
- );
+ 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();
+ 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();
- (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();
+ 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();
- (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',
+ }));
+ });
});
});
diff --git a/webapp/channels/src/components/admin_console/reset_email_modal/reset_email_modal.tsx b/webapp/channels/src/components/admin_console/reset_email_modal/reset_email_modal.tsx
index 0d48203ac7c..9e7be184ab4 100644
--- a/webapp/channels/src/components/admin_console/reset_email_modal/reset_email_modal.tsx
+++ b/webapp/channels/src/components/admin_console/reset_email_modal/reset_email_modal.tsx
@@ -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 {
- private emailRef: React.RefObject;
- private currentPasswordRef: React.RefObject;
+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(null);
+ const [emailError, setEmailError] = useState(null);
+ const [passwordError, setPasswordError] = useState(null);
- this.state = {
- show: true,
- error: null,
- isEmailError: false,
- isCurrentPasswordError: false,
- };
+ const emailRef = useRef(null);
+ const currentPasswordRef = useRef(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 = (
-
- );
- 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 = (
-
- );
-
- this.setState({error: errMsg, isCurrentPasswordError: true});
- return false;
- }
- this.setState({error: null, isCurrentPasswordError: false});
- return true;
- };
-
- private doSubmit = async (e: React.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 ;
+ return;
}
- const groupClass = 'input-group input-group--limit mb-5';
+ // Clear previous errors
+ setError(null);
+ setEmailError(null);
+ setPasswordError(null);
- const title = (
-
- );
+ // Validate email
+ if (!email || !isEmail(email)) {
+ setEmailError(formatMessage({
+ id: 'user.settings.general.validEmail',
+ defaultMessage: 'Please enter a valid email address.',
+ }));
+ return;
+ }
- return (
-
-
-
- {title}
-
-
-
-
- );
+ 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 (
+ {error} : undefined}
+ dataTestId='resetEmailModal'
+ >
+
+ }
+ 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 && (
+ }
+ 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}
+ />
+ )}
+
+
+ );
}
diff --git a/webapp/channels/src/components/admin_console/reset_password_modal/__snapshots__/reset_password_modal.test.tsx.snap b/webapp/channels/src/components/admin_console/reset_password_modal/__snapshots__/reset_password_modal.test.tsx.snap
deleted file mode 100644
index 941eeca048b..00000000000
--- a/webapp/channels/src/components/admin_console/reset_password_modal/__snapshots__/reset_password_modal.test.tsx.snap
+++ /dev/null
@@ -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`] = `
-
-
-
-
-
-
-
-
-`;
-
-exports[`components/admin_console/reset_password_modal/reset_password_modal.tsx should match snapshot when there is no user 1`] = ``;
diff --git a/webapp/channels/src/components/admin_console/reset_password_modal/reset_password_modal.test.tsx b/webapp/channels/src/components/admin_console/reset_password_modal/reset_password_modal.test.tsx
index 6b8de10cf88..935994b57b7 100644
--- a/webapp/channels/src/components/admin_console/reset_password_modal/reset_password_modal.test.tsx
+++ b/webapp/channels/src/components/admin_console/reset_password_modal/reset_password_modal.test.tsx
@@ -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(
- ,
- );
- 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();
+
+ 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(
- ,
- );
- expect(wrapper).toMatchSnapshot();
+ const {container} = renderWithContext();
+
+ 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();
+
+ expect(screen.getByText(/Switch account to Email\/Password/i)).toBeInTheDocument();
+ });
+
+ test('should show current password field when resetting own password', () => {
+ renderWithContext();
+
+ 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();
+
+ 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();
+ renderWithContext();
- (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();
+ renderWithContext();
- (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(
- );
- 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();
- const props = {...baseProps, currentUserId: '2', actions: {updateUserPassword}};
- const wrapper = mountWithIntl();
+ 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();
+
+ 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();
+ });
});
});
diff --git a/webapp/channels/src/components/admin_console/reset_password_modal/reset_password_modal.tsx b/webapp/channels/src/components/admin_console/reset_password_modal/reset_password_modal.tsx
index 30470c06417..4b3c8c7dda9 100644
--- a/webapp/channels/src/components/admin_console/reset_password_modal/reset_password_modal.tsx
+++ b/webapp/channels/src/components/admin_console/reset_password_modal/reset_password_modal.tsx
@@ -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 {
- private currentPasswordRef: React.RefObject;
- private passwordRef: React.RefObject;
+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(null);
+ const [errorCurrentPass, setErrorCurrentPass] = useState(null);
- this.state = {
- show: true,
- serverErrorNewPass: null,
- serverErrorCurrentPass: null,
- };
+ const currentPasswordRef = useRef(null);
+ const newPasswordRef = useRef(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) => {
- 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 = (
-
- );
- 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 ;
- }
-
- let urlClass = 'input-group input-group--limit';
- let serverErrorNewPass = null;
-
- if (this.state.serverErrorNewPass) {
- urlClass += ' has-error';
- serverErrorNewPass = {this.state.serverErrorNewPass}
;
- }
-
- let title;
- if (user.auth_service) {
- title = (
-
- );
- } else {
- title = (
-
- );
- }
-
- 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 = {this.state.serverErrorCurrentPass}
;
- }
- currentPassword = (
-
- );
- }
-
- return (
-
-
-
- {title}
-
-
-
-
+ 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 (
+ {errorCurrentPass} : undefined}
+ >
+
+ {isResettingOwnPassword && (
+ }
+ 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}
+ />
+ )}
+ }
+ 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}
+ />
+
+
+ );
}
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json
index e1ef6ce86bb..cefad218923 100644
--- a/webapp/channels/src/i18n/en.json
+++ b/webapp/channels/src/i18n/en.json
@@ -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}",
diff --git a/webapp/platform/components/src/generic_modal/generic_modal.scss b/webapp/platform/components/src/generic_modal/generic_modal.scss
index 6064859567f..19d2be2848a 100644
--- a/webapp/platform/components/src/generic_modal/generic_modal.scss
+++ b/webapp/platform/components/src/generic_modal/generic_modal.scss
@@ -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);