Files
calendar/packages/features/ee/teams/components/EditMemberSheet.tsx
T

241 lines
8.2 KiB
TypeScript

import { zodResolver } from "@hookform/resolvers/zod";
import type { Dispatch } from "react";
import { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { shallow } from "zustand/shallow";
import { DisplayInfo } from "@calcom/features/users/components/UserTable/EditSheet/DisplayInfo";
import { SheetFooterControls } from "@calcom/features/users/components/UserTable/EditSheet/SheetFooterControls";
import { useEditMode } from "@calcom/features/users/components/UserTable/EditSheet/store";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import {
Avatar,
Icon,
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetBody,
Skeleton,
Tooltip,
ToggleGroup,
Form,
showToast,
Loader,
} from "@calcom/ui";
import { updateRoleInCache } from "./MemberChangeRoleModal";
import type { Action, State, User } from "./MemberList";
const formSchema = z.object({
role: z.enum([MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER]),
});
type FormSchema = z.infer<typeof formSchema>;
export function EditMemberSheet({
state,
dispatch,
currentMember,
teamId,
}: {
state: State;
dispatch: Dispatch<Action>;
currentMember: MembershipRole;
teamId: number;
}) {
const { t } = useLocale();
const { user } = state.editSheet;
const selectedUser = user as User;
const [editMode, setEditMode, setMutationLoading] = useEditMode(
(state) => [state.editMode, state.setEditMode, state.setMutationLoading],
shallow
);
const [role, setRole] = useState(selectedUser.role);
const name =
selectedUser.name ||
(() => {
const emailName = selectedUser.email.split("@")[0] as string;
return emailName.charAt(0).toUpperCase() + emailName.slice(1);
})();
const bookerUrl = selectedUser.bookerUrl;
const utils = trpc.useUtils();
const bookerUrlWithoutProtocol = bookerUrl.replace(/^https?:\/\//, "");
const bookingLink = !!selectedUser.username ? `${bookerUrlWithoutProtocol}/${selectedUser.username}` : "";
const options = useMemo(() => {
return [
{
label: t("member"),
value: MembershipRole.MEMBER,
},
{
label: t("admin"),
value: MembershipRole.ADMIN,
},
{
label: t("owner"),
value: MembershipRole.OWNER,
},
].filter(({ value }) => value !== MembershipRole.OWNER || currentMember === MembershipRole.OWNER);
}, [t, currentMember]);
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
role: selectedUser.role,
},
});
const { data: getUserConnectedApps, isPending } = trpc.viewer.teams.getUserConnectedApps.useQuery({
userIds: [selectedUser.id],
teamId,
});
const connectedApps = getUserConnectedApps?.[selectedUser.id];
const changeRoleMutation = trpc.viewer.teams.changeMemberRole.useMutation({
onMutate: async ({ teamId, memberId, role }) => {
await utils.viewer.teams.listMembers.cancel();
const previousValue = utils.viewer.teams.listMembers.getInfiniteData({
limit: 10,
teamId,
searchTerm: undefined,
});
if (previousValue) {
updateRoleInCache({ utils, teamId, memberId, role, searchTerm: undefined });
}
return { previousValue };
},
onSuccess: async (_data, { role }) => {
setRole(role);
setMutationLoading(false);
await utils.viewer.teams.get.invalidate();
await utils.viewer.organizations.listMembers.invalidate();
showToast(t("profile_updated_successfully"), "success");
setEditMode(false);
},
async onError(err) {
showToast(err.message, "error");
setMutationLoading(false);
},
});
function changeRole(values: FormSchema) {
setMutationLoading(true);
changeRoleMutation.mutate({
teamId: teamId,
memberId: user?.id as number,
role: values.role,
});
}
const appList = (connectedApps || []).map(({ logo, name, externalId }) => {
return logo ? (
externalId ? (
<div className="ltr:mr-2 rtl:ml-2 ">
<Tooltip content={externalId}>
<img className="h-5 w-5" src={logo} alt={`${name} logo`} />
</Tooltip>
</div>
) : (
<div className="ltr:mr-2 rtl:ml-2">
<img className="h-5 w-5" src={logo} alt={`${name} logo`} />
</div>
)
) : null;
});
return (
<Sheet
open={true}
onOpenChange={() => {
setEditMode(false);
dispatch({ type: "CLOSE_MODAL" });
}}>
<SheetContent className="bg-muted">
{!isPending ? (
<Form form={form} handleSubmit={changeRole} className="flex h-full flex-col">
<SheetHeader showCloseButton={false} className="w-full">
<div className="border-sublte bg-default w-full rounded-xl border p-4">
<div
className="block w-full rounded-lg ring-1 ring-[#0000000F]"
style={{
background: "linear-gradient(to top right, var(--cal-bg-emphasis), var(--cal-bg))",
height: "110px",
}}
/>
<div className="bg-default ml-3 w-fit translate-y-[-50%] rounded-full p-1 ring-1 ring-[#0000000F]">
<Avatar asChild size="lg" alt={`${name} avatar`} imageSrc={selectedUser.avatarUrl} />
</div>
<Skeleton as="p" waitForTranslation={false}>
<h2 className="text-emphasis font-sans text-2xl font-semibold">
{name || "Nameless User"}
</h2>
</Skeleton>
<Skeleton as="p" waitForTranslation={false}>
<p className="text-subtle max-h-[3em] overflow-hidden text-ellipsis text-sm font-normal">
{selectedUser.bio ? selectedUser?.bio : t("user_has_no_bio")}
</p>
</Skeleton>
</div>
</SheetHeader>
<SheetBody className="flex flex-col space-y-4 p-4">
<div className="mb-4 flex flex-col space-y-4">
<h3 className="text-emphasis mb-1 text-base font-semibold">{t("profile")}</h3>
<DisplayInfo label="Cal" value={bookingLink} icon="external-link" />
<DisplayInfo label={t("email")} value={selectedUser.email} icon="at-sign" />
{!editMode ? (
<DisplayInfo label={t("role")} value={[role]} icon="fingerprint" />
) : (
<div className="flex items-center gap-6">
<div className="flex w-[110px] items-center gap-2">
<Icon className="h-4 w-4" name="fingerprint" />
<label className="text-sm font-medium">{t("role")}</label>
</div>
<div className="flex flex-1">
<ToggleGroup
isFullWidth
defaultValue={role}
value={form.watch("role")}
options={options}
onValueChange={(value: FormSchema["role"]) => {
form.setValue("role", value);
}}
/>
</div>
</div>
)}
<div className="flex items-center gap-6">
<div className="flex w-[110px] items-center gap-2">
<Icon className="text-subtle h-4 w-4" name="grid-3x3" />
<label className="text-subtle text-sm font-medium">{t("apps")}</label>
</div>
<div className="flex flex-1">
{!connectedApps ? (
<div>{t("user_has_no_app_installed")}</div>
) : (
<div className="flex">{appList}</div>
)}
</div>
</div>
</div>
</SheetBody>
<SheetFooter className="mt-auto">
<SheetFooterControls />
</SheetFooter>
</Form>
) : (
<Loader />
)}
</SheetContent>
</Sheet>
);
}