2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
import { classNames } from "@calcom/lib";
import { useCopy } from "@calcom/lib/hooks/useCopy";
import type { BadgeProps } from "@calcom/ui";
import { Badge, Button, Label } from "@calcom/ui";
type DisplayInfoType<T extends boolean> = {
label: string;
value: T extends true ? string[] : string;
asBadge?: boolean;
isArray?: T;
displayCopy?: boolean;
badgeColor?: BadgeProps["variant"];
} & (T extends false
? { displayCopy?: boolean; displayCount?: never }
: { displayCopy?: never; displayCount?: number }); // Only show displayCopy if its not an array is false
export function DisplayInfo<T extends boolean>({
label,
value,
asBadge,
isArray,
displayCopy,
displayCount,
badgeColor,
}: DisplayInfoType<T>) {
const { copyToClipboard, isCopied } = useCopy();
const values = (isArray ? value : [value]) as string[];
return (
<div className="flex flex-col">
<Label className="text-subtle mb-1 text-xs font-semibold uppercase leading-none">
{label} {displayCount && `(${displayCount})`}
</Label>
<div className={classNames(asBadge ? "mt-0.5 flex space-x-2" : "flex flex-col")}>
<>
{values.map((v) => {
const content = (
<span
className={classNames(
"text-emphasis inline-flex items-center gap-1 font-normal leading-5",
asBadge ? "text-xs" : "text-sm"
)}>
{v}
{displayCopy && (
<Button
size="sm"
variant="icon"
onClick={() => copyToClipboard(v)}
color="minimal"
className="text-subtle rounded-md"
StartIcon={isCopied ? "clipboard-check" : "clipboard"}
/>
)}
</span>
);
return asBadge ? (
<Badge variant={badgeColor} size="sm">
{content}
</Badge>
) : (
content
);
})}
</>
</div>
</div>
);
}

View File

@@ -0,0 +1,203 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useSession } from "next-auth/react";
import type { Dispatch } from "react";
import { useMemo } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
import { shallow } from "zustand/shallow";
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,
TextAreaField,
TimezoneSelect,
Label,
showToast,
Avatar,
ImageUploader,
SheetHeader,
SheetBody,
SheetFooter,
} from "@calcom/ui";
import type { Action } from "../UserListTable";
import { SheetFooterControls } from "./SheetFooterControls";
import { useEditMode } from "./store";
type MembershipOption = {
value: MembershipRole;
label: string;
};
const editSchema = z.object({
name: z.string(),
username: z.string(),
email: z.string().email(),
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()),
});
type EditSchema = z.infer<typeof editSchema>;
export function EditForm({
selectedUser,
avatarUrl,
domainUrl,
dispatch,
}: {
selectedUser: RouterOutputs["viewer"]["organizations"]["getUser"];
avatarUrl: string;
domainUrl: string;
dispatch: Dispatch<Action>;
}) {
const [setMutationLoading] = useEditMode((state) => [state.setMutationloading], shallow);
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<MembershipOption[]>(() => {
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 (
<>
<Form
form={form}
id="edit-user-form"
className="flex h-full flex-col"
handleSubmit={(values) => {
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,
});
}}>
<SheetHeader>
<div className="flex flex-col gap-2">
<Controller
control={form.control}
name="avatar"
render={({ field: { value } }) => (
<div className="flex items-center">
<Avatar alt={`${selectedUser?.name} avatar`} imageSrc={value} size="lg" />
<div className="ml-4">
<ImageUploader
target="avatar"
id="avatar-upload"
buttonMsg={t("change_avatar")}
handleAvatarChange={(newAvatar) => {
form.setValue("avatar", newAvatar, { shouldDirty: true });
}}
imageSrc={value || undefined}
/>
</div>
</div>
)}
/>
<div className="space-between flex flex-col leading-none">
<span className="text-emphasis text-lg font-semibold">
{selectedUser?.name ?? "Nameless User"}
</span>
<p className="subtle text-sm font-normal">
{domainUrl}/{selectedUser?.username}
</p>
</div>
</div>
</SheetHeader>
<SheetBody className="mt-6 flex h-full flex-col space-y-3">
<TextField label={t("username")} {...form.register("username")} />
<TextField label={t("name")} {...form.register("name")} />
<TextField label={t("email")} {...form.register("email")} />
<TextAreaField label={t("bio")} {...form.register("bio")} className="min-h-52" />
<div>
<Label>{t("role")}</Label>
<ToggleGroup
isFullWidth
defaultValue={selectedUser?.role ?? "MEMBER"}
value={form.watch("role")}
options={membershipOptions}
onValueChange={(value: EditSchema["role"]) => {
form.setValue("role", value);
}}
/>
</div>
<div>
<Label>{t("timezone")}</Label>
<TimezoneSelect value={watchTimezone ?? "America/Los_Angeles"} />
</div>
</SheetBody>
<SheetFooter>
<SheetFooterControls />
</SheetFooter>
</Form>
</>
);
}

View File

