NODEDC_TASKMANAGER/plane-src/apps/web/app/(all)/invitations/page.tsx

240 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import useSWR, { mutate } from "swr";
import { ArrowRight, Bell, CheckCircle2, MailCheck, Sparkles } from "lucide-react";
// plane imports
import { ROLE_DETAILS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IWorkspaceMemberInvitation } from "@plane/types";
import { truncateText } from "@plane/utils";
import { NodeDCStandaloneShell } from "@/components/nodedc/standalone-shell";
import { WorkspaceLogo } from "@/components/workspace/logo";
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
// hooks
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useUser, useUserProfile } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
// services
import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper";
// plane web services
import { WorkspaceService } from "@/services/workspace.service";
const workspaceService = new WorkspaceService();
function UserInvitationsPage() {
// states
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false);
// router
const router = useAppRouter();
// store hooks
const { t } = useTranslation();
const { data: currentUser } = useUser();
const { updateUserProfile } = useUserProfile();
const { fetchWorkspaces, workspaces } = useWorkspace();
const { unreadNotificationsCount, getUnreadNotificationsCount } = useWorkspaceNotifications();
const { data: invitations } = useSWR("USER_WORKSPACE_INVITATIONS", () => workspaceService.userWorkspaceInvitations());
useSWR(USER_WORKSPACES_LIST, () => fetchWorkspaces());
const fallbackWorkspaceSlug = Object.values(workspaces ?? {})?.[0]?.slug;
useSWR(
fallbackWorkspaceSlug ? ["STANDALONE_UNREAD_NOTIFICATION_COUNT", fallbackWorkspaceSlug] : null,
fallbackWorkspaceSlug ? () => getUnreadNotificationsCount(fallbackWorkspaceSlug) : null
);
const notificationsCount =
unreadNotificationsCount.mention_unread_notifications_count > 0
? unreadNotificationsCount.mention_unread_notifications_count
: unreadNotificationsCount.total_unread_notifications_count || invitations?.length || 0;
const redirectWorkspaceSlug =
// currentUserSettings?.workspace?.last_workspace_slug ||
// currentUserSettings?.workspace?.fallback_workspace_slug ||
"";
const hasInvitations = !!invitations && invitations.length > 0;
const handleInvitation = (workspace_invitation: IWorkspaceMemberInvitation, action: "accepted" | "withdraw") => {
if (action === "accepted") {
setInvitationsRespond((prevData) => [...prevData, workspace_invitation.id]);
} else if (action === "withdraw") {
setInvitationsRespond((prevData) => prevData.filter((item: string) => item !== workspace_invitation.id));
}
};
const submitInvitations = async () => {
if (invitationsRespond.length === 0) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("error"),
message: t("please_select_at_least_one_invitation"),
});
return;
}
setIsJoiningWorkspaces(true);
try {
await workspaceService.joinWorkspaces({ invitations: invitationsRespond });
void mutate(USER_WORKSPACES_LIST);
const firstInviteId = invitationsRespond[0];
const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace;
await updateUserProfile({ last_workspace_id: redirectWorkspace?.id });
await fetchWorkspaces();
router.push(redirectWorkspace?.slug ? `/${redirectWorkspace.slug}` : "/");
} catch {
setToast({
type: TOAST_TYPE.ERROR,
title: t("error"),
message: t("something_went_wrong_please_try_again"),
});
} finally {
setIsJoiningWorkspaces(false);
}
};
const openNotifications = () => {
if (fallbackWorkspaceSlug) {
router.push(`/${fallbackWorkspaceSlug}?workspaceNotifications=open`);
return;
}
router.push("/invitations");
};
return (
<AuthenticationWrapper>
<NodeDCStandaloneShell
notificationsCount={notificationsCount}
onOpenNotifications={openNotifications}
showUserControls={!!currentUser}
>
{invitations ? (
hasInvitations ? (
<div className="flex flex-1 items-center justify-center py-10">
<div className="w-full max-w-4xl space-y-7">
<div className="nodedc-glass-surface rounded-[2rem] border-0 px-6 py-6 sm:px-8">
<div className="flex flex-wrap items-start justify-between gap-5">
<div className="min-w-0 space-y-3">
<div className="inline-flex items-center gap-2 rounded-full bg-white/6 px-3 py-1.5 text-11 font-semibold tracking-[0.16em] text-[rgb(var(--nodedc-accent-rgb))] uppercase">
<Bell className="size-3.5" />
Новые приглашения
</div>
<div>
<h1 className="text-28 font-semibold tracking-[-0.03em] text-primary">Принять доступ</h1>
<p className="mt-2 max-w-2xl text-13 leading-6 text-secondary">
Выберите рабочие пространства, к которым хотите присоединиться. После принятия Tasker
откроет первый выбранный workspace.
</p>
</div>
</div>
<div className="flex size-14 items-center justify-center rounded-[1.15rem] bg-[rgb(var(--nodedc-card-active-rgb))] text-[rgb(var(--nodedc-on-card-active-rgb))]">
<MailCheck className="size-7" />
</div>
</div>
</div>
<div className="max-h-[48vh] space-y-3 overflow-y-auto pr-1 md:max-h-[54vh]">
{invitations.map((invitation) => {
const isSelected = invitationsRespond.includes(invitation.id);
return (
<button
type="button"
key={invitation.id}
className={`group flex w-full cursor-pointer items-center gap-4 rounded-[1.6rem] px-4 py-4 text-left transition ${
isSelected
? "bg-[rgb(var(--nodedc-card-active-rgb))] text-[rgb(var(--nodedc-on-card-active-rgb))]"
: "nodedc-settings-card hover:bg-white/[0.055]"
}`}
onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")}
>
<div className="flex-shrink-0 rounded-full bg-black/10 p-1">
<WorkspaceLogo
logo={invitation.workspace.logo_url}
name={invitation.workspace.name}
classNames="size-11 flex-shrink-0"
/>
</div>
<div className="min-w-0 flex-1">
<div className="text-15 font-semibold">{truncateText(invitation.workspace.name, 42)}</div>
<p className={`mt-1 text-12 ${isSelected ? "opacity-70" : "text-secondary"}`}>
{t(ROLE_DETAILS[invitation.role as keyof typeof ROLE_DETAILS]?.i18n_title || "")}
</p>
</div>
<span className={`flex-shrink-0 ${isSelected ? "opacity-100" : "text-tertiary"}`}>
<CheckCircle2 className="h-5 w-5" />
</span>
</button>
);
})}
</div>
<div className="flex flex-wrap items-center gap-3">
<Button
variant="primary"
type="submit"
size="lg"
onClick={submitInvitations}
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
loading={isJoiningWorkspaces}
className="nodedc-empty-state-primary min-w-[12rem]"
>
Принять выбранные
</Button>
<Link href={`/${redirectWorkspaceSlug}`}>
<span>
<Button variant="secondary" size="lg" className="nodedc-empty-state-secondary">
Вернуться на главную
</Button>
</span>
</Link>
</div>
</div>
</div>
) : (
<div className="flex flex-1 items-center justify-center py-10">
<div className="nodedc-glass-surface relative w-full max-w-[34rem] overflow-hidden rounded-[2.2rem] px-8 py-9 text-center">
<div className="pointer-events-none absolute inset-x-8 top-0 h-px bg-gradient-to-r from-transparent via-[rgb(var(--nodedc-accent-rgb))]/55 to-transparent" />
<div className="mx-auto flex size-24 items-center justify-center rounded-[2rem] bg-white/[0.035] text-[rgb(var(--nodedc-accent-rgb))]">
<Sparkles className="size-11" />
</div>
<div className="mt-6 space-y-2">
<h1 className="text-24 font-semibold tracking-[-0.03em]">Нет ожидающих приглашений</h1>
<p className="mx-auto max-w-sm text-13 leading-6 text-secondary">
Когда вас пригласят в workspace, здесь появится карточка доступа с возможностью принять приглашение.
</p>
</div>
<Button
variant="primary"
size="lg"
onClick={() => router.push("/")}
className="nodedc-empty-state-primary mt-7"
appendIcon={<ArrowRight className="size-4" />}
>
Вернуться на главную
</Button>
</div>
</div>
)
) : null}
</NodeDCStandaloneShell>
</AuthenticationWrapper>
);
}
export default observer(UserInvitationsPage);