mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Wired user invite modal in admin-x
refs https://github.com/TryGhost/Team/issues/3351 Allows logged in user to send invites to new users, with allowed roles that can be invited restricted based on the role of logged in user.
This commit is contained in:
parent
8bf113930f
commit
2b0a6bc454
6 changed files with 180 additions and 41 deletions
|
@ -4,6 +4,7 @@ import {UserRole} from '../../types/api';
|
|||
|
||||
interface RolesContextProps {
|
||||
roles: UserRole[];
|
||||
assignableRoles: UserRole[];
|
||||
}
|
||||
|
||||
interface RolesProviderProps {
|
||||
|
@ -11,18 +12,26 @@ interface RolesProviderProps {
|
|||
}
|
||||
|
||||
const RolesContext = createContext<RolesContextProps>({
|
||||
roles: []
|
||||
roles: [],
|
||||
assignableRoles: []
|
||||
});
|
||||
|
||||
const RolesProvider: React.FC<RolesProviderProps> = ({children}) => {
|
||||
const {api} = useContext(ServicesContext);
|
||||
const [roles, setRoles] = useState <UserRole[]> ([]);
|
||||
const [assignableRoles, setAssignableRoles] = useState <UserRole[]> ([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRoles = async (): Promise<void> => {
|
||||
try {
|
||||
const rolesData = await api.roles.browse();
|
||||
const assignableRolesData = await api.roles.browse({
|
||||
queryParams: {
|
||||
permissions: 'assign'
|
||||
}
|
||||
});
|
||||
setRoles(rolesData.roles);
|
||||
setAssignableRoles(assignableRolesData.roles);
|
||||
} catch (error) {
|
||||
// Log error in API
|
||||
}
|
||||
|
@ -33,7 +42,8 @@ const RolesProvider: React.FC<RolesProviderProps> = ({children}) => {
|
|||
|
||||
return (
|
||||
<RolesContext.Provider value={{
|
||||
roles
|
||||
roles,
|
||||
assignableRoles
|
||||
}}>
|
||||
{children}
|
||||
</RolesContext.Provider>
|
||||
|
|
|
@ -8,6 +8,7 @@ interface UsersContextProps {
|
|||
invites: UserInvite[];
|
||||
currentUser: User|null;
|
||||
updateUser?: (user: User) => Promise<void>;
|
||||
setInvites: (invites: UserInvite[]) => void;
|
||||
}
|
||||
|
||||
interface UsersProviderProps {
|
||||
|
@ -17,7 +18,8 @@ interface UsersProviderProps {
|
|||
const UsersContext = createContext<UsersContextProps>({
|
||||
users: [],
|
||||
invites: [],
|
||||
currentUser: null
|
||||
currentUser: null,
|
||||
setInvites: () => {}
|
||||
});
|
||||
|
||||
const UsersProvider: React.FC<UsersProviderProps> = ({children}) => {
|
||||
|
@ -66,7 +68,8 @@ const UsersProvider: React.FC<UsersProviderProps> = ({children}) => {
|
|||
users,
|
||||
invites,
|
||||
currentUser,
|
||||
updateUser
|
||||
updateUser,
|
||||
setInvites
|
||||
}}>
|
||||
{children}
|
||||
</UsersContext.Provider>
|
||||
|
|
|
@ -2,66 +2,141 @@ import Modal from '../../../../admin-x-ds/global/Modal';
|
|||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import Radio from '../../../../admin-x-ds/global/Radio';
|
||||
import TextField from '../../../../admin-x-ds/global/TextField';
|
||||
import {useEffect, useRef} from 'react';
|
||||
import useRoles from '../../../../hooks/useRoles';
|
||||
import useStaffUsers from '../../../../hooks/useStaffUsers';
|
||||
import validator from 'validator';
|
||||
import {ServicesContext} from '../../../providers/ServiceProvider';
|
||||
import {useContext, useEffect, useRef, useState} from 'react';
|
||||
|
||||
type RoleType = 'administrator' | 'editor' | 'author' | 'contributor';
|
||||
|
||||
const InviteUserModal = NiceModal.create(() => {
|
||||
const {api} = useContext(ServicesContext);
|
||||
const {roles, assignableRoles, getRoleId} = useRoles();
|
||||
const {invites, setInvites} = useStaffUsers();
|
||||
|
||||
const focusRef = useRef<HTMLInputElement>(null);
|
||||
const [email, setEmail] = useState<string>('');
|
||||
const [saveState, setSaveState] = useState<'saving' | 'saved' | 'error' | ''>('');
|
||||
const [role, setRole] = useState<RoleType>('contributor');
|
||||
const [errors, setErrors] = useState<{
|
||||
email?: string;
|
||||
}>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (focusRef.current) {
|
||||
focusRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (saveState === 'saved') {
|
||||
setTimeout(() => {
|
||||
setSaveState('');
|
||||
}, 2000);
|
||||
}
|
||||
}, [saveState]);
|
||||
|
||||
let okLabel = 'Send invitation now';
|
||||
if (saveState === 'saving') {
|
||||
okLabel = 'Sending...';
|
||||
} else if (saveState === 'saved') {
|
||||
okLabel = 'Invite sent!';
|
||||
} else if (saveState === 'error') {
|
||||
okLabel = 'Failed to send. Retry?';
|
||||
}
|
||||
|
||||
const handleSendInvitation = async () => {
|
||||
if (saveState === 'saving') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validator.isEmail(email)) {
|
||||
setErrors({
|
||||
email: 'Please enter a valid email address.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
setSaveState('saving');
|
||||
try {
|
||||
const res = await api.invites.add({
|
||||
email,
|
||||
roleId: getRoleId(role, roles)
|
||||
});
|
||||
|
||||
// Update invites list
|
||||
setInvites([...invites, res.invites[0]]);
|
||||
|
||||
setSaveState('saved');
|
||||
} catch (e: any) {
|
||||
setSaveState('error');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const roleOptions = [
|
||||
{
|
||||
hint: 'Can create and edit their own posts, but cannot publish. An Editor needs to approve and publish for them.',
|
||||
label: 'Contributor',
|
||||
value: 'contributor'
|
||||
},
|
||||
{
|
||||
hint: 'A trusted user who can create, edit and publish their own posts, but can’t modify others.',
|
||||
label: 'Author',
|
||||
value: 'author'
|
||||
},
|
||||
{
|
||||
hint: 'Can invite and manage other Authors and Contributors, as well as edit and publish any posts on the site.',
|
||||
label: 'Editor',
|
||||
value: 'editor'
|
||||
},
|
||||
{
|
||||
hint: 'Trusted staff user who should be able to manage all content and users, as well as site settings and options.',
|
||||
label: 'Administrator',
|
||||
value: 'administrator'
|
||||
}
|
||||
];
|
||||
|
||||
const allowedRoleOptions = roleOptions.filter((option) => {
|
||||
return assignableRoles.some((r) => {
|
||||
return r.name === option.label;
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Modal
|
||||
cancelLabel=''
|
||||
okLabel='Send invitation now'
|
||||
okLabel={okLabel}
|
||||
size={540}
|
||||
title='Invite a new staff user'
|
||||
onOk={() => {
|
||||
// Handle invite user
|
||||
}}
|
||||
onOk={handleSendInvitation}
|
||||
>
|
||||
<div className='flex flex-col gap-6 py-4'>
|
||||
<p>
|
||||
Send an invitation for a new person to create a staff account on your site, and select a role that matches what you’d like them to be able to do.
|
||||
</p>
|
||||
<TextField
|
||||
<TextField
|
||||
clearBg={true}
|
||||
error={!!errors.email}
|
||||
hint={errors.email}
|
||||
inputRef={focusRef}
|
||||
placeholder='jamie@example.com'
|
||||
title='Email address'
|
||||
value={email}
|
||||
onChange={(event) => {
|
||||
setEmail(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<Radio
|
||||
defaultSelectedOption={'contributor'}
|
||||
id='role'
|
||||
options={[
|
||||
{
|
||||
hint: 'Can create and edit their own posts, but cannot publish. An Editor needs to approve and publish for them.',
|
||||
label: 'Contributor',
|
||||
value: 'contributor'
|
||||
},
|
||||
{
|
||||
hint: 'A trusted user who can create, edit and publish their own posts, but can’t modify others.',
|
||||
label: 'Author',
|
||||
value: 'author'
|
||||
},
|
||||
{
|
||||
hint: 'Can invite and manage other Authors and Contributors, as well as edit and publish any posts on the site.',
|
||||
label: 'Editor',
|
||||
value: 'editor'
|
||||
},
|
||||
{
|
||||
hint: 'Trusted staff user who should be able to manage all content and users, as well as site settings and options.',
|
||||
label: 'Administrator',
|
||||
value: 'administrator'
|
||||
}
|
||||
]}
|
||||
options={allowedRoleOptions}
|
||||
separator={true}
|
||||
title="Role"
|
||||
onSelect={() => {}}
|
||||
onSelect={(value) => {
|
||||
setRole(value as RoleType);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,13 +4,25 @@ import {useContext} from 'react';
|
|||
|
||||
export type RolesHook = {
|
||||
roles: UserRole[];
|
||||
assignableRoles: UserRole[];
|
||||
getRoleId: (roleName: string, roles: UserRole[]) => string;
|
||||
};
|
||||
|
||||
function getRoleId(roleName: string, roles: UserRole[]): string {
|
||||
const role = roles.find((r) => {
|
||||
return r.name.toLowerCase() === roleName?.toLowerCase();
|
||||
});
|
||||
|
||||
return role?.id || '';
|
||||
}
|
||||
|
||||
const useRoles = (): RolesHook => {
|
||||
const {roles} = useContext(RolesContext);
|
||||
const {roles, assignableRoles} = useContext(RolesContext);
|
||||
|
||||
return {
|
||||
roles
|
||||
roles,
|
||||
assignableRoles,
|
||||
getRoleId
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ export type UsersHook = {
|
|||
contributorUsers: User[];
|
||||
currentUser: User|null;
|
||||
updateUser?: (user: User) => Promise<void>;
|
||||
setInvites: (invites: UserInvite[]) => void;
|
||||
};
|
||||
|
||||
function getUsersByRole(users: User[], role: string): User[] {
|
||||
|
@ -29,7 +30,7 @@ function getOwnerUser(users: User[]): User {
|
|||
}
|
||||
|
||||
const useStaffUsers = (): UsersHook => {
|
||||
const {users, currentUser, updateUser, invites} = useContext(UsersContext);
|
||||
const {users, currentUser, updateUser, invites, setInvites} = useContext(UsersContext);
|
||||
const {roles} = useContext(RolesContext);
|
||||
const ownerUser = getOwnerUser(users);
|
||||
const adminUsers = getUsersByRole(users, 'Administrator');
|
||||
|
@ -45,6 +46,7 @@ const useStaffUsers = (): UsersHook => {
|
|||
role: role?.name
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
users,
|
||||
ownerUser,
|
||||
|
@ -54,7 +56,8 @@ const useStaffUsers = (): UsersHook => {
|
|||
contributorUsers,
|
||||
currentUser,
|
||||
invites: mappedInvites,
|
||||
updateUser
|
||||
updateUser,
|
||||
setInvites
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -68,6 +68,12 @@ interface RequestOptions {
|
|||
};
|
||||
}
|
||||
|
||||
interface BrowseRoleOptions {
|
||||
queryParams: {
|
||||
[key: string]: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface UpdatePasswordOptions {
|
||||
newPassword: string;
|
||||
confirmNewPassword: string;
|
||||
|
@ -87,7 +93,7 @@ interface API {
|
|||
updatePassword: (options: UpdatePasswordOptions) => Promise<PasswordUpdateResponseType>;
|
||||
};
|
||||
roles: {
|
||||
browse: () => Promise<RolesResponseType>;
|
||||
browse: (options?: BrowseRoleOptions) => Promise<RolesResponseType>;
|
||||
};
|
||||
site: {
|
||||
browse: () => Promise<SiteResponseType>;
|
||||
|
@ -97,6 +103,13 @@ interface API {
|
|||
};
|
||||
invites: {
|
||||
browse: () => Promise<InvitesResponseType>;
|
||||
add: ({email, roleId} : {
|
||||
email: string;
|
||||
roleId: string;
|
||||
expires?: number;
|
||||
status?: string;
|
||||
token?: string;
|
||||
}) => Promise<InvitesResponseType>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -195,8 +208,14 @@ function setupGhostApi({ghostVersion}: GhostApiOptions): API {
|
|||
}
|
||||
},
|
||||
roles: {
|
||||
browse: async () => {
|
||||
const response = await fetcher(`/roles/?limit=all`, {});
|
||||
browse: async (options?: BrowseRoleOptions) => {
|
||||
const queryParams = options?.queryParams || {};
|
||||
queryParams.limit = 'all';
|
||||
const queryString = Object.keys(options?.queryParams || {})
|
||||
.map(key => `${key}=${options?.queryParams[key]}`)
|
||||
.join('&');
|
||||
|
||||
const response = await fetcher(`/roles/?${queryString}`, {});
|
||||
const data: RolesResponseType = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
@ -228,6 +247,23 @@ function setupGhostApi({ghostVersion}: GhostApiOptions): API {
|
|||
const response = await fetcher(`/invites/`, {});
|
||||
const data: InvitesResponseType = await response.json();
|
||||
return data;
|
||||
},
|
||||
add: async ({email, roleId}) => {
|
||||
const payload = JSON.stringify({
|
||||
invites: [{
|
||||
email: email,
|
||||
role_id: roleId,
|
||||
expires: null,
|
||||
status: null,
|
||||
token: null
|
||||
}]
|
||||
});
|
||||
const response = await fetcher(`/invites/`, {
|
||||
method: 'POST',
|
||||
body: payload
|
||||
});
|
||||
const data: InvitesResponseType = await response.json();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue