diff --git a/frontend/src/component/admin/roles/RoleForm/RoleForm.tsx b/frontend/src/component/admin/roles/RoleForm/RoleForm.tsx index 5f16a92048..5e47919d5e 100644 --- a/frontend/src/component/admin/roles/RoleForm/RoleForm.tsx +++ b/frontend/src/component/admin/roles/RoleForm/RoleForm.tsx @@ -1,4 +1,4 @@ -import { styled } from '@mui/material'; +import { Alert, styled } from '@mui/material'; import Input from 'component/common/Input/Input'; import { PermissionAccordion } from './PermissionAccordion/PermissionAccordion'; import { @@ -23,6 +23,7 @@ import { PROJECT_ROLE_TYPES, ROOT_ROLE_TYPE, } from '@server/util/constants'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; const StyledInputDescription = styled('p')(({ theme }) => ({ display: 'flex', @@ -38,28 +39,38 @@ const StyledInput = styled(Input)(({ theme }) => ({ maxWidth: theme.spacing(50), })); +const StyledInputFullWidth = styled(Input)({ + width: '100%', +}); + interface IRoleFormProps { type?: PredefinedRoleType; name: string; - onSetName: (name: string) => void; + setName: React.Dispatch>; + validateName: (name: string) => boolean; description: string; setDescription: React.Dispatch>; + validateDescription: (description: string) => boolean; checkedPermissions: ICheckedPermissions; setCheckedPermissions: React.Dispatch< React.SetStateAction >; errors: IRoleFormErrors; + showErrors: boolean; } export const RoleForm = ({ type = ROOT_ROLE_TYPE, name, - onSetName, + setName, description, setDescription, checkedPermissions, setCheckedPermissions, errors, + showErrors, + validateName, + validateDescription, }: IRoleFormProps) => { const { permissions } = usePermissions({ revalidateIfStale: false, @@ -95,6 +106,10 @@ export const RoleForm = ({ setCheckedPermissions(newCheckedPermissions); }; + const handleOnBlur = (callback: Function) => { + setTimeout(() => callback(), 300); + }; + return (
@@ -102,27 +117,34 @@ export const RoleForm = ({ onSetName(e.target.value)} + onChange={(e) => setName(e.target.value)} + onBlur={(e) => handleOnBlur(() => validateName(e.target.value))} autoComplete='off' - required /> What is your new role description? - setDescription(e.target.value)} + onBlur={(e) => + handleOnBlur(() => validateDescription(e.target.value)) + } autoComplete='off' - required /> What is your role allowed to do? + + You must select at least one permission. + {categories.map(({ label, type, permissions }) => ( onCheckAll(permissions)} /> ))} + ( + +
    + {Object.values(errors) + .filter(Boolean) + .map((error) => ( +
  • {error}
  • + ))} +
+
+ )} + />
); }; diff --git a/frontend/src/component/admin/roles/RoleForm/useRoleForm.ts b/frontend/src/component/admin/roles/RoleForm/useRoleForm.ts index af67e14a7b..a926d9218a 100644 --- a/frontend/src/component/admin/roles/RoleForm/useRoleForm.ts +++ b/frontend/src/component/admin/roles/RoleForm/useRoleForm.ts @@ -7,11 +7,17 @@ import { ROOT_ROLE_TYPE } from '@server/util/constants'; enum ErrorField { NAME = 'name', + DESCRIPTION = 'description', + PERMISSIONS = 'permissions', } -export interface IRoleFormErrors { - [ErrorField.NAME]?: string; -} +const DEFAULT_ERRORS = { + [ErrorField.NAME]: undefined, + [ErrorField.DESCRIPTION]: undefined, + [ErrorField.PERMISSIONS]: undefined, +}; + +export type IRoleFormErrors = Record; export const useRoleForm = ( initialName = '', @@ -24,7 +30,8 @@ export const useRoleForm = ( const [description, setDescription] = useState(initialDescription); const [checkedPermissions, setCheckedPermissions] = useState({}); - const [errors, setErrors] = useState({}); + const [errors, setErrors] = useState(DEFAULT_ERRORS); + const [validated, setValidated] = useState(false); useEffect(() => { setName(initialName); @@ -40,6 +47,28 @@ export const useRoleForm = ( setCheckedPermissions(newCheckedPermissions); }, [initialPermissions.length]); + useEffect(() => { + if (name !== '') { + validateName(name); + } else { + clearError(ErrorField.NAME); + } + }, [name]); + + useEffect(() => { + if (description !== '') { + validateDescription(description); + } else { + clearError(ErrorField.DESCRIPTION); + } + }, [description]); + + useEffect(() => { + if (validated) { + validatePermissions(checkedPermissions); + } + }, [checkedPermissions]); + const getRolePayload = (type: PredefinedRoleType = ROOT_ROLE_TYPE) => ({ name, description, @@ -70,29 +99,79 @@ export const useRoleForm = ( setErrors((errors) => ({ ...errors, [field]: error })); }; + const validateName = (name: string) => { + if (!isNotEmpty(name)) { + setError(ErrorField.NAME, 'Name is required.'); + return false; + } + + if (!isNameUnique(name)) { + setError(ErrorField.NAME, 'Name must be unique.'); + return false; + } + + clearError(ErrorField.NAME); + return true; + }; + + const validateDescription = (description: string) => { + if (!isNotEmpty(description)) { + setError(ErrorField.DESCRIPTION, 'Description is required.'); + return false; + } + + clearError(ErrorField.DESCRIPTION); + return true; + }; + + const validatePermissions = (permissions: ICheckedPermissions) => { + if (!hasPermissions(permissions)) { + setError( + ErrorField.PERMISSIONS, + 'You must select at least one permission.', + ); + return false; + } + + clearError(ErrorField.PERMISSIONS); + return true; + }; + + const validate = () => { + const validName = validateName(name); + const validDescription = validateDescription(description); + const validPermissions = validatePermissions(checkedPermissions); + + setValidated(true); + + return validName && validDescription && validPermissions; + }; + + const showErrors = validated && Object.values(errors).some(Boolean); + const reload = () => { setName(initialName); setDescription(initialDescription); setCheckedPermissions( permissionsToCheckedPermissions(initialPermissions), ); + setValidated(false); + setErrors(DEFAULT_ERRORS); }; return { name, - description, - checkedPermissions, - errors, setName, + validateName, + description, setDescription, + validateDescription, + checkedPermissions, setCheckedPermissions, getRolePayload, - clearError, - setError, - isNameUnique, - isNotEmpty, - hasPermissions, - ErrorField, + errors, + showErrors, + validate, reload, }; }; diff --git a/frontend/src/component/admin/roles/RoleModal/RoleModal.tsx b/frontend/src/component/admin/roles/RoleModal/RoleModal.tsx index 79b59dbf8d..eab3fb1d22 100644 --- a/frontend/src/component/admin/roles/RoleModal/RoleModal.tsx +++ b/frontend/src/component/admin/roles/RoleModal/RoleModal.tsx @@ -48,18 +48,16 @@ export const RoleModal = ({ const { name, setName, + validateName, description, setDescription, + validateDescription, checkedPermissions, setCheckedPermissions, getRolePayload, - isNameUnique, - isNotEmpty, - hasPermissions, errors, - setError, - clearError, - ErrorField, + showErrors, + validate, reload: reloadForm, } = useRoleForm(role?.name, role?.description, role?.permissions); const { refetch: refetchRoles } = useRoles(); @@ -68,11 +66,6 @@ export const RoleModal = ({ const { uiConfig } = useUiConfig(); const editing = role !== undefined; - const isValid = - isNameUnique(name) && - isNotEmpty(name) && - isNotEmpty(description) && - hasPermissions(checkedPermissions); const payload = getRolePayload(type); @@ -85,14 +78,6 @@ export const RoleModal = ({ --data-raw '${JSON.stringify(payload, undefined, 2)}'`; }; - const onSetName = (name: string) => { - clearError(ErrorField.NAME); - if (!isNameUnique(name)) { - setError(ErrorField.NAME, 'A role with that name already exists.'); - } - setName(name); - }; - const refetch = () => { refetchRoles(); refetchRole(); @@ -101,7 +86,7 @@ export const RoleModal = ({ const onSubmit = async (e: FormEvent) => { e.preventDefault(); - if (!isValid) return; + if (!validate()) return; try { if (editing) { @@ -151,19 +136,21 @@ export const RoleModal = ({