@@ -0,0 +1,133 @@
import type { Dispatch } from "react";
import { shallow } from "zustand/shallow";
import { useOrgBranding } from "@calcom/ee/organizations/context/provider";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
Avatar,
Label,
Loader,
Sheet,
SheetContent,
SheetBody,
Skeleton,
SheetHeader,
SheetDescription,
SheetTitle,
SheetFooter,
} from "@calcom/ui";
import type { Action, State } from "../UserListTable";
import { DisplayInfo } from "./DisplayInfo";
import { EditForm } from "./EditUserForm";
import { SheetFooterControls } from "./SheetFooterControls";
import { useEditMode } from "./store";
export function EditUserSheet({ state, dispatch }: { state: State; dispatch: Dispatch<Action> }) {
const { t } = useLocale();
const { user: selectedUser } = state.editSheet;
const orgBranding = useOrgBranding();
const [editMode, setEditMode] = useEditMode((state) => [state.editMode, state.setEditMode], shallow);
const { data: loadedUser, isPending } = trpc.viewer.organizations.getUser.useQuery({
userId: selectedUser?.id,
});
const avatarURL = `${orgBranding?.fullDomain ?? WEBAPP_URL}/${loadedUser?.username}/avatar.png`;
const schedulesNames = loadedUser?.schedules && loadedUser?.schedules.map((s) => s.name);
const teamNames =
loadedUser?.teams && loadedUser?.teams.map((t) => `${t.name} ${!t.accepted ? "(pending)" : ""}`);
return (
<Sheet
open={true}
onOpenChange={() => {
setEditMode(false);
dispatch({ type: "CLOSE_MODAL" });
}}>
<SheetContent>
{!isPending && loadedUser ? (
<>
{!editMode ? (
<>
<SheetHeader>
<Avatar
asChild
className="h-[36px] w-[36px]"
alt={`${loadedUser?.name} avatar`}
imageSrc={loadedUser.avatarUrl}
/>
<SheetTitle>
<Skeleton loading={isPending} as="p" waitForTranslation={false}>
<span className="text-emphasis text-lg font-semibold">
{loadedUser?.name ?? "Nameless User"}
</span>
</Skeleton>
</SheetTitle>
<SheetDescription>
<Skeleton loading={isPending} as="p" waitForTranslation={false}>
<p className="subtle text-sm font-normal">
{orgBranding?.fullDomain ?? WEBAPP_URL}/{loadedUser?.username}
</p>
</Skeleton>
</SheetDescription>
</SheetHeader>
<SheetBody className="flex flex-col space-y-5">
<DisplayInfo label={t("email")} value={loadedUser?.email ?? ""} displayCopy />
<DisplayInfo
label={t("bio")}
badgeColor="gray"
value={loadedUser?.bio ? loadedUser?.bio : t("user_has_no_bio")}
/>
<DisplayInfo label={t("role")} value={loadedUser?.role ?? ""} asBadge badgeColor="blue" />
<DisplayInfo label={t("timezone")} value={loadedUser?.timeZone ?? ""} />
<div className="flex flex-col">
<Label className="text-subtle mb-1 text-xs font-semibold uppercase leading-none">
{t("availability_schedules")}
</Label>
<div className="flex flex-col">
{schedulesNames
? schedulesNames.map((scheduleName) => (
<span
key={scheduleName}
className="text-emphasis inline-flex items-center gap-1 text-sm font-normal leading-5">
{scheduleName}
</span>
))
: t("user_has_no_schedules")}
</div>
</div>
<DisplayInfo
label={t("teams")}
displayCount={teamNames?.length ?? 0}
value={
teamNames && teamNames?.length === 0 ? [t("user_isnt_in_any_teams")] : teamNames ?? "" // TS wtf
}
asBadge={teamNames && teamNames?.length > 0}
/>
</SheetBody>
<SheetFooter>
<SheetFooterControls />
</SheetFooter>
</>
) : (
<>
<EditForm
selectedUser={loadedUser}
avatarUrl={loadedUser.avatarUrl ?? avatarURL}
domainUrl={orgBranding?.fullDomain ?? WEBAPP_URL}
dispatch={dispatch}
/>
</>
)}
</>
) : (
<Loader />
)}
</SheetContent>
</Sheet>
);
}

View File

@@ -0,0 +1,59 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SheetClose, Button } from "@calcom/ui";
import { useEditMode } from "./store";
function EditModeFooter() {
const { t } = useLocale();
const setEditMode = useEditMode((state) => state.setEditMode);
const isPending = useEditMode((state) => state.mutationLoading);
return (
<>
<Button
color="secondary"
type="button"
className="justify-center md:w-1/5"
onClick={() => {
setEditMode(false);
}}>
{t("cancel")}
</Button>
<Button type="submit" className="w-full justify-center" form="edit-user-form" loading={isPending}>
{t("update")}
</Button>
</>
);
}
function MoreInfoFooter() {
const { t } = useLocale();
const setEditMode = useEditMode((state) => state.setEditMode);
return (
<>
<SheetClose asChild>
<Button color="secondary" type="button" className="w-full justify-center lg:w-1/5">
{t("close")}
</Button>
</SheetClose>
<Button
type="button"
onClick={() => {
setEditMode(true);
}}
className="w-full justify-center gap-2"
variant="icon"
key="EDIT_BUTTON"
StartIcon="pencil">
{t("edit")}
</Button>
</>
);
}
export function SheetFooterControls() {
const editMode = useEditMode((state) => state.editMode);
return <>{editMode ? <EditModeFooter /> : <MoreInfoFooter />}</>;
}

View File

@@ -0,0 +1,15 @@
import { create } from "zustand";
interface EditModeState {
editMode: boolean;
setEditMode: (editMode: boolean) => void;
mutationLoading: boolean;
setMutationloading: (loading: boolean) => void;
}
export const useEditMode = create<EditModeState>((set) => ({
editMode: false,
setEditMode: (editMode) => set({ editMode }),
mutationLoading: false,
setMutationloading: (loading) => set({ mutationLoading: loading }),
}));