From eedf6913b828ffe2d265d11ef715ec669740712d Mon Sep 17 00:00:00 2001 From: Ritik Kumar <58480195+iamr-kumar@users.noreply.github.com> Date: Mon, 26 Jun 2023 17:05:20 +0530 Subject: [PATCH] feat: allow duplicate availability (#9615) Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> --- apps/web/pages/availability/index.tsx | 17 +++++ .../schedules/components/ScheduleListItem.tsx | 17 ++++- .../viewer/availability/schedule/_router.tsx | 20 ++++++ .../schedule/duplicate.handler.ts | 69 +++++++++++++++++++ .../availability/schedule/duplicate.schema.ts | 7 ++ 5 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 packages/trpc/server/routers/viewer/availability/schedule/duplicate.handler.ts create mode 100644 packages/trpc/server/routers/viewer/availability/schedule/duplicate.schema.ts diff --git a/apps/web/pages/availability/index.tsx b/apps/web/pages/availability/index.tsx index 21e07b500d..bb54f081bd 100644 --- a/apps/web/pages/availability/index.tsx +++ b/apps/web/pages/availability/index.tsx @@ -1,4 +1,5 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; +import { useRouter } from "next/router"; import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules"; import Shell from "@calcom/features/shell/Shell"; @@ -20,6 +21,8 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab const meQuery = trpc.viewer.me.useQuery(); + const router = useRouter(); + const deleteMutation = trpc.viewer.availability.schedule.delete.useMutation({ onMutate: async ({ scheduleId }) => { await utils.viewer.availability.list.cancel(); @@ -67,6 +70,19 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab }, }); + const duplicateMutation = trpc.viewer.availability.schedule.duplicate.useMutation({ + onSuccess: async ({ schedule }) => { + await router.push(`/availability/${schedule.id}`); + showToast(t("schedule_created_successfully", { scheduleName: schedule.name }), "success"); + }, + onError: (err) => { + if (err instanceof HttpError) { + const message = `${err.statusCode}: ${err.message}`; + showToast(message, "error"); + } + }, + }); + // Adds smooth delete button - item fades and old item slides into place const [animationParentRef] = useAutoAnimate(); @@ -96,6 +112,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab isDeletable={schedules.length !== 1} updateDefault={updateMutation.mutate} deleteFunction={deleteMutation.mutate} + duplicateFunction={duplicateMutation.mutate} /> ))} diff --git a/packages/features/schedules/components/ScheduleListItem.tsx b/packages/features/schedules/components/ScheduleListItem.tsx index b3139015f6..12c107e93a 100644 --- a/packages/features/schedules/components/ScheduleListItem.tsx +++ b/packages/features/schedules/components/ScheduleListItem.tsx @@ -15,7 +15,7 @@ import { DropdownMenuTrigger, showToast, } from "@calcom/ui"; -import { Globe, MoreHorizontal, Trash, Clock } from "@calcom/ui/components/icon"; +import { Globe, MoreHorizontal, Trash, Clock, Copy } from "@calcom/ui/components/icon"; export function ScheduleListItem({ schedule, @@ -23,6 +23,7 @@ export function ScheduleListItem({ displayOptions, updateDefault, isDeletable, + duplicateFunction, }: { schedule: RouterOutputs["viewer"]["availability"]["list"]["schedules"][number]; deleteFunction: ({ scheduleId }: { scheduleId: number }) => void; @@ -32,6 +33,7 @@ export function ScheduleListItem({ }; isDeletable: boolean; updateDefault: ({ scheduleId, isDefault }: { scheduleId: number; isDefault: boolean }) => void; + duplicateFunction: ({ scheduleId }: { scheduleId: number }) => void; }) { const { t, i18n } = useLocale(); @@ -102,6 +104,19 @@ export function ScheduleListItem({ )} + + { + duplicateFunction({ + scheduleId: schedule.id, + }); + }}> + {t("duplicate")} + + { + if (!UNSTABLE_HANDLER_CACHE.duplicate) { + UNSTABLE_HANDLER_CACHE.duplicate = await import("./duplicate.handler").then( + (mod) => mod.duplicateHandler + ); + } + + // Unreachable code but required for type safety + if (!UNSTABLE_HANDLER_CACHE.duplicate) { + throw new Error("Failed to load handler"); + } + + return UNSTABLE_HANDLER_CACHE.duplicate({ + ctx, + input, + }); + }), }); diff --git a/packages/trpc/server/routers/viewer/availability/schedule/duplicate.handler.ts b/packages/trpc/server/routers/viewer/availability/schedule/duplicate.handler.ts new file mode 100644 index 0000000000..c4514595ab --- /dev/null +++ b/packages/trpc/server/routers/viewer/availability/schedule/duplicate.handler.ts @@ -0,0 +1,69 @@ +import { prisma } from "@calcom/prisma"; + +import { TRPCError } from "@trpc/server"; + +import type { TrpcSessionUser } from "../../../../trpc"; +import type { TScheduleDuplicateSchema } from "./duplicate.schema"; +import type { Prisma } from ".prisma/client"; + +type DuplicateScheduleOptions = { + ctx: { + user: NonNullable; + }; + input: TScheduleDuplicateSchema; +}; + +export const duplicateHandler = async ({ ctx, input }: DuplicateScheduleOptions) => { + try { + const { scheduleId } = input; + const { user } = ctx; + const schedule = await prisma.schedule.findUnique({ + where: { + id: scheduleId, + }, + select: { + id: true, + userId: true, + name: true, + availability: true, + timeZone: true, + }, + }); + + if (!schedule || schedule.userId !== user.id) { + throw new TRPCError({ + code: "UNAUTHORIZED", + }); + } + + const { availability } = schedule; + + const data: Prisma.ScheduleCreateInput = { + name: `${schedule.name} (Copy)`, + user: { + connect: { + id: user.id, + }, + }, + timeZone: schedule.timeZone ?? user.timeZone, + availability: { + createMany: { + data: availability.map((schedule) => ({ + days: schedule.days, + startTime: schedule.startTime, + endTime: schedule.endTime, + date: schedule.date, + })), + }, + }, + }; + + const newSchedule = await prisma.schedule.create({ + data, + }); + + return { schedule: newSchedule }; + } catch (error) { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); + } +}; diff --git a/packages/trpc/server/routers/viewer/availability/schedule/duplicate.schema.ts b/packages/trpc/server/routers/viewer/availability/schedule/duplicate.schema.ts new file mode 100644 index 0000000000..d2ca738715 --- /dev/null +++ b/packages/trpc/server/routers/viewer/availability/schedule/duplicate.schema.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const ZScheduleDuplicateSchema = z.object({ + scheduleId: z.number(), +}); + +export type TScheduleDuplicateSchema = z.infer;