fix: workspace member invite to avoid lower permission user to invite higher permission member (#1309)

This commit is contained in:
pablohashescobar 2023-06-16 19:52:24 +05:30 committed by GitHub
parent 02111d779b
commit 4c0857233e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 421 additions and 338 deletions

View File

@ -195,6 +195,11 @@ class InviteWorkspaceEndpoint(BaseAPIView):
{"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST {"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
) )
# check for role level
requesting_user = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user)
if len([email for email in emails if int(email.get("role", 10)) > requesting_user.role]):
return Response({"error": "You cannot invite a user with higher role"}, status=status.HTTP_400_BAD_REQUEST)
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
# Check if user is already a member of workspace # Check if user is already a member of workspace

View File

@ -9,11 +9,13 @@ import { useForm, Controller } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// ui // ui
import { CustomSelect, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; import { CustomSelect, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
// hooks
import useToast from "hooks/use-toast";
// services // services
import projectService from "services/project.service"; import projectService from "services/project.service";
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// contexts
import { useProjectMyMembership } from "contexts/project-member.context";
// hooks
import useToast from "hooks/use-toast";
// types // types
import { ICurrentUserResponse, IProjectMemberInvitation } from "types"; import { ICurrentUserResponse, IProjectMemberInvitation } from "types";
// fetch-keys // fetch-keys
@ -46,6 +48,7 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { memberDetails } = useProjectMyMembership();
const { data: people } = useSWR( const { data: people } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null, workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null,
@ -202,11 +205,15 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
input input
width="w-full" width="w-full"
> >
{Object.entries(ROLE).map(([key, label]) => ( {Object.entries(ROLE).map(([key, label]) => {
<CustomSelect.Option key={key} value={key}> if (parseInt(key) > (memberDetails?.role ?? 5)) return null;
{label}
</CustomSelect.Option> return (
))} <CustomSelect.Option key={key} value={key}>
{label}
</CustomSelect.Option>
);
})}
</CustomSelect> </CustomSelect>
)} )}
/> />

View File

@ -1,17 +1,22 @@
import React from "react"; import React from "react";
import { mutate } from "swr"; import { mutate } from "swr";
// react-hook-form
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// headless // headless
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// services // services
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// ui // contexts
import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ui"; import { useWorkspaceMyMembership } from "contexts/workspace-member.context";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui
import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ui";
// types // types
import { ICurrentUserResponse, IWorkspaceMemberInvitation } from "types"; import { ICurrentUserResponse, IWorkspaceMemberInvitation } from "types";
// fetch keys // fetch-keys
import { WORKSPACE_INVITATIONS } from "constants/fetch-keys"; import { WORKSPACE_INVITATIONS } from "constants/fetch-keys";
// constants // constants
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
@ -37,6 +42,7 @@ const SendWorkspaceInvitationModal: React.FC<Props> = ({
user, user,
}) => { }) => {
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { memberDetails } = useWorkspaceMyMembership();
const { const {
control, control,
@ -145,11 +151,15 @@ const SendWorkspaceInvitationModal: React.FC<Props> = ({
width="w-full" width="w-full"
input input
> >
{Object.entries(ROLE).map(([key, value]) => ( {Object.entries(ROLE).map(([key, value]) => {
<CustomSelect.Option key={key} value={key}> if (parseInt(key) > (memberDetails?.role ?? 5)) return null;
{value}
</CustomSelect.Option> return (
))} <CustomSelect.Option key={key} value={key}>
{value}
</CustomSelect.Option>
);
})}
</CustomSelect> </CustomSelect>
)} )}
/> />

View File

@ -0,0 +1,61 @@
import { createContext, useContext } from "react";
// next
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import workspaceService from "services/workspace.service";
// types
import { IWorkspaceMember } from "types";
// fetch-keys
import { WORKSPACE_MEMBERS_ME } from "constants/fetch-keys";
type ContextType = {
loading: boolean;
memberDetails?: IWorkspaceMember;
error: any;
};
export const WorkspaceMemberContext = createContext<ContextType>({} as ContextType);
type Props = {
children: React.ReactNode;
};
export const WorkspaceMemberProvider: React.FC<Props> = (props) => {
const { children } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: memberDetails, error } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS_ME(workspaceSlug.toString()) : null,
workspaceSlug ? () => workspaceService.workspaceMemberMe(workspaceSlug.toString()) : null
);
const loading = !memberDetails && !error;
return (
<WorkspaceMemberContext.Provider value={{ loading, memberDetails, error }}>
{children}
</WorkspaceMemberContext.Provider>
);
};
export const useWorkspaceMyMembership = () => {
const context = useContext(WorkspaceMemberContext);
if (context === undefined)
throw new Error(`useWorkspaceMember must be used within a WorkspaceMemberProvider.`);
return {
...context,
memberRole: {
isOwner: context.memberDetails?.role === 20,
isMember: context.memberDetails?.role === 15,
isViewer: context.memberDetails?.role === 10,
isGuest: context.memberDetails?.role === 5,
},
};
};

