mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Wired delete and owner actions for staff users in adminX
refs https://github.com/TryGhost/Team/issues/3351 - allows owners/admins to delete users based on their permission level - allows admins to be made owner users only if owner is logged in
This commit is contained in:
parent
885531b345
commit
c88a073a5c
3 changed files with 77 additions and 31 deletions
|
@ -5,7 +5,7 @@ import Icon from '../../../../admin-x-ds/global/Icon';
|
||||||
import ImageUpload from '../../../../admin-x-ds/global/ImageUpload';
|
import ImageUpload from '../../../../admin-x-ds/global/ImageUpload';
|
||||||
import Menu from '../../../../admin-x-ds/global/Menu';
|
import Menu from '../../../../admin-x-ds/global/Menu';
|
||||||
import Modal from '../../../../admin-x-ds/global/Modal';
|
import Modal from '../../../../admin-x-ds/global/Modal';
|
||||||
import NiceModal from '@ebay/nice-modal-react';
|
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||||
import Radio from '../../../../admin-x-ds/global/Radio';
|
import Radio from '../../../../admin-x-ds/global/Radio';
|
||||||
import React, {useContext, useEffect, useRef, useState} from 'react';
|
import React, {useContext, useEffect, useRef, useState} from 'react';
|
||||||
import SettingGroup from '../../../../admin-x-ds/settings/SettingGroup';
|
import SettingGroup from '../../../../admin-x-ds/settings/SettingGroup';
|
||||||
|
@ -14,8 +14,10 @@ import TextField from '../../../../admin-x-ds/global/TextField';
|
||||||
import Toggle from '../../../../admin-x-ds/global/Toggle';
|
import Toggle from '../../../../admin-x-ds/global/Toggle';
|
||||||
import useRoles from '../../../../hooks/useRoles';
|
import useRoles from '../../../../hooks/useRoles';
|
||||||
import {FileService, ServicesContext} from '../../../providers/ServiceProvider';
|
import {FileService, ServicesContext} from '../../../providers/ServiceProvider';
|
||||||
|
import {MenuItem} from '../../../../admin-x-ds/global/Menu';
|
||||||
import {User} from '../../../../types/api';
|
import {User} from '../../../../types/api';
|
||||||
import {isOwnerUser} from '../../../../utils/helpers';
|
import {isAdminUser, isOwnerUser} from '../../../../utils/helpers';
|
||||||
|
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
||||||
|
|
||||||
interface CustomHeadingProps {
|
interface CustomHeadingProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
@ -393,34 +395,12 @@ const UserMenuTrigger = () => (
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const confirmMakeOwner = () => {
|
|
||||||
NiceModal.show(ConfirmationModal, {
|
|
||||||
title: 'Transfer Ownership',
|
|
||||||
prompt: 'Are you sure you want to transfer the ownership of this blog? You will not be able to undo this action.',
|
|
||||||
okLabel: 'Yep — I\'m sure',
|
|
||||||
okColor: 'red'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmDelete = () => {
|
|
||||||
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>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
okLabel: 'Delete user',
|
|
||||||
okColor: 'red'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const UserDetailModal:React.FC<UserDetailModalProps> = ({user, updateUser}) => {
|
const UserDetailModal:React.FC<UserDetailModalProps> = ({user, updateUser}) => {
|
||||||
const {api} = useContext(ServicesContext);
|
const {api} = useContext(ServicesContext);
|
||||||
const [userData, setUserData] = useState(user);
|
const [userData, setUserData] = useState(user);
|
||||||
const [saveState, setSaveState] = useState('');
|
const [saveState, setSaveState] = useState('');
|
||||||
const {fileService} = useContext(ServicesContext) as {fileService: FileService};
|
const {fileService} = useContext(ServicesContext) as {fileService: FileService};
|
||||||
|
const mainModal = useModal();
|
||||||
|
|
||||||
const confirmSuspend = (_user: User) => {
|
const confirmSuspend = (_user: User) => {
|
||||||
let warningText = 'This user will no longer be able to log in but their posts will be kept.';
|
let warningText = 'This user will no longer be able to log in but their posts will be kept.';
|
||||||
|
@ -447,6 +427,46 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({user, updateUser}) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const confirmDelete = (_user: 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>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
okLabel: 'Delete user',
|
||||||
|
okColor: 'red',
|
||||||
|
onOk: async (modal) => {
|
||||||
|
await api.users.delete(_user?.id);
|
||||||
|
modal?.remove();
|
||||||
|
mainModal?.remove();
|
||||||
|
showToast({
|
||||||
|
message: 'User deleted',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmMakeOwner = () => {
|
||||||
|
NiceModal.show(ConfirmationModal, {
|
||||||
|
title: 'Transfer Ownership',
|
||||||
|
prompt: 'Are you sure you want to transfer the ownership of this blog? You will not be able to undo this action.',
|
||||||
|
okLabel: 'Yep — I\'m sure',
|
||||||
|
okColor: 'red',
|
||||||
|
onOk: async (modal) => {
|
||||||
|
await api.users.makeOwner(user.id);
|
||||||
|
modal?.remove();
|
||||||
|
showToast({
|
||||||
|
message: 'Ownership transferred',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleImageUpload = async (image: string, file: File) => {
|
const handleImageUpload = async (image: string, file: File) => {
|
||||||
try {
|
try {
|
||||||
const imageUrl = await fileService.uploadImage(file);
|
const imageUrl = await fileService.uploadImage(file);
|
||||||
|
@ -477,17 +497,22 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({user, updateUser}) => {
|
||||||
|
|
||||||
let suspendUserLabel = user?.status === 'inactive' ? 'Un-suspend user' : 'Suspend user';
|
let suspendUserLabel = user?.status === 'inactive' ? 'Un-suspend user' : 'Suspend user';
|
||||||
|
|
||||||
const menuItems = [
|
let menuItems: MenuItem[] = [];
|
||||||
{
|
|
||||||
|
if (isAdminUser(user)) {
|
||||||
|
menuItems.push({
|
||||||
id: 'make-owner',
|
id: 'make-owner',
|
||||||
label: 'Make owner',
|
label: 'Make owner',
|
||||||
onClick: confirmMakeOwner
|
onClick: confirmMakeOwner
|
||||||
},
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
menuItems = menuItems.concat([
|
||||||
{
|
{
|
||||||
id: 'delete-user',
|
id: 'delete-user',
|
||||||
label: 'Delete user',
|
label: 'Delete user',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
confirmDelete();
|
confirmDelete(user);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -499,9 +524,12 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({user, updateUser}) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'view-user-activity',
|
id: 'view-user-activity',
|
||||||
label: 'View user activity'
|
label: 'View user activity',
|
||||||
|
onClick: () => {
|
||||||
|
// TODO: show user activity
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
]);
|
||||||
|
|
||||||
let okLabel = saveState === 'saved' ? 'Saved' : 'Save';
|
let okLabel = saveState === 'saved' ? 'Saved' : 'Save';
|
||||||
if (saveState === 'saving') {
|
if (saveState === 'saving') {
|
||||||
|
|
|
@ -98,6 +98,7 @@ interface API {
|
||||||
edit: (editedUser: User) => Promise<UsersResponseType>;
|
edit: (editedUser: User) => Promise<UsersResponseType>;
|
||||||
delete: (userId: string) => Promise<DeleteUserResponse>;
|
delete: (userId: string) => Promise<DeleteUserResponse>;
|
||||||
updatePassword: (options: UpdatePasswordOptions) => Promise<PasswordUpdateResponseType>;
|
updatePassword: (options: UpdatePasswordOptions) => Promise<PasswordUpdateResponseType>;
|
||||||
|
makeOwner: (userId: string) => Promise<UsersResponseType>;
|
||||||
};
|
};
|
||||||
roles: {
|
roles: {
|
||||||
browse: (options?: BrowseRoleOptions) => Promise<RolesResponseType>;
|
browse: (options?: BrowseRoleOptions) => Promise<RolesResponseType>;
|
||||||
|
@ -219,6 +220,19 @@ function setupGhostApi({ghostVersion}: GhostApiOptions): API {
|
||||||
});
|
});
|
||||||
const data: DeleteUserResponse = await response.json();
|
const data: DeleteUserResponse = await response.json();
|
||||||
return data;
|
return data;
|
||||||
|
},
|
||||||
|
makeOwner: async (userId: string) => {
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
owner: [{
|
||||||
|
id: userId
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
const response = await fetcher(`/users/owner/`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: payload
|
||||||
|
});
|
||||||
|
const data: UsersResponseType = await response.json();
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
roles: {
|
roles: {
|
||||||
|
|
|
@ -62,3 +62,7 @@ export function generateAvatarColor(name: string) {
|
||||||
export function isOwnerUser(user: User) {
|
export function isOwnerUser(user: User) {
|
||||||
return user.roles.some(role => role.name === 'Owner');
|
return user.roles.some(role => role.name === 'Owner');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAdminUser(user: User) {
|
||||||
|
return user.roles.some(role => role.name === 'Administrator');
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue