diff --git a/catalog/app/containers/Admin/UsersAndRoles/RoleSelect.tsx b/catalog/app/containers/Admin/UsersAndRoles/RoleSelect.tsx new file mode 100644 index 00000000000..50a1bdd8278 --- /dev/null +++ b/catalog/app/containers/Admin/UsersAndRoles/RoleSelect.tsx @@ -0,0 +1,234 @@ +import cx from 'classnames' +import * as FF from 'final-form' +import * as R from 'ramda' +import * as React from 'react' +import * as RF from 'react-final-form' +import * as M from '@material-ui/core' + +export interface Role { + id: string + name: string +} + +export interface Value { + selected: readonly Role[] + active: Role | null +} + +export const EMPTY_VALUE: Value = { selected: [], active: null } + +export const validate: FF.FieldValidator = (v) => { + if (!v.selected.length) return 'required' + if (!v.active) return 'active' +} + +export const ROLE_NAME_ASC = R.ascend((r: Role) => r.name) + +const useRoleSelectStyles = M.makeStyles((t) => ({ + grid: { + alignItems: 'center', + display: 'grid', + gap: t.spacing(1), + grid: 'auto-flow / 1fr auto 1fr', + marginTop: t.spacing(2), + }, + list: ({ roles }: { roles: number }) => ({ + height: `${46 * R.clamp(3, 4.5, roles)}px`, + overflowY: 'auto', + }), + listEmpty: { + alignItems: 'center', + display: 'flex', + flexDirection: 'column', + paddingTop: t.spacing(3), + }, + availableRole: { + paddingBottom: '5px', + paddingTop: '5px', + }, + defaultRole: { + fontWeight: t.typography.fontWeightMedium, + '&::after': { + content: '"*"', + }, + }, +})) + +interface RoleSelectProps extends RF.FieldRenderProps { + roles: readonly Role[] + defaultRole: Role | null +} + +export function RoleSelect({ + roles, + defaultRole, + input: { value, onChange }, + meta, +}: RoleSelectProps) { + const classes = useRoleSelectStyles({ roles: roles.length }) + + const error = meta.submitFailed && meta.error + const disabled = meta.submitting || meta.submitSucceeded + + const { active, selected } = value ?? EMPTY_VALUE + + const available = React.useMemo( + () => roles.filter((r) => !selected.find((r2) => r2.id === r.id)).sort(ROLE_NAME_ASC), + [roles, selected], + ) + + const add = (r: Role) => + onChange({ + selected: selected.concat(r).sort(ROLE_NAME_ASC), + active: active ?? r, + }) + + const remove = (r: Role) => { + const newSelected = selected.filter((r2) => r2.id !== r.id) + let newActive: Role | null + if (newSelected.length === 1) { + // select the only available role + newActive = newSelected[0] + } else if (newSelected.find((r2) => r2.id === active?.id)) { + // keep the active role if it's still available + newActive = active + } else { + newActive = null + } + onChange({ selected: newSelected, active: newActive }) + } + + const activate = (r: Role) => onChange({ selected, active: r }) + + const clear = () => onChange({ selected: [], active: null }) + + function roleNameDisplay(r: Role) { + if (r.id !== defaultRole?.id) return r.name + return ( + + {r.name} + + ) + } + + return ( + + {error ? ( + + {error === 'required' ? 'Assign at least one role' : 'Select an active role'} + + ) : ( + + User can assume any of the assigned roles + + )} + +
+ + + + + + + clear_all + + + + + {selected.length ? ( + + {selected.map((r) => ( + activate(r)} + > + + + + + {roleNameDisplay(r)} + + + + remove(r)}> + close + + + + + ))} + + ) : ( +
+ + No roles assigned + + {!!defaultRole && ( + <> + + add(defaultRole)} + disabled={disabled} + > + Assign default role + + + )} +
+ )} +
+ + sync_alt + + + + + + {available.length ? ( + + {available.map((r) => ( + add(r)}> + + {roleNameDisplay(r)} + + + ))} + + ) : ( +
+ All roles assigned +
+ )} +
+
+
+ ) +} diff --git a/catalog/app/containers/Admin/UsersAndRoles/Users.tsx b/catalog/app/containers/Admin/UsersAndRoles/Users.tsx index fd6c0a04ab9..292f06e846b 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/Users.tsx +++ b/catalog/app/containers/Admin/UsersAndRoles/Users.tsx @@ -19,6 +19,7 @@ import * as validators from 'utils/validators' import * as Form from '../Form' import * as Table from '../Table' +import * as RoleSelect from './RoleSelect' import USERS_QUERY from './gql/Users.generated' import USER_CREATE_MUTATION from './gql/UserCreate.generated' @@ -34,229 +35,6 @@ type Role = GQL.DataForDoc['roles'][number] const DIALOG_PROPS: Dialogs.ExtraDialogProps = { maxWidth: 'xs', fullWidth: true } -interface RoleSelectValue { - selected: readonly Role[] - active: Role | null -} - -const ROLE_SELECT_VALUE_EMPTY: RoleSelectValue = { selected: [], active: null } - -const validateRoleSelect: FF.FieldValidator = (v) => { - if (!v.selected.length) return 'required' - if (!v.active) return 'active' -} - -const ROLE_NAME_ASC = R.ascend((r: Role) => r.name) - -const useRoleSelectStyles = M.makeStyles((t) => ({ - grid: { - alignItems: 'center', - display: 'grid', - gap: t.spacing(1), - grid: 'auto-flow / 1fr auto 1fr', - marginTop: t.spacing(2), - }, - list: ({ roles }: { roles: number }) => ({ - height: `${46 * R.clamp(3, 4.5, roles)}px`, - overflowY: 'auto', - }), - listEmpty: { - alignItems: 'center', - display: 'flex', - flexDirection: 'column', - paddingTop: t.spacing(3), - }, - availableRole: { - paddingBottom: '5px', - paddingTop: '5px', - }, - defaultRole: { - fontWeight: t.typography.fontWeightMedium, - '&::after': { - content: '"*"', - }, - }, -})) - -interface RoleSelectProps extends RF.FieldRenderProps { - roles: readonly Role[] - defaultRole: Role | null -} - -function RoleSelect({ - roles, - defaultRole, - input: { value, onChange }, - meta, -}: RoleSelectProps) { - const classes = useRoleSelectStyles({ roles: roles.length }) - - const error = meta.submitFailed && meta.error - const disabled = meta.submitting || meta.submitSucceeded - - const { active, selected } = value ?? ROLE_SELECT_VALUE_EMPTY - - const available = React.useMemo( - () => roles.filter((r) => !selected.find((r2) => r2.id === r.id)).sort(ROLE_NAME_ASC), - [roles, selected], - ) - - const add = (r: Role) => - onChange({ - selected: selected.concat(r).sort(ROLE_NAME_ASC), - active: active ?? r, - }) - - const remove = (r: Role) => { - const newSelected = selected.filter((r2) => r2.id !== r.id) - let newActive: Role | null - if (newSelected.length === 1) { - // select the only available role - newActive = newSelected[0] - } else if (newSelected.find((r2) => r2.id === active?.id)) { - // keep the active role if it's still available - newActive = active - } else { - newActive = null - } - onChange({ selected: newSelected, active: newActive }) - } - - const activate = (r: Role) => onChange({ selected, active: r }) - - const clear = () => onChange({ selected: [], active: null }) - - function roleNameDisplay(r: Role) { - if (r.id !== defaultRole?.id) return r.name - return ( - - {r.name} - - ) - } - - return ( - - {error ? ( - - {error === 'required' ? 'Assign at least one role' : 'Select an active role'} - - ) : ( - - User can assume any of the assigned roles - - )} - -
- - - - - - - clear_all - - - - - {selected.length ? ( - - {selected.map((r) => ( - activate(r)} - > - - - - - {roleNameDisplay(r)} - - - - remove(r)}> - close - - - - - ))} - - ) : ( -
- - No roles assigned - - {!!defaultRole && ( - <> - - add(defaultRole)} - disabled={disabled} - > - Assign default role - - - )} -
- )} -
- - sync_alt - - - - - - {available.length ? ( - - {available.map((r) => ( - add(r)}> - - {roleNameDisplay(r)} - - - ))} - - ) : ( -
- All roles assigned -
- )} -
-
-
- ) -} - const useDialogFormStyles = M.makeStyles((t) => ({ root: { marginTop: t.spacing(-3), @@ -289,7 +67,7 @@ function Invite({ close, roles, defaultRole }: InviteProps) { interface FormValues { username: string email: string - roles: RoleSelectValue + roles: RoleSelect.Value } const onSubmit = React.useCallback( @@ -419,9 +197,13 @@ function Invite({ close, roles, defaultRole }: InviteProps) { /> Assign roles - name="roles" validate={validateRoleSelect}> + name="roles" validate={RoleSelect.validate}> {(props) => ( - + )} @@ -811,7 +593,7 @@ function EditRoles({ close, roles, defaultRole, user }: EditRolesProps) { const setRole = GQL.useMutation(USER_SET_ROLE_MUTATION) interface FormValues { - roles: RoleSelectValue + roles: RoleSelect.Value } const onSubmit = React.useCallback( @@ -862,7 +644,7 @@ function EditRoles({ close, roles, defaultRole, user }: EditRolesProps) { ) const selected = React.useMemo( - () => user.extraRoles.concat(user.role ?? []).sort(ROLE_NAME_ASC), + () => user.extraRoles.concat(user.role ?? []).sort(RoleSelect.ROLE_NAME_ASC), [user.extraRoles, user.role], ) @@ -886,9 +668,13 @@ function EditRoles({ close, roles, defaultRole, user }: EditRolesProps) { Assign roles to "{user.name}" - name="roles" validate={validateRoleSelect}> + name="roles" validate={RoleSelect.validate}> {(props) => ( - + )}