Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Delete the booking history of past meetings #18787 #19118

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ab424a7
Add delete button functionality to past bookings
Feb 5, 2025
65497f9
Merge branch 'main' of https://github.com/PAVANNAIK25/cal.com into fe…
Feb 5, 2025
559639f
Remove text variables from other languages
PAVANNAIK25 Feb 16, 2025
ad18631
add handler whwere offer seats case is handled
PAVANNAIK25 Feb 16, 2025
9c7906c
Merge branch 'main' into feat/delete-booking-history-past
TusharBhatt1 Feb 17, 2025
d49bf0d
update changes according to PR feedback
PAVANNAIK25 Feb 17, 2025
a39ac0a
Remove logs
PAVANNAIK25 Feb 17, 2025
bf28c12
Add tests to check the delete operation
PAVANNAIK25 Feb 17, 2025
0de08b8
Add test for past booking delete functionality
PAVANNAIK25 Feb 19, 2025
0d30ea4
Add dynamic import
PAVANNAIK25 Feb 19, 2025
351d8cc
Merge branch 'main' into feat/delete-booking-history-past
TusharBhatt1 Feb 19, 2025
485ce0e
Correct title for test
PAVANNAIK25 Feb 19, 2025
342b264
Merge branch 'feat/delete-booking-history-past' of https://github.com…
PAVANNAIK25 Feb 19, 2025
b1e4737
fix title and description
PAVANNAIK25 Feb 19, 2025
cc2787a
Merge branch 'main' into feat/delete-booking-history-past
TusharBhatt1 Feb 19, 2025
4e1a396
Remove unnecessary changes in locales
PAVANNAIK25 Feb 19, 2025
013eef1
Merge branch 'feat/delete-booking-history-past' of https://github.com…
PAVANNAIK25 Feb 19, 2025
3200003
Merge branch 'main' into feat/delete-booking-history-past
TusharBhatt1 Feb 21, 2025
d74432c
Merge branch 'main' into feat/delete-booking-history-past
TusharBhatt1 Feb 21, 2025
ac4c6ec
Merge branch 'main' into feat/delete-booking-history-past
anikdhabal Mar 3, 2025
19a45f9
Update common.json
anikdhabal Mar 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions apps/web/components/booking/BookingListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getSuccessPageLocationMessage, guessEventLocationType } from "@calcom/a
import dayjs from "@calcom/dayjs";
// TODO: Use browser locale, implement Intl in Dayjs maybe?
import "@calcom/dayjs/locales";
import { DeleteBookingDialog } from "@calcom/features/bookings/components/dialog/DeleteBookingDialog";
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
import classNames from "@calcom/lib/classNames";
import { formatTime } from "@calcom/lib/date-fns";
Expand Down Expand Up @@ -291,6 +292,18 @@ function BookingListItem(booking: BookingItemProps) {
});
}

if (isBookingInPast) {
editBookingActions.push({
id: "delete_history",
label: t("delete"),
onClick: () => {
setIsOpenDeleteBookingDialog(true);
},
icon: "trash" as const,
color: "destructive",
});
}

