2022-11-10 20:23:56 +00:00
|
|
|
import { useSession } from "next-auth/react";
|
|
|
|
import { useRouter } from "next/router";
|
|
|
|
import { useState } from "react";
|
|
|
|
import { z } from "zod";
|
|
|
|
|
|
|
|
import MemberInvitationModal from "@calcom/features/ee/teams/components/MemberInvitationModal";
|
|
|
|
import { classNames } from "@calcom/lib";
|
2023-01-13 18:58:41 +00:00
|
|
|
import { WEBAPP_URL, APP_NAME } from "@calcom/lib/constants";
|
2022-11-10 20:23:56 +00:00
|
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
2022-11-10 23:40:01 +00:00
|
|
|
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
2023-01-23 23:08:01 +00:00
|
|
|
import { Avatar, Badge, Button, showToast, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
|
|
|
import { FiPlus, FiArrowRight, FiTrash2 } from "@calcom/ui/components/icon";
|
2022-11-10 20:23:56 +00:00
|
|
|
|
|
|
|
const querySchema = z.object({
|
|
|
|
id: z.string().transform((val) => parseInt(val)),
|
|
|
|
});
|
|
|
|
|
2022-11-10 23:40:01 +00:00
|
|
|
type TeamMember = RouterOutputs["viewer"]["teams"]["get"]["members"][number];
|
2022-11-10 20:23:56 +00:00
|
|
|
|
|
|
|
type FormValues = {
|
|
|
|
members: TeamMember[];
|
|
|
|
};
|
|
|
|
|
|
|
|
const AddNewTeamMembers = () => {
|
|
|
|
const session = useSession();
|
|
|
|
const router = useRouter();
|
|
|
|
const { id: teamId } = router.isReady ? querySchema.parse(router.query) : { id: -1 };
|
2022-11-10 23:40:01 +00:00
|
|
|
const teamQuery = trpc.viewer.teams.get.useQuery({ teamId }, { enabled: router.isReady });
|
2022-11-10 20:23:56 +00:00
|
|
|
if (session.status === "loading" || !teamQuery.data) return <AddNewTeamMemberSkeleton />;
|
|
|
|
|
|
|
|
return <AddNewTeamMembersForm defaultValues={{ members: teamQuery.data.members }} teamId={teamId} />;
|
|
|
|
};
|
|
|
|
|
2023-01-21 15:32:00 +00:00
|
|
|
export const AddNewTeamMembersForm = ({
|
|
|
|
defaultValues,
|
|
|
|
teamId,
|
|
|
|
}: {
|
|
|
|
defaultValues: FormValues;
|
|
|
|
teamId: number;
|
|
|
|
}) => {
|
2022-11-10 20:23:56 +00:00
|
|
|
const { t, i18n } = useLocale();
|
|
|
|
const [memberInviteModal, setMemberInviteModal] = useState(false);
|
|
|
|
const utils = trpc.useContext();
|
|
|
|
const router = useRouter();
|
2022-11-10 23:40:01 +00:00
|
|
|
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation({
|
2022-11-10 20:23:56 +00:00
|
|
|
async onSuccess() {
|
2022-11-10 23:40:01 +00:00
|
|
|
await utils.viewer.teams.get.invalidate();
|
2022-11-10 20:23:56 +00:00
|
|
|
setMemberInviteModal(false);
|
|
|
|
},
|
|
|
|
onError: (error) => {
|
|
|
|
showToast(error.message, "error");
|
|
|
|
},
|
|
|
|
});
|
2022-11-10 23:40:01 +00:00
|
|
|
const publishTeamMutation = trpc.viewer.teams.publish.useMutation({
|
2022-11-10 20:23:56 +00:00
|
|
|
onSuccess(data) {
|
|
|
|
router.push(data.url);
|
|
|
|
},
|
|
|
|
onError: (error) => {
|
|
|
|
showToast(error.message, "error");
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<div>
|
|
|
|
<ul className="rounded-md border" data-testid="pending-member-list">
|
|
|
|
{defaultValues.members.map((member, index) => (
|
|
|
|
<PendingMemberItem key={member.email} member={member} index={index} teamId={teamId} />
|
|
|
|
))}
|
|
|
|
</ul>
|
|
|
|
<Button
|
|
|
|
color="secondary"
|
|
|
|
data-testid="new-member-button"
|
2023-01-23 23:08:01 +00:00
|
|
|
StartIcon={FiPlus}
|
2022-11-10 20:23:56 +00:00
|
|
|
onClick={() => setMemberInviteModal(true)}
|
|
|
|
className="mt-6 w-full justify-center">
|
|
|
|
{t("add_team_member")}
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
<MemberInvitationModal
|
|
|
|
isOpen={memberInviteModal}
|
|
|
|
onExit={() => setMemberInviteModal(false)}
|
|
|
|
onSubmit={(values) => {
|
|
|
|
inviteMemberMutation.mutate({
|
|
|
|
teamId,
|
|
|
|
language: i18n.language,
|
2022-11-25 12:59:25 +00:00
|
|
|
role: values.role,
|
2022-11-10 20:23:56 +00:00
|
|
|
usernameOrEmail: values.emailOrUsername,
|
|
|
|
sendEmailInvitation: values.sendInviteEmail,
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
members={defaultValues.members}
|
|
|
|
/>
|
|
|
|
<hr className="my-6 border-neutral-200" />
|
|
|
|
<Button
|
2023-01-23 23:08:01 +00:00
|
|
|
EndIcon={FiArrowRight}
|
2022-11-10 20:23:56 +00:00
|
|
|
className="mt-6 w-full justify-center"
|
|
|
|
disabled={publishTeamMutation.isLoading}
|
|
|
|
onClick={() => {
|
|
|
|
publishTeamMutation.mutate({ teamId });
|
|
|
|
}}>
|
|
|
|
{t("team_publish")}
|
|
|
|
</Button>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default AddNewTeamMembers;
|
|
|
|
|
|
|
|
const AddNewTeamMemberSkeleton = () => {
|
|
|
|
return (
|
|
|
|
<SkeletonContainer className="rounded-md border">
|
|
|
|
<div className="flex w-full justify-between p-4">
|
|
|
|
<div>
|
|
|
|
<p className="text-sm font-medium text-gray-900">
|
|
|
|
<SkeletonText className="h-4 w-56" />
|
|
|
|
</p>
|
|
|
|
<div className="mt-2.5 w-max">
|
|
|
|
<SkeletonText className="h-5 w-28" />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</SkeletonContainer>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const PendingMemberItem = (props: { member: TeamMember; index: number; teamId: number }) => {
|
|
|
|
const { member, index, teamId } = props;
|
|
|
|
const { t } = useLocale();
|
|
|
|
const utils = trpc.useContext();
|
|
|
|
|
2022-11-10 23:40:01 +00:00
|
|
|
const removeMemberMutation = trpc.viewer.teams.removeMember.useMutation({
|
2022-11-10 20:23:56 +00:00
|
|
|
async onSuccess() {
|
2022-11-10 23:40:01 +00:00
|
|
|
await utils.viewer.teams.get.invalidate();
|
2022-11-10 20:23:56 +00:00
|
|
|
showToast("Member removed", "success");
|
|
|
|
},
|
|
|
|
async onError(err) {
|
|
|
|
showToast(err.message, "error");
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
return (
|
|
|
|
<li
|
|
|
|
key={member.email}
|
|
|
|
className={classNames("flex items-center justify-between p-6 text-sm", index !== 0 && "border-t")}
|
|
|
|
data-testid="pending-member-item">
|
2023-01-04 07:38:45 +00:00
|
|
|
<div className="flex space-x-2 rtl:space-x-reverse">
|
2022-11-10 20:23:56 +00:00
|
|
|
<Avatar
|
|
|
|
gravatarFallbackMd5="teamMember"
|
|
|
|
size="mdLg"
|
|
|
|
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
|
|
|
|
alt="owner-avatar"
|
|
|
|
/>
|
|
|
|
<div>
|
|
|
|
<div className="flex space-x-1">
|
|
|
|
<p>{member.name || member.email || t("team_member")}</p>
|
|
|
|
{/* Assume that the first member of the team is the creator */}
|
|
|
|
{index === 0 && <Badge variant="green">{t("you")}</Badge>}
|
|
|
|
{!member.accepted && <Badge variant="orange">{t("pending")}</Badge>}
|
|
|
|
{member.role === "MEMBER" && <Badge variant="gray">{t("member")}</Badge>}
|
|
|
|
{member.role === "ADMIN" && <Badge variant="default">{t("admin")}</Badge>}
|
|
|
|
</div>
|
|
|
|
{member.username ? (
|
|
|
|
<p className="text-gray-600">{`${WEBAPP_URL}/${member.username}`}</p>
|
|
|
|
) : (
|
2023-01-13 18:58:41 +00:00
|
|
|
<p className="text-gray-600">{t("not_on_cal", { appName: APP_NAME })}</p>
|
2022-11-10 20:23:56 +00:00
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{member.role !== "OWNER" && (
|
|
|
|
<Button
|
|
|
|
data-testid="remove-member-button"
|
2023-01-23 23:08:01 +00:00
|
|
|
StartIcon={FiTrash2}
|
2023-01-19 14:55:32 +00:00
|
|
|
variant="icon"
|
2022-11-10 20:23:56 +00:00
|
|
|
color="secondary"
|
|
|
|
className="h-[36px] w-[36px]"
|
|
|
|
onClick={() => {
|
|
|
|
removeMemberMutation.mutate({ teamId, memberId: member.id });
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</li>
|
|
|
|
);
|
|
|
|
};
|