feat: allow duplicate availability (#9615)

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
This commit is contained in:
Ritik Kumar 2023-06-26 17:05:20 +05:30 committed by GitHub
parent 78393a2b8b
commit eedf6913b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 129 additions and 1 deletions

View File

@ -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<HTMLUListElement>();
@ -96,6 +112,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
isDeletable={schedules.length !== 1}
updateDefault={updateMutation.mutate}
deleteFunction={deleteMutation.mutate}
duplicateFunction={duplicateMutation.mutate}
/>
))}
</ul>

View File

@ -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({
</DropdownItem>
)}
</DropdownMenuItem>
<DropdownMenuItem className="outline-none">
<DropdownItem
type="button"
data-testid={"schedule-duplicate" + schedule.id}
StartIcon={Copy}
onClick={() => {
duplicateFunction({
scheduleId: schedule.id,
});
}}>
{t("duplicate")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem className="min-w-40 focus:ring-muted">
<DropdownItem
type="button"

View File

@ -2,6 +2,7 @@ import authedProcedure from "../../../../procedures/authedProcedure";
import { router } from "../../../../trpc";
import { ZCreateInputSchema } from "./create.schema";
import { ZDeleteInputSchema } from "./delete.schema";
import { ZScheduleDuplicateSchema } from "./duplicate.schema";
import { ZGetInputSchema } from "./get.schema";
import { ZUpdateInputSchema } from "./update.schema";
@ -10,6 +11,7 @@ type ScheduleRouterHandlerCache = {
create?: typeof import("./create.handler").createHandler;
delete?: typeof import("./delete.handler").deleteHandler;
update?: typeof import("./update.handler").updateHandler;
duplicate?: typeof import("./duplicate.handler").duplicateHandler;
};
const UNSTABLE_HANDLER_CACHE: ScheduleRouterHandlerCache = {};
@ -78,4 +80,22 @@ export const scheduleRouter = router({
input,
});
}),
duplicate: authedProcedure.input(ZScheduleDuplicateSchema).mutation(async ({ input, ctx }) => {
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,
});
}),
});

View File

@ -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<TrpcSessionUser>;
};
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" });
}
};

View File

@ -0,0 +1,7 @@
import { z } from "zod";
export const ZScheduleDuplicateSchema = z.object({
scheduleId: z.number(),
});
export type TScheduleDuplicateSchema = z.infer<typeof ZScheduleDuplicateSchema>;