View File

@ -7,6 +7,8 @@ import useSWR from "swr";
// services // services
import workspaceServices from "services/workspace.service"; import workspaceServices from "services/workspace.service";
// contexts
import { WorkspaceMemberProvider } from "contexts/workspace-member.context";
// layouts // layouts
import AppSidebar from "layouts/app-layout/app-sidebar"; import AppSidebar from "layouts/app-layout/app-sidebar";
import AppHeader from "layouts/app-layout/app-header"; import AppHeader from "layouts/app-layout/app-header";
@ -78,48 +80,50 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
return ( return (
<UserAuthorizationLayout> <UserAuthorizationLayout>
<CommandPalette /> <WorkspaceMemberProvider>
<div className="relative flex h-screen w-full overflow-hidden"> <CommandPalette />
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} /> <div className="relative flex h-screen w-full overflow-hidden">
{settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? ( <AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
<NotAuthorizedView {settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
actionButton={ <NotAuthorizedView
<Link href={`/${workspaceSlug}`}> actionButton={
<a> <Link href={`/${workspaceSlug}`}>
<PrimaryButton className="flex items-center gap-1"> <a>
<LayerDiagonalIcon height={16} width={16} color="white" /> Go to workspace <PrimaryButton className="flex items-center gap-1">
</PrimaryButton> <LayerDiagonalIcon height={16} width={16} color="white" /> Go to workspace
</a> </PrimaryButton>
</Link> </a>
} </Link>
type="workspace" }
/> type="workspace"
) : ( />
<main ) : (
className={`relative flex h-full w-full flex-col overflow-hidden ${ <main
bg === "primary" className={`relative flex h-full w-full flex-col overflow-hidden ${
? "bg-brand-surface-1" bg === "primary"
: bg === "secondary" ? "bg-brand-surface-1"
? "bg-brand-sidebar" : bg === "secondary"
: "bg-brand-base" ? "bg-brand-sidebar"
}`} : "bg-brand-base"
> }`}
{!noHeader && ( >
<AppHeader {!noHeader && (
breadcrumbs={breadcrumbs} <AppHeader
left={left} breadcrumbs={breadcrumbs}
right={right} left={left}
setToggleSidebar={setToggleSidebar} right={right}
/> setToggleSidebar={setToggleSidebar}
)} />
<div className="h-full w-full overflow-hidden"> )}
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll"> <div className="h-full w-full overflow-hidden">
{children} <div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
{children}
</div>
</div> </div>
</div> </main>
</main> )}
)} </div>
</div> </WorkspaceMemberProvider>
</UserAuthorizationLayout> </UserAuthorizationLayout>
); );
}; };

View File

