diff --git a/src/common/api/userApi.ts b/src/common/api/userApi.ts index 1de230dd0..4c8b97c19 100644 --- a/src/common/api/userApi.ts +++ b/src/common/api/userApi.ts @@ -40,6 +40,8 @@ export type UpdateProfilePictureRequest = { profilePicture: FormData; }; export type DeleteProfilePictureRequest = Pick; +export type DisableUserRequest = Pick; +export type EnableUserRequest = Pick; export const userApi = createApi({ reducerPath: 'userApi', @@ -76,7 +78,7 @@ export const userApi = createApi({ }), getUserHistory: builder.query>, string>({ - query: url => url + query: url => url, }), inviteUser: builder.mutation({ @@ -215,6 +217,22 @@ export const userApi = createApi({ }), invalidatesTags: ['User'], }), + + disableUser: builder.mutation({ + query: ({ id }) => ({ + url: `/users/${id}/disable/`, + method: 'POST', + }), + invalidatesTags: ['User'], + }), + + enableUser: builder.mutation({ + query: ({ id }) => ({ + url: `/users/${id}/enable/`, + method: 'POST', + }), + invalidatesTags: ['User'], + }), }), }); @@ -240,4 +258,6 @@ export const { useUpdateProfilePictureMutation, useDeleteProfilePictureMutation, useGetUserHistoryQuery, + useDisableUserMutation, + useEnableUserMutation, } = userApi; diff --git a/src/common/models/user.ts b/src/common/models/user.ts index 03be1de7c..e25d4203e 100644 --- a/src/common/models/user.ts +++ b/src/common/models/user.ts @@ -10,4 +10,6 @@ export interface User { profilePicture: Image | null; role: Role; newEmail: string | null; + isActive: boolean; + disabledAt: string | null; } diff --git a/src/features/user-dashboard/components/UserDetailForm.tsx b/src/features/user-dashboard/components/UserDetailForm.tsx index 168152a20..58bf7864b 100644 --- a/src/features/user-dashboard/components/UserDetailForm.tsx +++ b/src/features/user-dashboard/components/UserDetailForm.tsx @@ -5,14 +5,31 @@ import WithUnsavedChangesPrompt from 'common/components/WithUnsavedChangesPrompt import { addServerErrors } from 'common/error/utilities'; import { Role, User, RoleOption, ServerValidationErrors } from 'common/models'; import { FC, useEffect } from 'react'; -import { Col, Row } from 'react-bootstrap'; +import { Button, Col, Row } from 'react-bootstrap'; import Form from 'react-bootstrap/Form'; import { Controller, useForm } from 'react-hook-form'; import * as yup from 'yup'; +import styled from 'styled-components'; +import { useModalWithData } from 'common/hooks/useModalWithData'; +import { SimpleConfirmModal } from 'common/components/SimpleConfirmModal'; +import * as notificationService from 'common/services/notification'; +import { useDisableUserMutation, useEnableUserMutation } from 'common/api/userApi'; +import { handleApiError, isFetchBaseQueryError } from 'common/api/handleApiError'; import { Trans, useTranslation } from 'react-i18next'; import { Constants } from 'utils/constants'; -export type FormData = Pick; +const DisableButton = styled(Button)` + margin-left: 100px; + .spinner-container { + padding-right: 8px; + } +`; + +export type DisableUserRequest = { + id: string; +}; + +export type FormData = Pick; export interface Props { availableRoles: Role[]; @@ -26,9 +43,9 @@ export interface Props { export const UserDetailForm: FC = ({ availableRoles, defaultValues = {}, - isRoleSelectorDisabled, onSubmit, submitButtonLabel = 'Submit', + isRoleSelectorDisabled, serverValidationErrors, }) => { const { t } = useTranslation(['translation', 'common']); @@ -66,6 +83,62 @@ export const UserDetailForm: FC = ({ } }, [serverValidationErrors, setError]); + const [disableUser] = useDisableUserMutation(); + const [enableUser] = useEnableUserMutation(); + + const [showDisableModal, hideDisableModal] = useModalWithData( + user => + // eslint-disable-next-line react/no-unstable-nested-components + ({ in: open, onExited }) => { + const onSubmit = async () => { + try { + if (user.isActive) { + await disableUser({ id: user.id }).unwrap(); + notificationService.showSuccessMessage('User disabled.'); + } else { + await enableUser({ id: user.id }).unwrap(); + notificationService.showSuccessMessage('User enabled.'); + } + + hideDisableModal(); + } catch (error) { + if (isFetchBaseQueryError(error)) { + handleApiError(error); + } else { + if (user.isActive) { + notificationService.showErrorMessage('Could not disable user.'); + } else { + notificationService.showErrorMessage('Could not enable user.'); + } + throw error; + } + } + }; + + return ( + +

