* 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>
222 lines
7.9 KiB
TypeScript
222 lines
7.9 KiB
TypeScript
"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";
|
|
import { trpc } from "@calcom/trpc/react";
|
|
import classNames from "@calcom/ui/classNames";
|
|
import { Badge } from "@calcom/ui/components/badge";
|
|
import { Button } from "@calcom/ui/components/button";
|
|
import {
|
|
Dropdown,
|
|
DropdownItem,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@calcom/ui/components/dropdown";
|
|
import { Switch } from "@calcom/ui/components/form";
|
|
import { Icon } from "@calcom/ui/components/icon";
|
|
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 { DeleteWebhookDialog } from "./dialogs/DeleteWebhookDialog";
|
|
|
|
const MAX_BADGES_TWO_ROWS = 8; // Approximately 2 rows of badges
|
|
|
|
export default function WebhookListItem(props: {
|
|
webhook: Webhook;
|
|
canEditWebhook?: boolean;
|
|
onEditWebhook: () => void;
|
|
lastItem: boolean;
|
|
permissions: {
|
|
canEditWebhook?: boolean;
|
|
canDeleteWebhook?: boolean;
|
|
};
|
|
}) {
|
|
const { t } = useLocale();
|
|
const utils = trpc.useUtils();
|
|
const { webhook } = props;
|
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
|
|
const deleteWebhook = trpc.viewer.webhook.delete.useMutation({
|
|
async onSuccess() {
|
|
if (webhook.eventTypeId) revalidateEventTypeEditPage(webhook.eventTypeId);
|
|
revalidateWebhooksList();
|
|
showToast(t("webhook_removed_successfully"), "success");
|
|
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();
|
|
showToast(t(data?.active ? "enabled" : "disabled"), "success");
|
|
await utils.viewer.webhook.getByViewer.invalidate();
|
|
await utils.viewer.webhook.list.invalidate();
|
|
await utils.viewer.eventTypes.get.invalidate();
|
|
},
|
|
});
|
|
|
|
return (
|
|
<div
|
|
data-testid="webhook-list-item"
|
|
className={classNames(
|
|
"flex w-full justify-between p-4",
|
|
props.lastItem ? "" : "border-subtle border-b"
|
|
)}>
|
|
<div className="w-full truncate">
|
|
<div className="flex">
|
|
<Tooltip content={webhook.subscriberUrl}>
|
|
<p className="text-emphasis max-w-[600px] truncate text-sm font-medium">
|
|
{webhook.subscriberUrl}
|
|
</p>
|
|
</Tooltip>
|
|
{!props.permissions.canEditWebhook && (
|
|
<Badge variant="gray" className="ml-2 ">
|
|
{t("readonly")}
|
|
</Badge>
|
|
)}
|
|
<Tooltip content={t("webhook_version")}>
|
|
<div className="flex items-center">
|
|
<Badge variant="blue" className="ml-2">
|
|
{getWebhookVersionLabel(webhook.version)}
|
|
</Badge>
|
|
</div>
|
|
</Tooltip>
|
|
<Tooltip content={t("webhook_version_docs", { version: getWebhookVersionLabel(webhook.version) })}>
|
|
<Link
|
|
href={getWebhookVersionDocsUrl(webhook.version)}
|
|
target="_blank"
|
|
className="text-subtle hover:text-emphasis ml-1 flex items-center">
|
|
<Icon name="external-link" className="h-4 w-4" />
|
|
</Link>
|
|
</Tooltip>
|
|
</div>
|
|
<Tooltip content={t("triggers_when")}>
|
|
<div className="flex w-4/5 flex-wrap">
|
|
{webhook.eventTriggers.slice(0, MAX_BADGES_TWO_ROWS).map((trigger) => (
|
|
<Badge
|
|
key={trigger}
|
|
className="mt-2.5 basis-1/5 ltr:mr-2 rtl:ml-2"
|
|
variant="gray"
|
|
startIcon="zap">
|
|
{t(`${trigger.toLowerCase()}`)}
|
|
</Badge>
|
|
))}
|
|
{webhook.eventTriggers.length > MAX_BADGES_TWO_ROWS && (
|
|
<Tooltip
|
|
content={
|
|
<div className="flex flex-col gap-1 p-1">
|
|
{webhook.eventTriggers.slice(MAX_BADGES_TWO_ROWS).map((trigger) => (
|
|
<span key={trigger} className="text-xs">
|
|
{t(`${trigger.toLowerCase()}`)}
|
|
</span>
|
|
))}
|
|
</div>
|
|
}>
|
|
<Badge className="mt-2.5 cursor-help ltr:mr-2 rtl:ml-2" variant="gray">
|
|
+{webhook.eventTriggers.length - MAX_BADGES_TWO_ROWS} {t("more")}
|
|
</Badge>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
</Tooltip>
|
|
</div>
|
|
{(props.permissions.canEditWebhook || props.permissions.canDeleteWebhook) && (
|
|
<div className="ml-2 flex items-center space-x-4">
|
|
<Switch
|
|
defaultChecked={webhook.active}
|
|
data-testid="webhook-switch"
|
|
disabled={!props.permissions.canEditWebhook}
|
|
onCheckedChange={(checked) =>
|
|
toggleWebhook.mutate({
|
|
id: webhook.id,
|
|
active: checked,
|
|
payloadTemplate: webhook.payloadTemplate,
|
|
eventTypeId: webhook.eventTypeId || undefined,
|
|
})
|
|
}
|
|
/>
|
|
|
|
{props.permissions.canEditWebhook && (
|
|
<Button
|
|
className="hidden lg:flex"
|
|
color="secondary"
|
|
onClick={props.onEditWebhook}
|
|
data-testid="webhook-edit-button">
|
|
{t("edit")}
|
|
</Button>
|
|
)}
|
|
|
|
{props.permissions.canDeleteWebhook && (
|
|
<Button
|
|
className="hidden lg:flex"
|
|
color="destructive"
|
|
StartIcon="trash"
|
|
variant="icon"
|
|
data-testid="delete-webhook"
|
|
onClick={() => setDeleteDialogOpen(true)}
|
|
disabled={deleteWebhook.isPending}
|
|
/>
|
|
)}
|
|
|
|
<Dropdown>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button className="lg:hidden" StartIcon="ellipsis" variant="icon" color="secondary" />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
{props.permissions.canEditWebhook && (
|
|
<DropdownMenuItem>
|
|
<DropdownItem StartIcon="pencil" color="secondary" onClick={props.onEditWebhook}>
|
|
{t("edit")}
|
|
</DropdownItem>
|
|
</DropdownMenuItem>
|
|
)}
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
{props.permissions.canDeleteWebhook && (
|
|
<DropdownMenuItem>
|
|
<DropdownItem
|
|
StartIcon="trash"
|
|
color="destructive"
|
|
onClick={() => setDeleteDialogOpen(true)}
|
|
disabled={deleteWebhook.isPending}>
|
|
{t("delete")}
|
|
</DropdownItem>
|
|
</DropdownMenuItem>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</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>
|
|
);
|
|
}
|