241 lines
8.2 KiB
TypeScript
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>
|
|
);
|
|
}
|