From 695857ae96fbe30045f69a4ac834df5f91bc9117 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 6 Jun 2023 15:08:44 +0530 Subject: [PATCH] Wired save and error handling for user details modal refs https://github.com/TryGhost/Team/issues/3351 - updates user state on suspend/delete actions to show the latest - shows errors for email and url on user detail screen --- .../components/providers/UsersProvider.tsx | 7 +- .../src/components/settings/general/Users.tsx | 11 ++- .../general/modals/UserDetailModal.tsx | 81 ++++++++++++++----- .../src/hooks/useStaffUsers.tsx | 8 +- 4 files changed, 83 insertions(+), 24 deletions(-) diff --git a/ghost/admin-x-settings/src/components/providers/UsersProvider.tsx b/ghost/admin-x-settings/src/components/providers/UsersProvider.tsx index 373f55339f..812472097f 100644 --- a/ghost/admin-x-settings/src/components/providers/UsersProvider.tsx +++ b/ghost/admin-x-settings/src/components/providers/UsersProvider.tsx @@ -9,6 +9,7 @@ interface UsersContextProps { currentUser: User|null; updateUser?: (user: User) => Promise; setInvites: (invites: UserInvite[]) => void; + setUsers: React.Dispatch> } interface UsersProviderProps { @@ -19,7 +20,8 @@ const UsersContext = createContext({ users: [], invites: [], currentUser: null, - setInvites: () => {} + setInvites: () => {}, + setUsers: () => {} }); const UsersProvider: React.FC = ({children}) => { @@ -69,7 +71,8 @@ const UsersProvider: React.FC = ({children}) => { invites, currentUser, updateUser, - setInvites + setInvites, + setUsers }}> {children} diff --git a/ghost/admin-x-settings/src/components/settings/general/Users.tsx b/ghost/admin-x-settings/src/components/settings/general/Users.tsx index 9db2352f58..1bd7c1dcb9 100644 --- a/ghost/admin-x-settings/src/components/settings/general/Users.tsx +++ b/ghost/admin-x-settings/src/components/settings/general/Users.tsx @@ -67,6 +67,10 @@ const UsersList: React.FC = ({users, updateUser}) => { return ( {users.map((user) => { + let title = user.name || ''; + if (user.status === 'inactive') { + title = `${title} (Suspended)`; + } return ( = ({users, updateUser}) => { detail={user.email} hideActions={true} id={`list-item-${user.id}`} - title={user.name || ''} + title={title} onClick={() => showDetailModal(user)} /> ); })} @@ -85,6 +89,7 @@ const UsersList: React.FC = ({users, updateUser}) => { const UserInviteActions: React.FC<{invite: UserInvite}> = ({invite}) => { const {api} = useContext(ServicesContext); + const {setInvites} = useStaffUsers(); const [revokeState, setRevokeState] = useState<'progress'|''>(''); const [resendState, setResendState] = useState<'progress'|''>(''); let revokeActionLabel = 'Revoke'; @@ -104,6 +109,8 @@ const UserInviteActions: React.FC<{invite: UserInvite}> = ({invite}) => { onClick={async () => { setRevokeState('progress'); await api.invites.delete(invite.id); + const res = await api.invites.browse(); + setInvites(res.invites); setRevokeState(''); showToast({ message: `Invitation revoked(${invite.email})`, @@ -123,6 +130,8 @@ const UserInviteActions: React.FC<{invite: UserInvite}> = ({invite}) => { email: invite.email, roleId: invite.role_id }); + const res = await api.invites.browse(); + setInvites(res.invites); setResendState(''); showToast({ message: `Invitation resent!(${invite.email})`, diff --git a/ghost/admin-x-settings/src/components/settings/general/modals/UserDetailModal.tsx b/ghost/admin-x-settings/src/components/settings/general/modals/UserDetailModal.tsx index 5126c50be5..99b83e6fa3 100644 --- a/ghost/admin-x-settings/src/components/settings/general/modals/UserDetailModal.tsx +++ b/ghost/admin-x-settings/src/components/settings/general/modals/UserDetailModal.tsx @@ -13,6 +13,8 @@ import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupCon import TextField from '../../../../admin-x-ds/global/TextField'; import Toggle from '../../../../admin-x-ds/global/Toggle'; import useRoles from '../../../../hooks/useRoles'; +import useStaffUsers from '../../../../hooks/useStaffUsers'; +import validator from 'validator'; import {FileService, ServicesContext} from '../../../providers/ServiceProvider'; import {MenuItem} from '../../../../admin-x-ds/global/Menu'; import {User} from '../../../../types/api'; @@ -26,6 +28,10 @@ interface CustomHeadingProps { interface UserDetailProps { user: User; setUserData?: (user: User) => void; + errors?: { + url?: string; + email?: string; + }; } const CustomHeader: React.FC = ({children}) => { @@ -84,7 +90,7 @@ const RoleSelector: React.FC = ({user, setUserData}) => { /> ); }; -const BasicInputs: React.FC = ({user, setUserData}) => { +const BasicInputs: React.FC = ({errors, user, setUserData}) => { return ( = ({user, setUserData}) => { }} /> { @@ -107,19 +115,19 @@ const BasicInputs: React.FC = ({user, setUserData}) => { ); }; -const Basic: React.FC = ({user, setUserData}) => { +const Basic: React.FC = ({errors, user, setUserData}) => { return ( Basic info} title='Basic' > - + ); }; -const DetailsInputs: React.FC = ({user, setUserData}) => { +const DetailsInputs: React.FC = ({errors, user, setUserData}) => { return ( = ({user, setUserData}) => { }} /> { @@ -170,14 +180,14 @@ const DetailsInputs: React.FC = ({user, setUserData}) => { ); }; -const Details: React.FC = ({user, setUserData}) => { +const Details: React.FC = ({errors, user, setUserData}) => { return ( Details} title='Details' > - + ); }; @@ -397,8 +407,14 @@ const UserMenuTrigger = () => ( const UserDetailModal:React.FC = ({user, updateUser}) => { const {api} = useContext(ServicesContext); + const {users, setUsers, ownerUser} = useStaffUsers(); const [userData, setUserData] = useState(user); const [saveState, setSaveState] = useState(''); + const [errors, setErrors] = useState<{ + email?: string; + url?: string; + }>({}); + const {fileService} = useContext(ServicesContext) as {fileService: FileService}; const mainModal = useModal(); @@ -418,28 +434,41 @@ const UserDetailModal:React.FC = ({user, updateUser}) => { okRunningLabel: _user.status === 'inactive' ? 'Un-suspending...' : 'Suspending...', okColor: 'red', onOk: async (modal) => { - await api.users.edit({ + const updatedUserData = { ..._user, status: _user.status === 'inactive' ? 'active' : 'inactive' + }; + const res = await api.users.edit(updatedUserData); + const updatedUser = res.users[0]; + setUsers((_users) => { + return _users.map((u) => { + if (u.id === updatedUser.id) { + return updatedUser; + } + return u; + }); }); + setUserData(updatedUserData); modal?.remove(); } }); }; - const confirmDelete = (_user: User) => { + const confirmDelete = (_user: User, {owner}: {owner: User}) => { NiceModal.show(ConfirmationModal, { title: 'Are you sure you want to delete this user?', prompt: ( <> -

