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:
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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user