feat: add instant meeting expiry input (#15555)
* feat: add instant meeting expiry input * fix: type err * chore: remove type * chore: update label * chore: update column name
This commit is contained in:
@@ -2,7 +2,7 @@ import type { Webhook } from "@prisma/client";
|
||||
import { useSession } from "next-auth/react";
|
||||
import type { EventTypeSetup } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { useFormContext, Controller } from "react-hook-form";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
@@ -15,7 +15,17 @@ import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Alert, Button, EmptyScreen, SettingsToggle, Dialog, DialogContent, showToast } from "@calcom/ui";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
EmptyScreen,
|
||||
SettingsToggle,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
showToast,
|
||||
TextField,
|
||||
Label,
|
||||
} from "@calcom/ui";
|
||||
|
||||
type InstantEventControllerProps = {
|
||||
eventType: EventTypeSetup;
|
||||
@@ -85,7 +95,33 @@ export default function InstantEventController({
|
||||
}
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
{instantEventState && <InstantMeetingWebhooks eventType={eventType} />}
|
||||
{instantEventState && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Controller
|
||||
name="instantMeetingExpiryTimeOffsetInSeconds"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<Label>{t("set_instant_meeting_expiry_time_offset_description")}</Label>
|
||||
<TextField
|
||||
required
|
||||
name="instantMeetingExpiryTimeOffsetInSeconds"
|
||||
labelSrOnly
|
||||
type="number"
|
||||
defaultValue={value}
|
||||
min={10}
|
||||
containerClassName="max-w-80"
|
||||
addOnSuffix={<>{t("seconds")}</>}
|
||||
onChange={(e) => {
|
||||
onChange(Math.abs(Number(e.target.value)));
|
||||
}}
|
||||
data-testid="instant-meeting-expiry-time-offset"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<InstantMeetingWebhooks eventType={eventType} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
</>
|
||||
@@ -213,9 +249,6 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) =>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-default mb-4 text-sm font-normal">
|
||||
{t("warning_payment_instant_meeting_event")}
|
||||
</p>
|
||||
<EmptyScreen
|
||||
Icon="webhook"
|
||||
headline={t("create_your_first_webhook")}
|
||||
|
||||
@@ -251,6 +251,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
destinationCalendar: eventType.destinationCalendar,
|
||||
recurringEvent: eventType.recurringEvent || null,
|
||||
isInstantEvent: eventType.isInstantEvent,
|
||||
instantMeetingExpiryTimeOffsetInSeconds: eventType.instantMeetingExpiryTimeOffsetInSeconds,
|
||||
description: eventType.description ?? undefined,
|
||||
schedule: eventType.schedule || undefined,
|
||||
bookingLimits: eventType.bookingLimits || undefined,
|
||||
|
||||
@@ -1043,6 +1043,7 @@
|
||||
"seats_nearly_full": "Seats almost full",
|
||||
"seats_half_full": "Seats filling fast",
|
||||
"number_of_seats": "Number of seats per booking",
|
||||
"set_instant_meeting_expiry_time_offset_description": "Set meeting join window (seconds): The time frame in seconds within which host can join and start the meeting. After this period, the meeting join url will expire.",
|
||||
"enter_number_of_seats": "Enter number of seats",
|
||||
"you_can_manage_your_schedules": "You can manage your schedules on the Availability page.",
|
||||
"booking_full": "No more seats available",
|
||||
|
||||
@@ -204,11 +204,6 @@ export const useBookings = ({ event, hashedLink, bookingForm, metadata, teamMemb
|
||||
});
|
||||
},
|
||||
onError: (err, _, ctx) => {
|
||||
// TODO:
|
||||
// const vercelId = ctx?.meta?.headers?.get("x-vercel-id");
|
||||
// if (vercelId) {
|
||||
// setResponseVercelIdHeader(vercelId);
|
||||
// }
|
||||
bookerFormErrorRef && bookerFormErrorRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -34,6 +34,7 @@ export type FormValues = {
|
||||
eventName: string;
|
||||
slug: string;
|
||||
isInstantEvent: boolean;
|
||||
instantMeetingExpiryTimeOffsetInSeconds: number;
|
||||
length: number;
|
||||
offsetStart: number;
|
||||
description: string;
|
||||
|
||||
@@ -194,12 +194,26 @@ async function handler(req: NextApiRequest) {
|
||||
const newBooking = await prisma.booking.create(createBookingObj);
|
||||
|
||||
// Create Instant Meeting Token
|
||||
|
||||
const token = randomBytes(32).toString("hex");
|
||||
|
||||
const eventTypeWithExpiryTimeOffset = await prisma.eventType.findUniqueOrThrow({
|
||||
where: {
|
||||
id: req.body.eventTypeId,
|
||||
},
|
||||
select: {
|
||||
instantMeetingExpiryTimeOffsetInSeconds: true,
|
||||
},
|
||||
});
|
||||
|
||||
const instantMeetingExpiryTimeOffsetInSeconds =
|
||||
eventTypeWithExpiryTimeOffset?.instantMeetingExpiryTimeOffsetInSeconds ?? 90;
|
||||
|
||||
const instantMeetingToken = await prisma.instantMeetingToken.create({
|
||||
data: {
|
||||
token,
|
||||
// 90 Seconds
|
||||
expires: new Date(new Date().getTime() + 1000 * 90),
|
||||
// current time + offset Seconds
|
||||
expires: new Date(new Date().getTime() + 1000 * instantMeetingExpiryTimeOffsetInSeconds),
|
||||
team: {
|
||||
connect: {
|
||||
id: eventType.team.id,
|
||||
|
||||
@@ -84,6 +84,7 @@ export const getEventTypeById = async ({
|
||||
description: true,
|
||||
length: true,
|
||||
isInstantEvent: true,
|
||||
instantMeetingExpiryTimeOffsetInSeconds: true,
|
||||
aiPhoneCallConfig: true,
|
||||
offsetStart: true,
|
||||
hidden: true,
|
||||
|
||||
@@ -40,6 +40,7 @@ export const eventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
|
||||
slotInterval: true,
|
||||
successRedirectUrl: true,
|
||||
isInstantEvent: true,
|
||||
instantMeetingExpiryTimeOffsetInSeconds: true,
|
||||
aiPhoneCallConfig: true,
|
||||
assignAllTeamMembers: true,
|
||||
recurringEvent: true,
|
||||
|
||||
@@ -82,6 +82,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
|
||||
description: faker.lorem.paragraph(),
|
||||
position: 1,
|
||||
isInstantEvent: false,
|
||||
instantMeetingExpiryTimeOffsetInSeconds: 90,
|
||||
locations: null,
|
||||
length: 15,
|
||||
offsetStart: 0,
|
||||
|
||||
@@ -59,6 +59,7 @@ export type EventType = {
|
||||
bookingLimits: number | null;
|
||||
durationLimits: number | null;
|
||||
isInstantEvent: boolean;
|
||||
instantMeetingExpiryTimeOffsetInSeconds: number;
|
||||
assignAllTeamMembers: boolean;
|
||||
useEventTypeDestinationCalendarEmail: boolean;
|
||||
};
|
||||
|
||||
@@ -45,6 +45,7 @@ export type Event = {
|
||||
eventName: string;
|
||||
slug: string;
|
||||
isInstantEvent: boolean;
|
||||
instantMeetingExpiryTimeOffsetInSeconds: number;
|
||||
aiPhoneCallConfig: {
|
||||
eventTypeId: number;
|
||||
enabled: boolean;
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "EventType" ADD COLUMN "instantMeetingExpiryTimeOffsetInSeconds" INTEGER NOT NULL DEFAULT 90;
|
||||
@@ -77,65 +77,66 @@ model EventType {
|
||||
profileId Int?
|
||||
profile Profile? @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||||
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
teamId Int?
|
||||
hashedLink HashedLink?
|
||||
bookings Booking[]
|
||||
availability Availability[]
|
||||
webhooks Webhook[]
|
||||
destinationCalendar DestinationCalendar?
|
||||
eventName String?
|
||||
customInputs EventTypeCustomInput[]
|
||||
parentId Int?
|
||||
parent EventType? @relation("managed_eventtype", fields: [parentId], references: [id], onDelete: Cascade)
|
||||
children EventType[] @relation("managed_eventtype")
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
teamId Int?
|
||||
hashedLink HashedLink?
|
||||
bookings Booking[]
|
||||
availability Availability[]
|
||||
webhooks Webhook[]
|
||||
destinationCalendar DestinationCalendar?
|
||||
eventName String?
|
||||
customInputs EventTypeCustomInput[]
|
||||
parentId Int?
|
||||
parent EventType? @relation("managed_eventtype", fields: [parentId], references: [id], onDelete: Cascade)
|
||||
children EventType[] @relation("managed_eventtype")
|
||||
/// @zod.custom(imports.eventTypeBookingFields)
|
||||
bookingFields Json?
|
||||
timeZone String?
|
||||
periodType PeriodType @default(UNLIMITED)
|
||||
bookingFields Json?
|
||||
timeZone String?
|
||||
periodType PeriodType @default(UNLIMITED)
|
||||
/// @zod.custom(imports.coerceToDate)
|
||||
periodStartDate DateTime?
|
||||
periodStartDate DateTime?
|
||||
/// @zod.custom(imports.coerceToDate)
|
||||
periodEndDate DateTime?
|
||||
periodDays Int?
|
||||
periodCountCalendarDays Boolean?
|
||||
lockTimeZoneToggleOnBookingPage Boolean @default(false)
|
||||
requiresConfirmation Boolean @default(false)
|
||||
requiresBookerEmailVerification Boolean @default(false)
|
||||
periodEndDate DateTime?
|
||||
periodDays Int?
|
||||
periodCountCalendarDays Boolean?
|
||||
lockTimeZoneToggleOnBookingPage Boolean @default(false)
|
||||
requiresConfirmation Boolean @default(false)
|
||||
requiresBookerEmailVerification Boolean @default(false)
|
||||
/// @zod.custom(imports.recurringEventType)
|
||||
recurringEvent Json?
|
||||
disableGuests Boolean @default(false)
|
||||
hideCalendarNotes Boolean @default(false)
|
||||
recurringEvent Json?
|
||||
disableGuests Boolean @default(false)
|
||||
hideCalendarNotes Boolean @default(false)
|
||||
/// @zod.min(0)
|
||||
minimumBookingNotice Int @default(120)
|
||||
beforeEventBuffer Int @default(0)
|
||||
afterEventBuffer Int @default(0)
|
||||
seatsPerTimeSlot Int?
|
||||
onlyShowFirstAvailableSlot Boolean @default(false)
|
||||
seatsShowAttendees Boolean? @default(false)
|
||||
seatsShowAvailabilityCount Boolean? @default(true)
|
||||
schedulingType SchedulingType?
|
||||
schedule Schedule? @relation(fields: [scheduleId], references: [id])
|
||||
scheduleId Int?
|
||||
minimumBookingNotice Int @default(120)
|
||||
beforeEventBuffer Int @default(0)
|
||||
afterEventBuffer Int @default(0)
|
||||
seatsPerTimeSlot Int?
|
||||
onlyShowFirstAvailableSlot Boolean @default(false)
|
||||
seatsShowAttendees Boolean? @default(false)
|
||||
seatsShowAvailabilityCount Boolean? @default(true)
|
||||
schedulingType SchedulingType?
|
||||
schedule Schedule? @relation(fields: [scheduleId], references: [id])
|
||||
scheduleId Int?
|
||||
// price is deprecated. It has now moved to metadata.apps.stripe.price. Plan to drop this column.
|
||||
price Int @default(0)
|
||||
price Int @default(0)
|
||||
// currency is deprecated. It has now moved to metadata.apps.stripe.currency. Plan to drop this column.
|
||||
currency String @default("usd")
|
||||
slotInterval Int?
|
||||
currency String @default("usd")
|
||||
slotInterval Int?
|
||||
/// @zod.custom(imports.EventTypeMetaDataSchema)
|
||||
metadata Json?
|
||||
metadata Json?
|
||||
/// @zod.custom(imports.successRedirectUrl)
|
||||
successRedirectUrl String?
|
||||
forwardParamsSuccessRedirect Boolean? @default(true)
|
||||
workflows WorkflowsOnEventTypes[]
|
||||
successRedirectUrl String?
|
||||
forwardParamsSuccessRedirect Boolean? @default(true)
|
||||
workflows WorkflowsOnEventTypes[]
|
||||
/// @zod.custom(imports.intervalLimitsType)
|
||||
bookingLimits Json?
|
||||
bookingLimits Json?
|
||||
/// @zod.custom(imports.intervalLimitsType)
|
||||
durationLimits Json?
|
||||
isInstantEvent Boolean @default(false)
|
||||
assignAllTeamMembers Boolean @default(false)
|
||||
useEventTypeDestinationCalendarEmail Boolean @default(false)
|
||||
aiPhoneCallConfig AIPhoneCallConfiguration?
|
||||
durationLimits Json?
|
||||
isInstantEvent Boolean @default(false)
|
||||
instantMeetingExpiryTimeOffsetInSeconds Int @default(90)
|
||||
assignAllTeamMembers Boolean @default(false)
|
||||
useEventTypeDestinationCalendarEmail Boolean @default(false)
|
||||
aiPhoneCallConfig AIPhoneCallConfiguration?
|
||||
|
||||
secondaryEmailId Int?
|
||||
secondaryEmail SecondaryEmail? @relation(fields: [secondaryEmailId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@ -608,6 +608,7 @@ export const allManagedEventTypeProps: { [k in keyof Omit<Prisma.EventTypeSelect
|
||||
title: true,
|
||||
description: true,
|
||||
isInstantEvent: true,
|
||||
instantMeetingExpiryTimeOffsetInSeconds: true,
|
||||
aiPhoneCallConfig: true,
|
||||
currency: true,
|
||||
periodDays: true,
|
||||
|
||||
@@ -8,6 +8,7 @@ export const EventTypeUpdateInput = _EventTypeModel
|
||||
/** Optional fields */
|
||||
.extend({
|
||||
isInstantEvent: z.boolean().optional(),
|
||||
instantMeetingExpiryTimeOffsetInSeconds: z.number().optional(),
|
||||
aiPhoneCallConfig: z
|
||||
.object({
|
||||
generalPrompt: z.string(),
|
||||
|
||||
Reference in New Issue
Block a user