0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Wired invites list in admin-x settings

refs https://github.com/TryGhost/Team/issues/3351

- adds api to get list of all invites on the site
- wires invite list with Revoke/Resend option on the User settings group
This commit is contained in:
Rishabh 2023-06-02 18:41:00 +05:30
parent 1a0c1c0cd2
commit b307c59d54
5 changed files with 100 additions and 5 deletions

View file

@ -1,9 +1,11 @@
import React, {createContext, useCallback, useContext, useEffect, useState} from 'react';
import {ServicesContext} from './ServiceProvider';
import {User} from '../../types/api';
import {UserInvite} from '../../utils/api';
interface UsersContextProps {
users: User[];
invites: UserInvite[];
currentUser: User|null;
updateUser?: (user: User) => Promise<void>;
}
@ -14,12 +16,14 @@ interface UsersProviderProps {
const UsersContext = createContext<UsersContextProps>({
users: [],
invites: [],
currentUser: null
});
const UsersProvider: React.FC<UsersProviderProps> = ({children}) => {
const {api} = useContext(ServicesContext);
const [users, setUsers] = useState <User[]> ([]);
const [invites, setInvites] = useState <UserInvite[]> ([]);
const [currentUser, setCurrentUser] = useState <User|null> (null);
useEffect(() => {
@ -28,8 +32,10 @@ const UsersProvider: React.FC<UsersProviderProps> = ({children}) => {
// get list of staff users from the API
const data = await api.users.browse();
const user = await api.users.currentUser();
const invitesRes = await api.invites.browse();
setUsers(data.users);
setCurrentUser(user);
setInvites(invitesRes.invites);
} catch (error) {
// Log error in API
}
@ -58,6 +64,7 @@ const UsersProvider: React.FC<UsersProviderProps> = ({children}) => {
return (
<UsersContext.Provider value={{
users,
invites,
currentUser,
updateUser
}}>

View file

@ -11,6 +11,7 @@ import TabView from '../../../admin-x-ds/global/TabView';
import UserDetailModal from './modals/UserDetailModal';
import useStaffUsers from '../../../hooks/useStaffUsers';
import {User} from '../../../types/api';
import {UserInvite} from '../../../utils/api';
import {generateAvatarColor, getInitials} from '../../../utils/helpers';
interface OwnerProps {
@ -23,6 +24,11 @@ interface UsersListProps {
updateUser?: (user: User) => void;
}
interface InviteListProps {
users: UserInvite[];
updateUser?: (user: User) => void;
}
const Owner: React.FC<OwnerProps> = ({user, updateUser}) => {
const showDetailModal = () => {
NiceModal.show(UserDetailModal, {user, updateUser});
@ -67,7 +73,7 @@ const UsersList: React.FC<UsersListProps> = ({users, updateUser}) => {
detail={user.email}
hideActions={true}
id={`list-item-${user.id}`}
title={user.name}
title={user.name || ''}
onClick={() => showDetailModal(user)} />
);
})}
@ -75,6 +81,48 @@ const UsersList: React.FC<UsersListProps> = ({users, updateUser}) => {
);
};
const InvitesUserList: React.FC<InviteListProps> = ({users}) => {
if (!users || !users.length) {
return (
<NoValueLabel icon='single-user-neutral-block'>
No users found.
</NoValueLabel>
);
}
const actions = (
<div className='flex'>
<Button color='red' label='Revoke' link={true} onClick={() => {
// Revoke invite
}}/>
<Button className='ml-2' color='green' label='Resend' link={true} onClick={() => {
// Resend invite
}}/>
</div>
);
return (
<List>
{users.map((user) => {
return (
<ListItem
key={user.id}
action={actions}
avatar={(<Avatar bgColor={generateAvatarColor((user.email))} image={''} label={''} labelColor='white' />)}
detail={user.role}
hideActions={true}
id={`list-item-${user.id}`}
title={user.email}
onClick={() => {
// do nothing
}}
/>
);
})}
</List>
);
};
const Users: React.FC = () => {
const {
ownerUser,
@ -82,6 +130,7 @@ const Users: React.FC = () => {
editorUsers,
authorUsers,
contributorUsers,
invites,
updateUser
} = useStaffUsers();
@ -119,7 +168,7 @@ const Users: React.FC = () => {
{
id: 'users-invited',
title: 'Invited',
contents: (<></>)
contents: (<InvitesUserList updateUser={updateUser} users={invites} />)
}
];

View file

@ -1,9 +1,12 @@
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[];
invites: UserInvite[];
ownerUser: User;
adminUsers: User[];
editorUsers: User[];
@ -26,13 +29,22 @@ function getOwnerUser(users: User[]): User {
}
const useStaffUsers = (): UsersHook => {
const {users, currentUser, updateUser} = useContext(UsersContext);
const {users, currentUser, updateUser, invites} = useContext(UsersContext);
const {roles} = useContext(RolesContext);
const ownerUser = getOwnerUser(users);
const adminUsers = getUsersByRole(users, 'Administrator');
const editorUsers = getUsersByRole(users, 'Editor');
const authorUsers = getUsersByRole(users, 'Author');
const contributorUsers = getUsersByRole(users, 'Contributor');
const mappedInvites = invites?.map((invite) => {
let role = roles.find((r) => {
return invite.role_id === r.id;
});
return {
...invite,
role: role?.name
};
});
return {
users,
ownerUser,
@ -41,6 +53,7 @@ const useStaffUsers = (): UsersHook => {
authorUsers,
contributorUsers,
currentUser,
invites: mappedInvites,
updateUser
};
};

View file

@ -27,6 +27,22 @@ export interface RolesResponseType {
roles: UserRole[];
}
export interface UserInvite {
created_at: string;
email: string;
expires: string;
id: string;
role_id: string;
role?: string;
status: string;
updated_at: string;
}
export interface InvitesResponseType {
meta?: Meta;
invites: UserInvite[];
}
export interface SiteResponseType {
site: SiteData;
}
@ -79,6 +95,9 @@ interface API {
images: {
upload: ({file}: {file: File}) => Promise<ImagesResponseType>;
};
invites: {
browse: () => Promise<InvitesResponseType>;
}
}
interface GhostApiOptions {
@ -203,6 +222,13 @@ function setupGhostApi({ghostVersion}: GhostApiOptions): API {
const data: any = await response.json();
return data;
}
},
invites: {
browse: async () => {
const response = await fetcher(`/invites/`, {});
const data: InvitesResponseType = await response.json();
return data;
}
}
};

View file

@ -36,7 +36,7 @@ export function getOptionLabel(
return options?.find(option => option.value === value)?.label;
}
export function getInitials(name: string) {
export function getInitials(name: string = '') {
let rgx = new RegExp(/(\p{L}{1})\p{L}+/, 'gu');
let rgxInitials = [...name.matchAll(rgx)] || [];