feat: instant meeting improvements (#13258)
Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
co-authored by
Peer Richelsen
Omar López
parent
ede2357203
commit
850e222f0e
@@ -1,3 +1,4 @@
|
||||
import type { Webhook } from "@prisma/client";
|
||||
import { useSession } from "next-auth/react";
|
||||
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
@@ -5,10 +6,16 @@ import { useFormContext } from "react-hook-form";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||
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 { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Alert, Button, EmptyScreen, SettingsToggle } from "@calcom/ui";
|
||||
import { PhoneCall } from "@calcom/ui/components/icon";
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Alert, Button, EmptyScreen, SettingsToggle, Dialog, DialogContent, showToast } from "@calcom/ui";
|
||||
import { PhoneCall, Plus, Lock, Webhook as TbWebhook } from "@calcom/ui/components/icon";
|
||||
|
||||
type InstantEventControllerProps = {
|
||||
eventType: EventTypeSetup;
|
||||
@@ -82,11 +89,7 @@ export default function InstantEventController({
|
||||
}
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
{instantEventState && (
|
||||
<div data-testid="instant-event-collapsible" className="flex flex-col gap-2 text-sm">
|
||||
<p>{t("warning_payment_instant_meeting_event")}</p>
|
||||
</div>
|
||||
)}
|
||||
{instantEventState && <InstantMeetingWebhooks eventType={eventType} />}
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
</>
|
||||
@@ -97,3 +100,201 @@ export default function InstantEventController({
|
||||
</LicenseRequired>
|
||||
);
|
||||
}
|
||||
|
||||
const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const { data: webhooks } = trpc.viewer.webhook.list.useQuery({
|
||||
eventTypeId: eventType.id,
|
||||
eventTriggers: [WebhookTriggerEvents.INSTANT_MEETING],
|
||||
});
|
||||
const { data: installedApps, isLoading } = trpc.viewer.integrations.useQuery({
|
||||
variant: "other",
|
||||
onlyInstalled: true,
|
||||
});
|
||||
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [webhookToEdit, setWebhookToEdit] = useState<Webhook>();
|
||||
|
||||
const editWebhookMutation = trpc.viewer.webhook.edit.useMutation({
|
||||
async onSuccess() {
|
||||
setEditModalOpen(false);
|
||||
await utils.viewer.webhook.list.invalidate();
|
||||
showToast(t("webhook_updated_successfully"), "success");
|
||||
},
|
||||
onError(error) {
|
||||
showToast(`${error.message}`, "error");
|
||||
},
|
||||
});
|
||||
|
||||
const createWebhookMutation = trpc.viewer.webhook.create.useMutation({
|
||||
async onSuccess() {
|
||||
showToast(t("webhook_created_successfully"), "success");
|
||||
await utils.viewer.webhook.list.invalidate();
|
||||
setCreateModalOpen(false);
|
||||
},
|
||||
onError(error) {
|
||||
showToast(`${error.message}`, "error");
|
||||
},
|
||||
});
|
||||
|
||||
const onCreateWebhook = async (values: WebhookFormSubmitData) => {
|
||||
if (
|
||||
subscriberUrlReserved({
|
||||
subscriberUrl: values.subscriberUrl,
|
||||
id: values.id,
|
||||
webhooks,
|
||||
eventTypeId: eventType.id,
|
||||
})
|
||||
) {
|
||||
showToast(t("webhook_subscriber_url_reserved"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!values.payloadTemplate) {
|
||||
values.payloadTemplate = null;
|
||||
}
|
||||
|
||||
createWebhookMutation.mutate({
|
||||
subscriberUrl: values.subscriberUrl,
|
||||
eventTriggers: values.eventTriggers,
|
||||
active: values.active,
|
||||
payloadTemplate: values.payloadTemplate,
|
||||
secret: values.secret,
|
||||
eventTypeId: eventType.id,
|
||||
});
|
||||
};
|
||||
|
||||
const NewWebhookButton = () => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<Button
|
||||
color="secondary"
|
||||
data-testid="new_webhook"
|
||||
StartIcon={Plus}
|
||||
onClick={() => setCreateModalOpen(true)}>
|
||||
{t("new_webhook")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager(
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
const webhookLockedStatus = shouldLockDisableProps("webhooks");
|
||||
|
||||
return (
|
||||
<div>
|
||||
{webhooks && !isLoading && (
|
||||
<>
|
||||
<div>
|
||||
{webhooks.length ? (
|
||||
<>
|
||||
<div className="border-subtle my-2 rounded-md border">
|
||||
{webhooks.map((webhook, index) => {
|
||||
return (
|
||||
<WebhookListItem
|
||||
key={webhook.id}
|
||||
webhook={webhook}
|
||||
lastItem={webhooks.length === index + 1}
|
||||
canEditWebhook={!webhookLockedStatus.disabled}
|
||||
onEditWebhook={() => {
|
||||
setEditModalOpen(true);
|
||||
setWebhookToEdit(webhook);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-default text-sm font-normal">
|
||||
{t("warning_payment_instant_meeting_event")}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-default mb-4 text-sm font-normal">
|
||||
{t("warning_payment_instant_meeting_event")}
|
||||
</p>
|
||||
<EmptyScreen
|
||||
Icon={TbWebhook}
|
||||
headline={t("create_your_first_webhook")}
|
||||
description={t("create_instant_meeting_webhook_description")}
|
||||
buttonRaw={
|
||||
isChildrenManagedEventType && !isManagedEventType ? (
|
||||
<Button StartIcon={Lock} color="secondary" disabled>
|
||||
{t("locked_by_admin")}
|
||||
</Button>
|
||||
) : (
|
||||
<NewWebhookButton />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* New webhook dialog */}
|
||||
<Dialog open={createModalOpen} onOpenChange={(isOpen) => !isOpen && setCreateModalOpen(false)}>
|
||||
<DialogContent
|
||||
enableOverflow
|
||||
title={t("create_webhook")}
|
||||
description={t("create_webhook_team_event_type")}>
|
||||
<WebhookForm
|
||||
noRoutingFormTriggers={true}
|
||||
onSubmit={onCreateWebhook}
|
||||
onCancel={() => setCreateModalOpen(false)}
|
||||
apps={installedApps?.items.map((app) => app.slug)}
|
||||
selectOnlyInstantMeetingOption={true}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/* Edit webhook dialog */}
|
||||
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
|
||||
<DialogContent enableOverflow title={t("edit_webhook")}>
|
||||
<WebhookForm
|
||||
noRoutingFormTriggers={true}
|
||||
webhook={webhookToEdit}
|
||||
apps={installedApps?.items.map((app) => app.slug)}
|
||||
onCancel={() => setEditModalOpen(false)}
|
||||
onSubmit={(values: WebhookFormSubmitData) => {
|
||||
if (
|
||||
subscriberUrlReserved({
|
||||
subscriberUrl: values.subscriberUrl,
|
||||
id: webhookToEdit?.id,
|
||||
webhooks,
|
||||
eventTypeId: eventType.id,
|
||||
})
|
||||
) {
|
||||
showToast(t("webhook_subscriber_url_reserved"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.changeSecret) {
|
||||
values.secret = values.newSecret.length ? values.newSecret : null;
|
||||
}
|
||||
|
||||
if (!values.payloadTemplate) {
|
||||
values.payloadTemplate = null;
|
||||
}
|
||||
|
||||
editWebhookMutation.mutate({
|
||||
id: webhookToEdit?.id || "",
|
||||
subscriberUrl: values.subscriberUrl,
|
||||
eventTriggers: values.eventTriggers,
|
||||
active: values.active,
|
||||
payloadTemplate: values.payloadTemplate,
|
||||
secret: values.secret,
|
||||
eventTypeId: webhookToEdit?.eventTypeId || undefined,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
"requested_to_reschedule_subject_attendee": "Action Required Reschedule: Please book a new time for {{eventType}} with {{name}}",
|
||||
"hi_user_name": "Hi {{name}}",
|
||||
"ics_event_title": "{{eventType}} with {{name}}",
|
||||
"please_book_a_time_sometime_later":"No one was available at the moment. Please book for sometime later",
|
||||
"please_book_a_time_sometime_later":"Oops, we couldn't connect you this time. Please schedule a future call instead",
|
||||
"new_event_subject": "New event: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"join_by_entrypoint": "Join by {{entryPoint}}",
|
||||
"notes": "Notes",
|
||||
@@ -440,7 +440,7 @@
|
||||
"browse_api_documentation": "Browse our API documentation",
|
||||
"leverage_our_api": "Leverage our API for full control and customizability.",
|
||||
"create_webhook": "Create Webhook",
|
||||
"instant_meeting_created":"Instant Meeting Created",
|
||||
"instant_meeting":"Instant Meeting Created",
|
||||
"booking_cancelled": "Booking Canceled",
|
||||
"booking_rescheduled": "Booking Rescheduled",
|
||||
"recording_ready": "Recording Download Link Ready",
|
||||
@@ -555,6 +555,7 @@
|
||||
"enter_number_between_range": "Please enter a number between 1 and {{maxOccurences}}",
|
||||
"email_address": "Email address",
|
||||
"enter_valid_email": "Please enter a valid email",
|
||||
"please_schedule_future_call":" Please schedule a future call if we're not available in {{seconds}} seconds",
|
||||
"location": "Location",
|
||||
"address": "Address",
|
||||
"enter_address": "Enter address",
|
||||
@@ -1979,6 +1980,7 @@
|
||||
"connect_google_workspace": "Connect Google Workspace",
|
||||
"google_workspace_admin_tooltip": "You must be a Workspace Admin to use this feature",
|
||||
"first_event_type_webhook_description": "Create your first webhook for this event type",
|
||||
"create_instant_meeting_webhook_description":"Create your first webhook with 'Instant Meeting Created' as trigger for this event type",
|
||||
"install_app_on": "Install app on",
|
||||
"create_for": "Create for",
|
||||
"currency": "Currency",
|
||||
|
||||
@@ -440,7 +440,7 @@
|
||||
"browse_api_documentation": "עיון במסמכי ממשק תכנות היישומים (API) שלנו",
|
||||
"leverage_our_api": "מומלץ להיעזר בממשק תכנות היישומים (API) שלנו לקבלת שליטה מלאה ויכולת התאמה אישית.",
|
||||
"create_webhook": "יצירת Webhook",
|
||||
"instant_meeting_created": "נוצרה פגישה מיידית",
|
||||
"instant_meeting": "נוצרה פגישה מיידית",
|
||||
"booking_cancelled": "ההזמנה בוטלה",
|
||||
"booking_rescheduled": "מועד ההזמנה השתנה",
|
||||
"recording_ready": "הקישור להורדת ההקלטה מוכן",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import dayjs from "dayjs";
|
||||
import dayjsBusinessTime from "dayjs-business-days2";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import isBetween from "dayjs/plugin/isBetween";
|
||||
import isToday from "dayjs/plugin/isToday";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
@@ -21,6 +22,7 @@ dayjs.extend(timeZone);
|
||||
dayjs.extend(toArray);
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(minmax);
|
||||
dayjs.extend(duration);
|
||||
|
||||
export type Dayjs = dayjs.Dayjs;
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ const BookerComponent = ({
|
||||
event,
|
||||
});
|
||||
|
||||
const { handleBookEvent, hasInstantMeetingTokenExpired, errors, loadingStates } = useBookings({
|
||||
const { handleBookEvent, hasInstantMeetingTokenExpired, errors, loadingStates, expiryTime } = useBookings({
|
||||
event,
|
||||
hashedLink,
|
||||
bookingForm,
|
||||
@@ -245,6 +245,7 @@ const BookerComponent = ({
|
||||
isUserSessionRequiredToVerify={false}
|
||||
/>
|
||||
<RedirectToInstantMeetingModal
|
||||
expiryTime={expiryTime}
|
||||
hasInstantMeetingTokenExpired={hasInstantMeetingTokenExpired}
|
||||
bookingId={parseInt(getQueryParam("bookingId") || "0")}
|
||||
onGoBack={() => {
|
||||
@@ -279,6 +280,7 @@ const BookerComponent = ({
|
||||
setSeatedEventData,
|
||||
setSelectedTimeslot,
|
||||
setVerifiedEmail,
|
||||
expiryTime,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -301,8 +303,9 @@ const BookerComponent = ({
|
||||
{bookerState !== "booking" && event.data?.isInstantEvent && (
|
||||
<div
|
||||
className="animate-fade-in-up fixed bottom-2 z-40 my-2 opacity-0"
|
||||
style={{ animationDelay: "2s" }}>
|
||||
style={{ animationDelay: "1s" }}>
|
||||
<InstantBooking
|
||||
event={event.data}
|
||||
onConnectNow={() => {
|
||||
const newPath = `${pathname}?isInstantMeeting=true`;
|
||||
router.push(newPath);
|
||||
|
||||
@@ -51,6 +51,7 @@ export const BookEventForm = ({
|
||||
const timeslot = useBookerStore((state) => state.selectedTimeslot);
|
||||
const username = useBookerStore((state) => state.username);
|
||||
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting);
|
||||
const [expiryTime, setExpiryTime] = useState<Date | undefined>();
|
||||
|
||||
const [responseVercelIdHeader] = useState<string | null>(null);
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { type RouterOutputs } from "@calcom/trpc";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { UserAvatarGroupWithOrg } from "@calcom/web/components/ui/avatar/UserAvatarGroupWithOrg";
|
||||
|
||||
interface IInstantBookingProps {
|
||||
onConnectNow: () => void;
|
||||
}
|
||||
{
|
||||
/* TODO: max. show 4 people here */
|
||||
/* use AvatarGroup */
|
||||
event: NonNullable<RouterOutputs["viewer"]["public"]["event"]>;
|
||||
}
|
||||
|
||||
export const InstantBooking = ({ onConnectNow }: IInstantBookingProps) => {
|
||||
export const InstantBooking = ({ onConnectNow, event }: IInstantBookingProps) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div className=" bg-default border-subtle mx-2 block items-center gap-3 rounded-xl border p-[6px] text-sm shadow-sm delay-1000 sm:flex">
|
||||
<div className="flex items-center gap-3 ps-1">
|
||||
<div className="relative">
|
||||
<UserAvatarGroupWithOrg
|
||||
size="sm"
|
||||
className="border-muted"
|
||||
organization={{
|
||||
slug: event.entity.orgSlug,
|
||||
name: event.entity.name || "",
|
||||
}}
|
||||
users={event.schedulingType !== SchedulingType.ROUND_ROBIN ? event.users : []}
|
||||
/>
|
||||
<div className="border-muted absolute -bottom-0.5 -right-1 h-2 w-2 rounded-full border bg-green-500" />
|
||||
</div>
|
||||
<div>{t("dont_want_to_wait")}</div>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Dialog, DialogContent } from "@calcom/ui";
|
||||
import { Button } from "@calcom/ui";
|
||||
@@ -6,15 +9,60 @@ export const RedirectToInstantMeetingModal = ({
|
||||
hasInstantMeetingTokenExpired,
|
||||
bookingId,
|
||||
onGoBack,
|
||||
expiryTime,
|
||||
}: {
|
||||
hasInstantMeetingTokenExpired: boolean;
|
||||
bookingId: number;
|
||||
onGoBack: () => void;
|
||||
expiryTime?: Date;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const [timeRemaining, setTimeRemaining] = useState(calculateTimeRemaining());
|
||||
|
||||
function calculateTimeRemaining() {
|
||||
const now = dayjs();
|
||||
const expiration = dayjs(expiryTime);
|
||||
const duration = expiration.diff(now);
|
||||
return Math.max(duration, 0);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!expiryTime) return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setTimeRemaining(calculateTimeRemaining());
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [expiryTime]);
|
||||
|
||||
const formatTime = (milliseconds: number) => {
|
||||
const duration = dayjs.duration(milliseconds);
|
||||
const seconds = duration.seconds();
|
||||
const minutes = duration.minutes();
|
||||
|
||||
return `${minutes}m ${seconds}s`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hasInstantMeetingTokenExpired) return;
|
||||
|
||||
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||
const message = "/o";
|
||||
event.returnValue = message; // Standard for most browsers
|
||||
return message; // For some older browsers
|
||||
};
|
||||
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
};
|
||||
}, [hasInstantMeetingTokenExpired]);
|
||||
|
||||
return (
|
||||
<Dialog open={!!bookingId}>
|
||||
<Dialog open={!!bookingId && !!expiryTime}>
|
||||
<DialogContent enableOverflow className="py-8">
|
||||
<div>
|
||||
{hasInstantMeetingTokenExpired ? (
|
||||
@@ -32,18 +80,14 @@ export const RedirectToInstantMeetingModal = ({
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<p className="font-medium">{t("connecting_you_to_someone")}</p>
|
||||
{/* TODO: Add countdown from 60 seconds
|
||||
We are connecting you!
|
||||
Please schedule a future call if we're not available in XX seconds.
|
||||
*/}
|
||||
|
||||
{/* Once countdown ends:
|
||||
Oops, we couldn't connect you this time.
|
||||
Please schedule a future call instead. We value your time.
|
||||
*/}
|
||||
|
||||
<p className="font-medium">{t("please_do_not_close_this_tab")}</p>
|
||||
<div className="h-[414px]">
|
||||
<p className="mt-2 font-medium">
|
||||
{t("please_schedule_future_call", {
|
||||
seconds: formatTime(timeRemaining),
|
||||
})}
|
||||
</p>
|
||||
|
||||
<div className="mt-4 h-[414px]">
|
||||
<iframe className="mx-auto h-full w-[276px] rounded-lg" src="https://cal.games/" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -73,6 +73,7 @@ export const useBookings = ({ event, hashedLink, bookingForm }: IUseBookings) =>
|
||||
{
|
||||
enabled: !!bookingId && !hasInstantMeetingTokenExpired,
|
||||
refetchInterval: 2000,
|
||||
refetchIntervalInBackground: true,
|
||||
onSuccess: (data) => {
|
||||
try {
|
||||
showToast(t("something_went_wrong_on_our_end"), "error");
|
||||
|
||||
@@ -179,7 +179,8 @@ async function handler(req: NextApiRequest) {
|
||||
const instantMeetingToken = await prisma.instantMeetingToken.create({
|
||||
data: {
|
||||
token,
|
||||
expires: new Date(new Date().getTime() + 1000 * 60 * 5),
|
||||
// 90 Seconds
|
||||
expires: new Date(new Date().getTime() + 1000 * 90),
|
||||
team: {
|
||||
connect: {
|
||||
id: eventType.team.id,
|
||||
|
||||
@@ -41,7 +41,7 @@ const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2: Record<string, WebhookTriggerEve
|
||||
{ value: WebhookTriggerEvents.MEETING_ENDED, label: "meeting_ended" },
|
||||
{ value: WebhookTriggerEvents.MEETING_STARTED, label: "meeting_started" },
|
||||
{ value: WebhookTriggerEvents.RECORDING_READY, label: "recording_ready" },
|
||||
{ value: WebhookTriggerEvents.INSTANT_MEETING, label: "instant_meeting_created" },
|
||||
{ value: WebhookTriggerEvents.INSTANT_MEETING, label: "instant_meeting" },
|
||||
],
|
||||
"routing-forms": [{ value: WebhookTriggerEvents.FORM_SUBMITTED, label: "form_submitted" }],
|
||||
} as const;
|
||||
@@ -52,8 +52,9 @@ const WebhookForm = (props: {
|
||||
onSubmit: (event: WebhookFormSubmitData) => void;
|
||||
onCancel?: () => void;
|
||||
noRoutingFormTriggers: boolean;
|
||||
selectOnlyInstantMeetingOption?: boolean;
|
||||
}) => {
|
||||
const { apps = [] } = props;
|
||||
const { apps = [], selectOnlyInstantMeetingOption = false } = props;
|
||||
const { t } = useLocale();
|
||||
|
||||
const triggerOptions = [...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2["core"]];
|
||||
@@ -67,15 +68,21 @@ const WebhookForm = (props: {
|
||||
}
|
||||
const translatedTriggerOptions = triggerOptions.map((option) => ({ ...option, label: t(option.label) }));
|
||||
|
||||
const getEventTriggers = () => {
|
||||
if (props.webhook) return props.webhook.eventTriggers;
|
||||
|
||||
return (
|
||||
selectOnlyInstantMeetingOption
|
||||
? translatedTriggerOptions.filter((option) => option.value === WebhookTriggerEvents.INSTANT_MEETING)
|
||||
: translatedTriggerOptions.filter((option) => option.value !== WebhookTriggerEvents.INSTANT_MEETING)
|
||||
).map((option) => option.value);
|
||||
};
|
||||
|
||||
const formMethods = useForm({
|
||||
defaultValues: {
|
||||
subscriberUrl: props.webhook?.subscriberUrl || "",
|
||||
active: props.webhook ? props.webhook.active : true,
|
||||
eventTriggers: !props.webhook
|
||||
? translatedTriggerOptions
|
||||
.filter((option) => option.value !== WebhookTriggerEvents.INSTANT_MEETING)
|
||||
.map((option) => option.value)
|
||||
: props.webhook.eventTriggers,
|
||||
eventTriggers: getEventTriggers(),
|
||||
secret: props?.webhook?.secret || "",
|
||||
payloadTemplate: props?.webhook?.payloadTemplate || undefined,
|
||||
},
|
||||
|
||||
@@ -35,6 +35,10 @@ export const listHandler = async ({ ctx, input }: ListOptions) => {
|
||||
OR: [{ userId: ctx.user.id }, { teamId: { in: user?.teams.map((membership) => membership.teamId) } }],
|
||||
});
|
||||
}
|
||||
|
||||
if (input?.eventTriggers) {
|
||||
where.AND?.push({ eventTriggers: { hasEvery: input.eventTriggers } });
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.webhook.findMany({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { WEBHOOK_TRIGGER_EVENTS } from "@calcom/features/webhooks/lib/constants";
|
||||
|
||||
import { webhookIdAndEventTypeIdSchema } from "./types";
|
||||
|
||||
export const ZListInputSchema = webhookIdAndEventTypeIdSchema
|
||||
@@ -7,6 +9,7 @@ export const ZListInputSchema = webhookIdAndEventTypeIdSchema
|
||||
appId: z.string().optional(),
|
||||
teamId: z.number().optional(),
|
||||
eventTypeId: z.number().optional(),
|
||||
eventTriggers: z.enum(WEBHOOK_TRIGGER_EVENTS).array().optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user