@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
417 lines (415 loc) • 16.4 kB
JavaScript
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import { create } from '../../services/users';
import { showErrorDialog } from '../../state/reducers/dialogs/error';
import DialogBody from '../DialogBody/DialogBody';
import TextField from '@mui/material/TextField';
import PasswordTextField from '../PasswordTextField/PasswordTextField';
import DialogFooter from '../DialogFooter/DialogFooter';
import SecondaryButton from '../SecondaryButton';
import PrimaryButton from '../PrimaryButton';
import { makeStyles } from 'tss-react/mui';
import Grid from '@mui/material/Grid';
import UserGroupMembershipEditor from '../UserGroupMembershipEditor';
import { map, switchMap } from 'rxjs/operators';
import { forkJoin, of } from 'rxjs';
import { addUserToGroup } from '../../services/groups';
import { useSpreadState } from '../../hooks/useSpreadState';
import {
USER_FIRST_NAME_MIN_LENGTH,
USER_USERNAME_MIN_LENGTH,
USER_LAST_NAME_MIN_LENGTH,
USER_EMAIL_MAX_LENGTH,
USER_FIRST_NAME_MAX_LENGTH,
USER_LAST_NAME_MAX_LENGTH,
USER_PASSWORD_MAX_LENGTH,
USER_USERNAME_MAX_LENGTH,
isInvalidEmail,
isInvalidUsername,
validateFieldMinLength
} from '../UserManagement/utils';
import useUpdateRefs from '../../hooks/useUpdateRefs';
import { showSystemNotification } from '../../state/actions/system';
import { PasswordStrengthDisplayPopper } from '../PasswordStrengthDisplayPopper';
const useStyles = makeStyles()((theme) => ({
arrow: {
overflow: 'hidden',
position: 'absolute',
width: '1em',
height: '0.71em',
boxSizing: 'border-box',
color: theme.palette.background.paper,
'&::before': {
content: '""',
margin: 'auto',
display: 'block',
width: '100%',
height: '100%',
boxShadow: theme.shadows[1],
backgroundColor: 'currentColor',
transform: 'rotate(45deg)'
}
},
textField: {
marginBottom: theme.spacing(1)
},
form: {
display: 'contents'
},
dialogBody: {
overflow: 'auto'
}
}));
const translations = defineMessages({
invalidMinLength: {
id: 'createUserDialog.invalidMinLength',
defaultMessage: 'Min {length} characters'
},
userCreated: {
id: 'createUserDialog.userCreated',
defaultMessage: 'User created successfully'
}
});
export function CreateUserDialogContainer(props) {
const { onClose, passwordRequirementsMinComplexity, onCreateSuccess, isSubmitting, onSubmittingAndOrPendingChange } =
props;
const [newUser, setNewUser] = useSpreadState({
firstName: '',
lastName: '',
email: '',
username: '',
password: '',
enabled: true
});
const [submitted, setSubmitted] = useState(false);
const [passwordConfirm, setPasswordConfirm] = useState('');
const [validPassword, setValidPassword] = useState(false);
const [submitOk, setSubmitOk] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const { classes, cx } = useStyles();
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const selectedGroupsRef = useRef([]);
const functionRefs = useUpdateRefs({
onSubmittingAndOrPendingChange
});
const onSubmit = (e) => {
e.preventDefault();
if (submitOk) {
functionRefs.current.onSubmittingAndOrPendingChange({
isSubmitting: true
});
setSubmitted(true);
if (Object.values(newUser).every(Boolean)) {
const trimmedNewUser = {};
Object.entries(newUser).forEach(([key, value]) => {
trimmedNewUser[key] = typeof value === 'string' ? value.trim() : value;
});
create(trimmedNewUser)
.pipe(
switchMap((user) =>
selectedGroupsRef.current.length
? forkJoin(selectedGroupsRef.current.map((id) => addUserToGroup(Number(id), user.username))).pipe(
map(() => user)
)
: of(user)
)
)
.subscribe({
next() {
dispatch(
showSystemNotification({
message: formatMessage(translations.userCreated)
})
);
onCreateSuccess?.();
functionRefs.current.onSubmittingAndOrPendingChange({
isSubmitting: false
});
},
error({ response: { response } }) {
functionRefs.current.onSubmittingAndOrPendingChange({
isSubmitting: false
});
dispatch(showErrorDialog({ error: response }));
}
});
} else {
setSubmitted(false);
}
}
};
const validateRequiredField = (field) => {
return submitted && field.trim() === '';
};
const validatePasswordMatch = (password, match) => {
return (submitted && match === '') || match !== password;
};
const onSelectedGroupsChanged = (groupIds) => (selectedGroupsRef.current = groupIds);
const onChangeValue = (key, value) => {
let cleanValue = value;
if (key === 'username') {
cleanValue = value.trim();
}
setNewUser({
[key]: cleanValue
});
};
const refs = useUpdateRefs({
validateFieldMinLength
});
useEffect(() => {
setSubmitOk(
Boolean(
newUser.firstName.trim() &&
!refs.current.validateFieldMinLength('firstName', newUser.firstName) &&
newUser.lastName.trim() &&
!refs.current.validateFieldMinLength('lastName', newUser.lastName) &&
!isInvalidEmail(newUser.email) &&
newUser.username.trim() &&
!refs.current.validateFieldMinLength('username', newUser.username) &&
!isInvalidUsername(newUser.username) &&
newUser.password &&
validPassword &&
passwordConfirm &&
newUser.password === passwordConfirm
)
);
onSubmittingAndOrPendingChange({
hasPendingChanges: Boolean(
newUser.firstName || newUser.email || newUser.password || validPassword || passwordConfirm
)
});
}, [newUser, passwordConfirm, onSubmittingAndOrPendingChange, validPassword, refs]);
return React.createElement(
'form',
{ className: classes.form },
React.createElement(
DialogBody,
{ className: classes.dialogBody },
React.createElement(
Grid,
{ container: true, spacing: 2 },
React.createElement(
Grid,
{ item: true, sm: 6 },
React.createElement(
Grid,
{ container: true, spacing: 2 },
React.createElement(
Grid,
{ item: true, sm: 6 },
React.createElement(TextField, {
autoFocus: true,
className: cx(classes.textField),
label: React.createElement(FormattedMessage, {
id: 'createUserDialog.firstName',
defaultMessage: 'First Name'
}),
required: true,
fullWidth: true,
margin: 'normal',
value: newUser.firstName,
error:
validateRequiredField(newUser.firstName) || validateFieldMinLength('firstName', newUser.firstName),
helperText: validateRequiredField(newUser.firstName)
? React.createElement(FormattedMessage, {
id: 'createUserDialog.firstNameRequired',
defaultMessage: 'First Name is required.'
})
: validateFieldMinLength('firstName', newUser.firstName)
? formatMessage(translations.invalidMinLength, { length: USER_FIRST_NAME_MIN_LENGTH })
: null,
onChange: (e) => setNewUser({ firstName: e.target.value }),
inputProps: { maxLength: USER_FIRST_NAME_MAX_LENGTH }
})
),
React.createElement(
Grid,
{ item: true, sm: 6 },
React.createElement(TextField, {
className: cx(classes.textField),
label: React.createElement(FormattedMessage, {
id: 'createUserDialog.lastName',
defaultMessage: 'Last Name'
}),
required: true,
fullWidth: true,
margin: 'normal',
value: newUser.lastName,
error: validateRequiredField(newUser.lastName) || validateFieldMinLength('lastName', newUser.lastName),
helperText: validateRequiredField(newUser.lastName)
? React.createElement(FormattedMessage, {
id: 'createUserDialog.lastNameRequired',
defaultMessage: 'Last Name is required.'
})
: validateFieldMinLength('lastName', newUser.lastName)
? formatMessage(translations.invalidMinLength, { length: USER_LAST_NAME_MIN_LENGTH })
: null,
onChange: (e) => setNewUser({ lastName: e.target.value }),
inputProps: { maxLength: USER_LAST_NAME_MAX_LENGTH }
})
)
),
React.createElement(TextField, {
className: classes.textField,
label: React.createElement(FormattedMessage, { id: 'words.email', defaultMessage: 'E-mail' }),
required: true,
fullWidth: true,
value: newUser.email,
error: validateRequiredField(newUser.email) || isInvalidEmail(newUser.email),
helperText: validateRequiredField(newUser.email)
? React.createElement(FormattedMessage, {
id: 'createUserDialog.emailRequired',
defaultMessage: 'Email is required.'
})
: isInvalidEmail(newUser.email)
? React.createElement(FormattedMessage, {
id: 'createUserDialog.invalidEmail',
defaultMessage: 'Email is invalid.'
})
: null,
onChange: (e) => setNewUser({ email: e.target.value }),
inputProps: { maxLength: USER_EMAIL_MAX_LENGTH }
}),
React.createElement(TextField, {
className: classes.textField,
label: React.createElement(FormattedMessage, { id: 'words.username', defaultMessage: 'Username' }),
required: true,
fullWidth: true,
value: newUser.username,
error:
validateRequiredField(newUser.username) ||
isInvalidUsername(newUser.username) ||
validateFieldMinLength('username', newUser.username),
helperText: validateRequiredField(newUser.username)
? React.createElement(FormattedMessage, {
id: 'createUserDialog.usernameRequired',
defaultMessage: 'Username is required.'
})
: validateFieldMinLength('username', newUser.username)
? formatMessage(translations.invalidMinLength, { length: USER_USERNAME_MIN_LENGTH })
: null,
onChange: (e) => setNewUser({ username: e.target.value }),
inputProps: { maxLength: USER_USERNAME_MAX_LENGTH }
}),
React.createElement(
Grid,
{ container: true, spacing: 2 },
React.createElement(
Grid,
{ item: true, sm: 6 },
React.createElement(PasswordTextField, {
className: classes.textField,
label: React.createElement(FormattedMessage, { id: 'words.password', defaultMessage: 'Password' }),
required: true,
fullWidth: true,
value: newUser.password,
error: validateRequiredField(newUser.password) || (newUser.password !== '' && !validPassword),
helperText: validateRequiredField(newUser.password)
? React.createElement(FormattedMessage, {
id: 'createUserDialog.passwordRequired',
defaultMessage: 'Password is required.'
})
: newUser.password !== '' && !validPassword
? React.createElement(FormattedMessage, {
id: 'createUserDialog.passwordInvalid',
defaultMessage: 'Password is invalid.'
})
: null,
onChange: (e) => onChangeValue('password', e.target.value),
onFocus: (e) => setAnchorEl(e.target.parentElement),
onBlur: () => setAnchorEl(null),
inputProps: {
maxLength: USER_PASSWORD_MAX_LENGTH,
autoComplete: 'new-password'
}
})
),
React.createElement(
Grid,
{ item: true, sm: 6 },
React.createElement(PasswordTextField, {
className: classes.textField,
label: React.createElement(FormattedMessage, {
id: 'createUserDialog.passwordVerification',
defaultMessage: 'Password Verification'
}),
fullWidth: true,
required: true,
value: passwordConfirm,
error: validatePasswordMatch(newUser.password, passwordConfirm),
helperText:
validatePasswordMatch(newUser.password, passwordConfirm) &&
React.createElement(FormattedMessage, {
id: 'createUserDialog.passwordMatch',
defaultMessage: 'Must match the previous password.'
}),
onChange: (e) => setPasswordConfirm(e.target.value)
})
)
)
),
React.createElement(
Grid,
{ item: true, sm: 6 },
React.createElement(UserGroupMembershipEditor, { onChange: onSelectedGroupsChanged })
)
),
React.createElement(PasswordStrengthDisplayPopper, {
open: Boolean(anchorEl),
anchorEl: anchorEl,
placement: 'top',
value: newUser.password,
passwordRequirementsMinComplexity: passwordRequirementsMinComplexity,
onValidStateChanged: setValidPassword
})
),
React.createElement(
DialogFooter,
null,
React.createElement(
SecondaryButton,
{ onClick: (e) => onClose(e, null) },
React.createElement(FormattedMessage, { id: 'words.cancel', defaultMessage: 'Cancel' })
),
React.createElement(
PrimaryButton,
{ type: 'submit', onClick: onSubmit, disabled: !submitOk || isSubmitting, loading: isSubmitting },
React.createElement(FormattedMessage, { id: 'words.submit', defaultMessage: 'Submit' })
)
)
);
}
export default CreateUserDialogContainer;