[WEB-3368] feat: enhance workspace invitations with copyable invite links (#6601)
* feat: invitation link url * feat: copy invite link from workspace invitations list * invitation reponse cleanup and logo url fix --------- Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
This commit is contained in:
parent
39ecfbe7e1
commit
3528d2c934
14 changed files with 142 additions and 87 deletions
|
|
@ -59,7 +59,7 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||||
class WorkspaceLiteSerializer(BaseSerializer):
|
class WorkspaceLiteSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Workspace
|
model = Workspace
|
||||||
fields = ["name", "slug", "id"]
|
fields = ["name", "slug", "id", "logo_url"]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -90,9 +90,11 @@ class WorkspaceMemberAdminSerializer(DynamicBaseSerializer):
|
||||||
|
|
||||||
|
|
||||||
class WorkSpaceMemberInviteSerializer(BaseSerializer):
|
class WorkSpaceMemberInviteSerializer(BaseSerializer):
|
||||||
workspace = WorkSpaceSerializer(read_only=True)
|
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||||
total_members = serializers.IntegerField(read_only=True)
|
invite_link = serializers.SerializerMethodField()
|
||||||
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
|
|
||||||
|
def get_invite_link(self, obj):
|
||||||
|
return f"/workspace-invitations/?invitation_id={obj.id}&email={obj.email}&slug={obj.workspace.slug}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WorkspaceMemberInvite
|
model = WorkspaceMemberInvite
|
||||||
|
|
@ -106,6 +108,7 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
|
||||||
"responded_at",
|
"responded_at",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
"invite_link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -148,12 +151,12 @@ class WorkspaceUserLinkSerializer(BaseSerializer):
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
# Filtering the WorkspaceUserLink with the given url to check if the link already exists.
|
# Filtering the WorkspaceUserLink with the given url to check if the link already exists.
|
||||||
|
|
||||||
url = validated_data.get("url")
|
url = validated_data.get("url")
|
||||||
|
|
||||||
workspace_user_link = WorkspaceUserLink.objects.filter(
|
workspace_user_link = WorkspaceUserLink.objects.filter(
|
||||||
url=url,
|
url=url,
|
||||||
workspace_id=validated_data.get("workspace_id"),
|
workspace_id=validated_data.get("workspace_id"),
|
||||||
owner_id=validated_data.get("owner_id")
|
owner_id=validated_data.get("owner_id")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -170,8 +173,8 @@ class WorkspaceUserLinkSerializer(BaseSerializer):
|
||||||
url = validated_data.get("url")
|
url = validated_data.get("url")
|
||||||
|
|
||||||
workspace_user_link = WorkspaceUserLink.objects.filter(
|
workspace_user_link = WorkspaceUserLink.objects.filter(
|
||||||
url=url,
|
url=url,
|
||||||
workspace_id=instance.workspace_id,
|
workspace_id=instance.workspace_id,
|
||||||
owner=instance.owner
|
owner=instance.owner
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -251,8 +251,7 @@ class UserWorkspaceInvitationsViewSet(BaseViewSet):
|
||||||
super()
|
super()
|
||||||
.get_queryset()
|
.get_queryset()
|
||||||
.filter(email=self.request.user.email)
|
.filter(email=self.request.user.email)
|
||||||
.select_related("workspace", "workspace__owner", "created_by")
|
.select_related("workspace")
|
||||||
.annotate(total_members=Count("workspace__workspace_member"))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@invalidate_cache(path="/api/workspaces/", user=False)
|
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||||
|
|
|
||||||
|
|
@ -672,7 +672,9 @@
|
||||||
"disconnect": "Disconnect",
|
"disconnect": "Disconnect",
|
||||||
"disconnecting": "Disconnecting",
|
"disconnecting": "Disconnecting",
|
||||||
"installing": "Installing",
|
"installing": "Installing",
|
||||||
"install": "Install"
|
"install": "Install",
|
||||||
|
"pending": "Pending",
|
||||||
|
"invite": "Invite"
|
||||||
},
|
},
|
||||||
|
|
||||||
"form": {
|
"form": {
|
||||||
|
|
@ -1279,6 +1281,7 @@
|
||||||
"members": {
|
"members": {
|
||||||
"title": "Members",
|
"title": "Members",
|
||||||
"add_member": "Add member",
|
"add_member": "Add member",
|
||||||
|
"pending_invites": "Pending invites",
|
||||||
"invitations_sent_successfully": "Invitations sent successfully",
|
"invitations_sent_successfully": "Invitations sent successfully",
|
||||||
"leave_confirmation": "Are you sure you want to leave the workspace? You will no longer have access to this workspace. This action cannot be undone.",
|
"leave_confirmation": "Are you sure you want to leave the workspace? You will no longer have access to this workspace. This action cannot be undone.",
|
||||||
"details": {
|
"details": {
|
||||||
|
|
|
||||||
|
|
@ -842,7 +842,9 @@
|
||||||
"disconnect": "Desconectar",
|
"disconnect": "Desconectar",
|
||||||
"disconnecting": "Desconectando",
|
"disconnecting": "Desconectando",
|
||||||
"installing": "Instalando",
|
"installing": "Instalando",
|
||||||
"install": "Instalar"
|
"install": "Instalar",
|
||||||
|
"pending": "Pendiente",
|
||||||
|
"invite": "Invitar"
|
||||||
},
|
},
|
||||||
|
|
||||||
"form": {
|
"form": {
|
||||||
|
|
@ -1448,6 +1450,7 @@
|
||||||
"members": {
|
"members": {
|
||||||
"title": "Miembros",
|
"title": "Miembros",
|
||||||
"add_member": "Agregar miembro",
|
"add_member": "Agregar miembro",
|
||||||
|
"pending_invites": "Invitaciones pendientes",
|
||||||
"invitations_sent_successfully": "Invitaciones enviadas exitosamente",
|
"invitations_sent_successfully": "Invitaciones enviadas exitosamente",
|
||||||
"leave_confirmation": "¿Estás seguro de que quieres abandonar el espacio de trabajo? Ya no tendrás acceso a este espacio de trabajo. Esta acción no se puede deshacer.",
|
"leave_confirmation": "¿Estás seguro de que quieres abandonar el espacio de trabajo? Ya no tendrás acceso a este espacio de trabajo. Esta acción no se puede deshacer.",
|
||||||
"details": {
|
"details": {
|
||||||
|
|
|
||||||
|
|
@ -842,7 +842,9 @@
|
||||||
"disconnect": "Déconnecter",
|
"disconnect": "Déconnecter",
|
||||||
"disconnecting": "Déconnexion",
|
"disconnecting": "Déconnexion",
|
||||||
"installing": "Installation",
|
"installing": "Installation",
|
||||||
"install": "Installer"
|
"install": "Installer",
|
||||||
|
"pending": "En attente",
|
||||||
|
"invite": "Inviter"
|
||||||
},
|
},
|
||||||
|
|
||||||
"form": {
|
"form": {
|
||||||
|
|
@ -1448,6 +1450,7 @@
|
||||||
"members": {
|
"members": {
|
||||||
"title": "Membres",
|
"title": "Membres",
|
||||||
"add_member": "Ajouter un membre",
|
"add_member": "Ajouter un membre",
|
||||||
|
"pending_invites": "Invitations en attente",
|
||||||
"invitations_sent_successfully": "Invitations envoyées avec succès",
|
"invitations_sent_successfully": "Invitations envoyées avec succès",
|
||||||
"leave_confirmation": "Êtes-vous sûr de vouloir quitter l'espace de travail ? Vous n'aurez plus accès à cet espace de travail. Cette action ne peut pas être annulée.",
|
"leave_confirmation": "Êtes-vous sûr de vouloir quitter l'espace de travail ? Vous n'aurez plus accès à cet espace de travail. Cette action ne peut pas être annulée.",
|
||||||
"details": {
|
"details": {
|
||||||
|
|
|
||||||
|
|
@ -842,7 +842,9 @@
|
||||||
"disconnect": "切断",
|
"disconnect": "切断",
|
||||||
"disconnecting": "切断中",
|
"disconnecting": "切断中",
|
||||||
"installing": "インストール中",
|
"installing": "インストール中",
|
||||||
"install": "インストール"
|
"install": "インストール",
|
||||||
|
"pending": "保留中",
|
||||||
|
"invite": "招待"
|
||||||
},
|
},
|
||||||
|
|
||||||
"form": {
|
"form": {
|
||||||
|
|
@ -1448,6 +1450,7 @@
|
||||||
"members": {
|
"members": {
|
||||||
"title": "メンバー",
|
"title": "メンバー",
|
||||||
"add_member": "メンバーを追加",
|
"add_member": "メンバーを追加",
|
||||||
|
"pending_invites": "保留中の招待",
|
||||||
"invitations_sent_successfully": "招待が正常に送信されました",
|
"invitations_sent_successfully": "招待が正常に送信されました",
|
||||||
"leave_confirmation": "ワークスペースから退出してもよろしいですか?このワークスペースにアクセスできなくなります。この操作は取り消せません。",
|
"leave_confirmation": "ワークスペースから退出してもよろしいですか?このワークスペースにアクセスできなくなります。この操作は取り消せません。",
|
||||||
"details": {
|
"details": {
|
||||||
|
|
|
||||||
|
|
@ -842,7 +842,9 @@
|
||||||
"disconnect": "断开连接",
|
"disconnect": "断开连接",
|
||||||
"disconnecting": "正在断开连接",
|
"disconnecting": "正在断开连接",
|
||||||
"installing": "正在安装",
|
"installing": "正在安装",
|
||||||
"install": "安装"
|
"install": "安装",
|
||||||
|
"pending": "待处理",
|
||||||
|
"invite": "邀请"
|
||||||
},
|
},
|
||||||
|
|
||||||
"form": {
|
"form": {
|
||||||
|
|
@ -1448,6 +1450,7 @@
|
||||||
"members": {
|
"members": {
|
||||||
"title": "成员",
|
"title": "成员",
|
||||||
"add_member": "添加成员",
|
"add_member": "添加成员",
|
||||||
|
"pending_invites": "待处理邀请",
|
||||||
"invitations_sent_successfully": "邀请发送成功",
|
"invitations_sent_successfully": "邀请发送成功",
|
||||||
"leave_confirmation": "您确定要离开工作区吗?您将无法再访问此工作区。此操作无法撤消。",
|
"leave_confirmation": "您确定要离开工作区吗?您将无法再访问此工作区。此操作无法撤消。",
|
||||||
"details": {
|
"details": {
|
||||||
|
|
|
||||||
3
packages/types/src/workspace.d.ts
vendored
3
packages/types/src/workspace.d.ts
vendored
|
|
@ -40,9 +40,10 @@ export interface IWorkspaceMemberInvitation {
|
||||||
responded_at: Date;
|
responded_at: Date;
|
||||||
role: TUserPermissions;
|
role: TUserPermissions;
|
||||||
token: string;
|
token: string;
|
||||||
|
invite_link: string;
|
||||||
workspace: {
|
workspace: {
|
||||||
id: string;
|
id: string;
|
||||||
logo: string;
|
logo_url: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
||||||
onSubmit={handleWorkspaceInvite}
|
onSubmit={handleWorkspaceInvite}
|
||||||
/>
|
/>
|
||||||
<section
|
<section
|
||||||
className={cn("w-full overflow-y-auto", {
|
className={cn("w-full h-full overflow-y-auto", {
|
||||||
"opacity-60": !canPerformWorkspaceMemberActions,
|
"opacity-60": !canPerformWorkspaceMemberActions,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import type { IWorkspaceMemberInvitation } from "@plane/types";
|
||||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { EmptyState } from "@/components/common";
|
import { EmptyState } from "@/components/common";
|
||||||
|
import { WorkspaceLogo } from "@/components/workspace/logo";
|
||||||
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
|
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
|
||||||
// helpers
|
// helpers
|
||||||
import { truncateText } from "@/helpers/string.helper";
|
import { truncateText } from "@/helpers/string.helper";
|
||||||
|
|
@ -167,21 +168,11 @@ const UserInvitationsPage = observer(() => {
|
||||||
onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")}
|
onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")}
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<div className="grid h-9 w-9 place-items-center rounded">
|
<WorkspaceLogo
|
||||||
{invitation.workspace.logo && invitation.workspace.logo.trim() !== "" ? (
|
logo={invitation.workspace.logo_url}
|
||||||
<img
|
name={invitation.workspace.name}
|
||||||
src={invitation.workspace.logo}
|
classNames="size-9 flex-shrink-0"
|
||||||
height="100%"
|
/>
|
||||||
width="100%"
|
|
||||||
className="rounded"
|
|
||||||
alt={invitation.workspace.name}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="grid h-9 w-9 place-items-center rounded bg-gray-700 px-3 py-1.5 uppercase text-white">
|
|
||||||
{invitation.workspace.name[0]}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="text-sm font-medium">{truncateText(invitation.workspace.name, 30)}</div>
|
<div className="text-sm font-medium">{truncateText(invitation.workspace.name, 30)}</div>
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ export const AuthHeader: FC<TAuthHeader> = observer((props) => {
|
||||||
header: (
|
header: (
|
||||||
<div className="relative inline-flex items-center gap-2">
|
<div className="relative inline-flex items-center gap-2">
|
||||||
{t("common.join")}{" "}
|
{t("common.join")}{" "}
|
||||||
<WorkspaceLogo logo={workspace?.logo} name={workspace?.name} classNames="w-8 h-9 flex-shrink-0" />{" "}
|
<WorkspaceLogo logo={workspace?.logo_url} name={workspace?.name} classNames="size-9 flex-shrink-0" />{" "}
|
||||||
{workspace.name}
|
{workspace.name}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { IWorkspaceMemberInvitation } from "@plane/types";
|
||||||
import { Button, Checkbox, Spinner } from "@plane/ui";
|
import { Button, Checkbox, Spinner } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
// helpers
|
// helpers
|
||||||
|
import { WorkspaceLogo } from "@/components/workspace/logo";
|
||||||
import { truncateText } from "@/helpers/string.helper";
|
import { truncateText } from "@/helpers/string.helper";
|
||||||
import { getUserRole } from "@/helpers/user.helper";
|
import { getUserRole } from "@/helpers/user.helper";
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -94,21 +95,11 @@ export const Invitations: React.FC<Props> = (props) => {
|
||||||
onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")}
|
onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")}
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<div className="grid h-9 w-9 place-items-center rounded">
|
<WorkspaceLogo
|
||||||
{invitedWorkspace?.logo && invitedWorkspace.logo !== "" ? (
|
logo={invitedWorkspace?.logo_url}
|
||||||
<img
|
name={invitedWorkspace?.name}
|
||||||
src={invitedWorkspace.logo}
|
classNames="size-9 flex-shrink-0"
|
||||||
height="100%"
|
/>
|
||||||
width="100%"
|
|
||||||
className="rounded"
|
|
||||||
alt={invitedWorkspace.name}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="grid h-9 w-9 place-items-center rounded bg-gray-700 px-3 py-1.5 uppercase text-white">
|
|
||||||
{invitedWorkspace?.name[0]}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="text-sm font-medium">{truncateText(invitedWorkspace?.name, 30)}</div>
|
<div className="text-sm font-medium">{truncateText(invitedWorkspace?.name, 30)}</div>
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,16 @@
|
||||||
import { useState, FC } from "react";
|
import { useState, FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { ChevronDown, XCircle } from "lucide-react";
|
import { ChevronDown, LinkIcon, Trash2 } from "lucide-react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { ROLE , EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
import { ROLE, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
import { CustomSelect, TOAST_TYPE, setToast, TContextMenuItem, CustomMenu } from "@plane/ui";
|
||||||
|
import { cn, copyTextToClipboard } from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
import { ConfirmWorkspaceMemberRemove } from "@/components/workspace";
|
import { ConfirmWorkspaceMemberRemove } from "@/components/workspace";
|
||||||
// constants
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useMember, useUserPermissions } from "@/hooks/store";
|
import { useMember, useUserPermissions } from "@/hooks/store";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
invitationId: string;
|
invitationId: string;
|
||||||
|
|
@ -21,22 +20,31 @@ type Props = {
|
||||||
|
|
||||||
export const WorkspaceInvitationsListItem: FC<Props> = observer((props) => {
|
export const WorkspaceInvitationsListItem: FC<Props> = observer((props) => {
|
||||||
const { invitationId } = props;
|
const { invitationId } = props;
|
||||||
// states
|
|
||||||
const [removeMemberModal, setRemoveMemberModal] = useState(false);
|
|
||||||
// router
|
// router
|
||||||
const { workspaceSlug } = useParams();
|
const { workspaceSlug } = useParams();
|
||||||
|
// states
|
||||||
|
const [removeMemberModal, setRemoveMemberModal] = useState(false);
|
||||||
|
// plane hooks
|
||||||
|
const { t } = useTranslation();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { allowPermissions, workspaceInfoBySlug } = useUserPermissions();
|
const { allowPermissions, workspaceInfoBySlug } = useUserPermissions();
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
workspace: { updateMemberInvitation, deleteMemberInvitation, getWorkspaceInvitationDetails },
|
workspace: { updateMemberInvitation, deleteMemberInvitation, getWorkspaceInvitationDetails },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
const { isMobile } = usePlatformOS();
|
|
||||||
// derived values
|
// derived values
|
||||||
const invitationDetails = getWorkspaceInvitationDetails(invitationId);
|
const invitationDetails = getWorkspaceInvitationDetails(invitationId);
|
||||||
const currentWorkspaceMemberInfo = workspaceInfoBySlug(workspaceSlug.toString());
|
const currentWorkspaceMemberInfo = workspaceInfoBySlug(workspaceSlug.toString());
|
||||||
const currentWorkspaceRole = currentWorkspaceMemberInfo?.role;
|
const currentWorkspaceRole = currentWorkspaceMemberInfo?.role;
|
||||||
|
// is the current logged in user admin
|
||||||
|
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||||
|
// role change access-
|
||||||
|
// 1. user cannot change their own role
|
||||||
|
// 2. only admin or member can change role
|
||||||
|
// 3. user cannot change role of higher role
|
||||||
|
const hasRoleChangeAccess = allowPermissions(
|
||||||
|
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||||
|
EUserPermissionsLevel.WORKSPACE
|
||||||
|
);
|
||||||
|
|
||||||
const handleRemoveInvitation = async () => {
|
const handleRemoveInvitation = async () => {
|
||||||
if (!workspaceSlug || !invitationDetails) return;
|
if (!workspaceSlug || !invitationDetails) return;
|
||||||
|
|
@ -58,21 +66,41 @@ export const WorkspaceInvitationsListItem: FC<Props> = observer((props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!invitationDetails) return null;
|
if (!invitationDetails || !currentWorkspaceMemberInfo) return null;
|
||||||
|
|
||||||
// is the current logged in user admin
|
const handleCopyText = () => {
|
||||||
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
try {
|
||||||
|
const inviteLink = new URL(invitationDetails.invite_link, window.location.origin).href;
|
||||||
|
copyTextToClipboard(inviteLink).then(() => {
|
||||||
|
setToast({
|
||||||
|
type: TOAST_TYPE.SUCCESS,
|
||||||
|
title: t("common.link_copied"),
|
||||||
|
message: t("entity.link_copied_to_clipboard", { entity: t("common.invite") }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error generating invite link:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// role change access-
|
const MENU_ITEMS: TContextMenuItem[] = [
|
||||||
// 1. user cannot change their own role
|
{
|
||||||
// 2. only admin or member can change role
|
key: "copy-link",
|
||||||
// 3. user cannot change role of higher role
|
action: handleCopyText,
|
||||||
const hasRoleChangeAccess = allowPermissions(
|
title: t("common.actions.copy_link"),
|
||||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
icon: LinkIcon,
|
||||||
EUserPermissionsLevel.WORKSPACE
|
shouldRender: !!invitationDetails.invite_link,
|
||||||
);
|
},
|
||||||
|
{
|
||||||
if (!currentWorkspaceMemberInfo) return null;
|
key: "remove",
|
||||||
|
action: () => setRemoveMemberModal(true),
|
||||||
|
title: t("common.remove"),
|
||||||
|
icon: Trash2,
|
||||||
|
shouldRender: isAdmin,
|
||||||
|
className: "text-red-500",
|
||||||
|
iconClassName: "text-red-500",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -85,7 +113,7 @@ export const WorkspaceInvitationsListItem: FC<Props> = observer((props) => {
|
||||||
}}
|
}}
|
||||||
onSubmit={handleRemoveInvitation}
|
onSubmit={handleRemoveInvitation}
|
||||||
/>
|
/>
|
||||||
<div className="group flex items-center justify-between px-3 py-4 hover:bg-custom-background-90 w-full">
|
<div className="group flex items-center justify-between px-3 py-4 hover:bg-custom-background-90 w-full h-full">
|
||||||
<div className="flex items-center gap-x-4 gap-y-2">
|
<div className="flex items-center gap-x-4 gap-y-2">
|
||||||
<span className="relative flex h-10 w-10 items-center justify-center rounded bg-gray-700 p-4 capitalize text-white">
|
<span className="relative flex h-10 w-10 items-center justify-center rounded bg-gray-700 p-4 capitalize text-white">
|
||||||
{(invitationDetails.email ?? "?")[0]}
|
{(invitationDetails.email ?? "?")[0]}
|
||||||
|
|
@ -96,7 +124,7 @@ export const WorkspaceInvitationsListItem: FC<Props> = observer((props) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<div className="flex items-center justify-center rounded bg-yellow-500/20 px-2.5 py-1 text-center text-xs font-medium text-yellow-500">
|
<div className="flex items-center justify-center rounded bg-yellow-500/20 px-2.5 py-1 text-center text-xs font-medium text-yellow-500">
|
||||||
<p>{t("pending")}</p>
|
<p>{t("common.pending")}</p>
|
||||||
</div>
|
</div>
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
customButton={
|
customButton={
|
||||||
|
|
@ -144,17 +172,43 @@ export const WorkspaceInvitationsListItem: FC<Props> = observer((props) => {
|
||||||
})}
|
})}
|
||||||
</CustomSelect>
|
</CustomSelect>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<Tooltip tooltipContent="Remove member" disabled={!isAdmin} isMobile={isMobile}>
|
<CustomMenu ellipsis placement="bottom-end" closeOnSelect>
|
||||||
<button
|
{MENU_ITEMS.map((item) => {
|
||||||
type="button"
|
if (item.shouldRender === false) return null;
|
||||||
onClick={() => setRemoveMemberModal(true)}
|
return (
|
||||||
className={`pointer-events-none opacity-0 ${
|
<CustomMenu.MenuItem
|
||||||
isAdmin ? "group-hover:pointer-events-auto group-hover:opacity-100" : ""
|
key={item.key}
|
||||||
}`}
|
onClick={(e) => {
|
||||||
>
|
e.preventDefault();
|
||||||
<XCircle className="h-3.5 w-3.5 text-red-500" strokeWidth={2} />
|
e.stopPropagation();
|
||||||
</button>
|
item.action();
|
||||||
</Tooltip>
|
}}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2",
|
||||||
|
{
|
||||||
|
"text-custom-text-400": item.disabled,
|
||||||
|
},
|
||||||
|
item.className
|
||||||
|
)}
|
||||||
|
disabled={item.disabled}
|
||||||
|
>
|
||||||
|
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||||
|
<div>
|
||||||
|
<h5>{item.title}</h5>
|
||||||
|
{item.description && (
|
||||||
|
<p
|
||||||
|
className={cn("text-custom-text-300 whitespace-pre-line", {
|
||||||
|
"text-custom-text-400": item.disabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CustomMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -62,10 +62,11 @@ export const WorkspaceMembersList: FC<{ searchQuery: string; isAdmin: boolean }>
|
||||||
isOpen={showPendingInvites}
|
isOpen={showPendingInvites}
|
||||||
onToggle={() => setShowPendingInvites((prev) => !prev)}
|
onToggle={() => setShowPendingInvites((prev) => !prev)}
|
||||||
buttonClassName="w-full"
|
buttonClassName="w-full"
|
||||||
|
className="h-full"
|
||||||
title={
|
title={
|
||||||
<div className="flex w-full items-center justify-between pt-4">
|
<div className="flex w-full items-center justify-between pt-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<h4 className="text-xl font-medium pt-2 pb-2">{t("pending_invites")}</h4>
|
<h4 className="text-xl font-medium pt-2 pb-2">{t("workspace_settings.settings.members.pending_invites")}</h4>
|
||||||
{searchedInvitationsIds && (
|
{searchedInvitationsIds && (
|
||||||
<CountChip count={searchedInvitationsIds.length} className="h-5 m-auto ml-2" />
|
<CountChip count={searchedInvitationsIds.length} className="h-5 m-auto ml-2" />
|
||||||
)}
|
)}
|
||||||
|
|
@ -75,7 +76,7 @@ export const WorkspaceMembersList: FC<{ searchQuery: string; isAdmin: boolean }>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Disclosure.Panel>
|
<Disclosure.Panel>
|
||||||
<div className="ml-auto items-center gap-1.5 rounded-md bg-custom-background-100 py-1.5">
|
<div className="ml-auto items-center gap-1.5 rounded-md bg-custom-background-100 py-1.5">
|
||||||
{searchedInvitationsIds?.map((invitationId) => (
|
{searchedInvitationsIds?.map((invitationId) => (
|
||||||
<WorkspaceInvitationsListItem key={invitationId} invitationId={invitationId} />
|
<WorkspaceInvitationsListItem key={invitationId} invitationId={invitationId} />
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue