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 <keithwillcode@gmail.com>
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 <romitgabani@icloud.com>
This commit is contained in:
authored and GitHub committed 2026-02-05 08:32:51 +05:30
1 parent 9a287ac876
commit 9a5323bbb6
5 files changed
+128 -26

No files matched your search

@@ -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 (
<div
className={classNames(
@@ -130,7 +130,9 @@ export default function EventTypeWebhookListItem(props: {
color="destructive"
StartIcon="trash"
variant="icon"
onClick={onDeleteWebhook}
data-testid="delete-webhook"
onClick={() => setDeleteDialogOpen(true)}
disabled={deleteWebhook.isPending}
/>
<Dropdown>
@@ -146,7 +148,11 @@ export default function EventTypeWebhookListItem(props: {
<DropdownMenuSeparator />
<DropdownMenuItem>
<DropdownItem StartIcon="trash" color="destructive" onClick={onDeleteWebhook}>
<DropdownItem
StartIcon="trash"
color="destructive"
onClick={() => setDeleteDialogOpen(true)}
disabled={deleteWebhook.isPending}>
{t("delete")}
</DropdownItem>
</DropdownMenuItem>
@@ -154,6 +160,19 @@ export default function EventTypeWebhookListItem(props: {
</Dropdown>
</div>
)}
<DeleteWebhookDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
isPending={deleteWebhook.isPending}
onConfirm={() => {
deleteWebhook.mutate({
id: webhook.id,
eventTypeId: webhook.eventTypeId || undefined,
teamId: webhook.teamId || undefined,
});
}}
/>
</div>
);
}
@@ -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 (
<div
data-testid="webhook-list-item"
className={classNames(
"flex w-full justify-between p-4",
props.lastItem ? "" : "border-subtle border-b"
@@ -166,7 +167,9 @@ export default function WebhookListItem(props: {
color="destructive"
StartIcon="trash"
variant="icon"
onClick={onDeleteWebhook}
data-testid="delete-webhook"
onClick={() => setDeleteDialogOpen(true)}
disabled={deleteWebhook.isPending}
/>
)}
@@ -187,7 +190,11 @@ export default function WebhookListItem(props: {
{props.permissions.canDeleteWebhook && (
<DropdownMenuItem>
<DropdownItem StartIcon="trash" color="destructive" onClick={onDeleteWebhook}>
<DropdownItem
StartIcon="trash"
color="destructive"
onClick={() => setDeleteDialogOpen(true)}
disabled={deleteWebhook.isPending}>
{t("delete")}
</DropdownItem>
</DropdownMenuItem>
@@ -196,6 +203,19 @@ export default function WebhookListItem(props: {
</Dropdown>
</div>
)}
<DeleteWebhookDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
isPending={deleteWebhook.isPending}
onConfirm={() => {
deleteWebhook.mutate({
id: webhook.id,
eventTypeId: webhook.eventTypeId || undefined,
teamId: webhook.teamId || undefined,
});
}}
/>
</div>
);
}
@@ -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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<ConfirmationDialogContent
variety="danger"
title={t("delete_webhook")}
confirmBtnText={t("confirm_delete_webhook")}
loadingText={t("confirm_delete_webhook")}
isPending={isPending}
onConfirm={(e) => {
e.preventDefault();
onConfirm();
}}>
{t("delete_webhook_confirmation_message", { appName: APP_NAME })}
</ConfirmationDialogContent>
</Dialog>
);
}
+32
View File
@@ -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();
});
});
@@ -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",