Skip to content

Commit

Permalink
feat: add instant meeting expiry input (#15555)
Browse files Browse the repository at this point in the history
* feat: add instant meeting expiry input

* fix: type err

* chore: remove type

* chore: update label

* chore: update column name
  • Loading branch information
Udit-takkar authored Jun 26, 2024
1 parent a28eb63 commit d25595b
Show file tree
Hide file tree
Showing 15 changed files with 116 additions and 61 deletions.
45 changes: 39 additions & 6 deletions apps/web/components/eventtype/InstantEventController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -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>
</>
Expand Down Expand Up @@ -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")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
},
});
Expand Down
1 change: 1 addition & 0 deletions packages/features/eventtypes/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type FormValues = {
eventName: string;
slug: string;
isInstantEvent: boolean;
instantMeetingExpiryTimeOffsetInSeconds: number;
length: number;
offsetStart: number;
description: string;
Expand Down
18 changes: 16 additions & 2 deletions packages/features/instant-meeting/handleInstantMeeting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/lib/event-types/getEventTypeById.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const getEventTypeById = async ({
description: true,
length: true,
isInstantEvent: true,
instantMeetingExpiryTimeOffsetInSeconds: true,
aiPhoneCallConfig: true,
offsetStart: true,
hidden: true,
Expand Down
1 change: 1 addition & 0 deletions packages/lib/server/eventTypeSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/lib/test/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export type EventType = {
bookingLimits: number | null;
durationLimits: number | null;
isInstantEvent: boolean;
instantMeetingExpiryTimeOffsetInSeconds: number;
assignAllTeamMembers: boolean;
useEventTypeDestinationCalendarEmail: boolean;
};
1 change: 1 addition & 0 deletions packages/platform/sdk/src/endpoints/events/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export type Event = {
eventName: string;
slug: string;
isInstantEvent: boolean;
instantMeetingExpiryTimeOffsetInSeconds: number;
aiPhoneCallConfig: {
eventTypeId: number;
enabled: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "EventType" ADD COLUMN "instantMeetingExpiryTimeOffsetInSeconds" INTEGER NOT NULL DEFAULT 90;
97 changes: 49 additions & 48 deletions packages/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions packages/prisma/zod-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/trpc/server/routers/viewer/eventTypes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down

0 comments on commit d25595b

Please sign in to comment.