feat: allow duplicate availability (#9615)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
This commit is contained in:
parent
78393a2b8b
commit
eedf6913b8
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -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" });
|
||||
}
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const ZScheduleDuplicateSchema = z.object({
|
||||
scheduleId: z.number(),
|
||||
});
|
||||
|
||||
export type TScheduleDuplicateSchema = z.infer<typeof ZScheduleDuplicateSchema>;
|
Loading…
Reference in New Issue
Block a user