@ -89,7 +89,17 @@ const MembersSettings: NextPage = () => {
const currentUser = projectMembers?.find((item) => item.member.id === user?.id); const currentUser = projectMembers?.find((item) => item.member.id === user?.id);
return ( return (
<> <ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${projectDetails?.name ?? "Project"}`}
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
/>
<BreadcrumbItem title="Members Settings" />
</Breadcrumbs>
}
>
<ConfirmProjectMemberRemove <ConfirmProjectMemberRemove
isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)} isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)}
onClose={() => { onClose={() => {
@ -136,149 +146,137 @@ const MembersSettings: NextPage = () => {
members={members} members={members}
user={user} user={user}
/> />
<ProjectAuthorizationWrapper <div className="p-8">
breadcrumbs={ <SettingsHeader />
<Breadcrumbs> <section className="space-y-5">
<BreadcrumbItem <div className="flex items-end justify-between gap-4">
title={`${projectDetails?.name ?? "Project"}`} <h3 className="text-2xl font-semibold">Members</h3>
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`} <button
/> type="button"
<BreadcrumbItem title="Members Settings" /> className="flex items-center gap-2 text-brand-accent outline-none"
</Breadcrumbs> onClick={() => setInviteModal(true)}
} >
> <PlusIcon className="h-4 w-4" />
<div className="p-8"> Add Member
<SettingsHeader /> </button>
<section className="space-y-5"> </div>
<div className="flex items-end justify-between gap-4"> {!projectMembers || !projectInvitations ? (
<h3 className="text-2xl font-semibold">Members</h3> <Loader className="space-y-5">
<button <Loader.Item height="40px" />
type="button" <Loader.Item height="40px" />
className="flex items-center gap-2 text-brand-accent outline-none" <Loader.Item height="40px" />
onClick={() => setInviteModal(true)} <Loader.Item height="40px" />
> </Loader>
<PlusIcon className="h-4 w-4" /> ) : (
Add Member <div className="divide-y divide-brand-base rounded-[10px] border border-brand-base bg-brand-base px-6">
</button> {members.length > 0
</div> ? members.map((member) => (
{!projectMembers || !projectInvitations ? ( <div key={member.id} className="flex items-center justify-between py-6">
<Loader className="space-y-5"> <div className="flex items-center gap-x-6 gap-y-2">
<Loader.Item height="40px" /> <div className="relative flex h-10 w-10 items-center justify-center rounded-lg bg-gray-700 p-4 capitalize text-white">
<Loader.Item height="40px" /> {member.avatar && member.avatar !== "" ? (
<Loader.Item height="40px" /> <img
<Loader.Item height="40px" /> src={member.avatar}
</Loader> alt={member.first_name}
) : ( className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
<div className="divide-y divide-brand-base rounded-[10px] border border-brand-base bg-brand-base px-6"> />
{members.length > 0 ) : member.first_name !== "" ? (
? members.map((member) => ( member.first_name.charAt(0)
<div key={member.id} className="flex items-center justify-between py-6"> ) : (
<div className="flex items-center gap-x-6 gap-y-2"> member.email.charAt(0)
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg bg-gray-700 p-4 capitalize text-white">
{member.avatar && member.avatar !== "" ? (
<img
src={member.avatar}
alt={member.first_name}
className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
/>
) : member.first_name !== "" ? (
member.first_name.charAt(0)
) : (
member.email.charAt(0)
)}
</div>
<div>
<h4 className="text-sm">
{member.first_name} {member.last_name}
</h4>
<p className="mt-0.5 text-xs text-brand-secondary">{member.email}</p>
</div>
</div>
<div className="flex items-center gap-2 text-xs">
{!member.member && (
<div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500">
Pending
</div>
)} )}
<CustomSelect </div>
label={ROLE[member.role as keyof typeof ROLE]} <div>
value={member.role} <h4 className="text-sm">
onChange={(value: 5 | 10 | 15 | 20 | undefined) => { {member.first_name} {member.last_name}
if (!activeWorkspace || !projectDetails) return; </h4>
<p className="mt-0.5 text-xs text-brand-secondary">{member.email}</p>
mutateMembers(
(prevData: any) =>
prevData.map((m: any) =>
m.id === member.id ? { ...m, role: value } : m
),
false
);
projectService
.updateProjectMember(
activeWorkspace.slug,
projectDetails.id,
member.id,
{
role: value,
}
)
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message:
"An error occurred while updating member role. Please try again.",
});
});
}}
position="right"
disabled={
member.memberId === user?.id ||
!member.member ||
(currentUser &&
currentUser.role !== 20 &&
currentUser.role < member.role)
}
>
{Object.keys(ROLE).map((key) => {
if (
currentUser &&
currentUser.role !== 20 &&
currentUser.role < parseInt(key)
)
return null;
return (
<CustomSelect.Option key={key} value={key}>
<>{ROLE[parseInt(key) as keyof typeof ROLE]}</>
</CustomSelect.Option>
);
})}
</CustomSelect>
<CustomMenu ellipsis>
<CustomMenu.MenuItem
onClick={() => {
if (member.member) setSelectedRemoveMember(member.id);
else setSelectedInviteRemoveMember(member.id);
}}
>
<span className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" />
<span>Remove member</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div> </div>
</div> </div>
)) <div className="flex items-center gap-2 text-xs">
: null} {!member.member && (
</div> <div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500">
)} Pending
</section> </div>
</div> )}
</ProjectAuthorizationWrapper> <CustomSelect
</> label={ROLE[member.role as keyof typeof ROLE]}
value={member.role}
onChange={(value: 5 | 10 | 15 | 20 | undefined) => {
if (!activeWorkspace || !projectDetails) return;
mutateMembers(
(prevData: any) =>
prevData.map((m: any) =>
m.id === member.id ? { ...m, role: value } : m
),
false
);
projectService
.updateProjectMember(
activeWorkspace.slug,
projectDetails.id,
member.id,
{
role: value,
}
)
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message:
"An error occurred while updating member role. Please try again.",
});
});
}}
position="right"
disabled={
member.memberId === user?.id ||
!member.member ||
(currentUser &&
currentUser.role !== 20 &&
currentUser.role < member.role)
}
>
{Object.keys(ROLE).map((key) => {
if (
currentUser &&
currentUser.role !== 20 &&
currentUser.role < parseInt(key)
)
return null;
return (
<CustomSelect.Option key={key} value={key}>
<>{ROLE[parseInt(key) as keyof typeof ROLE]}</>
</CustomSelect.Option>
);
})}
</CustomSelect>
<CustomMenu ellipsis>
<CustomMenu.MenuItem
onClick={() => {
if (member.member) setSelectedRemoveMember(member.id);
else setSelectedInviteRemoveMember(member.id);
}}
>
<span className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" />
<span>Remove member</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
))
: null}
</div>
)}
</section>
</div>
</ProjectAuthorizationWrapper>
); );
}; };

