"use client";
import { keepPreviousData } from "@tanstack/react-query";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import posthog from "posthog-js";
import { useState, useMemo } from "react";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { APP_NAME } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MembershipRole } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import classNames from "@calcom/ui/classNames";
import { UserAvatar } from "@calcom/ui/components/avatar";
import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
import { showToast } from "@calcom/ui/components/toast";
import InviteLinkSettingsModal from "@calcom/web/modules/ee/teams/components/InviteLinkSettingsModal";
import { MemberInvitationModalWithoutMembers } from "~/ee/teams/components/MemberInvitationModal";
type TeamMember = RouterOutputs["viewer"]["teams"]["listMembers"]["members"][number];
const AddNewTeamMembers = ({ isOrg = false }: { isOrg?: boolean }) => {
const searchParams = useCompatSearchParams();
const session = useSession();
const teamId = searchParams?.get("id") ? Number(searchParams.get("id")) : -1;
const teamQuery = trpc.viewer.teams.get.useQuery(
{ teamId, isOrg },
{ enabled: session.status === "authenticated" }
);
if (session.status === "loading" || !teamQuery.data) return ;
return ;
};
export const AddNewTeamMembersForm = ({ teamId, isOrg }: { teamId: number; isOrg?: boolean }) => {
const searchParams = useCompatSearchParams();
const { t } = useLocale();
const router = useRouter();
const orgBranding = useOrgBranding();
const showDialog = searchParams?.get("inviteModal") === "true";
const [memberInviteModal, setMemberInviteModal] = useState(showDialog);
const [inviteLinkSettingsModal, setInviteLinkSettingsModal] = useState(false);
const { data: team, isPending } = trpc.viewer.teams.get.useQuery({ teamId, isOrg }, { enabled: !!teamId });
const { data: orgMembersNotInThisTeam } = trpc.viewer.organizations.getMembers.useQuery(
{
teamIdToExclude: teamId,
distinctUser: true,
},
{
enabled: orgBranding !== null,
}
);
const { data, fetchNextPage, isFetchingNextPage, hasNextPage } =
trpc.viewer.teams.listMembers.useInfiniteQuery(
{
limit: 10,
teamId,
},
{
enabled: !!teamId,
getNextPageParam: (lastPage) => lastPage.nextCursor,
placeholderData: keepPreviousData,
refetchOnWindowFocus: true,
refetchOnMount: true,
staleTime: 0,
}
);
const flatData = useMemo(() => data?.pages?.flatMap((page) => page.members) ?? [], [data]) as TeamMember[];
const totalFetched = flatData.length;
const publishTeamMutation = trpc.viewer.teams.publish.useMutation({
onSuccess(data) {
router.push(data.url);
},
onError: (error) => {
showToast(error.message, "error");
},
});
return (
<>
{flatData.map((member, index) => (
))}
{totalFetched > 0 && (
fetchNextPage()}>
{hasNextPage ? t("load_more_results") : t("no_more_results")}
)}
setMemberInviteModal(true)}
className={classNames("w-full justify-center", totalFetched > 0 && "mt-6")}>
{isOrg ? t("add_org_members") : t("add_team_member")}
{isPending ? (
) : (
<>
setMemberInviteModal(false)}
onSettingsOpen={() => {
setMemberInviteModal(false);
setInviteLinkSettingsModal(true);
}}
/>
{team?.inviteToken && (
{
setInviteLinkSettingsModal(false);
setMemberInviteModal(true);
}}
/>
)}
>
)}
{
posthog.capture("onboard_members_continue_clicked", {
team_id: teamId,
is_org: isOrg,
members_count: totalFetched,
});
let uri = `/settings/teams/${teamId}/event-type`;
if (isOrg) {
uri = `/settings/organizations/${teamId}/add-teams`;
}
router.push(uri);
}}>
{t("continue")}
>
);
};
export default AddNewTeamMembers;
const AddNewTeamMemberSkeleton = () => {
return (
);
};
const PendingMemberItem = (props: { member: TeamMember; index: number; teamId: number; isOrg?: boolean }) => {
const { member, index, teamId } = props;
const { t } = useLocale();
const utils = trpc.useUtils();
const session = useSession();
const isAdminOrOwner = checkAdminOrOwner(session.data?.user?.org?.role);
const bookerUrl = member.bookerUrl;
const removeMemberMutation = trpc.viewer.teams.removeMember.useMutation({
async onSuccess() {
await utils.viewer.teams.get.invalidate();
await utils.viewer.teams.listMembers.invalidate();
await utils.viewer.eventTypes.invalidate();
showToast(t("member_removed"), "success");
},
async onError(err) {
showToast(err.message, "error");
},
});
return (
{member.name || member.email || t("team_member")}
{/* Assume that the first member of the team is the creator */}
{member.id === session.data?.user.id &&
{t("you")} }
{!member.accepted &&
{t("pending")} }
{member.role === MembershipRole.MEMBER &&
{t("member")} }
{member.role === MembershipRole.ADMIN &&
{t("admin")} }
{member.role === MembershipRole.OWNER &&
{t("owner")} }
{member.username ? (
{`${bookerUrl}/${member.username}`}
) : (
{t("not_on_cal", { appName: APP_NAME })}
)}
{(member.role !== "OWNER" || isAdminOrOwner) && member.id !== session.data?.user.id && (
{
removeMemberMutation.mutate({
teamIds: [teamId],
memberIds: [member.id],
isOrg: !!props.isOrg,
});
}}
/>
)}
);
};