let bookedActions: ActionType[] = [
{
id: "cancel",
Expand Down Expand Up @@ -347,6 +360,7 @@ function BookingListItem(booking: BookingItemProps) {
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
const [isOpenAddGuestsDialog, setIsOpenAddGuestsDialog] = useState(false);
const [rerouteDialogIsOpen, setRerouteDialogIsOpen] = useState(false);
const [isOpenDeleteBookingDialog, setIsOpenDeleteBookingDialog] = useState(false);
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
onSuccess: () => {
showToast(t("location_updated"), "success");
Expand Down Expand Up @@ -461,6 +475,11 @@ function BookingListItem(booking: BookingItemProps) {
setIsOpenDialog={setIsOpenAddGuestsDialog}
bookingId={booking.id}
/>
<DeleteBookingDialog
isOpenDialog={isOpenDeleteBookingDialog}
setIsOpenDialog={setIsOpenDeleteBookingDialog}
bookingId={booking.id}
/>
{booking.paid && booking.payment[0] && (
<ChargeCardDialog
isOpenDialog={chargeCardDialogIsOpen}
Expand Down
1 change: 1 addition & 0 deletions apps/web/public/static/locales/ar/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -2957,4 +2957,5 @@
"verify_email": "التحقق من البريد الإلكتروني",
"verify_email_change": "تأكيد تغيير البريد الإلكتروني",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"

}
6 changes: 5 additions & 1 deletion apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -2957,5 +2957,9 @@
"desc": "Desc",
"verify_email": "Verify email",
"verify_email_change": "Verify email change",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"delete_booking_title": "Delete booking?",
"delete_booking_description": "Are you sure you want to delete this booking?",
"unable_to_delete_booking": "Unable to delete booking",
"booking_delete_successfully": "Booking deleted successfully"
}
1 change: 1 addition & 0 deletions apps/web/public/static/locales/he/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -2957,4 +2957,5 @@
"verify_email": "אימות אימייל",
"verify_email_change": "אימות שינוי כתובת דוא\"ל",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"

}
1 change: 1 addition & 0 deletions apps/web/public/static/locales/it/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -2957,4 +2957,5 @@
"verify_email": "Verifica email",
"verify_email_change": "Verifica la modifica dell'email",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"

}
1 change: 1 addition & 0 deletions apps/web/public/static/locales/ja/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -2957,4 +2957,5 @@
"verify_email": "メールアドレスを確認",
"verify_email_change": "メールアドレスの変更を確認",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Dispatch, SetStateAction } from "react";

import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc";
import { ConfirmationDialogContent, Dialog } from "@calcom/ui";
import { showToast } from "@calcom/ui";

interface IDeleteBookingDialog {
isOpenDialog: boolean;
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
bookingId: number;
}

export const DeleteBookingDialog = (props: IDeleteBookingDialog) => {
const { t } = useLocale();
const { isOpenDialog, setIsOpenDialog, bookingId } = props;
const utils = trpc.useUtils();

const deleteBookingMutation = trpc.viewer.bookings.deleteBooking.useMutation({
onSuccess: async () => {
showToast(t("booking_delete_successfully"), "success");
setIsOpenDialog(false);
utils.viewer.bookings.invalidate();
},
onError: (err) => {
const message = `${err.data?.code}: ${t(err.message)}`;
showToast(message || t("unable_to_delete_booking"), "error");
},
});

return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<ConfirmationDialogContent
isPending={deleteBookingMutation.isPending}
variety="danger"
title={t("delete_booking_title")}
confirmBtnText={t("confirm")}
onConfirm={(e) => {
e.preventDefault();
deleteBookingMutation.mutate({ id: bookingId });
}}>
{t("delete_booking_description")}
</ConfirmationDialogContent>
</Dialog>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { render, screen, fireEvent } from "@testing-library/react";
import * as React from "react";
import { vi } from "vitest";

import { DeleteBookingDialog } from "../DeleteBookingDialog";

const invalidateMock = vi.fn();

vi.mock("@calcom/trpc", () => ({
trpc: {
useUtils: vi.fn(() => ({
viewer: {
bookings: {
invalidate: invalidateMock,
},
},
})),
viewer: {
bookings: {
deleteBooking: {
useMutation: vi.fn(({ onSuccess }: { onSuccess: () => void }) => ({
mutate: () => {
onSuccess();
},
isPending: false,
})),
},
},
},
},
}));

vi.mock("@calcom/lib/hooks/useLocale", () => ({
useLocale: () => ({ t: (key: string) => key }),
}));

describe("DeleteBookingDialog", () => {
const mockProps = {
bookingId: 123,
isOpenDialog: true,
setIsOpenDialog: vi.fn(),
};

beforeEach(() => {
vi.clearAllMocks();
});

it("renders the dialog when open", () => {
render(<DeleteBookingDialog {...mockProps} />);
expect(screen.getByText("delete_booking_title")).toBeInTheDocument();
expect(screen.getByText("delete_booking_description")).toBeInTheDocument();
});

it("closes the dialog when cancel is clicked", async () => {
render(<DeleteBookingDialog {...mockProps} />);
fireEvent.click(screen.getByText("cancel"));
expect(mockProps.setIsOpenDialog).toHaveBeenCalledWith(false);
});

it("deletes the booking and closes the dialog", () => {
render(<DeleteBookingDialog {...mockProps} />);

fireEvent.click(screen.getByText("confirm"));
expect(mockProps.setIsOpenDialog).toHaveBeenCalledWith(false);
expect(invalidateMock).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import prismaMock from "../../../../../../../tests/libs/__mocks__/prismaMock";

import { describe, expect, it, beforeEach, vi } from "vitest";

import { deleteBookingHandler } from "@calcom/trpc/server/routers/viewer/bookings/deleteBooking.handler";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";

describe("Booking deletion", () => {
beforeEach(() => {
vi.clearAllMocks();
});

const ctx = {
user: {
id: 123,
name: "test",
timeZone: "timeZone",
username: "test_username",
} as NonNullable<TrpcSessionUser>,
};

it("should delete a single booking successfully", async () => {
const mockBooking: any = { id: 1, userId: 123 };

// Set the spy to resolve with your mock data
prismaMock.booking.delete.mockResolvedValueOnce(mockBooking);

await deleteBookingHandler({
ctx,
input: { id: 1 },
});

expect(prismaMock.booking.delete).toHaveBeenCalledTimes(1);
expect(prismaMock.booking.delete).toHaveBeenCalledWith({
where: { id: 1, userId: 123 },
});
});
});
21 changes: 21 additions & 0 deletions packages/trpc/server/routers/viewer/bookings/_router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import publicProcedure from "../../../procedures/publicProcedure";
import { router } from "../../../trpc";
import { ZAddGuestsInputSchema } from "./addGuests.schema";
import { ZConfirmInputSchema } from "./confirm.schema";
import { ZDeleteBookingInputSchema } from "./deleteBooking.schema";
import { ZEditLocationInputSchema } from "./editLocation.schema";
import { ZFindInputSchema } from "./find.schema";
import { ZGetInputSchema } from "./get.schema";
Expand All @@ -16,6 +17,7 @@ type BookingsRouterHandlerCache = {
requestReschedule?: typeof import("./requestReschedule.handler").requestRescheduleHandler;
editLocation?: typeof import("./editLocation.handler").editLocationHandler;
addGuests?: typeof import("./addGuests.handler").addGuestsHandler;
deleteBooking?: typeof import("./deleteBooking.handler").deleteBookingHandler;
confirm?: typeof import("./confirm.handler").confirmHandler;
getBookingAttendees?: typeof import("./getBookingAttendees.handler").getBookingAttendeesHandler;
find?: typeof import("./find.handler").getHandler;
Expand Down Expand Up @@ -93,6 +95,25 @@ export const bookingsRouter = router({
input,
});
}),
deleteBooking: authedProcedure.input(ZDeleteBookingInputSchema).mutation(async ({ input, ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.deleteBooking) {
// Dynamically import the deleteBooking handler
UNSTABLE_HANDLER_CACHE.deleteBooking = await import("./deleteBooking.handler").then(
(mod) => mod.deleteBookingHandler
);
}

// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.deleteBooking) {
throw new Error("Failed to load handler");
}

// Execute the handler with the context and input
return UNSTABLE_HANDLER_CACHE.deleteBooking({
ctx,
input,
});
}),

confirm: authedProcedure.input(ZConfirmInputSchema).mutation(async ({ input, ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.confirm) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";

import type { TDeleteInputSchema } from "./deleteBooking.schema";

type DeleteOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TDeleteInputSchema;
};

export const deleteBookingHandler = async ({ ctx, input }: DeleteOptions) => {
const { user } = ctx;
const { id } = input;
await prisma.booking.delete({
where: {
userId: user.id,
id,
},
});

return {
id,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from "zod";

export const ZDeleteBookingInputSchema = z.object({
id: z.number(),
});

export type TDeleteInputSchema = z.infer<typeof ZDeleteBookingInputSchema>;
Loading