The [user] will be permanently deleted and all their posts will be automatically assigned to the [site owner name].

-

To make these easy to find in the future, each post will be given an internal tag of [new internal tag with username]

+

{_user.name || _user.email} will be permanently deleted and all their posts will be automatically assigned to the {owner.name}.

+

To make these easy to find in the future, each post will be given an internal tag of #{user.slug}

), okLabel: 'Delete user', okColor: 'red', onOk: async (modal) => { await api.users.delete(_user?.id); + const newUsers = users.filter(u => u.id !== _user.id); + setUsers(newUsers); modal?.remove(); mainModal?.remove(); showToast({ @@ -457,7 +486,8 @@ const UserDetailModal:React.FC = ({user, updateUser}) => { okLabel: 'Yep — I\'m sure', okColor: 'red', onOk: async (modal) => { - await api.users.makeOwner(user.id); + const res = await api.users.makeOwner(user.id); + setUsers(res.users); modal?.remove(); showToast({ message: 'Ownership transferred', @@ -503,11 +533,11 @@ const UserDetailModal:React.FC = ({user, updateUser}) => { } }; - let suspendUserLabel = user?.status === 'inactive' ? 'Un-suspend user' : 'Suspend user'; + let suspendUserLabel = userData?.status === 'inactive' ? 'Un-suspend user' : 'Suspend user'; let menuItems: MenuItem[] = []; - if (isAdminUser(user)) { + if (isAdminUser(userData)) { menuItems.push({ id: 'make-owner', label: 'Make owner', @@ -520,14 +550,14 @@ const UserDetailModal:React.FC = ({user, updateUser}) => { id: 'delete-user', label: 'Delete user', onClick: () => { - confirmDelete(user); + confirmDelete(user, {owner: ownerUser}); } }, { id: 'suspend-user', label: suspendUserLabel, onClick: () => { - confirmSuspend(user); + confirmSuspend(userData); } }, { @@ -555,7 +585,7 @@ const UserDetailModal:React.FC = ({user, updateUser}) => { const fileUploadButtonClasses = 'absolute right-[104px] bottom-12 bg-[rgba(0,0,0,0.75)] rounded text-sm text-white flex items-center justify-center px-3 h-8 opacity-80 hover:opacity-100 transition cursor-pointer font-medium z-10'; - const suspendedText = user.status === 'inactive' ? ' (Suspended)' : ''; + const suspendedText = userData.status === 'inactive' ? ' (Suspended)' : ''; return ( = ({user, updateUser}) => { size='lg' onOk={async () => { setSaveState('saving'); + if (!validator.isEmail(userData.email)) { + setErrors?.((_errors) => { + return {..._errors, email: 'Please enter a valid email address'}; + }); + setSaveState(''); + return; + } + if (!validator.isURL(userData.url)) { + setErrors?.((_errors) => { + return {..._errors, url: 'Please enter a valid URL'}; + }); + setSaveState(''); + return; + } + await updateUser?.(userData); setSaveState('saved'); }} @@ -614,8 +659,8 @@ const UserDetailModal:React.FC = ({user, updateUser}) => {
- -
+ +
diff --git a/ghost/admin-x-settings/src/hooks/useStaffUsers.tsx b/ghost/admin-x-settings/src/hooks/useStaffUsers.tsx index cecc5c32e4..bfc7c7037f 100644 --- a/ghost/admin-x-settings/src/hooks/useStaffUsers.tsx +++ b/ghost/admin-x-settings/src/hooks/useStaffUsers.tsx @@ -1,8 +1,8 @@ +import React, {useContext} from 'react'; import {RolesContext} from '../components/providers/RolesProvider'; import {User} from '../types/api'; import {UserInvite} from '../utils/api'; import {UsersContext} from '../components/providers/UsersProvider'; -import {useContext} from 'react'; export type UsersHook = { users: User[]; @@ -15,6 +15,7 @@ export type UsersHook = { currentUser: User|null; updateUser?: (user: User) => Promise; setInvites: (invites: UserInvite[]) => void; + setUsers: React.Dispatch> }; function getUsersByRole(users: User[], role: string): User[] { @@ -30,7 +31,7 @@ function getOwnerUser(users: User[]): User { } const useStaffUsers = (): UsersHook => { - const {users, currentUser, updateUser, invites, setInvites} = useContext(UsersContext); + const {users, currentUser, updateUser, invites, setInvites, setUsers} = useContext(UsersContext); const {roles} = useContext(RolesContext); const ownerUser = getOwnerUser(users); const adminUsers = getUsersByRole(users, 'Administrator'); @@ -57,7 +58,8 @@ const useStaffUsers = (): UsersHook => { currentUser, invites: mappedInvites, updateUser, - setInvites + setInvites, + setUsers }; };