ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: self-service workspace invite approval
This commit is contained in:
@@ -238,9 +238,9 @@ export const IssueView = observer(function IssueView(props: IIssueView) {
|
||||
shouldRenderPeekSurface
|
||||
? "flex flex-col overflow-hidden border border-subtle/70 bg-surface-1/80 backdrop-blur-2xl transition-all duration-300"
|
||||
: "h-full w-full",
|
||||
!embedIssue && "absolute z-[25]",
|
||||
!embedIssue && "absolute z-[80]",
|
||||
!embedIssue && {
|
||||
"top-3 right-3 bottom-3 w-[calc(100%-1.5rem)] rounded-[28px] border md:min-w-[640px] md:max-w-[calc(100vw-1.5rem)]":
|
||||
"top-[5.35rem] right-3 bottom-[5.85rem] w-[calc(100%-1.5rem)] rounded-[28px] border md:min-w-[640px] md:max-w-[calc(100vw-1.5rem)]":
|
||||
peekMode === "side-peek",
|
||||
"top-[8.33%] left-[8.33%] size-5/6 rounded-[28px]": peekMode === "modal",
|
||||
"absolute inset-0 m-4 rounded-[28px]": peekMode === "full-screen",
|
||||
@@ -273,7 +273,7 @@ export const IssueView = observer(function IssueView(props: IIssueView) {
|
||||
>
|
||||
{shouldAllowPeekResize && peekMode === "side-peek" && (
|
||||
<div
|
||||
className="absolute top-0 left-0 z-[26] h-full w-4 -translate-x-1/2 cursor-ew-resize rounded-l-[28px] bg-transparent"
|
||||
className="absolute top-0 left-0 z-[81] h-full w-4 -translate-x-1/2 cursor-ew-resize rounded-l-[28px] bg-transparent"
|
||||
onMouseDown={startPeekResizing}
|
||||
role="separator"
|
||||
aria-label="Resize issue panel"
|
||||
|
||||
@@ -61,26 +61,24 @@ export const ConfirmWorkspaceMemberRemove = observer(function ConfirmWorkspaceMe
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-[40rem]">
|
||||
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-danger-subtle sm:mx-0 sm:h-10 sm:w-10">
|
||||
<AlertTriangle className="h-6 w-6 text-danger-primary" aria-hidden="true" />
|
||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-[rgba(var(--nodedc-accent-rgb),0.16)] sm:mx-0 sm:h-10 sm:w-10">
|
||||
<AlertTriangle className="h-6 w-6 text-[rgb(var(--nodedc-accent-rgb))]" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<Dialog.Title as="h3" className="text-16 leading-6 font-medium text-primary">
|
||||
{currentUser?.id === userDetails.id
|
||||
? "Leave workspace?"
|
||||
: `Remove ${userDetails?.display_name}?`}
|
||||
? "Покинуть рабочее пространство?"
|
||||
: `Удалить ${userDetails?.display_name}?`}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
{currentUser?.id === userDetails.id ? (
|
||||
<p className="text-13 text-secondary">
|
||||
Are you sure you want to leave the workspace? You will no longer have access to this
|
||||
workspace. This action cannot be undone.
|
||||
Вы потеряете доступ к этому рабочему пространству. Действие нельзя отменить.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-13 text-secondary">
|
||||
Are you sure you want to remove member-{" "}
|
||||
<span className="font-bold">{userDetails?.display_name}</span>? They will no longer have
|
||||
access to this workspace. This action cannot be undone.
|
||||
Вы уверены, что хотите удалить <span className="font-bold">{userDetails?.display_name}</span>?
|
||||
Доступ к этому рабочему пространству будет закрыт. Действие нельзя отменить.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -89,16 +87,16 @@ export const ConfirmWorkspaceMemberRemove = observer(function ConfirmWorkspaceMe
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 p-4 sm:px-6">
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
Cancel
|
||||
Отменить
|
||||
</Button>
|
||||
<Button variant="error-fill" tabIndex={1} onClick={handleDeletion} loading={isRemoving}>
|
||||
<Button variant="primary" tabIndex={1} onClick={handleDeletion} loading={isRemoving}>
|
||||
{currentUser?.id === userDetails.id
|
||||
? isRemoving
|
||||
? "Leaving"
|
||||
: "Leave"
|
||||
? "Выход..."
|
||||
: "Покинуть"
|
||||
: isRemoving
|
||||
? "Removing"
|
||||
: "Remove"}
|
||||
? "Удаление..."
|
||||
: "Удалить"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
|
||||
@@ -7,9 +7,8 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
// ui
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/propel/button";
|
||||
// ui
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
@@ -30,7 +29,6 @@ export const ConfirmWorkspaceMemberRemove = observer(function ConfirmWorkspaceMe
|
||||
const [isRemoving, setIsRemoving] = useState(false);
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
@@ -49,23 +47,24 @@ export const ConfirmWorkspaceMemberRemove = observer(function ConfirmWorkspaceMe
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
|
||||
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mx-auto flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-danger-subtle sm:mx-0 sm:h-10 sm:w-10">
|
||||
<AlertTriangle className="h-6 w-6 text-danger-primary" aria-hidden="true" />
|
||||
<div className="mx-auto flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-[rgba(var(--nodedc-accent-rgb),0.16)] sm:mx-0 sm:h-10 sm:w-10">
|
||||
<AlertTriangle className="h-6 w-6 text-[rgb(var(--nodedc-accent-rgb))]" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 className="text-h5-medium leading-6 text-primary">
|
||||
{currentUser?.id === userDetails.id ? "Leave workspace?" : `Remove ${userDetails?.display_name}?`}
|
||||
{currentUser?.id === userDetails.id
|
||||
? "Покинуть рабочее пространство?"
|
||||
: `Удалить ${userDetails?.display_name}?`}
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
{currentUser?.id === userDetails.id ? (
|
||||
<p className="text-body-xs-regular text-secondary">
|
||||
{t("workspace_settings.settings.members.leave_confirmation")}
|
||||
Вы потеряете доступ к этому рабочему пространству. Действие нельзя отменить.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-body-xs-regular text-secondary">
|
||||
{/* TODO: Add translation here */}
|
||||
Are you sure you want to remove member- <span className="font-bold">{userDetails?.display_name}</span>
|
||||
? They will no longer have access to this workspace. This action cannot be undone.
|
||||
Вы уверены, что хотите удалить <span className="font-bold">{userDetails?.display_name}</span>? Доступ
|
||||
к этому рабочему пространству будет закрыт. Действие нельзя отменить.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -74,16 +73,16 @@ export const ConfirmWorkspaceMemberRemove = observer(function ConfirmWorkspaceMe
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 p-4 sm:px-6">
|
||||
<Button variant="secondary" size="lg" onClick={handleClose}>
|
||||
{t("cancel")}
|
||||
Отменить
|
||||
</Button>
|
||||
<Button variant="error-fill" size="lg" tabIndex={1} onClick={handleDeletion} loading={isRemoving}>
|
||||
<Button variant="primary" size="lg" tabIndex={1} onClick={handleDeletion} loading={isRemoving}>
|
||||
{currentUser?.id === userDetails.id
|
||||
? isRemoving
|
||||
? t("leaving")
|
||||
: t("leave")
|
||||
? "Выход..."
|
||||
: "Покинуть"
|
||||
: isRemoving
|
||||
? t("removing")
|
||||
: t("remove")}
|
||||
? "Удаление..."
|
||||
: "Удалить"}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalCore>
|
||||
|
||||
@@ -10,11 +10,9 @@ import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { ROLE, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { LinkIcon, TrashIcon, ChevronDownIcon } from "@plane/propel/icons";
|
||||
import { ChevronDownIcon, CopyLinkIcon, TrashIcon } from "@plane/propel/icons";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { TContextMenuItem } from "@plane/ui";
|
||||
import { ActionDropdown } from "@plane/ui";
|
||||
import { copyTextToClipboard } from "@plane/utils";
|
||||
import { cn, copyTextToClipboard } from "@plane/utils";
|
||||
import { SelectionDropdown } from "@/components/common/selection-dropdown";
|
||||
// components
|
||||
import { ConfirmWorkspaceMemberRemove } from "@/components/workspace/confirm-workspace-member-remove";
|
||||
@@ -26,6 +24,8 @@ type Props = {
|
||||
invitationId: string;
|
||||
};
|
||||
|
||||
type NodeDCInvitationApprovalStatus = "not_required" | "pending" | "approved" | "rejected";
|
||||
|
||||
export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitationsListItem(props: Props) {
|
||||
const { invitationId } = props;
|
||||
// router
|
||||
@@ -43,6 +43,10 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
|
||||
const invitationDetails = getWorkspaceInvitationDetails(invitationId);
|
||||
const currentWorkspaceMemberInfo = workspaceInfoBySlug(workspaceSlug.toString());
|
||||
const currentWorkspaceRole = currentWorkspaceMemberInfo?.role;
|
||||
const nodedcApprovalStatus =
|
||||
(invitationDetails as { nodedc_approval_status?: NodeDCInvitationApprovalStatus } | undefined)
|
||||
?.nodedc_approval_status ?? "not_required";
|
||||
const isNodeDCApprovalLocked = nodedcApprovalStatus === "pending" || nodedcApprovalStatus === "rejected";
|
||||
// is the current logged in user admin
|
||||
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
// role change access-
|
||||
@@ -61,15 +65,15 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
|
||||
await deleteMemberInvitation(workspaceSlug.toString(), invitationDetails.id);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Invitation removed successfully.",
|
||||
title: "Инвайт удалён",
|
||||
message: "Пользователь больше не увидит это приглашение.",
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const error = err as { error?: string };
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: error?.error || "Something went wrong. Please try again.",
|
||||
title: "Ошибка",
|
||||
message: error?.error || "Не удалось удалить инвайт. Попробуйте ещё раз.",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -78,39 +82,19 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
|
||||
|
||||
const handleCopyText = async () => {
|
||||
try {
|
||||
if (!invitationDetails.invite_link) return;
|
||||
const inviteLink = new URL(invitationDetails.invite_link, window.location.origin).href;
|
||||
await copyTextToClipboard(inviteLink);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("common.link_copied"),
|
||||
message: t("entity.link_copied_to_clipboard", { entity: t("common.invite") }),
|
||||
title: "Инвайт скопирован",
|
||||
message: "Передайте ссылку приглашённому пользователю.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error generating invite link:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const MENU_ITEMS: TContextMenuItem[] = [
|
||||
{
|
||||
key: "copy-link",
|
||||
action: () => void handleCopyText(),
|
||||
title: t("common.actions.copy_link"),
|
||||
icon: LinkIcon,
|
||||
shouldRender: !!invitationDetails.invite_link,
|
||||
},
|
||||
{
|
||||
key: "remove",
|
||||
action: () => {
|
||||
setRemoveMemberModal(true);
|
||||
},
|
||||
title: t("common.remove"),
|
||||
icon: TrashIcon,
|
||||
shouldRender: isAdmin,
|
||||
className: "text-danger-primary",
|
||||
iconClassName: "text-danger-primary",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmWorkspaceMemberRemove
|
||||
@@ -132,8 +116,8 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-11">
|
||||
<div className="flex items-center justify-center rounded-full bg-label-yellow-bg-strong/20 px-2.5 py-1 text-center text-caption-sm-medium text-label-yellow-text">
|
||||
<p>{t("common.pending")}</p>
|
||||
<div className={cn("nodedc-settings-chip flex min-h-10 min-w-[9.25rem] items-center justify-center px-4 py-2 text-center text-caption-sm-medium", nodedcInviteStatusClassName(nodedcApprovalStatus))}>
|
||||
<p>{nodedcInviteStatusLabel(nodedcApprovalStatus, t("common.pending"))}</p>
|
||||
</div>
|
||||
<SelectionDropdown
|
||||
options={Object.keys(ROLE)
|
||||
@@ -167,7 +151,7 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
|
||||
},
|
||||
}))}
|
||||
menuButton={
|
||||
<div className="nodedc-settings-chip item-center flex gap-1 px-3 py-1">
|
||||
<div className="nodedc-settings-chip flex min-h-10 min-w-[6.5rem] items-center justify-center gap-1 px-4 py-2">
|
||||
<span
|
||||
className={`flex items-center rounded-sm text-caption-sm-medium ${
|
||||
hasRoleChangeAccess ? "" : "text-placeholder"
|
||||
@@ -182,14 +166,48 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
disabled={!hasRoleChangeAccess}
|
||||
disabled={!hasRoleChangeAccess || isNodeDCApprovalLocked}
|
||||
placement="bottom-end"
|
||||
/>
|
||||
{isAdmin && (
|
||||
<ActionDropdown placement="bottom-end" items={MENU_ITEMS} />
|
||||
<div className="flex items-center gap-1.5">
|
||||
{invitationDetails.invite_link && (
|
||||
<button
|
||||
aria-label={`Копировать инвайт ${invitationDetails.email}`}
|
||||
className="grid h-10 w-10 place-items-center rounded-full border-0 bg-white/6 text-secondary transition-colors hover:bg-white/10 hover:text-primary"
|
||||
title="Копировать инвайт"
|
||||
type="button"
|
||||
onClick={() => void handleCopyText()}
|
||||
>
|
||||
<CopyLinkIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
aria-label={`Удалить инвайт ${invitationDetails.email}`}
|
||||
className="grid h-10 w-10 place-items-center rounded-full border-0 bg-white/6 text-secondary transition-colors hover:bg-red-500/15 hover:text-red-300"
|
||||
title="Удалить"
|
||||
type="button"
|
||||
onClick={() => setRemoveMemberModal(true)}
|
||||
>
|
||||
<TrashIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
function nodedcInviteStatusLabel(status: string, fallback: string) {
|
||||
if (status === "pending") return "Ожидает";
|
||||
if (status === "approved") return "Подтверждено NDC";
|
||||
if (status === "rejected") return "Отклонено";
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function nodedcInviteStatusClassName(status: string) {
|
||||
if (status === "approved") return "bg-lime-400/15 text-lime-300";
|
||||
if (status === "rejected") return "bg-red-500/15 text-red-300";
|
||||
return "bg-label-yellow-bg-strong/20 text-label-yellow-text";
|
||||
}
|
||||
|
||||
@@ -53,7 +53,8 @@ export const WorkspaceMembersList = observer(function WorkspaceMembersList(props
|
||||
await fetchWorkspaceMemberInvitations(workspaceSlug.toString());
|
||||
await fetchWorkspaceMembers(workspaceSlug.toString());
|
||||
}
|
||||
: null
|
||||
: null,
|
||||
{ refreshInterval: 5000 }
|
||||
);
|
||||
|
||||
if (!workspaceMemberIds && !workspaceMemberInvitationIds) return <MembersSettingsLoader />;
|
||||
|
||||
@@ -59,8 +59,11 @@ export function WorkspaceMembersSettingsContent({ workspaceSlug }: TWorkspaceMem
|
||||
setInviteModal(false);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: t("workspace_settings.settings.members.invitations_sent_successfully"),
|
||||
title: nodedcWorkspacePolicy?.invite_approval === "nodedc" ? "Запрос отправлен" : "Success!",
|
||||
message:
|
||||
nodedcWorkspacePolicy?.invite_approval === "nodedc"
|
||||
? "Приглашение отправлено на подтверждение NODE.DC."
|
||||
: t("workspace_settings.settings.members.invitations_sent_successfully"),
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? (error as Error & { error?: string }).error : undefined;
|
||||
|
||||
Reference in New Issue
Block a user