feat: apply Frame component to webhooks settings page (#27291)

* feat: apply Frame component to webhooks settings page

Co-Authored-By: peer@cal.com <peer@cal.com>

* refactor: include header and description in Frame component

Co-Authored-By: peer@cal.com <peer@cal.com>

* refactor: wrap each webhook in individual FramePanel

Co-Authored-By: peer@cal.com <peer@cal.com>

* refactor: implement two-row badge limit with overflow tooltip for webhooks

Co-Authored-By: peer@cal.com <peer@cal.com>

* style: improve spacing between webhook items and header

Co-Authored-By: peer@cal.com <peer@cal.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Peer Richelsen
2026-01-27 14:48:33 +00:00
committed by GitHub
co-authored by peer@cal.com Devin AI
parent cb7960f571
commit 85f3cd6228
2 changed files with 62 additions and 46 deletions
@@ -1,8 +1,6 @@
"use client";
import Link from "next/link";
import { getWebhookVersionLabel, getWebhookVersionDocsUrl } from "@calcom/features/webhooks/lib/constants";
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";
@@ -23,6 +21,9 @@ 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";
const MAX_BADGES_TWO_ROWS = 8; // Approximately 2 rows of badges
export default function WebhookListItem(props: {
webhook: Webhook;
@@ -105,7 +106,7 @@ export default function WebhookListItem(props: {
</div>
<Tooltip content={t("triggers_when")}>
<div className="flex w-4/5 flex-wrap">
{webhook.eventTriggers.map((trigger) => (
{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"
@@ -114,6 +115,22 @@ export default function WebhookListItem(props: {
{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>
@@ -1,17 +1,15 @@
"use client";
import { useRouter } from "next/navigation";
import { useBookerUrl } from "@calcom/features/bookings/hooks/useBookerUrl";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import classNames from "@calcom/ui/classNames";
import { Avatar } from "@calcom/ui/components/avatar";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { WebhookListItem, CreateNewWebhookButton } from "../components";
import { Frame, FrameDescription, FrameHeader, FramePanel, FrameTitle } from "@coss/ui/components/frame";
import { useRouter } from "next/navigation";
import React from "react";
import { CreateNewWebhookButton, WebhookListItem } from "../components";
type WebhooksByViewer = RouterOutputs["viewer"]["webhook"]["getByViewer"];
@@ -36,17 +34,20 @@ const WebhooksList = ({ webhooksByViewer }: { webhooksByViewer: WebhooksByViewer
const hasTeams = profiles && profiles.length > 1;
return (
<SettingsHeader
title={t("webhooks")}
description={t("add_webhook_description", { appName: APP_NAME })}
CTA={<CreateNewWebhookButton />}
borderInShellHeader={true}>
{webhookGroups.length ? (
<Frame>
<FrameHeader className="mb-2 flex-row items-center justify-between">
<div>
<FrameTitle className="text-lg">{t("webhooks")}</FrameTitle>
<FrameDescription>{t("add_webhook_description", { appName: APP_NAME })}</FrameDescription>
</div>
<CreateNewWebhookButton />
</FrameHeader>
{webhookGroups.length ? (
<>
{webhookGroups.map((group) => (
<div key={group.teamId}>
<React.Fragment key={group.teamId}>
{hasTeams && (
<div className="items-centers flex">
<div className="mb-2 mt-4 flex items-center px-1">
<Avatar
alt={group.profile.image || ""}
imageSrc={group.profile.image || `${bookerUrl}/${group.profile.name}/avatar.png`}
@@ -58,37 +59,35 @@ const WebhooksList = ({ webhooksByViewer }: { webhooksByViewer: WebhooksByViewer
</div>
</div>
)}
<div className="flex flex-col" key={group.profile.slug}>
<div className={classNames("border-subtle mb-8 rounded-b-lg border border-t-0", hasTeams && "mt-3")}>
{group.webhooks.map((webhook, index) => (
<WebhookListItem
key={webhook.id}
webhook={webhook}
lastItem={group.webhooks.length === index + 1}
permissions={{
canEditWebhook: group?.metadata?.canModify ?? false,
canDeleteWebhook: group?.metadata?.canDelete ?? false,
}}
onEditWebhook={() =>
router.push(`${WEBAPP_URL}/settings/developer/webhooks/${webhook.id}`)
}
/>
))}
</div>
</div>
</div>
{group.webhooks.map((webhook) => (
<FramePanel key={webhook.id} className="mt-2">
<WebhookListItem
webhook={webhook}
lastItem={true}
permissions={{
canEditWebhook: group?.metadata?.canModify ?? false,
canDeleteWebhook: group?.metadata?.canDelete ?? false,
}}
onEditWebhook={() =>
router.push(`${WEBAPP_URL}/settings/developer/webhooks/${webhook.id}`)
}
/>
</FramePanel>
))}
</React.Fragment>
))}
</div>
</>
) : (
<EmptyScreen
Icon="link"
headline={t("create_your_first_webhook")}
description={t("create_your_first_webhook_description", { appName: APP_NAME })}
className="rounded-b-lg rounded-t-none border-t-0"
buttonRaw={<CreateNewWebhookButton />}
/>
<FramePanel>
<EmptyScreen
Icon="link"
headline={t("create_your_first_webhook")}
description={t("create_your_first_webhook_description", { appName: APP_NAME })}
buttonRaw={<CreateNewWebhookButton />}
/>
</FramePanel>
)}
</SettingsHeader>
</Frame>
);
};