feat: Webhook support for Managed Events (#17986)
* managed event webhooks * more fixes * fix types --WIP * fix types * fix test * add test * remove remnant comments * address feedback * fix test * fix test * fix tests * change of plans \(00)/ * remove console log * remove unnecessary comments * revert tests * list active webhooks in child event types * add translations * allow editing own event types * add locked info for children * secure create handler * fix for isLocked check --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
1 parent
4181b5988a
commit
4bf5e3cdf5
8 files changed
+257
-46
No files matched your search
@@ -185,14 +185,6 @@ export default async function handleChildrenEventTypes({
|
||||
*/
|
||||
rrSegmentQueryValue: undefined,
|
||||
assignRRMembersUsingSegment: false,
|
||||
|
||||
// Reserved for future releases
|
||||
/*
|
||||
webhooks: eventType.webhooks && {
|
||||
createMany: {
|
||||
data: eventType.webhooks?.map((wh) => ({ ...wh, eventTypeId: undefined })),
|
||||
},
|
||||
},*/
|
||||
},
|
||||
});
|
||||
})
|
||||
@@ -275,23 +267,6 @@ export default async function handleChildrenEventTypes({
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Reserved for future releases
|
||||
/**
|
||||
const updatedOldWebhooks = await prisma.webhook.updateMany({
|
||||
where: {
|
||||
userId: {
|
||||
in: oldUserIds,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
...eventType.webhooks,
|
||||
},
|
||||
});
|
||||
console.log(
|
||||
"handleChildrenEventTypes:updatedOldWebhooks",
|
||||
JSON.stringify({ updatedOldWebhooks }, null, 2)
|
||||
);*/
|
||||
}
|
||||
|
||||
// Old users deleted
|
||||
|
||||
@@ -7,8 +7,8 @@ import { useFormContext } from "react-hook-form";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { FormValues, EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
|
||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||
import EventTypeWebhookListItem from "@calcom/features/webhooks/components/EventTypeWebhookListItem";
|
||||
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
||||
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
||||
import { subscriberUrlReserved } from "@calcom/features/webhooks/lib/subscriberUrlReserved";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -103,8 +103,9 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
translate: t,
|
||||
formMethods,
|
||||
});
|
||||
const webhookLockedStatus = shouldLockDisableProps("webhooks");
|
||||
|
||||
const webhooksDisableProps = shouldLockDisableProps("webhooks", { simple: true });
|
||||
const lockedText = webhooksDisableProps.isLocked ? "locked" : "unlocked";
|
||||
const cannotEditWebhooks = isChildrenManagedEventType ? webhooksDisableProps.isLocked : false;
|
||||
return (
|
||||
<div>
|
||||
{webhooks && !isLoading && (
|
||||
@@ -112,12 +113,31 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
<div>
|
||||
<div>
|
||||
<>
|
||||
{isManagedEventType && (
|
||||
{(isManagedEventType || isChildrenManagedEventType) && (
|
||||
<Alert
|
||||
severity="neutral"
|
||||
severity={webhooksDisableProps.isLocked ? "neutral" : "green"}
|
||||
className="mb-2"
|
||||
title={t("locked_for_members")}
|
||||
message={t("locked_webhooks_description")}
|
||||
title={
|
||||
<Trans
|
||||
i18nKey={`${lockedText}_${isManagedEventType ? "for_members" : "by_team_admins"}`}>
|
||||
{lockedText[0].toUpperCase()}
|
||||
{lockedText.slice(1)} {isManagedEventType ? "for members" : "by team admins"}
|
||||
</Trans>
|
||||
}
|
||||
actions={
|
||||
<div className="flex h-full items-center">{webhooksDisableProps.LockedIcon}</div>
|
||||
}
|
||||
message={
|
||||
<Trans
|
||||
i18nKey={`webhooks_${lockedText}_${
|
||||
isManagedEventType ? "for_members" : "by_team_admins"
|
||||
}_description`}>
|
||||
{isManagedEventType ? "Members" : "You"}{" "}
|
||||
{webhooksDisableProps.isLocked
|
||||
? "will be able to see the active webhooks but will not be able to edit any webhook settings"
|
||||
: "will be able to see the active webhooks and will be able to edit any webhook settings"}
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{webhooks.length ? (
|
||||
@@ -130,7 +150,7 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
{t("add_webhook_description", { appName: APP_NAME })}
|
||||
</p>
|
||||
</div>
|
||||
{isChildrenManagedEventType && !isManagedEventType ? (
|
||||
{cannotEditWebhooks ? (
|
||||
<Button StartIcon="lock" color="secondary" disabled>
|
||||
{t("locked_by_team_admin")}
|
||||
</Button>
|
||||
@@ -142,15 +162,15 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
<div className="border-subtle my-8 rounded-md border">
|
||||
{webhooks.map((webhook, index) => {
|
||||
return (
|
||||
<WebhookListItem
|
||||
<EventTypeWebhookListItem
|
||||
key={webhook.id}
|
||||
webhook={webhook}
|
||||
lastItem={webhooks.length === index + 1}
|
||||
canEditWebhook={!webhookLockedStatus.disabled}
|
||||
onEditWebhook={() => {
|
||||
setEditModalOpen(true);
|
||||
setWebhookToEdit(webhook);
|
||||
}}
|
||||
readOnly={isChildrenManagedEventType && webhook.eventTypeId !== eventType.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -174,7 +194,7 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
headline={t("create_your_first_webhook")}
|
||||
description={t("first_event_type_webhook_description")}
|
||||
buttonRaw={
|
||||
isChildrenManagedEventType && !isManagedEventType ? (
|
||||
cannotEditWebhooks ? (
|
||||
<Button StartIcon="lock" color="secondary" disabled>
|
||||
{t("locked_by_team_admin")}
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
showToast,
|
||||
Switch,
|
||||
Tooltip,
|
||||
} from "@calcom/ui";
|
||||
|
||||
type WebhookProps = {
|
||||
id: string;
|
||||
subscriberUrl: string;
|
||||
payloadTemplate: string | null;
|
||||
active: boolean;
|
||||
eventTriggers: WebhookTriggerEvents[];
|
||||
secret: string | null;
|
||||
eventTypeId: number | null;
|
||||
teamId: number | null;
|
||||
};
|
||||
|
||||
export default function EventTypeWebhookListItem(props: {
|
||||
webhook: WebhookProps;
|
||||
onEditWebhook: () => void;
|
||||
lastItem: boolean;
|
||||
readOnly?: boolean;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
const { webhook } = props;
|
||||
|
||||
const deleteWebhook = trpc.viewer.webhook.delete.useMutation({
|
||||
async onSuccess() {
|
||||
showToast(t("webhook_removed_successfully"), "success");
|
||||
await utils.viewer.webhook.getByViewer.invalidate();
|
||||
await utils.viewer.webhook.list.invalidate();
|
||||
await utils.viewer.eventTypes.get.invalidate();
|
||||
},
|
||||
});
|
||||
const toggleWebhook = trpc.viewer.webhook.edit.useMutation({
|
||||
async onSuccess(data) {
|
||||
// TODO: Better success message
|
||||
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();
|
||||
},
|
||||
});
|
||||
|
||||
const onDeleteWebhook = () => {
|
||||
// TODO: Confimation dialog before deleting
|
||||
deleteWebhook.mutate({
|
||||
id: webhook.id,
|
||||
eventTypeId: webhook.eventTypeId || undefined,
|
||||
teamId: webhook.teamId || undefined,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
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.readOnly && (
|
||||
<Badge variant="gray" className="ml-2 ">
|
||||
{t("readonly")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip content={t("triggers_when")}>
|
||||
<div className="flex w-4/5 flex-wrap">
|
||||
{webhook.eventTriggers.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>
|
||||
))}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{!props.readOnly && (
|
||||
<div className="ml-2 flex items-center space-x-4">
|
||||
<Switch
|
||||
defaultChecked={webhook.active}
|
||||
data-testid="webhook-switch"
|
||||
onCheckedChange={() =>
|
||||
toggleWebhook.mutate({
|
||||
id: webhook.id,
|
||||
active: !webhook.active,
|
||||
payloadTemplate: webhook.payloadTemplate,
|
||||
eventTypeId: webhook.eventTypeId || undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="hidden lg:flex"
|
||||
color="secondary"
|
||||
onClick={props.onEditWebhook}
|
||||
data-testid="webhook-edit-button">
|
||||
{t("edit")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="hidden lg:flex"
|
||||
color="destructive"
|
||||
StartIcon="trash"
|
||||
variant="icon"
|
||||
onClick={onDeleteWebhook}
|
||||
/>
|
||||
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="lg:hidden" StartIcon="ellipsis" variant="icon" color="secondary" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem StartIcon="pencil" color="secondary" onClick={props.onEditWebhook}>
|
||||
{t("edit")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem StartIcon="trash" color="destructive" onClick={onDeleteWebhook}>
|
||||
{t("delete")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,20 @@ const getWebhooks = async (options: GetSubscriberOptions, prisma: PrismaClient =
|
||||
const orgId = options.orgId ?? 0;
|
||||
const oAuthClientId = options.oAuthClientId ?? "";
|
||||
|
||||
const managedChildEventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
id: eventTypeId,
|
||||
parentId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
parentId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const managedParentEventTypeId = managedChildEventType?.parentId ?? 0;
|
||||
|
||||
// if we have userId and teamId it is a managed event type and should trigger for team and user
|
||||
const allWebhooks = await prisma.webhook.findMany({
|
||||
where: {
|
||||
@@ -32,6 +46,9 @@ const getWebhooks = async (options: GetSubscriberOptions, prisma: PrismaClient =
|
||||
{
|
||||
eventTypeId,
|
||||
},
|
||||
{
|
||||
eventTypeId: managedParentEventTypeId,
|
||||
},
|
||||
{
|
||||
teamId: {
|
||||
in: [...teamIds, orgId],
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
|
||||
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { useMemo } from "react";
|
||||
@@ -114,8 +113,8 @@ export const useTabsNavigations = ({
|
||||
}`,
|
||||
});
|
||||
}
|
||||
const showWebhooks = !(isManagedEventType || isChildrenManagedEventType);
|
||||
if (showWebhooks) {
|
||||
const showInstant = !(isManagedEventType || isChildrenManagedEventType);
|
||||
if (showInstant) {
|
||||
if (team) {
|
||||
navigation.push({
|
||||
name: "instant_tab_title",
|
||||
@@ -124,13 +123,13 @@ export const useTabsNavigations = ({
|
||||
info: `instant_event_tab_description`,
|
||||
});
|
||||
}
|
||||
navigation.push({
|
||||
name: "webhooks",
|
||||
href: `/event-types/${formMethods.getValues("id")}?tabName=webhooks`,
|
||||
icon: "webhook",
|
||||
info: `${activeWebhooksNumber} ${t("active")}`,
|
||||
});
|
||||
}
|
||||
navigation.push({
|
||||
name: "webhooks",
|
||||
href: `/event-types/${formMethods.getValues("id")}?tabName=webhooks`,
|
||||
icon: "webhook",
|
||||
info: `${activeWebhooksNumber} ${t("active")}`,
|
||||
});
|
||||
const hidden = true; // hidden while in alpha trial. you can access it with tabName=ai
|
||||
if (team && hidden) {
|
||||
navigation.push({
|
||||
|
||||
@@ -4,6 +4,7 @@ import { v4 } from "uuid";
|
||||
|
||||
import { updateTriggerForExistingBookings } from "@calcom/features/webhooks/lib/scheduleTrigger";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -33,6 +34,29 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
|
||||
webhookData.user = { connect: { id: user.id } };
|
||||
}
|
||||
|
||||
if (input.eventTypeId) {
|
||||
const parentManagedEvt = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
id: input.eventTypeId,
|
||||
parentId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
parentId: true,
|
||||
metadata: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (parentManagedEvt?.parentId) {
|
||||
const isLocked = !EventTypeMetaDataSchema.parse(parentManagedEvt.metadata)?.managedEventConfig
|
||||
?.unlockedFields?.webhooks;
|
||||
if (isLocked) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let newWebhook: Webhook;
|
||||
try {
|
||||
newWebhook = await prisma.webhook.create({
|
||||
|
||||
@@ -29,7 +29,25 @@ export const listHandler = async ({ ctx, input }: ListOptions) => {
|
||||
|
||||
if (Array.isArray(where.AND)) {
|
||||
if (input?.eventTypeId) {
|
||||
where.AND?.push({ eventTypeId: input.eventTypeId });
|
||||
const managedParentEvt = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
id: input.eventTypeId,
|
||||
parentId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
parentId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (managedParentEvt?.parentId) {
|
||||
where.AND?.push({
|
||||
OR: [{ eventTypeId: input.eventTypeId }, { eventTypeId: managedParentEvt.parentId, active: true }],
|
||||
});
|
||||
} else {
|
||||
where.AND?.push({ eventTypeId: input.eventTypeId });
|
||||
}
|
||||
} else {
|
||||
where.AND?.push({
|
||||
OR: [{ userId: ctx.user.id }, { teamId: { in: user?.teams.map((membership) => membership.teamId) } }],
|
||||
|
||||
Reference in new issue
Block a user