From 9a5323bbb681e1ff488a4042ed9d83cfc0a9c732 Mon Sep 17 00:00:00 2001 From: Kartik Labhshetwar Date: Thu, 5 Feb 2026 08:32:51 +0530 Subject: [PATCH] feat: add delete confirmation dialog to webhook list items (#26305) * feat: add delete confirmation dialog to webhook list items * adds appname for translation key used in alert * extract delete wehook dialog into separate component * add e2e test * chore update e2e locator --------- Co-authored-by: Keith Williams Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Romit <85230081+romitg2@users.noreply.github.com> Co-authored-by: Romit --- .../components/EventTypeWebhookListItem.tsx | 43 ++++++++++++----- .../webhooks/components/WebhookListItem.tsx | 46 +++++++++++++------ .../dialogs/DeleteWebhookDialog.tsx | 31 +++++++++++++ apps/web/playwright/webhook.e2e.ts | 32 +++++++++++++ apps/web/public/static/locales/en/common.json | 2 +- 5 files changed, 128 insertions(+), 26 deletions(-) create mode 100644 apps/web/modules/webhooks/components/dialogs/DeleteWebhookDialog.tsx diff --git a/apps/web/modules/webhooks/components/EventTypeWebhookListItem.tsx b/apps/web/modules/webhooks/components/EventTypeWebhookListItem.tsx index 49f0b72fa1..5eb0b4e43f 100644 --- a/apps/web/modules/webhooks/components/EventTypeWebhookListItem.tsx +++ b/apps/web/modules/webhooks/components/EventTypeWebhookListItem.tsx @@ -1,5 +1,7 @@ "use client"; +import { useState } from "react"; + import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { WebhookTriggerEvents } from "@calcom/prisma/enums"; import { trpc } from "@calcom/trpc/react"; @@ -19,6 +21,8 @@ import { showToast } from "@calcom/ui/components/toast"; import { Tooltip } from "@calcom/ui/components/tooltip"; import { revalidateEventTypeEditPage } from "@calcom/web/app/(use-page-wrapper)/event-types/[type]/actions"; +import { DeleteWebhookDialog } from "./dialogs/DeleteWebhookDialog"; + type WebhookProps = { id: string; subscriberUrl: string; @@ -39,6 +43,7 @@ export default function EventTypeWebhookListItem(props: { const { t } = useLocale(); const utils = trpc.useUtils(); const { webhook } = props; + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const deleteWebhook = trpc.viewer.webhook.delete.useMutation({ async onSuccess() { @@ -47,12 +52,16 @@ export default function EventTypeWebhookListItem(props: { await utils.viewer.webhook.getByViewer.invalidate(); await utils.viewer.webhook.list.invalidate(); await utils.viewer.eventTypes.get.invalidate(); + setDeleteDialogOpen(false); + }, + onError() { + showToast(t("something_went_wrong"), "error"); + setDeleteDialogOpen(false); }, }); const toggleWebhook = trpc.viewer.webhook.edit.useMutation({ async onSuccess(data) { if (webhook.eventTypeId) revalidateEventTypeEditPage(webhook.eventTypeId); - // TODO: Better success message showToast(t(data?.active ? "enabled" : "disabled"), "success"); await utils.viewer.webhook.getByViewer.invalidate(); await utils.viewer.webhook.list.invalidate(); @@ -60,15 +69,6 @@ export default function EventTypeWebhookListItem(props: { }, }); - const onDeleteWebhook = () => { - // TODO: Confimation dialog before deleting - deleteWebhook.mutate({ - id: webhook.id, - eventTypeId: webhook.eventTypeId || undefined, - teamId: webhook.teamId || undefined, - }); - }; - return (
setDeleteDialogOpen(true)} + disabled={deleteWebhook.isPending} /> @@ -146,7 +148,11 @@ export default function EventTypeWebhookListItem(props: { - + setDeleteDialogOpen(true)} + disabled={deleteWebhook.isPending}> {t("delete")} @@ -154,6 +160,19 @@ export default function EventTypeWebhookListItem(props: {
)} + + { + deleteWebhook.mutate({ + id: webhook.id, + eventTypeId: webhook.eventTypeId || undefined, + teamId: webhook.teamId || undefined, + }); + }} + /> ); } diff --git a/apps/web/modules/webhooks/components/WebhookListItem.tsx b/apps/web/modules/webhooks/components/WebhookListItem.tsx index be795d8163..656b9d0adc 100644 --- a/apps/web/modules/webhooks/components/WebhookListItem.tsx +++ b/apps/web/modules/webhooks/components/WebhookListItem.tsx @@ -1,5 +1,8 @@ "use client"; +import Link from "next/link"; +import { useState } from "react"; + import { getWebhookVersionDocsUrl, getWebhookVersionLabel } from "@calcom/features/webhooks/lib/constants"; import type { Webhook } from "@calcom/features/webhooks/lib/dto/types"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -21,7 +24,8 @@ import { showToast } from "@calcom/ui/components/toast"; import { Tooltip } from "@calcom/ui/components/tooltip"; import { revalidateEventTypeEditPage } from "@calcom/web/app/(use-page-wrapper)/event-types/[type]/actions"; import { revalidateWebhooksList } from "@calcom/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/actions"; -import Link from "next/link"; + +import { DeleteWebhookDialog } from "./dialogs/DeleteWebhookDialog"; const MAX_BADGES_TWO_ROWS = 8; // Approximately 2 rows of badges @@ -38,6 +42,7 @@ export default function WebhookListItem(props: { const { t } = useLocale(); const utils = trpc.useUtils(); const { webhook } = props; + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const deleteWebhook = trpc.viewer.webhook.delete.useMutation({ async onSuccess() { @@ -47,13 +52,17 @@ export default function WebhookListItem(props: { await utils.viewer.webhook.getByViewer.invalidate(); await utils.viewer.webhook.list.invalidate(); await utils.viewer.eventTypes.get.invalidate(); + setDeleteDialogOpen(false); + }, + onError() { + showToast(t("something_went_wrong"), "error"); + setDeleteDialogOpen(false); }, }); const toggleWebhook = trpc.viewer.webhook.edit.useMutation({ async onSuccess(data) { if (webhook.eventTypeId) revalidateEventTypeEditPage(webhook.eventTypeId); revalidateWebhooksList(); - // TODO: Better success message showToast(t(data?.active ? "enabled" : "disabled"), "success"); await utils.viewer.webhook.getByViewer.invalidate(); await utils.viewer.webhook.list.invalidate(); @@ -61,17 +70,9 @@ export default function WebhookListItem(props: { }, }); - const onDeleteWebhook = () => { - // TODO: Confimation dialog before deleting - deleteWebhook.mutate({ - id: webhook.id, - eventTypeId: webhook.eventTypeId || undefined, - teamId: webhook.teamId || undefined, - }); - }; - return (
setDeleteDialogOpen(true)} + disabled={deleteWebhook.isPending} /> )} @@ -187,7 +190,11 @@ export default function WebhookListItem(props: { {props.permissions.canDeleteWebhook && ( - + setDeleteDialogOpen(true)} + disabled={deleteWebhook.isPending}> {t("delete")} @@ -196,6 +203,19 @@ export default function WebhookListItem(props: {
)} + + { + deleteWebhook.mutate({ + id: webhook.id, + eventTypeId: webhook.eventTypeId || undefined, + teamId: webhook.teamId || undefined, + }); + }} + /> ); } diff --git a/apps/web/modules/webhooks/components/dialogs/DeleteWebhookDialog.tsx b/apps/web/modules/webhooks/components/dialogs/DeleteWebhookDialog.tsx new file mode 100644 index 0000000000..4511d8b146 --- /dev/null +++ b/apps/web/modules/webhooks/components/dialogs/DeleteWebhookDialog.tsx @@ -0,0 +1,31 @@ +import { APP_NAME } from "@calcom/lib/constants"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Dialog, ConfirmationDialogContent } from "@calcom/ui/components/dialog"; + +interface DeleteWebhookDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onConfirm: () => void; + isPending: boolean; +} + +export function DeleteWebhookDialog({ open, onOpenChange, onConfirm, isPending }: DeleteWebhookDialogProps) { + const { t } = useLocale(); + + return ( + + { + e.preventDefault(); + onConfirm(); + }}> + {t("delete_webhook_confirmation_message", { appName: APP_NAME })} + + + ); +} diff --git a/apps/web/playwright/webhook.e2e.ts b/apps/web/playwright/webhook.e2e.ts index c2705e5a75..c1701a0f10 100644 --- a/apps/web/playwright/webhook.e2e.ts +++ b/apps/web/playwright/webhook.e2e.ts @@ -876,3 +876,35 @@ test.describe("OOO_CREATED", async () => { webhookReceiver.close(); }); }); + +test.describe("Webhook deletion", async () => { + test("shows confirmation dialog and deletes webhook on confirm", async ({ page, users }) => { + const user = await users.create(); + await user.apiLogin(); + + await page.goto("/settings/developer/webhooks"); + await page.click('[data-testid="new_webhook"]'); + await page.fill('[name="subscriberUrl"]', "https://example.com/test-webhook"); + + await Promise.all([ + page.click("[type=submit]"), + page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")), + ]); + + const webhookListItem = page.getByTestId("webhook-list-item"); + await expect(webhookListItem).toBeVisible(); + + const deleteButton = page.getByTestId("delete-webhook"); + await expect(deleteButton).toBeVisible(); + await deleteButton.click(); + + await expect(page.getByTestId("dialog-confirmation")).toBeVisible(); + + const deleteResponsePromise = page.waitForResponse((res) => res.url().includes("/api/trpc/webhook/delete")); + await page.getByTestId("dialog-confirmation").click(); + const deleteResponse = await deleteResponsePromise; + expect(deleteResponse.ok()).toBe(true); + + await expect(webhookListItem).not.toBeVisible(); + }); +}); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 13af35248e..e146e10b3b 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -254,7 +254,7 @@ "hey_there": "Hey there", "there": "there", "forgot_your_password_calcom": "Forgot your password? - {{appName}}", - "delete_webhook_confirmation_message": "Are you sure you want to delete this webhook? You will no longer receive {{appName}} meeting data at a specified URL, in real-time, when an event is scheduled or canceled.", + "delete_webhook_confirmation_message": "Are you sure you want to delete this webhook? You will no longer receive {{appName}} data at the specified URL for your configured event triggers.", "confirm_delete_webhook": "Delete webhook", "edit_webhook": "Edit webhook", "delete_webhook": "Delete webhook",