+ {user.isActive + ? 'Are you sure you want to disable this user?' + : 'Are you sure you want to enable this user?'} +

+ + } + /> + ); + }, + [], + ); + return (
@@ -138,10 +211,19 @@ export const UserDetailForm: FC = ({ -
- - {submitButtonLabel} - +
+
+ + {submitButtonLabel} + +
+
+ {defaultValues.id ? ( + showDisableModal(defaultValues as User)} variant='btn btn-danger'> + {defaultValues.isActive ? 'Disable' : 'Enable'} + + ) : null} +
diff --git a/src/features/user-dashboard/hooks/useUserTableData.tsx b/src/features/user-dashboard/hooks/useUserTableData.tsx index b8eea5f0e..7a510d1bb 100644 --- a/src/features/user-dashboard/hooks/useUserTableData.tsx +++ b/src/features/user-dashboard/hooks/useUserTableData.tsx @@ -23,8 +23,10 @@ export type UserTableItem = { role: RoleType; profilePicture: Image | null; actions: ActionButtonProps[]; + isActive: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + disabledAt: string | null | any; }; - export const useUserTableData = (users: User[] = []) => { const { userHasPermission } = useRbac(); const { t, i18n } = useTranslation(['translation', 'common']); @@ -194,6 +196,11 @@ export const useUserTableData = (users: User[] = []) => { ), }, + { + accessor: 'isActive', + Header: 'Account Status', + Cell: ({ value: isActive }) => {isActive ? 'Enabled' : 'Disabled'}, + }, { accessor: 'activatedAt', Header: t('activatedDate', { ns: 'common' })!, @@ -212,6 +219,21 @@ export const useUserTableData = (users: User[] = []) => { ), }, + { + accessor: 'disabledAt', + Header: 'Disabled On', + Cell: ({ value: disabledAt }) => ( + <> + {disabledAt instanceof Date ? ( + + ) : ( + <> + )} + + ), + }, { accessor: 'actions', Header: '', @@ -246,7 +268,9 @@ export const useUserTableData = (users: User[] = []) => { firstName: user.firstName, email: user.email, role: user.role, + isActive: user.isActive, profilePicture: user.profilePicture, + disabledAt: user.disabledAt ? new Date(user.disabledAt) : '', activatedAt: user.activatedAt ? new Date(user.activatedAt) : { diff --git a/src/features/user-dashboard/pages/UserListView.tsx b/src/features/user-dashboard/pages/UserListView.tsx index 3ce97190a..ad798c724 100644 --- a/src/features/user-dashboard/pages/UserListView.tsx +++ b/src/features/user-dashboard/pages/UserListView.tsx @@ -57,11 +57,24 @@ export const UserListView: FC = () => { { label: t('admin', { ns: 'common' }), value: 'ADMIN' }, ]), }, + { + attribute: 'active', + attributeLabel: 'Account Status', + FilterUI: EnumFilter([ + { label: 'Enabled', value: 'ENABLED' }, + { label: 'Disabled', value: 'DISABLED' }, + ]), + }, { attribute: 'activatedAt', attributeLabel: t('activatedDate', { ns: 'common' }), FilterUI: RecentDateFilter([30, 90, 180]), }, + { + attribute: 'deactivatedAt', + attributeLabel: 'Deactivated Date', + FilterUI: RecentDateFilter([30, 90, 180]), + }, ], [t], );