View File

@ -85,7 +85,17 @@ const MembersSettings: NextPage = () => {
const currentUser = workspaceMembers?.find((item) => item.member?.id === user?.id); const currentUser = workspaceMembers?.find((item) => item.member?.id === user?.id);
return ( return (
<> <WorkspaceAuthorizationLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${activeWorkspace?.name ?? "Workspace"}`}
link={`/${workspaceSlug}`}
/>
<BreadcrumbItem title="Members Settings" />
</Breadcrumbs>
}
>
<ConfirmWorkspaceMemberRemove <ConfirmWorkspaceMemberRemove
isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)} isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)}
onClose={() => { onClose={() => {
@ -137,149 +147,137 @@ const MembersSettings: NextPage = () => {
members={members} members={members}
user={user} user={user}
/> />
<WorkspaceAuthorizationLayout <div className="p-8">
breadcrumbs={ <SettingsHeader />
<Breadcrumbs> <section className="space-y-5">
<BreadcrumbItem <div className="flex items-end justify-between gap-4">
title={`${activeWorkspace?.name ?? "Workspace"}`} <h3 className="text-2xl font-semibold">Members</h3>
link={`/${workspaceSlug}`} <button
/> type="button"
<BreadcrumbItem title="Members Settings" /> className="flex items-center gap-2 text-brand-accent outline-none"
</Breadcrumbs> onClick={() => setInviteModal(true)}
} >
> <PlusIcon className="h-4 w-4" />
<div className="p-8"> Add Member
<SettingsHeader /> </button>
<section className="space-y-5"> </div>
<div className="flex items-end justify-between gap-4"> {!workspaceMembers || !workspaceInvitations ? (
<h3 className="text-2xl font-semibold">Members</h3> <Loader className="space-y-5">
<button <Loader.Item height="40px" />
type="button" <Loader.Item height="40px" />
className="flex items-center gap-2 text-brand-accent outline-none" <Loader.Item height="40px" />
onClick={() => setInviteModal(true)} <Loader.Item height="40px" />
> </Loader>
<PlusIcon className="h-4 w-4" /> ) : (
Add Member <div className="divide-y divide-brand-base rounded-[10px] border border-brand-base bg-brand-base px-6">
</button> {members.length > 0
</div> ? members.map((member) => (
{!workspaceMembers || !workspaceInvitations ? ( <div key={member.id} className="flex items-center justify-between py-6">
<Loader className="space-y-5"> <div className="flex items-center gap-x-8 gap-y-2">
<Loader.Item height="40px" /> <div className="relative flex h-10 w-10 items-center justify-center rounded-lg bg-gray-700 p-4 capitalize text-white">
<Loader.Item height="40px" /> {member.avatar && member.avatar !== "" ? (
<Loader.Item height="40px" /> <img
<Loader.Item height="40px" /> src={member.avatar}
</Loader> className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
) : ( alt={member.first_name}
<div className="divide-y divide-brand-base rounded-[10px] border border-brand-base bg-brand-base px-6"> />
{members.length > 0 ) : member.first_name !== "" ? (
? members.map((member) => ( member.first_name.charAt(0)
<div key={member.id} className="flex items-center justify-between py-6"> ) : (
<div className="flex items-center gap-x-8 gap-y-2"> member.email.charAt(0)
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg bg-gray-700 p-4 capitalize text-white"> )}
{member.avatar && member.avatar !== "" ? (
<img
src={member.avatar}
className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
alt={member.first_name}
/>
) : member.first_name !== "" ? (
member.first_name.charAt(0)
) : (
member.email.charAt(0)
)}
</div>
<div>
<h4 className="text-sm">
{member.first_name} {member.last_name}
</h4>
<p className="text-xs text-brand-secondary">{member.email}</p>
</div>
</div> </div>
<div className="flex items-center gap-2 text-xs"> <div>
{!member?.status && ( <h4 className="text-sm">
<div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500"> {member.first_name} {member.last_name}
<p>Pending</p> </h4>
</div> <p className="text-xs text-brand-secondary">{member.email}</p>
)}
{member?.status && !member?.accountCreated && (
<div className="mr-2 flex items-center justify-center rounded-full bg-blue-500/20 px-2 py-1 text-center text-xs text-blue-500">
<p>Account not created</p>
</div>
)}
<CustomSelect
label={ROLE[member.role as keyof typeof ROLE]}
value={member.role}
onChange={(value: any) => {
if (!workspaceSlug) return;
mutateMembers(
(prevData) =>
prevData?.map((m) =>
m.id === member.id ? { ...m, role: value } : m
),
false
);
workspaceService
.updateWorkspaceMember(workspaceSlug?.toString(), member.id, {
role: value,
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message:
"An error occurred while updating member role. Please try again.",
});
});
}}
position="right"
disabled={
member.memberId === currentUser?.member.id ||
!member.status ||
(currentUser &&
currentUser.role !== 20 &&
currentUser.role < member.role)
}
>
{Object.keys(ROLE).map((key) => {
if (
currentUser &&
currentUser.role !== 20 &&
currentUser.role < parseInt(key)
)
return null;
return (
<CustomSelect.Option key={key} value={key}>
<>{ROLE[parseInt(key) as keyof typeof ROLE]}</>
</CustomSelect.Option>
);
})}
</CustomSelect>
<CustomMenu ellipsis>
<CustomMenu.MenuItem
onClick={() => {
if (member.member) {
setSelectedRemoveMember(member.id);
} else {
setSelectedInviteRemoveMember(member.id);
}
}}
>
Remove member
</CustomMenu.MenuItem>
</CustomMenu>
</div> </div>
</div> </div>
)) <div className="flex items-center gap-2 text-xs">
: null} {!member?.status && (
</div> <div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500">
)} <p>Pending</p>
</section> </div>
</div> )}
</WorkspaceAuthorizationLayout> {member?.status && !member?.accountCreated && (
</> <div className="mr-2 flex items-center justify-center rounded-full bg-blue-500/20 px-2 py-1 text-center text-xs text-blue-500">
<p>Account not created</p>
</div>
)}
<CustomSelect
label={ROLE[member.role as keyof typeof ROLE]}
value={member.role}
onChange={(value: any) => {
if (!workspaceSlug) return;
mutateMembers(
(prevData) =>
prevData?.map((m) =>
m.id === member.id ? { ...m, role: value } : m
),
false
);
workspaceService
.updateWorkspaceMember(workspaceSlug?.toString(), member.id, {
role: value,
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message:
"An error occurred while updating member role. Please try again.",
});
});
}}
position="right"
disabled={
member.memberId === currentUser?.member.id ||
!member.status ||
(currentUser &&
currentUser.role !== 20 &&
currentUser.role < member.role)
}
>
{Object.keys(ROLE).map((key) => {
if (
currentUser &&
currentUser.role !== 20 &&
currentUser.role < parseInt(key)
)
return null;
return (
<CustomSelect.Option key={key} value={key}>
<>{ROLE[parseInt(key) as keyof typeof ROLE]}</>
</CustomSelect.Option>
);
})}
</CustomSelect>
<CustomMenu ellipsis>
<CustomMenu.MenuItem
onClick={() => {
if (member.member) {
setSelectedRemoveMember(member.id);
} else {
setSelectedInviteRemoveMember(member.id);
}
}}
>
Remove member
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
))
: null}
</div>
)}
</section>
</div>
</WorkspaceAuthorizationLayout>
); );
}; };