[MM-63041] Convert many inputs to the Input component, replace clientError with more correct client-side validation that conforms to the input (#31279)

* [MM-63041] Convert many inputs to the Input component, replace clientError with more correct client-side validation that conforms to the input

* Fix line length

* PR feedback

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Devin Binnie
2025-06-11 17:02:32 -04:00
коммит произвёл GitHub
родитель e6be282568
Коммит 65d3d5984f
13 изменённых файлов: 255 добавлений и 220 удалений

Просмотреть файл

@@ -36,7 +36,7 @@ describe('Account Settings', () => {
cy.uiClose(); cy.uiClose();
}); });
it('MM-T2081 Password: Error on blank', () => { it('MM-T2081 Password: Save should be disabled on blank', () => {
// # Go to Profile > Security // # Go to Profile > Security
cy.uiOpenProfileModal('Security'); cy.uiOpenProfileModal('Security');
@@ -49,12 +49,8 @@ describe('Account Settings', () => {
// # Click "Edit" to the right of "Password" // # Click "Edit" to the right of "Password"
cy.get('#passwordEdit').should('be.visible').click(); cy.get('#passwordEdit').should('be.visible').click();
// # Save the settings // # Check that save button is disabled
cy.uiSave(); cy.get('button[type="submit"]').should('be.disabled');
// * Check that there is an error
cy.get('#clientError').should('be.visible').should('contain', 'Please enter your current password.');
cy.get('#serverError').should('not.exist');
cy.uiClose(); cy.uiClose();
}); });

Просмотреть файл

@@ -81,11 +81,11 @@ describe('Profile > Profile Settings > Email', () => {
// # Click "Edit" to the right of "Email" // # Click "Edit" to the right of "Email"
cy.get('#emailEdit').should('be.visible').click(); cy.get('#emailEdit').should('be.visible').click();
// # Save the settings // # Click on the input and blur it
cy.uiSave().wait(TIMEOUTS.HALF_SEC); cy.get('#primaryEmail').should('be.visible').click().blur();
// * Check that the correct error message is shown. // * Check that the correct error message is shown.
cy.get('#clientError').should('be.visible').should('have.text', 'Please enter a valid email address'); cy.get('#error_primaryEmail').should('be.visible').should('have.text', 'Please enter a valid email address');
}); });
it('MM-T2067 email address already taken error', () => { it('MM-T2067 email address already taken error', () => {
@@ -113,11 +113,8 @@ describe('Profile > Profile Settings > Email', () => {
cy.get('#confirmEmail').should('be.visible').clear(); cy.get('#confirmEmail').should('be.visible').clear();
cy.get('#currentPassword').should('be.visible').type('randompass'); cy.get('#currentPassword').should('be.visible').type('randompass');
// # Save the settings
cy.uiSave().wait(TIMEOUTS.HALF_SEC);
// * Check that the correct error message is shown. // * Check that the correct error message is shown.
cy.get('#clientError').should('be.visible').should('have.text', 'The new emails you entered do not match.'); cy.get('#error_confirmEmail').should('be.visible').should('have.text', 'The new emails you entered do not match.');
}); });
// This test is a combination of 4 sub-tests because they are sub-parts of the same test. // This test is a combination of 4 sub-tests because they are sub-parts of the same test.

Просмотреть файл

@@ -47,11 +47,10 @@ describe('Settings > Sidebar > General > Edit', () => {
it('MM-T2050 Username cannot be blank', () => { it('MM-T2050 Username cannot be blank', () => {
// # Clear the username textfield contents // # Clear the username textfield contents
cy.get('#usernameEdit').click(); cy.get('#usernameEdit').click();
cy.get('#username').clear(); cy.get('#username').click().clear().blur();
cy.uiSave();
// * Check if element is present and contains expected text values // * Check if element is present and contains expected text values
cy.get('#clientError').should('be.visible').should('contain', 'Username must begin with a letter, and contain between 3 to 22 lowercase characters made up of numbers, letters, and the symbols \'.\', \'-\', and \'_\'.'); cy.get('#error_username').should('be.visible').should('contain', 'Username must begin with a letter, and contain between 3 to 22 lowercase characters made up of numbers, letters, and the symbols \'.\', \'-\', and \'_\'.');
// # Click "x" button to close Profile modal // # Click "x" button to close Profile modal
cy.uiClose(); cy.uiClose();
@@ -62,11 +61,10 @@ describe('Settings > Sidebar > General > Edit', () => {
cy.get('#usernameEdit').click(); cy.get('#usernameEdit').click();
// # Add the username to textfield contents // # Add the username to textfield contents
cy.get('#username').clear().type('te'); cy.get('#username').clear().type('te').blur();
cy.uiSave();
// * Check if element is present and contains expected text values // * Check if element is present and contains expected text values
cy.get('#clientError').should('be.visible').should('contain', 'Username must begin with a letter, and contain between 3 to 22 lowercase characters made up of numbers, letters, and the symbols \'.\', \'-\', and \'_\'.'); cy.get('#error_username').should('be.visible').should('contain', 'Username must begin with a letter, and contain between 3 to 22 lowercase characters made up of numbers, letters, and the symbols \'.\', \'-\', and \'_\'.');
// # Click "x" button to close Profile modal // # Click "x" button to close Profile modal
cy.uiClose(); cy.uiClose();
@@ -151,11 +149,10 @@ describe('Settings > Sidebar > General > Edit', () => {
for (const prefix of prefixes) { for (const prefix of prefixes) {
// # Add username to textfield contents // # Add username to textfield contents
cy.get('#username').clear().type(prefix).type('{backspace}.').type(otherUser.username); cy.get('#username').clear().type(prefix).type('{backspace}.').type(otherUser.username).blur();
cy.uiSave();
// * Check if element is present and contains expected text values // * Check if element is present and contains expected text values
cy.get('#clientError').should('be.visible').should('contain', 'Username must begin with a letter, and contain between 3 to 22 lowercase characters made up of numbers, letters, and the symbols \'.\', \'-\', and \'_\'.'); cy.get('#error_username').should('be.visible').should('contain', 'Username must begin with a letter, and contain between 3 to 22 lowercase characters made up of numbers, letters, and the symbols \'.\', \'-\', and \'_\'.');
} }
// # Click "x" button to close Profile modal // # Click "x" button to close Profile modal
@@ -175,11 +172,10 @@ describe('Settings > Sidebar > General > Edit', () => {
for (const username of usernames) { for (const username of usernames) {
// # Add username to textfield contents // # Add username to textfield contents
cy.get('#username').clear().type(username); cy.get('#username').clear().type(username).blur();
cy.uiSave();
// * Check if element is present and contains expected text values // * Check if element is present and contains expected text values
cy.get('#clientError').should('be.visible').should('contain', 'This username is reserved, please choose a new one.'); cy.get('#error_username').should('be.visible').should('contain', 'This username is reserved, please choose a new one.');
} }
// # Click "x" button to close Profile modal // # Click "x" button to close Profile modal

Просмотреть файл

@@ -54,11 +54,15 @@ describe('Profile', () => {
// # Enter valid values in password change fields // # Enter valid values in password change fields
enterPasswords(testUser.password, 'passwd', 'passwd'); enterPasswords(testUser.password, 'passwd', 'passwd');
// * Check that there are no errors
cy.get('#error_currentPassword').should('not.exist');
cy.get('#error_newPassword').should('not.exist');
cy.get('#error_confirmPassword').should('not.exist');
// # Save the settings // # Save the settings
cy.uiSave(); cy.uiSave();
// * Check that there are no errors // * Check that there are no errors
cy.get('#clientError').should('not.exist');
cy.get('#serverError').should('not.exist'); cy.get('#serverError').should('not.exist');
}); });
@@ -66,22 +70,16 @@ describe('Profile', () => {
// # Enter mismatching passwords for new password and confirm fields // # Enter mismatching passwords for new password and confirm fields
enterPasswords(testUser.password, 'newPW', 'NewPW'); enterPasswords(testUser.password, 'newPW', 'NewPW');
// # Save
cy.uiSave();
// * Verify for error message: "The new passwords you entered do not match." // * Verify for error message: "The new passwords you entered do not match."
cy.get('#clientError').should('be.visible').should('have.text', 'The new passwords you entered do not match.'); cy.get('#error_confirmPassword').should('be.visible').should('have.text', 'The new passwords you entered do not match.');
}); });
it('MM-T2083 Password: Too few characters in new password produces error', () => { it('MM-T2083 Password: Too few characters in new password produces error', () => {
// # Enter a New password two letters long // # Enter a New password two letters long
enterPasswords(testUser.password, 'pw', 'pw'); enterPasswords(testUser.password, 'pw', 'pw');
// # Save
cy.uiSave();
// * Verify for error message: "Your password must be 5-72 characters long." // * Verify for error message: "Your password must be 5-72 characters long."
cy.get('#clientError').should('be.visible').should('have.text', 'Your password must be 5-72 characters long.'); cy.get('#error_newPassword').should('be.visible').should('have.text', 'Your password must be 5-72 characters long.');
}); });
it('MM-T2084 Password: Cancel out of password changes causes no changes to be made', () => { it('MM-T2084 Password: Cancel out of password changes causes no changes to be made', () => {
@@ -151,4 +149,7 @@ function enterPasswords(currentPassword, newPassword, confirmPassword) {
// # Retype New password incorrectly // # Retype New password incorrectly
cy.get('#confirmPassword').should('be.visible').type(confirmPassword); cy.get('#confirmPassword').should('be.visible').type(confirmPassword);
// # Click on the input and blur it
cy.get('#currentPassword').should('be.visible').click().blur();
} }

Просмотреть файл

@@ -16,8 +16,6 @@ export default class ProfileModal {
readonly saveButton; readonly saveButton;
readonly cancelButton; readonly cancelButton;
readonly errorText;
constructor(container: Locator) { constructor(container: Locator) {
this.container = container; this.container = container;
@@ -30,8 +28,6 @@ export default class ProfileModal {
this.closeButton = container.getByRole('button', {name: 'Close'}); this.closeButton = container.getByRole('button', {name: 'Close'});
this.saveButton = container.locator('button:has-text("Save")'); this.saveButton = container.locator('button:has-text("Save")');
this.cancelButton = container.locator('button:has-text("Cancel")'); this.cancelButton = container.locator('button:has-text("Cancel")');
this.errorText = container.locator('#clientError');
} }
async toBeVisible() { async toBeVisible() {

Просмотреть файл

@@ -346,13 +346,13 @@ test('MM-T5772 URL Validation in Custom Profile Attributes @custom_profile_attri
await profileModal.container.locator(`#customAttribute_${fieldId}`).scrollIntoViewIfNeeded(); await profileModal.container.locator(`#customAttribute_${fieldId}`).scrollIntoViewIfNeeded();
await profileModal.container.locator(`#customAttribute_${fieldId}`).clear(); await profileModal.container.locator(`#customAttribute_${fieldId}`).clear();
await profileModal.container.locator(`#customAttribute_${fieldId}`).fill(TEST_INVALID_URL); await profileModal.container.locator(`#customAttribute_${fieldId}`).fill(TEST_INVALID_URL);
await profileModal.container.locator(`#customAttribute_${fieldId}`).blur();
// 4. Try to save the changes
await profileModal.saveButton.click();
// * Save button doesn't complete the operation with invalid URL // * Save button doesn't complete the operation with invalid URL
await expect(profileModal.errorText).toBeVisible(); await expect(profileModal.container.locator(`#error_customAttribute_${fieldId}`)).toBeVisible();
await expect(profileModal.errorText).toHaveText('Please enter a valid url.'); await expect(profileModal.container.locator(`#error_customAttribute_${fieldId}`)).toHaveText(
'Please enter a valid url.',
);
// 5. Edit Website field and enter a valid URL // 5. Edit Website field and enter a valid URL
await profileModal.container.locator(`#customAttribute_${fieldId}`).clear(); await profileModal.container.locator(`#customAttribute_${fieldId}`).clear();
@@ -362,6 +362,6 @@ test('MM-T5772 URL Validation in Custom Profile Attributes @custom_profile_attri
await profileModal.saveButton.click(); await profileModal.saveButton.click();
// * Valid URL saves successfully with no error message // * Valid URL saves successfully with no error message
await expect(profileModal.errorText).not.toBeVisible(); await expect(profileModal.container.locator(`#error_customAttribute_${fieldId}`)).not.toBeVisible();
await expect(profileModal.container).toContainText(TEST_VALID_URL); await expect(profileModal.container).toContainText(TEST_VALID_URL);
}); });

Просмотреть файл

@@ -78,33 +78,7 @@ exports[`components/SettingItemMax should match snapshot, on clientError 1`] = `
<hr /> <hr />
<div <div
role="alert" role="alert"
> />
<div
className="form-group"
>
<label
className="col-sm-12 has-error"
>
<i
className="icon icon-alert-circle-outline"
role="img"
/>
<span
className="sr-only"
>
<MemoizedFormattedMessage
defaultMessage="Error"
id="setting_item_max.error"
/>
</span>
<span
id="clientError"
>
clientError
</span>
</label>
</div>
</div>
<SaveButton <SaveButton
disabled={false} disabled={false}
onClick={[Function]} onClick={[Function]}

Просмотреть файл

@@ -19,11 +19,6 @@ type Props = {
containerStyle?: string; containerStyle?: string;
serverError?: ReactNode; serverError?: ReactNode;
/**
* Client error
*/
clientError?: ReactNode;
/** /**
* Settings extra information * Settings extra information
*/ */
@@ -51,6 +46,7 @@ type Props = {
shiftEnter?: boolean; shiftEnter?: boolean;
saveButtonText?: string; saveButtonText?: string;
saveButtonClassName?: string; saveButtonClassName?: string;
isValid?: boolean;
} }
export default class SettingItemMax extends React.PureComponent<Props> { export default class SettingItemMax extends React.PureComponent<Props> {
settingList: React.RefObject<HTMLDivElement>; settingList: React.RefObject<HTMLDivElement>;
@@ -120,31 +116,6 @@ export default class SettingItemMax extends React.PureComponent<Props> {
}; };
render() { render() {
let clientError = null;
if (this.props.clientError) {
clientError = (
<div className='form-group'>
<label
className='col-sm-12 has-error'
>
<i
className='icon icon-alert-circle-outline'
role='img'
/>
<span className='sr-only'>
<FormattedMessage
id='setting_item_max.error'
defaultMessage='Error'
/>
</span>
<span id='clientError'>
{this.props.clientError}
</span>
</label>
</div>
);
}
let serverError = null; let serverError = null;
if (this.props.serverError) { if (this.props.serverError) {
serverError = ( serverError = (
@@ -193,7 +164,7 @@ export default class SettingItemMax extends React.PureComponent<Props> {
<SaveButton <SaveButton
defaultMessage={this.props.saveButtonText} defaultMessage={this.props.saveButtonText}
saving={this.props.saving} saving={this.props.saving}
disabled={this.props.saving} disabled={this.props.saving || this.props.isValid === false}
onClick={this.handleSubmit} onClick={this.handleSubmit}
btnClass={this.props.saveButtonClassName} btnClass={this.props.saveButtonClassName}
/> />
@@ -267,7 +238,6 @@ export default class SettingItemMax extends React.PureComponent<Props> {
role='alert' role='alert'
> >
{serverError} {serverError}
{clientError}
</div> </div>
{submit} {submit}
<button <button

Просмотреть файл

@@ -141,8 +141,8 @@ describe('components/user_settings/general/UserSettingsGeneral', () => {
<UserSettingsGeneral {...props}/> <UserSettingsGeneral {...props}/>
</Provider>, </Provider>,
); );
expect(wrapper.find('#position').length).toBe(1); expect(wrapper.find('#position').length).toBe(2);
expect(wrapper.find('#position').is('input')).toBeTruthy(); expect(wrapper.find('#position.Input').is('input')).toBeTruthy();
props.ldapPositionAttributeSet = true; props.ldapPositionAttributeSet = true;
props.samlPositionAttributeSet = false; props.samlPositionAttributeSet = false;
@@ -767,7 +767,7 @@ describe('components/user_settings/general/UserSettingsGeneral', () => {
renderWithContext(<UserSettingsGeneral {...props}/>); renderWithContext(<UserSettingsGeneral {...props}/>);
userEvent.type(screen.getByRole('textbox', {name: urlAttribute.name}), 'ftp://invalid-scheme'); userEvent.type(screen.getByRole('textbox', {name: urlAttribute.name}), 'ftp://invalid-scheme');
userEvent.click(screen.getByRole('button', {name: 'Save'})); userEvent.tab();
expect(await screen.findByText('Please enter a valid url.')).toBeInTheDocument(); expect(await screen.findByText('Please enter a valid url.')).toBeInTheDocument();
expect(saveCustomProfileAttribute).not.toHaveBeenCalled(); expect(saveCustomProfileAttribute).not.toHaveBeenCalled();
@@ -804,7 +804,7 @@ describe('components/user_settings/general/UserSettingsGeneral', () => {
renderWithContext(<UserSettingsGeneral {...props}/>); renderWithContext(<UserSettingsGeneral {...props}/>);
userEvent.type(screen.getByRole('textbox', {name: emailAttribute.name}), 'invalid-email'); userEvent.type(screen.getByRole('textbox', {name: emailAttribute.name}), 'invalid-email');
userEvent.click(screen.getByRole('button', {name: 'Save'})); userEvent.tab();
expect(await screen.findByText('Please enter a valid email address.')).toBeInTheDocument(); expect(await screen.findByText('Please enter a valid email address.')).toBeInTheDocument();
expect(saveCustomProfileAttribute).not.toHaveBeenCalled(); expect(saveCustomProfileAttribute).not.toHaveBeenCalled();

Просмотреть файл

@@ -22,6 +22,7 @@ import {trackEvent} from 'actions/telemetry_actions.jsx';
import SettingItem from 'components/setting_item'; import SettingItem from 'components/setting_item';
import SettingItemMax from 'components/setting_item_max'; import SettingItemMax from 'components/setting_item_max';
import SettingPicture from 'components/setting_picture'; import SettingPicture from 'components/setting_picture';
import Input from 'components/widgets/inputs/input/input';
import LoadingWrapper from 'components/widgets/loading/loading_wrapper'; import LoadingWrapper from 'components/widgets/loading/loading_wrapper';
import {AnnouncementBarMessages, AnnouncementBarTypes, AcceptedProfileImageTypes, Constants, ValidationErrors} from 'utils/constants'; import {AnnouncementBarMessages, AnnouncementBarTypes, AcceptedProfileImageTypes, Constants, ValidationErrors} from 'utils/constants';
@@ -183,7 +184,7 @@ type State = {
sectionIsSaving: boolean; sectionIsSaving: boolean;
showSpinner: boolean; showSpinner: boolean;
resendStatus?: string; resendStatus?: string;
clientError?: string | null; pictureError?: string | null;
serverError?: string | {server_error_id: string; message: string}; serverError?: string | {server_error_id: string; message: string};
emailError?: string; emailError?: string;
customAttributeValues: Record<string, string | string[]>; customAttributeValues: Record<string, string | string[]>;
@@ -245,19 +246,6 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
const user = Object.assign({}, this.props.user); const user = Object.assign({}, this.props.user);
const username = this.state.username.trim().toLowerCase(); const username = this.state.username.trim().toLowerCase();
const {formatMessage} = this.props.intl;
const usernameError = Utils.isValidUsername(username);
if (usernameError) {
let errObj;
if (usernameError.id === ValidationErrors.RESERVED_NAME) {
errObj = {clientError: formatMessage(holders.usernameReserved), serverError: ''};
} else {
errObj = {clientError: formatMessage(holders.usernameRestrictions, {min: Constants.MIN_USERNAME_LENGTH, max: Constants.MAX_USERNAME_LENGTH}), serverError: ''};
}
this.setState(errObj);
return;
}
if (user.username === username) { if (user.username === username) {
this.updateSection(''); this.updateSection('');
return; return;
@@ -310,34 +298,37 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
const confirmEmail = this.state.confirmEmail.trim().toLowerCase(); const confirmEmail = this.state.confirmEmail.trim().toLowerCase();
const currentPassword = this.state.currentPassword; const currentPassword = this.state.currentPassword;
const {formatMessage} = this.props.intl;
if (email === user.email && (confirmEmail === '' || confirmEmail === user.email)) { if (email === user.email && (confirmEmail === '' || confirmEmail === user.email)) {
this.updateSection(''); this.updateSection('');
return; return;
} }
if (email === '' || !isEmail(email)) {
this.setState({emailError: formatMessage(holders.validEmail), clientError: '', serverError: ''});
return;
}
if (email !== confirmEmail) {
this.setState({emailError: formatMessage(holders.emailMatch), clientError: '', serverError: ''});
return;
}
if (currentPassword === '') {
this.setState({emailError: formatMessage(holders.emptyPassword), clientError: '', serverError: ''});
return;
}
user.email = email; user.email = email;
user.password = currentPassword; user.password = currentPassword;
trackEvent('settings', 'user_settings_update', {field: 'email'}); trackEvent('settings', 'user_settings_update', {field: 'email'});
this.submitUser(user, true); this.submitUser(user, true);
}; };
isEmailValid = () => {
const email = this.state.email.trim().toLowerCase();
const confirmEmail = this.state.confirmEmail.trim().toLowerCase();
const currentPassword = this.state.currentPassword;
if (email === '' || !isEmail(email)) {
return false;
}
if (email !== confirmEmail) {
return false;
}
if (currentPassword === '') {
return false;
}
return true;
};
submitUser = (user: UserProfile, emailUpdated: boolean) => { submitUser = (user: UserProfile, emailUpdated: boolean) => {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
this.setState({sectionIsSaving: true}); this.setState({sectionIsSaving: true});
@@ -367,7 +358,7 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
} else { } else {
serverError = err; serverError = err;
} }
this.setState({serverError, emailError: '', clientError: '', sectionIsSaving: false}); this.setState({serverError, emailError: '', sectionIsSaving: false});
} }
}); });
}; };
@@ -384,7 +375,7 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
} else { } else {
serverError = err; serverError = err;
} }
this.setState({serverError, emailError: '', clientError: '', sectionIsSaving: false}); this.setState({serverError, emailError: '', pictureError: '', sectionIsSaving: false});
} }
}; };
@@ -403,10 +394,10 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
const file = this.state.pictureFile; const file = this.state.pictureFile;
if (!AcceptedProfileImageTypes.includes(file.type)) { if (!AcceptedProfileImageTypes.includes(file.type)) {
this.setState({clientError: formatMessage(holders.validImage), serverError: ''}); this.setState({pictureError: formatMessage(holders.validImage), serverError: ''});
return; return;
} else if (file.size > this.props.maxFileSize) { } else if (file.size > this.props.maxFileSize) {
this.setState({clientError: formatMessage(holders.imageTooLarge), serverError: ''}); this.setState({pictureError: formatMessage(holders.imageTooLarge), serverError: ''});
return; return;
} }
@@ -442,8 +433,6 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
}; };
submitAttribute = async (settings: string[]) => { submitAttribute = async (settings: string[]) => {
const {formatMessage} = this.props.intl;
const attributeID = settings[0]; const attributeID = settings[0];
const attributeField = this.props.customProfileAttributeFields.find((field) => field.id === attributeID); const attributeField = this.props.customProfileAttributeFields.find((field) => field.id === attributeID);
if (attributeField === undefined) { if (attributeField === undefined) {
@@ -454,7 +443,7 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
if (typeof attributeValue === 'string' && attributeField.attrs && attributeField.attrs.value_type) { if (typeof attributeValue === 'string' && attributeField.attrs && attributeField.attrs.value_type) {
if (attributeField.attrs.value_type === 'email') { if (attributeField.attrs.value_type === 'email') {
if (attributeValue !== '' && !isEmail(attributeValue)) { if (attributeValue !== '' && !isEmail(attributeValue)) {
this.setState({clientError: formatMessage(holders.validEmail), emailError: '', serverError: ''}); this.setState({emailError: '', serverError: ''});
return; return;
} }
} }
@@ -462,7 +451,7 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
if (attributeValue !== '') { if (attributeValue !== '') {
const validURL = validHttpUrl(attributeValue); const validURL = validHttpUrl(attributeValue);
if (!validURL) { if (!validURL) {
this.setState({clientError: formatMessage(holders.validUrl), emailError: '', serverError: ''}); this.setState({emailError: '', serverError: ''});
return; return;
} }
let validLink = validURL.toString(); let validLink = validURL.toString();
@@ -487,7 +476,7 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
this.setState({customAttributeValues: {...this.state.customAttributeValues, ...data}}); this.setState({customAttributeValues: {...this.state.customAttributeValues, ...data}});
} else if (err) { } else if (err) {
const serverError = err.message; const serverError = err.message;
this.setState({serverError, emailError: '', clientError: '', sectionIsSaving: false}); this.setState({serverError, emailError: '', sectionIsSaving: false});
} }
}); });
}; };
@@ -529,7 +518,7 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
this.setState({pictureFile: e.target.files[0]}); this.setState({pictureFile: e.target.files[0]});
this.submitActive = true; this.submitActive = true;
this.setState({clientError: null}); this.setState({pictureError: null});
} else { } else {
this.setState({pictureFile: null}); this.setState({pictureFile: null});
} }
@@ -564,7 +553,7 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
}; };
updateSection = (section: string) => { updateSection = (section: string) => {
this.setState(Object.assign({}, this.setupInitialState(this.props), {clientError: '', serverError: '', emailError: '', sectionIsSaving: false})); this.setState(Object.assign({}, this.setupInitialState(this.props), {pictureError: '', serverError: '', emailError: '', sectionIsSaving: false}));
this.submitActive = false; this.submitActive = false;
this.props.updateSection(section); this.props.updateSection(section);
}; };
@@ -647,15 +636,24 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
/> />
</label> </label>
<div className='col-sm-7'> <div className='col-sm-7'>
<input <Input
autoFocus={true} autoFocus={true}
id='primaryEmail' id='primaryEmail'
className='form-control' name='primaryEmail'
type='email' type='email'
onChange={this.updateEmail} onChange={this.updateEmail}
maxLength={Constants.MAX_EMAIL_LENGTH} maxLength={Constants.MAX_EMAIL_LENGTH}
value={this.state.email} value={this.state.email}
aria-label={formatMessage({id: 'user.settings.general.newEmail', defaultMessage: 'New Email'})} aria-label={formatMessage({id: 'user.settings.general.newEmail', defaultMessage: 'New Email'})}
validate={(value) => {
if (value === '' || !isEmail(value as string)) {
return {
type: 'error',
value: formatMessage(holders.validEmail),
};
}
return undefined;
}}
/> />
</div> </div>
</div> </div>
@@ -675,14 +673,23 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
/> />
</label> </label>
<div className='col-sm-7'> <div className='col-sm-7'>
<input <Input
id='confirmEmail' id='confirmEmail'
className='form-control' name='confirmEmail'
type='email' type='email'
onChange={this.updateConfirmEmail} onChange={this.updateConfirmEmail}
maxLength={Constants.MAX_EMAIL_LENGTH} maxLength={Constants.MAX_EMAIL_LENGTH}
value={this.state.confirmEmail} value={this.state.confirmEmail}
aria-label={formatMessage({id: 'user.settings.general.confirmEmail', defaultMessage: 'Confirm Email'})} aria-label={formatMessage({id: 'user.settings.general.confirmEmail', defaultMessage: 'Confirm Email'})}
validate={(value) => {
if (this.state.email !== value) {
return {
type: 'error',
value: formatMessage(holders.emailMatch),
};
}
return undefined;
}}
/> />
</div> </div>
</div> </div>
@@ -702,13 +709,22 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
/> />
</label> </label>
<div className='col-sm-7'> <div className='col-sm-7'>
<input <Input
id='currentPassword' id='currentPassword'
className='form-control' name='currentPassword'
type='password' type='password'
onChange={this.updateCurrentPassword} onChange={this.updateCurrentPassword}
value={this.state.currentPassword} value={this.state.currentPassword}
aria-label={formatMessage({id: 'user.settings.general.currentPassword', defaultMessage: 'Current Password'})} aria-label={formatMessage({id: 'user.settings.general.currentPassword', defaultMessage: 'Current Password'})}
validate={(value) => {
if (value === '') {
return {
type: 'error',
value: formatMessage(holders.emptyPassword),
};
}
return undefined;
}}
/> />
</div> </div>
</div> </div>
@@ -838,8 +854,8 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
submit={submit} submit={submit}
saving={this.state.sectionIsSaving} saving={this.state.sectionIsSaving}
serverError={this.state.serverError} serverError={this.state.serverError}
clientError={this.state.emailError}
updateSection={this.updateSection} updateSection={this.updateSection}
isValid={this.isEmailValid()}
/> />
); );
} }
@@ -959,10 +975,10 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
/> />
</label> </label>
<div className='col-sm-7'> <div className='col-sm-7'>
<input <Input
id='firstName' id='firstName'
name='firstName'
autoFocus={true} autoFocus={true}
className='form-control'
type='text' type='text'
onChange={this.updateFirstName} onChange={this.updateFirstName}
maxLength={Constants.MAX_FIRSTNAME_LENGTH} maxLength={Constants.MAX_FIRSTNAME_LENGTH}
@@ -989,9 +1005,9 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
/> />
</label> </label>
<div className='col-sm-7'> <div className='col-sm-7'>
<input <Input
id='lastName' id='lastName'
className='form-control' name='lastName'
type='text' type='text'
onChange={this.updateLastName} onChange={this.updateLastName}
maxLength={Constants.MAX_LASTNAME_LENGTH} maxLength={Constants.MAX_LASTNAME_LENGTH}
@@ -1042,7 +1058,6 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
submit={submit} submit={submit}
saving={this.state.sectionIsSaving} saving={this.state.sectionIsSaving}
serverError={this.state.serverError} serverError={this.state.serverError}
clientError={this.state.clientError}
updateSection={this.updateSection} updateSection={this.updateSection}
extraInfo={extraInfo} extraInfo={extraInfo}
/> />
@@ -1125,10 +1140,10 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
> >
<label className='col-sm-5 control-label'>{nicknameLabel}</label> <label className='col-sm-5 control-label'>{nicknameLabel}</label>
<div className='col-sm-7'> <div className='col-sm-7'>
<input <Input
id='nickname' id='nickname'
name='nickname'
autoFocus={true} autoFocus={true}
className='form-control'
type='text' type='text'
onChange={this.updateNickname} onChange={this.updateNickname}
value={this.state.nickname} value={this.state.nickname}
@@ -1159,7 +1174,6 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
submit={submit} submit={submit}
saving={this.state.sectionIsSaving} saving={this.state.sectionIsSaving}
serverError={this.state.serverError} serverError={this.state.serverError}
clientError={this.state.clientError}
updateSection={this.updateSection} updateSection={this.updateSection}
extraInfo={extraInfo} extraInfo={extraInfo}
/> />
@@ -1227,17 +1241,33 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
> >
<label className='col-sm-5 control-label'>{usernameLabel}</label> <label className='col-sm-5 control-label'>{usernameLabel}</label>
<div className='col-sm-7'> <div className='col-sm-7'>
<input <Input
id='username' id='username'
name='username'
autoFocus={true} autoFocus={true}
maxLength={Constants.MAX_USERNAME_LENGTH} maxLength={Constants.MAX_USERNAME_LENGTH}
className='form-control'
type='text' type='text'
onChange={this.updateUsername} onChange={this.updateUsername}
value={this.state.username} value={this.state.username}
autoCapitalize='off' autoCapitalize='off'
onFocus={Utils.moveCursorToEnd} onFocus={Utils.moveCursorToEnd}
aria-label={formatMessage({id: 'user.settings.general.username', defaultMessage: 'Username'})} aria-label={formatMessage({id: 'user.settings.general.username', defaultMessage: 'Username'})}
validate={(value) => {
const usernameError = Utils.isValidUsername(value as string);
if (usernameError) {
if (usernameError.id === ValidationErrors.RESERVED_NAME) {
return {
type: 'error',
value: formatMessage(holders.usernameReserved),
};
}
return {
type: 'error',
value: formatMessage(holders.usernameRestrictions, {min: Constants.MIN_USERNAME_LENGTH, max: Constants.MAX_USERNAME_LENGTH}),
};
}
return undefined;
}}
/> />
</div> </div>
</div>, </div>,
@@ -1271,9 +1301,9 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
submit={submit} submit={submit}
saving={this.state.sectionIsSaving} saving={this.state.sectionIsSaving}
serverError={this.state.serverError} serverError={this.state.serverError}
clientError={this.state.clientError}
updateSection={this.updateSection} updateSection={this.updateSection}
extraInfo={extraInfo} extraInfo={extraInfo}
isValid={Utils.isValidUsername(this.state.username) === undefined}
/> />
); );
} }
@@ -1328,10 +1358,10 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
> >
<label className='col-sm-5 control-label'>{positionLabel}</label> <label className='col-sm-5 control-label'>{positionLabel}</label>
<div className='col-sm-7'> <div className='col-sm-7'>
<input <Input
id='position' id='position'
name='position'
autoFocus={true} autoFocus={true}
className='form-control'
type='text' type='text'
onChange={this.updatePosition} onChange={this.updatePosition}
value={this.state.position} value={this.state.position}
@@ -1363,7 +1393,6 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
submit={submit} submit={submit}
saving={this.state.sectionIsSaving} saving={this.state.sectionIsSaving}
serverError={this.state.serverError} serverError={this.state.serverError}
clientError={this.state.clientError}
updateSection={this.updateSection} updateSection={this.updateSection}
extraInfo={extraInfo} extraInfo={extraInfo}
/> />
@@ -1450,6 +1479,31 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
let extraInfo: JSX.Element|string; let extraInfo: JSX.Element|string;
let submit = null; let submit = null;
const validate = () => {
if (attribute.attrs?.value_type === 'email') {
const value = this.state.customAttributeValues[attribute.id] as string;
if (value && !isEmail(value)) {
return {
type: 'error' as const,
value: formatMessage(holders.validEmail),
};
}
}
if (attribute.attrs?.value_type === 'url') {
const value = this.state.customAttributeValues[attribute.id] as string;
if (value) {
const validURL = validHttpUrl(value);
if (!validURL) {
return {
type: 'error' as const,
value: formatMessage(holders.validUrl),
};
}
}
}
return undefined;
};
if ((this.props.user.auth_service === Constants.LDAP_SERVICE && attribute.attrs?.ldap) || if ((this.props.user.auth_service === Constants.LDAP_SERVICE && attribute.attrs?.ldap) ||
(this.props.user.auth_service === Constants.SAML_SERVICE && attribute.attrs?.saml)) { (this.props.user.auth_service === Constants.SAML_SERVICE && attribute.attrs?.saml)) {
extraInfo = ( extraInfo = (
@@ -1504,10 +1558,10 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
> >
<label className='col-sm-5 control-label'>{attributeLabel}</label> <label className='col-sm-5 control-label'>{attributeLabel}</label>
<div className='col-sm-7'> <div className='col-sm-7'>
<input <Input
id={sectionName} id={sectionName}
name={sectionName}
autoFocus={true} autoFocus={true}
className='form-control'
type={inputType} type={inputType}
onChange={this.updateAttribute} onChange={this.updateAttribute}
value={getDisplayValue(this.state.customAttributeValues[attribute.id]) as string} value={getDisplayValue(this.state.customAttributeValues[attribute.id]) as string}
@@ -1515,6 +1569,7 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
autoCapitalize='off' autoCapitalize='off'
onFocus={Utils.moveCursorToEnd} onFocus={Utils.moveCursorToEnd}
aria-label={attribute.name} aria-label={attribute.name}
validate={validate}
/> />
</div> </div>
</div>, </div>,
@@ -1540,9 +1595,9 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
submit={submit} submit={submit}
saving={this.state.sectionIsSaving} saving={this.state.sectionIsSaving}
serverError={this.state.serverError} serverError={this.state.serverError}
clientError={this.state.clientError}
updateSection={this.updateSection} updateSection={this.updateSection}
extraInfo={extraInfo} extraInfo={extraInfo}
isValid={validate() === undefined}
/> />
); );
} }
@@ -1638,7 +1693,7 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
src={imgSrc} src={imgSrc}
defaultImageSrc={Utils.defaultImageURLForUser(user.id)} defaultImageSrc={Utils.defaultImageURLForUser(user.id)}
serverError={this.state.serverError} serverError={this.state.serverError}
clientError={this.state.clientError} clientError={this.state.pictureError}
updateSection={(e: React.MouseEvent) => { updateSection={(e: React.MouseEvent) => {
this.updateSection(''); this.updateSection('');
e.preventDefault(); e.preventDefault();

Просмотреть файл

@@ -20,6 +20,7 @@ import ExternalLink from 'components/external_link';
import SettingItem from 'components/setting_item'; import SettingItem from 'components/setting_item';
import SettingItemMax from 'components/setting_item_max'; import SettingItemMax from 'components/setting_item_max';
import ToggleModalButton from 'components/toggle_modal_button'; import ToggleModalButton from 'components/toggle_modal_button';
import Input from 'components/widgets/inputs/input/input';
import icon50 from 'images/icon50x50.png'; import icon50 from 'images/icon50x50.png';
import Constants from 'utils/constants'; import Constants from 'utils/constants';
@@ -75,7 +76,6 @@ type State = {
currentPassword: string; currentPassword: string;
newPassword: string; newPassword: string;
confirmPassword: string; confirmPassword: string;
passwordError: React.ReactNode;
serverError: string | null; serverError: string | null;
tokenError: string; tokenError: string;
savingPassword: boolean; savingPassword: boolean;
@@ -93,7 +93,6 @@ export class SecurityTab extends React.PureComponent<Props, State> {
currentPassword: '', currentPassword: '',
newPassword: '', newPassword: '',
confirmPassword: '', confirmPassword: '',
passwordError: '',
serverError: '', serverError: '',
tokenError: '', tokenError: '',
authService: this.props.user.auth_service, authService: this.props.user.auth_service,
@@ -123,41 +122,8 @@ export class SecurityTab extends React.PureComponent<Props, State> {
const user = this.props.user; const user = this.props.user;
const currentPassword = this.state.currentPassword; const currentPassword = this.state.currentPassword;
const newPassword = this.state.newPassword; const newPassword = this.state.newPassword;
const confirmPassword = this.state.confirmPassword;
if (currentPassword === '') { if (!this.isPasswordValid()) {
this.setState({
passwordError: this.props.intl.formatMessage({
id: 'user.settings.security.currentPasswordError',
defaultMessage: 'Please enter your current password.',
}),
serverError: '',
});
return;
}
const {valid, error} = isValidPassword(
newPassword,
this.props.passwordConfig,
);
if (!valid && error) {
this.setState({
passwordError: error,
serverError: '',
});
return;
}
if (newPassword !== confirmPassword) {
const defaultState = Object.assign(this.getDefaultState(), {
passwordError: this.props.intl.formatMessage({
id: 'user.settings.security.passwordMatchError',
defaultMessage:
'The new passwords you entered do not match.',
}),
serverError: '',
});
this.setState(defaultState);
return; return;
} }
@@ -180,11 +146,30 @@ export class SecurityTab extends React.PureComponent<Props, State> {
} else { } else {
state.serverError = err; state.serverError = err;
} }
state.passwordError = '';
this.setState(state); this.setState(state);
} }
}; };
isPasswordValid = () => {
if (this.state.currentPassword === '') {
return false;
}
const {valid, error} = isValidPassword(
this.state.newPassword,
this.props.passwordConfig,
);
if (!valid && error) {
return false;
}
if (this.state.newPassword !== this.state.confirmPassword) {
return false;
}
return true;
};
updateCurrentPassword = (e: React.ChangeEvent<HTMLInputElement>) => { updateCurrentPassword = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({currentPassword: e.target.value}); this.setState({currentPassword: e.target.value});
}; };
@@ -233,7 +218,6 @@ export class SecurityTab extends React.PureComponent<Props, State> {
newPassword: '', newPassword: '',
confirmPassword: '', confirmPassword: '',
serverError: null, serverError: null,
passwordError: null,
}); });
break; break;
default: default:
@@ -268,10 +252,10 @@ export class SecurityTab extends React.PureComponent<Props, State> {
/> />
</label> </label>
<div className='col-sm-7'> <div className='col-sm-7'>
<input <Input
id='currentPassword' id='currentPassword'
name='currentPassword'
autoFocus={true} autoFocus={true}
className='form-control'
type='password' type='password'
onChange={this.updateCurrentPassword} onChange={this.updateCurrentPassword}
value={this.state.currentPassword} value={this.state.currentPassword}
@@ -279,6 +263,20 @@ export class SecurityTab extends React.PureComponent<Props, State> {
id: 'user.settings.security.currentPassword', id: 'user.settings.security.currentPassword',
defaultMessage: 'Current Password', defaultMessage: 'Current Password',
})} })}
validate={(value) => {
if (typeof value !== 'string' || value === '') {
return {
type: 'error' as const,
value: (
<FormattedMessage
id='user.settings.security.currentPasswordError'
defaultMessage='Please enter your current password.'
/>
),
};
}
return undefined;
}}
/> />
</div> </div>
</div>, </div>,
@@ -298,9 +296,9 @@ export class SecurityTab extends React.PureComponent<Props, State> {
/> />
</label> </label>
<div className='col-sm-7'> <div className='col-sm-7'>
<input <Input
id='newPassword' id='newPassword'
className='form-control' name='newPassword'
type='password' type='password'
onChange={this.updateNewPassword} onChange={this.updateNewPassword}
value={this.state.newPassword} value={this.state.newPassword}
@@ -308,6 +306,19 @@ export class SecurityTab extends React.PureComponent<Props, State> {
id: 'user.settings.security.newPassword', id: 'user.settings.security.newPassword',
defaultMessage: 'New Password', defaultMessage: 'New Password',
})} })}
validate={(value) => {
const {valid, error} = isValidPassword(
value as string,
this.props.passwordConfig,
);
if (!valid) {
return {
type: 'error' as const,
value: error,
};
}
return undefined;
}}
/> />
</div> </div>
</div>, </div>,
@@ -327,9 +338,9 @@ export class SecurityTab extends React.PureComponent<Props, State> {
/> />
</label> </label>
<div className='col-sm-7'> <div className='col-sm-7'>
<input <Input
id='confirmPassword' id='confirmPassword'
className='form-control' name='confirmPassword'
type='password' type='password'
onChange={this.updateConfirmPassword} onChange={this.updateConfirmPassword}
value={this.state.confirmPassword} value={this.state.confirmPassword}
@@ -337,6 +348,23 @@ export class SecurityTab extends React.PureComponent<Props, State> {
id: 'user.settings.security.retypePassword', id: 'user.settings.security.retypePassword',
defaultMessage: 'Retype New Password', defaultMessage: 'Retype New Password',
})} })}
validate={(value) => {
if (typeof value !== 'string') {
return undefined;
}
if (this.state.newPassword !== value) {
return {
type: 'error' as const,
value: (
<FormattedMessage
id='user.settings.security.passwordMatchError'
defaultMessage='The new passwords you entered do not match.'
/>
),
};
}
return undefined;
}}
/> />
</div> </div>
</div>, </div>,
@@ -435,8 +463,8 @@ export class SecurityTab extends React.PureComponent<Props, State> {
submit={submit} submit={submit}
saving={this.state.savingPassword} saving={this.state.savingPassword}
serverError={this.state.serverError} serverError={this.state.serverError}
clientError={this.state.passwordError}
updateSection={this.handleUpdateSection} updateSection={this.handleUpdateSection}
isValid={this.isPasswordValid()}
/> />
); );
} }

Просмотреть файл

@@ -43,6 +43,7 @@ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
clearable?: boolean; clearable?: boolean;
clearableTooltipText?: string; clearableTooltipText?: string;
onClear?: () => void; onClear?: () => void;
validate?: (value: React.InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement>['value']) => CustomMessageInputType | undefined;
} }
const Input = React.forwardRef(( const Input = React.forwardRef((
@@ -75,6 +76,7 @@ const Input = React.forwardRef((
onBlur, onBlur,
onChange, onChange,
onClear, onClear,
validate,
...otherProps ...otherProps
}: InputProps, }: InputProps,
ref?: React.Ref<HTMLInputElement | HTMLTextAreaElement>, ref?: React.Ref<HTMLInputElement | HTMLTextAreaElement>,
@@ -151,6 +153,13 @@ const Input = React.forwardRef((
}; };
const validateInput = () => { const validateInput = () => {
if (validate) {
const validationError = validate(value);
if (validationError) {
setCustomInputLabel(validationError);
}
}
// Only check for required field validation on blur // Only check for required field validation on blur
// Length validation is handled through derived values in the render function // Length validation is handled through derived values in the render function
if (required && (value === null || value === '')) { if (required && (value === null || value === '')) {

Просмотреть файл

@@ -296,6 +296,19 @@
margin-block-start: 24px; margin-block-start: 24px;
} }
} }
.Input_fieldset {
background: var(--center-channel-bg);
.Input_wrapper {
padding: 0 12px;
.Input {
height: 28px;
border: none;
}
}
}
} }
.timezone-container { .timezone-container {