Skip to content

Commit 9a5323b

Browse files
KartikLabhshetwarkeithwillcodedevin-ai-integration[bot]romitg2
authored
feat: add delete confirmation dialog to webhook list items (calcom#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>
1 parent 9a287ac commit 9a5323b

5 files changed

Lines changed: 128 additions & 26 deletions

File tree

apps/web/modules/webhooks/components/EventTypeWebhookListItem.tsx

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"use client";
22

3+
import { useState } from "react";
4+
35
import { useLocale } from "@calcom/lib/hooks/useLocale";
46
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
57
import { trpc } from "@calcom/trpc/react";
@@ -19,6 +21,8 @@ import { showToast } from "@calcom/ui/components/toast";
1921
import { Tooltip } from "@calcom/ui/components/tooltip";
2022
import { revalidateEventTypeEditPage } from "@calcom/web/app/(use-page-wrapper)/event-types/[type]/actions";
2123

24+
import { DeleteWebhookDialog } from "./dialogs/DeleteWebhookDialog";
25+
2226
type WebhookProps = {
2327
id: string;
2428
subscriberUrl: string;
@@ -39,6 +43,7 @@ export default function EventTypeWebhookListItem(props: {
3943
const { t } = useLocale();
4044
const utils = trpc.useUtils();
4145
const { webhook } = props;
46+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
4247

4348
const deleteWebhook = trpc.viewer.webhook.delete.useMutation({
4449
async onSuccess() {
@@ -47,28 +52,23 @@ export default function EventTypeWebhookListItem(props: {
4752
await utils.viewer.webhook.getByViewer.invalidate();
4853
await utils.viewer.webhook.list.invalidate();
4954
await utils.viewer.eventTypes.get.invalidate();
55+
setDeleteDialogOpen(false);
56+
},
57+
onError() {
58+
showToast(t("something_went_wrong"), "error");
59+
setDeleteDialogOpen(false);
5060
},
5161
});
5262
const toggleWebhook = trpc.viewer.webhook.edit.useMutation({
5363
async onSuccess(data) {
5464
if (webhook.eventTypeId) revalidateEventTypeEditPage(webhook.eventTypeId);
55-
// TODO: Better success message
5665
showToast(t(data?.active ? "enabled" : "disabled"), "success");
5766
await utils.viewer.webhook.getByViewer.invalidate();
5867
await utils.viewer.webhook.list.invalidate();
5968
await utils.viewer.eventTypes.get.invalidate();
6069
},
6170
});
6271

63-
const onDeleteWebhook = () => {
64-
// TODO: Confimation dialog before deleting
65-
deleteWebhook.mutate({
66-
id: webhook.id,
67-
eventTypeId: webhook.eventTypeId || undefined,
68-
teamId: webhook.teamId || undefined,
69-
});
70-
};
71-
7272
return (
7373
<div
7474
className={classNames(
@@ -130,7 +130,9 @@ export default function EventTypeWebhookListItem(props: {
130130
color="destructive"
131131
StartIcon="trash"
132132
variant="icon"
133-
onClick={onDeleteWebhook}
133+
data-testid="delete-webhook"
134+
onClick={() => setDeleteDialogOpen(true)}
135+
disabled={deleteWebhook.isPending}
134136
/>
135137

136138
<Dropdown>
@@ -146,14 +148,31 @@ export default function EventTypeWebhookListItem(props: {
146148
<DropdownMenuSeparator />
147149

148150
<DropdownMenuItem>
149-
<DropdownItem StartIcon="trash" color="destructive" onClick={onDeleteWebhook}>
151+
<DropdownItem
152+
StartIcon="trash"
153+
color="destructive"
154+
onClick={() => setDeleteDialogOpen(true)}
155+
disabled={deleteWebhook.isPending}>
150156
{t("delete")}
151157
</DropdownItem>
152158
</DropdownMenuItem>
153159
</DropdownMenuContent>
154160
</Dropdown>
155161
</div>
156162
)}
163+
164+
<DeleteWebhookDialog
165+
open={deleteDialogOpen}
166+
onOpenChange={setDeleteDialogOpen}
167+
isPending={deleteWebhook.isPending}
168+
onConfirm={() => {
169+
deleteWebhook.mutate({
170+
id: webhook.id,
171+
eventTypeId: webhook.eventTypeId || undefined,
172+
teamId: webhook.teamId || undefined,
173+
});
174+
}}
175+
/>
157176
</div>
158177
);
159178
}

apps/web/modules/webhooks/components/WebhookListItem.tsx

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"use client";
22

3+
import Link from "next/link";
4+
import { useState } from "react";
5+
36
import { getWebhookVersionDocsUrl, getWebhookVersionLabel } from "@calcom/features/webhooks/lib/constants";
47
import type { Webhook } from "@calcom/features/webhooks/lib/dto/types";
58
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -21,7 +24,8 @@ import { showToast } from "@calcom/ui/components/toast";
2124
import { Tooltip } from "@calcom/ui/components/tooltip";
2225
import { revalidateEventTypeEditPage } from "@calcom/web/app/(use-page-wrapper)/event-types/[type]/actions";
2326
import { revalidateWebhooksList } from "@calcom/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/actions";
24-
import Link from "next/link";
27+
28+
import { DeleteWebhookDialog } from "./dialogs/DeleteWebhookDialog";
2529

2630
const MAX_BADGES_TWO_ROWS = 8; // Approximately 2 rows of badges
2731

@@ -38,6 +42,7 @@ export default function WebhookListItem(props: {
3842
const { t } = useLocale();
3943
const utils = trpc.useUtils();
4044
const { webhook } = props;
45+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
4146

4247
const deleteWebhook = trpc.viewer.webhook.delete.useMutation({
4348
async onSuccess() {
@@ -47,31 +52,27 @@ export default function WebhookListItem(props: {
4752
await utils.viewer.webhook.getByViewer.invalidate();
4853
await utils.viewer.webhook.list.invalidate();
4954
await utils.viewer.eventTypes.get.invalidate();
55+
setDeleteDialogOpen(false);
56+
},
57+
onError() {
58+
showToast(t("something_went_wrong"), "error");
59+
setDeleteDialogOpen(false);
5060
},
5161
});
5262
const toggleWebhook = trpc.viewer.webhook.edit.useMutation({
5363
async onSuccess(data) {
5464
if (webhook.eventTypeId) revalidateEventTypeEditPage(webhook.eventTypeId);
5565
revalidateWebhooksList();
56-
// TODO: Better success message
5766
showToast(t(data?.active ? "enabled" : "disabled"), "success");
5867
await utils.viewer.webhook.getByViewer.invalidate();
5968
await utils.viewer.webhook.list.invalidate();
6069
await utils.viewer.eventTypes.get.invalidate();
6170
},
6271
});
6372

64-
const onDeleteWebhook = () => {
65-
// TODO: Confimation dialog before deleting
66-
deleteWebhook.mutate({
67-
id: webhook.id,
68-
eventTypeId: webhook.eventTypeId || undefined,
69-
teamId: webhook.teamId || undefined,
70-
});
71-
};
72-
7373
return (
7474
<div
75+
data-testid="webhook-list-item"
7576
className={classNames(
7677
"flex w-full justify-between p-4",
7778
props.lastItem ? "" : "border-subtle border-b"
@@ -166,7 +167,9 @@ export default function WebhookListItem(props: {
166167
color="destructive"
167168
StartIcon="trash"
168169
variant="icon"
169-
onClick={onDeleteWebhook}
170+
data-testid="delete-webhook"
171+
onClick={() => setDeleteDialogOpen(true)}
172+
disabled={deleteWebhook.isPending}
170173
/>
171174
)}
172175

@@ -187,7 +190,11 @@ export default function WebhookListItem(props: {
187190

188191
{props.permissions.canDeleteWebhook && (
189192
<DropdownMenuItem>
190-
<DropdownItem StartIcon="trash" color="destructive" onClick={onDeleteWebhook}>
193+
<DropdownItem
194+
StartIcon="trash"
195+
color="destructive"
196+
onClick={() => setDeleteDialogOpen(true)}
197+
disabled={deleteWebhook.isPending}>
191198
{t("delete")}
192199
</DropdownItem>
193200
</DropdownMenuItem>
@@ -196,6 +203,19 @@ export default function WebhookListItem(props: {
196203
</Dropdown>
197204
</div>
198205
)}
206+
207+
<DeleteWebhookDialog
208+
open={deleteDialogOpen}
209+
onOpenChange={setDeleteDialogOpen}
210+
isPending={deleteWebhook.isPending}
211+
onConfirm={() => {
212+
deleteWebhook.mutate({
213+
id: webhook.id,
214+
eventTypeId: webhook.eventTypeId || undefined,
215+
teamId: webhook.teamId || undefined,
216+
});
217+
}}
218+
/>
199219
</div>
200220
);
201221
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { APP_NAME } from "@calcom/lib/constants";
2+
import { useLocale } from "@calcom/lib/hooks/useLocale";
3+
import { Dialog, ConfirmationDialogContent } from "@calcom/ui/components/dialog";
4+
5+
interface DeleteWebhookDialogProps {
6+
open: boolean;
7+
onOpenChange: (open: boolean) => void;
8+
onConfirm: () => void;
9+
isPending: boolean;
10+
}
11+
12+
export function DeleteWebhookDialog({ open, onOpenChange, onConfirm, isPending }: DeleteWebhookDialogProps) {
13+
const { t } = useLocale();
14+
15+
return (
16+
<Dialog open={open} onOpenChange={onOpenChange}>
17+
<ConfirmationDialogContent
18+
variety="danger"
19+
title={t("delete_webhook")}
20+
confirmBtnText={t("confirm_delete_webhook")}
21+
loadingText={t("confirm_delete_webhook")}
22+
isPending={isPending}
23+
onConfirm={(e) => {
24+
e.preventDefault();
25+
onConfirm();
26+
}}>
27+
{t("delete_webhook_confirmation_message", { appName: APP_NAME })}
28+
</ConfirmationDialogContent>
29+
</Dialog>
30+
);
31+
}

apps/web/playwright/webhook.e2e.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,3 +876,35 @@ test.describe("OOO_CREATED", async () => {
876876
webhookReceiver.close();
877877
});
878878
});
879+
880+
test.describe("Webhook deletion", async () => {
881+
test("shows confirmation dialog and deletes webhook on confirm", async ({ page, users }) => {
882+
const user = await users.create();
883+
await user.apiLogin();
884+
885+
await page.goto("/settings/developer/webhooks");
886+
await page.click('[data-testid="new_webhook"]');
887+
await page.fill('[name="subscriberUrl"]', "https://example.com/test-webhook");
888+
889+
await Promise.all([
890+
page.click("[type=submit]"),
891+
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
892+
]);
893+
894+
const webhookListItem = page.getByTestId("webhook-list-item");
895+
await expect(webhookListItem).toBeVisible();
896+
897+
const deleteButton = page.getByTestId("delete-webhook");
898+
await expect(deleteButton).toBeVisible();
899+
await deleteButton.click();
900+
901+
await expect(page.getByTestId("dialog-confirmation")).toBeVisible();
902+
903+
const deleteResponsePromise = page.waitForResponse((res) => res.url().includes("/api/trpc/webhook/delete"));
904+
await page.getByTestId("dialog-confirmation").click();
905+
const deleteResponse = await deleteResponsePromise;
906+
expect(deleteResponse.ok()).toBe(true);
907+
908+
await expect(webhookListItem).not.toBeVisible();
909+
});
910+
});

apps/web/public/static/locales/en/common.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@
254254
"hey_there": "Hey there",
255255
"there": "there",
256256
"forgot_your_password_calcom": "Forgot your password? - {{appName}}",
257-
"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.",
257+
"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.",
258258
"confirm_delete_webhook": "Delete webhook",
259259
"edit_webhook": "Edit webhook",
260260
"delete_webhook": "Delete webhook",

0 commit comments

Comments
 (0)