import { zodResolver } from "@hookform/resolvers/zod"; import { useSession } from "next-auth/react"; import type { Dispatch } from "react"; import { useMemo, useState } from "react"; import { Controller, useForm, useFormContext } from "react-hook-form"; import { z } from "zod"; import { emailSchema } from "@calcom/lib/emailSchema"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { MembershipRole } from "@calcom/prisma/enums"; import { trpc, type RouterOutputs } from "@calcom/trpc/react"; import { Form, TextField, ToggleGroup, InputField, TextAreaField, TimezoneSelect, Label, showToast, Avatar, ImageUploader, SelectField, SheetHeader, SheetBody, SheetFooter, Button, SheetTitle, } from "@calcom/ui"; import type { UserTableAction } from "../types"; import { useEditMode } from "./store"; type MembershipOption = { value: MembershipRole; label: string; }; const stringOrNumber = z.string().or(z.number()); const attributeSchema = z.object({ id: z.string(), options: z.array(z.object({ label: z.string().optional(), value: stringOrNumber.optional() })).optional(), value: stringOrNumber.optional(), }); const editSchema = z.object({ name: z.string(), username: z.string(), email: emailSchema, avatar: z.string(), bio: z.string(), role: z.enum([MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER]), timeZone: z.string(), // schedules: z.array(z.string()), // teams: z.array(z.string()), attributes: z.array(attributeSchema).optional(), }); type EditSchema = z.infer; export function EditForm({ selectedUser, avatarUrl, domainUrl, dispatch, }: { selectedUser: RouterOutputs["viewer"]["organizations"]["getUser"]; avatarUrl: string; domainUrl: string; dispatch: Dispatch; }) { const setEditMode = useEditMode((state) => state.setEditMode); const [mutationLoading, setMutationLoading] = useState(false); const { t } = useLocale(); const session = useSession(); const org = session?.data?.user?.org; const utils = trpc.useUtils(); const form = useForm({ resolver: zodResolver(editSchema), defaultValues: { name: selectedUser?.name ?? "", username: selectedUser?.username ?? "", email: selectedUser?.email ?? "", avatar: avatarUrl, bio: selectedUser?.bio ?? "", role: selectedUser?.role ?? "", timeZone: selectedUser?.timeZone ?? "", }, }); const isOwner = org?.role === MembershipRole.OWNER; const membershipOptions = useMemo(() => { const options: MembershipOption[] = [ { value: MembershipRole.MEMBER, label: t("member"), }, { value: MembershipRole.ADMIN, label: t("admin"), }, ]; if (isOwner) { options.push({ value: MembershipRole.OWNER, label: t("owner"), }); } return options; }, [t, isOwner]); const mutation = trpc.viewer.organizations.updateUser.useMutation({ onSuccess: () => { dispatch({ type: "CLOSE_MODAL" }); utils.viewer.organizations.listMembers.invalidate(); showToast(t("profile_updated_successfully"), "success"); }, onError: (error) => { showToast(error.message, "error"); }, onSettled: () => { /** * /We need to do this as the submit button lives out side * the form for some complicated reason so we can't relay on mutationState */ setMutationLoading(false); }, }); const watchTimezone = form.watch("timeZone"); return ( <>
{ setMutationLoading(true); mutation.mutate({ userId: selectedUser?.id ?? "", role: values.role, username: values.username, name: values.name, email: values.email, avatar: values.avatar, bio: values.bio, timeZone: values.timeZone, // @ts-expect-error theyre there in local types but for some reason it errors? attributeOptions: values.attributes ? // @ts-expect-error same as above { userId: selectedUser?.id ?? "", attributes: values.attributes } : undefined, }); setEditMode(false); }}> {t("update_profile")}
(
{ form.setValue("avatar", newAvatar, { shouldDirty: true }); }} imageSrc={value || undefined} />
)} />
{ form.setValue("role", value); }} />
); } type AttributeType = z.infer; type DefaultValueType = { [key: `attributes.${number}`]: AttributeType; }; function AttributesList(props: { selectedUserId: number }) { const { data: usersAttributes, isPending: usersAttributesPending } = trpc.viewer.attributes.getByUserId.useQuery({ userId: props.selectedUserId, }); const { data: attributes, isPending: attributesPending } = trpc.viewer.attributes.list.useQuery(); const enabledAttributes = attributes?.filter((attr) => attr.enabled); const { t } = useLocale(); const { control, watch, getFieldState, setValue } = useFormContext(); const getOptionsByAttributeId = (attributeId: string) => { const attribute = attributes?.find((attr) => attr.id === attributeId); return attribute ? attribute.options.map((option) => ({ value: option.id, label: option.value, })) : []; }; const defaultValues = useMemo(() => { if (!usersAttributes || usersAttributesPending || !enabledAttributes) return {}; return enabledAttributes.reduce((acc, enabledAttr, index) => { const attr = usersAttributes.find((attr) => attr.id === enabledAttr.id); const key = `attributes.${index}` as const; if (!attr) { acc[key] = { id: enabledAttr.id, value: "", options: [] }; } else if (attr.type === "MULTI_SELECT") { acc[key] = { id: attr.id, options: attr.options.map((option) => ({ label: option.value, value: option.id })), }; } else if (attr.type === "SINGLE_SELECT") { acc[key] = { id: attr.id, options: [{ label: attr.options[0]?.value, value: attr.options[0]?.id }], }; } else { acc[key] = { id: attr.id, value: attr.options[0]?.value || "", }; } return acc; }, {}); }, [usersAttributes, usersAttributesPending, enabledAttributes]); if (!enabledAttributes || !usersAttributes) return null; const attributeFieldState = getFieldState("attributes"); return (
{attributeFieldState.error && (

{JSON.stringify(attributeFieldState.error)}

)} {enabledAttributes.map((attr, index) => ( { return (
{["TEXT", "NUMBER"].includes(attr.type) && ( { field.onChange({ id: attr.id, value: e.target.value, }); }} /> )} {["SINGLE_SELECT", "MULTI_SELECT"].includes(attr.type) && ( { if (attr.type === "MULTI_SELECT") { field.onChange({ id: attr.id, options: value.map((v: any) => ({ label: v.label, value: v.value })), }); } else { field.onChange({ id: attr.id, options: [{ label: value.label, value: value.value }], }); } }} /> )}
); }} /> ))}
); }