mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-30 21:40:39 +03:00
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
This commit is contained in:
parent
31549933a1
commit
695857ae96
@ -9,6 +9,7 @@ interface UsersContextProps {
|
||||
currentUser: User|null;
|
||||
updateUser?: (user: User) => Promise<void>;
|
||||
setInvites: (invites: UserInvite[]) => void;
|
||||
setUsers: React.Dispatch<React.SetStateAction<User[]>>
|
||||
}
|
||||
|
||||
interface UsersProviderProps {
|
||||
@ -19,7 +20,8 @@ const UsersContext = createContext<UsersContextProps>({
|
||||
users: [],
|
||||
invites: [],
|
||||
currentUser: null,
|
||||
setInvites: () => {}
|
||||
setInvites: () => {},
|
||||
setUsers: () => {}
|
||||
});
|
||||
|
||||
const UsersProvider: React.FC<UsersProviderProps> = ({children}) => {
|
||||
@ -69,7 +71,8 @@ const UsersProvider: React.FC<UsersProviderProps> = ({children}) => {
|
||||
invites,
|
||||
currentUser,
|
||||
updateUser,
|
||||
setInvites
|
||||
setInvites,
|
||||
setUsers
|
||||
}}>
|
||||
{children}
|
||||
</UsersContext.Provider>
|
||||
|
@ -67,6 +67,10 @@ const UsersList: React.FC<UsersListProps> = ({users, updateUser}) => {
|
||||
return (
|
||||
<List>
|
||||
{users.map((user) => {
|
||||
let title = user.name || '';
|
||||
if (user.status === 'inactive') {
|
||||
title = `${title} (Suspended)`;
|
||||
}
|
||||
return (
|
||||
<ListItem
|
||||
key={user.id}
|
||||
@ -75,7 +79,7 @@ const UsersList: React.FC<UsersListProps> = ({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<UsersListProps> = ({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})`,
|
||||
|
@ -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<CustomHeadingProps> = ({children}) => {
|
||||
@ -84,7 +90,7 @@ const RoleSelector: React.FC<UserDetailProps> = ({user, setUserData}) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
const BasicInputs: React.FC<UserDetailProps> = ({user, setUserData}) => {
|
||||
const BasicInputs: React.FC<UserDetailProps> = ({errors, user, setUserData}) => {
|
||||
return (
|
||||
<SettingGroupContent>
|
||||
<TextField
|
||||
@ -96,6 +102,8 @@ const BasicInputs: React.FC<UserDetailProps> = ({user, setUserData}) => {
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
error={!!errors?.email}
|
||||
hint={errors?.email || ''}
|
||||
title="Email"
|
||||
value={user.email}
|
||||
onChange={(e) => {
|
||||
@ -107,19 +115,19 @@ const BasicInputs: React.FC<UserDetailProps> = ({user, setUserData}) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Basic: React.FC<UserDetailProps> = ({user, setUserData}) => {
|
||||
const Basic: React.FC<UserDetailProps> = ({errors, user, setUserData}) => {
|
||||
return (
|
||||
<SettingGroup
|
||||
border={false}
|
||||
customHeader={<CustomHeader>Basic info</CustomHeader>}
|
||||
title='Basic'
|
||||
>
|
||||
<BasicInputs setUserData={setUserData} user={user} />
|
||||
<BasicInputs errors={errors} setUserData={setUserData} user={user} />
|
||||
</SettingGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const DetailsInputs: React.FC<UserDetailProps> = ({user, setUserData}) => {
|
||||
const DetailsInputs: React.FC<UserDetailProps> = ({errors, user, setUserData}) => {
|
||||
return (
|
||||
<SettingGroupContent>
|
||||
<TextField
|
||||
@ -138,6 +146,8 @@ const DetailsInputs: React.FC<UserDetailProps> = ({user, setUserData}) => {
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
error={!!errors?.url}
|
||||
hint={errors?.url || ''}
|
||||
title="Website"
|
||||
value={user.website}
|
||||
onChange={(e) => {
|
||||
@ -170,14 +180,14 @@ const DetailsInputs: React.FC<UserDetailProps> = ({user, setUserData}) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Details: React.FC<UserDetailProps> = ({user, setUserData}) => {
|
||||
const Details: React.FC<UserDetailProps> = ({errors, user, setUserData}) => {
|
||||
return (
|
||||
<SettingGroup
|
||||
border={false}
|
||||
customHeader={<CustomHeader>Details</CustomHeader>}
|
||||
title='Details'
|
||||
>
|
||||
<DetailsInputs setUserData={setUserData} user={user} />
|
||||
<DetailsInputs errors={errors} setUserData={setUserData} user={user} />
|
||||
</SettingGroup>
|
||||
);
|
||||
};
|
||||
@ -397,8 +407,14 @@ const UserMenuTrigger = () => (
|
||||
|
||||
const UserDetailModal:React.FC<UserDetailModalProps> = ({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<UserDetailModalProps> = ({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: (
|
||||
<>
|
||||
<p className='mb-3'>The [user] will be permanently deleted and all their posts will be automatically assigned to the [site owner name].</p>
|
||||
<p>To make these easy to find in the future, each post will be given an internal tag of [new internal tag with username]</p>
|
||||
<p className='mb-3'><span className='font-bold'>{_user.name || _user.email}</span> will be permanently deleted and all their posts will be automatically assigned to the <span className='font-bold'>{owner.name}</span>.</p>
|
||||
<p>To make these easy to find in the future, each post will be given an internal tag of <span className='font-bold'>#{user.slug}</span></p>
|
||||
</>
|
||||
),
|
||||
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<UserDetailModalProps> = ({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<UserDetailModalProps> = ({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<UserDetailModalProps> = ({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<UserDetailModalProps> = ({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 (
|
||||
<Modal
|
||||
@ -564,6 +594,21 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({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<UserDetailModalProps> = ({user, updateUser}) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-10 grid grid-cols-2 gap-x-12 gap-y-20 pb-10'>
|
||||
<Basic setUserData={setUserData} user={userData} />
|
||||
<Details setUserData={setUserData} user={userData} />
|
||||
<Basic errors={errors} setUserData={setUserData} user={userData} />
|
||||
<Details errors={errors} setUserData={setUserData} user={userData} />
|
||||
<EmailNotifications setUserData={setUserData} user={userData} />
|
||||
<Password user={userData} />
|
||||
</div>
|
||||
|
@ -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<void>;
|
||||
setInvites: (invites: UserInvite[]) => void;
|
||||
setUsers: React.Dispatch<React.SetStateAction<User[]>>
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user