feat: cal video settings (#21347)
* feat: cal video settings * fix: var names * feat: finish setting * chore: add enabled * chore: remove logs * feat: support redirect url on exit * feat: finish settings * refactor: improve UI * chore: remove logs * fix: type err * fix: finish redirect url * fix: add type * chore: change schema * fix: type error * chore: improvements * chore: remove index * feat: add organizer * chore: remove log * chore: use safe parse * fix: add nullable * refactor: hide toggles if platorm * chore: update description --------- Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: supalarry <laurisskraucis@gmail.com>
This commit is contained in:
co-authored by
Alex van Andel
supalarry
parent
a1007398f6
commit
ed4b3f082a
@@ -94,6 +94,7 @@ const getEventTypes = async (userId: number, teamIds?: number[]) => {
|
||||
userId: true,
|
||||
destinationCalendar: true,
|
||||
bookingFields: true,
|
||||
calVideoSettings: true,
|
||||
});
|
||||
let eventTypeGroups: TEventTypeGroup[] | null = [];
|
||||
|
||||
|
||||
@@ -12,12 +12,62 @@ import { getCalVideoReference } from "@calcom/features/get-cal-video-reference";
|
||||
import { CAL_VIDEO_MEETING_LINK_FOR_TESTING } from "@calcom/lib/constants";
|
||||
import { isENVDev } from "@calcom/lib/env";
|
||||
import { BookingRepository } from "@calcom/lib/server/repository/booking";
|
||||
import { EventTypeRepository } from "@calcom/lib/server/repository/eventType";
|
||||
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
|
||||
type CalVideoSettings = {
|
||||
disableRecordingForGuests: boolean;
|
||||
disableRecordingForOrganizer: boolean;
|
||||
};
|
||||
|
||||
const shouldEnableRecordButton = ({
|
||||
hasTeamPlan,
|
||||
calVideoSettings,
|
||||
isOrganizer,
|
||||
}: {
|
||||
hasTeamPlan: boolean;
|
||||
calVideoSettings?: CalVideoSettings | null;
|
||||
isOrganizer: boolean;
|
||||
}) => {
|
||||
if (!hasTeamPlan) return false;
|
||||
if (!calVideoSettings) return true;
|
||||
|
||||
if (isOrganizer) {
|
||||
return !calVideoSettings.disableRecordingForOrganizer;
|
||||
}
|
||||
|
||||
return !calVideoSettings.disableRecordingForGuests;
|
||||
};
|
||||
|
||||
const checkIfUserIsHost = async ({
|
||||
booking,
|
||||
sessionUserId,
|
||||
}: {
|
||||
booking: {
|
||||
user: { id: number } | null;
|
||||
eventTypeId: number | undefined;
|
||||
};
|
||||
sessionUserId?: number;
|
||||
}) => {
|
||||
if (!sessionUserId) return false;
|
||||
|
||||
if (!booking.eventTypeId) {
|
||||
return booking.user?.id === sessionUserId;
|
||||
}
|
||||
|
||||
const eventType = await EventTypeRepository.findByIdWithUserAccess({
|
||||
id: booking.eventTypeId,
|
||||
userId: sessionUserId,
|
||||
});
|
||||
|
||||
// If eventType exists, it means user is either owner, host or user
|
||||
return !eventType;
|
||||
};
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { req } = context;
|
||||
|
||||
@@ -107,13 +157,20 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
});
|
||||
|
||||
const sessionUserId = !!session?.user?.impersonatedBy ? session.user.impersonatedBy.id : session?.user.id;
|
||||
const isOrganizer = await checkIfUserIsHost({
|
||||
booking: {
|
||||
eventTypeId: bookingObj.eventType?.id,
|
||||
user: bookingObj.user,
|
||||
},
|
||||
sessionUserId,
|
||||
});
|
||||
|
||||
// set meetingPassword for guests
|
||||
if (sessionUserId !== bookingObj.user?.id) {
|
||||
const guestMeetingPassword = await generateGuestMeetingTokenFromOwnerMeetingToken(
|
||||
videoReferencePassword,
|
||||
sessionUserId
|
||||
);
|
||||
if (!isOrganizer) {
|
||||
const guestMeetingPassword = await generateGuestMeetingTokenFromOwnerMeetingToken({
|
||||
meetingToken: videoReferencePassword,
|
||||
userId: sessionUserId,
|
||||
});
|
||||
|
||||
bookingObj.references.forEach((bookRef) => {
|
||||
bookRef.meetingPassword = guestMeetingPassword;
|
||||
@@ -141,6 +198,12 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
? await featureRepo.checkIfTeamHasFeature(profile.organizationId, "cal-video-log-in-overlay")
|
||||
: false;
|
||||
|
||||
const showRecordingButton = shouldEnableRecordButton({
|
||||
hasTeamPlan: !!hasTeamPlan,
|
||||
calVideoSettings: bookingObj.eventType?.calVideoSettings,
|
||||
isOrganizer: sessionUserId === bookingObj.user?.id,
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
meetingUrl: videoReference.meetingUrl ?? "",
|
||||
@@ -161,6 +224,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
calVideoLogo,
|
||||
displayLogInOverlay,
|
||||
loggedInUserName: sessionUserId ? session?.user?.name : undefined,
|
||||
showRecordingButton,
|
||||
rediectAttendeeToOnExit: isOrganizer
|
||||
? undefined
|
||||
: bookingObj.eventType?.calVideoSettings?.redirectUrlOnExit,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,7 +37,13 @@ import { STEPS } from "~/apps/installation/[[...step]]/constants";
|
||||
export type TEventType = EventTypeAppSettingsComponentProps["eventType"] &
|
||||
Pick<
|
||||
EventTypeModel,
|
||||
"metadata" | "schedulingType" | "slug" | "requiresConfirmation" | "position" | "destinationCalendar"
|
||||
| "metadata"
|
||||
| "schedulingType"
|
||||
| "slug"
|
||||
| "requiresConfirmation"
|
||||
| "position"
|
||||
| "destinationCalendar"
|
||||
| "calVideoSettings"
|
||||
> & {
|
||||
selected: boolean;
|
||||
locations: LocationFormValues["locations"];
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { DailyCall } from "@daily-co/daily-js";
|
||||
import DailyIframe from "@daily-co/daily-js";
|
||||
import { DailyProvider } from "@daily-co/daily-react";
|
||||
import { useDailyEvent } from "@daily-co/daily-react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
@@ -33,6 +34,8 @@ export default function JoinCall(props: PageProps) {
|
||||
calVideoLogo,
|
||||
displayLogInOverlay,
|
||||
loggedInUserName,
|
||||
showRecordingButton,
|
||||
rediectAttendeeToOnExit,
|
||||
} = props;
|
||||
const [daily, setDaily] = useState<DailyCall | null>(null);
|
||||
|
||||
@@ -65,12 +68,16 @@ export default function JoinCall(props: PageProps) {
|
||||
...(typeof meetingPassword === "string" && { token: meetingPassword }),
|
||||
...(hasTeamPlan && {
|
||||
customTrayButtons: {
|
||||
recording: {
|
||||
label: "Record",
|
||||
tooltip: "Start or stop recording",
|
||||
iconPath: RECORDING_DEFAULT_ICON,
|
||||
iconPathDarkMode: RECORDING_DEFAULT_ICON,
|
||||
},
|
||||
...(showRecordingButton
|
||||
? {
|
||||
recording: {
|
||||
label: "Record",
|
||||
tooltip: "Start or stop recording",
|
||||
iconPath: RECORDING_DEFAULT_ICON,
|
||||
iconPathDarkMode: RECORDING_DEFAULT_ICON,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
transcription: {
|
||||
label: "Cal.ai",
|
||||
tooltip: "Transcription powered by AI",
|
||||
@@ -126,7 +133,7 @@ export default function JoinCall(props: PageProps) {
|
||||
</div>
|
||||
{displayLogInOverlay && <LogInOverlay isLoggedIn={!!loggedInUserName} bookingUid={booking.uid} />}
|
||||
|
||||
<VideoMeetingInfo booking={booking} />
|
||||
<VideoMeetingInfo booking={booking} rediectAttendeeToOnExit={rediectAttendeeToOnExit} />
|
||||
</DailyProvider>
|
||||
);
|
||||
}
|
||||
@@ -255,16 +262,23 @@ export function LogInOverlay(props: LogInOverlayProps) {
|
||||
|
||||
interface VideoMeetingInfo {
|
||||
booking: PageProps["booking"];
|
||||
rediectAttendeeToOnExit?: string | null;
|
||||
}
|
||||
|
||||
export function VideoMeetingInfo(props: VideoMeetingInfo) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { booking } = props;
|
||||
const { booking, rediectAttendeeToOnExit } = props;
|
||||
const { t } = useLocale();
|
||||
|
||||
const endTime = new Date(booking.endTime);
|
||||
const startTime = new Date(booking.startTime);
|
||||
|
||||
useDailyEvent("left-meeting", () => {
|
||||
if (rediectAttendeeToOnExit) {
|
||||
window.location.href = rediectAttendeeToOnExit;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside
|
||||
|
||||
@@ -44,6 +44,10 @@
|
||||
"event_cancelled_subject": "Canceled: {{title}} at {{date}}",
|
||||
"event_reassigned_subject": "Reassigned: {{title}} at {{date}}",
|
||||
"event_request_declined": "Your event request has been declined",
|
||||
"customize_calvideo_settings": "Customize Cal Video Settings",
|
||||
"calvideo_settings_description": "Configure video meeting preferences including recording permissions for hosts and attendees.",
|
||||
"disable_recording_for_guests": "Disable recording for guests",
|
||||
"disable_recording_for_organizer": "Disable recording for organizer",
|
||||
"event_request_declined_recurring": "Your recurring event request has been declined",
|
||||
"event_request_cancelled": "Your scheduled event was canceled",
|
||||
"event_request_reassigned": "Your scheduled event was reassigned",
|
||||
@@ -2006,6 +2010,7 @@
|
||||
"add_a_booking_question": "Add a question",
|
||||
"identifier": "Identifier",
|
||||
"duplicate_email": "Email is duplicate",
|
||||
"enter_redirect_url_on_exit_description": "Enter a redirect URL to which the attendee will be sent once they leave the meeting.",
|
||||
"booking_with_payment_cancelled": "Paying for this event is no longer possible",
|
||||
"booking_with_payment_cancelled_already_paid": "A refund for this booking payment is on its way.",
|
||||
"booking_with_payment_cancelled_refunded": "This booking payment has been refunded.",
|
||||
@@ -2911,6 +2916,7 @@
|
||||
"routing_form_insights_booking_at": "Booking At",
|
||||
"routing_form_insights_submitted_at": "Submitted At",
|
||||
"copy_redirect_url": "Copy Redirect URL",
|
||||
"redirect_url_on_exit": "Redirect URL on Exit",
|
||||
"total_bookings_per_period": "Total Bookings per period",
|
||||
"routed_to_per_period": "Routed to per period",
|
||||
"per_day": "Per day",
|
||||
|
||||
@@ -146,10 +146,13 @@ export const updateMeetingTokenIfExpired = async ({
|
||||
return meetingToken;
|
||||
};
|
||||
|
||||
export const generateGuestMeetingTokenFromOwnerMeetingToken = async (
|
||||
meetingToken: string | null,
|
||||
userId?: number
|
||||
) => {
|
||||
export const generateGuestMeetingTokenFromOwnerMeetingToken = async ({
|
||||
meetingToken,
|
||||
userId,
|
||||
}: {
|
||||
meetingToken: string | null;
|
||||
userId?: number;
|
||||
}) => {
|
||||
if (!meetingToken) return null;
|
||||
|
||||
const token = await fetcher(`/meeting-tokens/${meetingToken}`).then(ZGetMeetingTokenResponseSchema.parse);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { ErrorMessage } from "@hookform/error-message";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFieldArray } from "react-hook-form";
|
||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||
import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "react-hook-form";
|
||||
|
||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
LocationFormValues,
|
||||
EventTypeSetupProps,
|
||||
CheckboxClassNames,
|
||||
FormValues,
|
||||
} from "@calcom/features/eventtypes/lib/types";
|
||||
import CheckboxField from "@calcom/features/form/components/CheckboxField";
|
||||
import type {
|
||||
@@ -25,11 +26,13 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { TextField } from "@calcom/ui/components/form";
|
||||
import { SettingsToggle } from "@calcom/ui/components/form";
|
||||
import { Input } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
export type TEventTypeLocation = Pick<EventTypeSetupProps["eventType"], "locations">;
|
||||
export type TEventTypeLocation = Pick<EventTypeSetupProps["eventType"], "locations" | "calVideoSettings">;
|
||||
export type TLocationOptions = Pick<EventTypeSetupProps, "locationOptions">["locationOptions"];
|
||||
export type TDestinationCalendar = { integration: string } | null;
|
||||
export type TPrefillLocation = { credentialId?: number; type: string };
|
||||
@@ -129,6 +132,8 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
name: "locations",
|
||||
});
|
||||
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
|
||||
const locationOptions = props.locationOptions.map((locationOption) => {
|
||||
const options = locationOption.options.filter((option) => {
|
||||
// Skip "Organizer's Default App" for non-team members
|
||||
@@ -244,7 +249,7 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [prefillLocation, seatsEnabled]);
|
||||
|
||||
const isPlatorm = useIsPlatform();
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
return (
|
||||
<div className={classNames("w-full", customClassNames?.container)}>
|
||||
@@ -253,6 +258,8 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
const eventLocationType = getEventLocationType(field.type);
|
||||
const defaultLocation = field;
|
||||
|
||||
const isCalVideo = field.type === "integrations:daily";
|
||||
|
||||
const option = getLocationFromType(field.type, locationOptions);
|
||||
return (
|
||||
<li key={field.id}>
|
||||
@@ -315,7 +322,9 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
data-testid={`delete-locations.${index}.type`}
|
||||
className={classNames("min-h-9 block h-9 px-2", customClassNames?.removeLocationButton)}
|
||||
type="button"
|
||||
onClick={() => remove(index)}
|
||||
onClick={() => {
|
||||
remove(index);
|
||||
}}
|
||||
aria-label={t("remove")}>
|
||||
<div className="h-4 w-4">
|
||||
<Icon
|
||||
@@ -330,6 +339,64 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isCalVideo && (
|
||||
<div className="bg-muted mt-2 space-y-2 rounded-lg p-4">
|
||||
<div className="w-full">
|
||||
<div className="flex flex-col gap-2">
|
||||
{!isPlatform && (
|
||||
<Controller
|
||||
name="calVideoSettings.disableRecordingForGuests"
|
||||
defaultValue={!!eventType.calVideoSettings?.disableRecordingForGuests}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("disable_recording_for_guests")}
|
||||
labelClassName="text-sm"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isPlatform && (
|
||||
<Controller
|
||||
name="calVideoSettings.disableRecordingForOrganizer"
|
||||
defaultValue={!!eventType.calVideoSettings?.disableRecordingForOrganizer}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("disable_recording_for_organizer")}
|
||||
labelClassName="text-sm"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label={t("enter_redirect_url_on_exit_description")}
|
||||
defaultValue={eventType.calVideoSettings?.redirectUrlOnExit || ""}
|
||||
data-testid="calVideoSettings.redirectUrlOnExit"
|
||||
containerClassName="mt-2"
|
||||
{...formMethods.register("calVideoSettings.redirectUrlOnExit", {
|
||||
setValueAs: (v) => (!v || v.trim() === "" ? null : v),
|
||||
})}
|
||||
/>
|
||||
<ErrorMessage
|
||||
errors={formMethods.formState.errors?.calVideoSettings}
|
||||
name="redirectUrlOnExit"
|
||||
className={classNames("text-error text-sm")}
|
||||
as="div"
|
||||
id="calVideoSettings.redirectUrlOnExit-error"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{eventLocationType?.organizerInputType && (
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className="w-full">
|
||||
@@ -482,7 +549,7 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
{props.showAppStoreLink && !isPlatorm && (
|
||||
{props.showAppStoreLink && !isPlatform && (
|
||||
<p className="text-default mt-2 text-sm">
|
||||
<ServerTrans
|
||||
t={t}
|
||||
|
||||
@@ -151,6 +151,11 @@ export type FormValues = {
|
||||
secondaryEmailId?: number;
|
||||
isRRWeightsEnabled: boolean;
|
||||
maxLeadThreshold?: number;
|
||||
calVideoSettings?: {
|
||||
disableRecordingForOrganizer?: boolean;
|
||||
disableRecordingForGuests?: boolean;
|
||||
redirectUrlOnExit?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type LocationFormValues = Pick<FormValues, "id" | "locations" | "bookingFields" | "seatsPerTimeSlot">;
|
||||
|
||||
@@ -374,7 +374,15 @@ export class BookingRepository {
|
||||
isRecorded: true,
|
||||
eventType: {
|
||||
select: {
|
||||
id: true,
|
||||
hideOrganizerEmail: true,
|
||||
calVideoSettings: {
|
||||
select: {
|
||||
disableRecordingForGuests: true,
|
||||
disableRecordingForOrganizer: true,
|
||||
redirectUrlOnExit: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
export class CalVideoSettingsRepository {
|
||||
static async deleteCalVideoSettings(eventTypeId: number) {
|
||||
return await prisma.calVideoSettings.delete({
|
||||
where: { eventTypeId },
|
||||
});
|
||||
}
|
||||
|
||||
static async createOrUpdateCalVideoSettings({
|
||||
eventTypeId,
|
||||
calVideoSettings,
|
||||
}: {
|
||||
eventTypeId: number;
|
||||
calVideoSettings: {
|
||||
disableRecordingForGuests?: boolean | null;
|
||||
disableRecordingForOrganizer?: boolean | null;
|
||||
redirectUrlOnExit?: string | null;
|
||||
};
|
||||
}) {
|
||||
return await prisma.calVideoSettings.upsert({
|
||||
where: { eventTypeId },
|
||||
update: {
|
||||
disableRecordingForGuests: calVideoSettings.disableRecordingForGuests ?? false,
|
||||
disableRecordingForOrganizer: calVideoSettings.disableRecordingForOrganizer ?? false,
|
||||
redirectUrlOnExit: calVideoSettings.redirectUrlOnExit ?? null,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
create: {
|
||||
disableRecordingForGuests: calVideoSettings.disableRecordingForGuests ?? false,
|
||||
disableRecordingForOrganizer: calVideoSettings.disableRecordingForOrganizer ?? false,
|
||||
redirectUrlOnExit: calVideoSettings.redirectUrlOnExit ?? null,
|
||||
eventTypeId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -453,6 +453,15 @@ export class EventTypeRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findByIdWithUserAccess({ id, userId }: { id: number; userId: number }) {
|
||||
return await prisma.eventType.findUnique({
|
||||
where: {
|
||||
id,
|
||||
OR: [{ userId }, { hosts: { some: { userId } } }, { users: { some: { id: userId } } }],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static async findById({ id, userId }: { id: number; userId: number }) {
|
||||
const userSelect = Prisma.validator<Prisma.UserSelect>()({
|
||||
name: true,
|
||||
@@ -677,6 +686,13 @@ export class EventTypeRepository {
|
||||
maxLeadThreshold: true,
|
||||
includeNoShowInRRCalculation: true,
|
||||
useEventLevelSelectedCalendars: true,
|
||||
calVideoSettings: {
|
||||
select: {
|
||||
disableRecordingForGuests: true,
|
||||
disableRecordingForOrganizer: true,
|
||||
redirectUrlOnExit: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// This is more efficient than using a complex join with team.members in the query
|
||||
|
||||
@@ -130,6 +130,7 @@ export const useEventTypeForm = ({
|
||||
includeNoShowInRRCalculation: eventType.includeNoShowInRRCalculation,
|
||||
useEventLevelSelectedCalendars: eventType.useEventLevelSelectedCalendars,
|
||||
customReplyToEmail: eventType.customReplyToEmail || null,
|
||||
calVideoSettings: eventType.calVideoSettings,
|
||||
};
|
||||
}, [eventType, periodDates]);
|
||||
|
||||
@@ -162,6 +163,14 @@ export const useEventTypeForm = ({
|
||||
offsetStart: z.union([z.string().transform((val) => +val), z.number()]).optional(),
|
||||
bookingFields: eventTypeBookingFieldsSchema,
|
||||
locations: locationsResolver(t),
|
||||
calVideoSettings: z
|
||||
.object({
|
||||
redirectUrlOnExit: z.string().url().nullish(),
|
||||
disableRecordingForOrganizer: z.boolean().nullable(),
|
||||
disableRecordingForGuests: z.boolean().nullable(),
|
||||
})
|
||||
.optional()
|
||||
.nullable(),
|
||||
})
|
||||
// TODO: Add schema for other fields later.
|
||||
.passthrough()
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "CalVideoSettings" (
|
||||
"eventTypeId" INTEGER NOT NULL,
|
||||
"disableRecordingForOrganizer" BOOLEAN NOT NULL DEFAULT false,
|
||||
"disableRecordingForGuests" BOOLEAN NOT NULL DEFAULT false,
|
||||
"redirectUrlOnExit" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "CalVideoSettings_pkey" PRIMARY KEY ("eventTypeId")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CalVideoSettings" ADD CONSTRAINT "CalVideoSettings_eventTypeId_fkey" FOREIGN KEY ("eventTypeId") REFERENCES "EventType"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -68,6 +68,18 @@ model Host {
|
||||
@@index([scheduleId])
|
||||
}
|
||||
|
||||
model CalVideoSettings {
|
||||
eventTypeId Int @id
|
||||
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
|
||||
|
||||
disableRecordingForOrganizer Boolean @default(false)
|
||||
disableRecordingForGuests Boolean @default(false)
|
||||
redirectUrlOnExit String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model EventType {
|
||||
id Int @id @default(autoincrement())
|
||||
/// @zod.min(1)
|
||||
@@ -176,6 +188,7 @@ model EventType {
|
||||
hideOrganizerEmail Boolean @default(false)
|
||||
/// @zod.custom(imports.emailSchema)
|
||||
customReplyToEmail String?
|
||||
calVideoSettings CalVideoSettings?
|
||||
|
||||
/// @zod.custom(imports.eventTypeColor)
|
||||
eventTypeColor Json?
|
||||
|
||||
@@ -25,6 +25,15 @@ const aiPhoneCallConfig = z
|
||||
})
|
||||
.optional();
|
||||
|
||||
const calVideoSettingsSchema = z
|
||||
.object({
|
||||
disableRecordingForGuests: z.boolean().optional().nullable(),
|
||||
disableRecordingForOrganizer: z.boolean().optional().nullable(),
|
||||
redirectUrlOnExit: z.string().url().optional().nullable(),
|
||||
})
|
||||
.optional()
|
||||
.nullable();
|
||||
|
||||
const hostSchema = z.object({
|
||||
userId: z.number(),
|
||||
profileId: z.number().or(z.null()).optional(),
|
||||
@@ -51,6 +60,7 @@ const BaseEventTypeUpdateInput = _EventTypeModel
|
||||
instantMeetingParameters: z.array(z.string()),
|
||||
instantMeetingExpiryTimeOffsetInSeconds: z.number(),
|
||||
aiPhoneCallConfig,
|
||||
calVideoSettings: calVideoSettingsSchema,
|
||||
calAiPhoneScript: z.string(),
|
||||
customInputs: z.array(customInputSchema),
|
||||
destinationCalendar: _DestinationCalendarModel
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Prisma } from "@prisma/client";
|
||||
import type { NextApiResponse, GetServerSidePropsContext } from "next";
|
||||
|
||||
import type { appDataSchemas } from "@calcom/app-store/apps.schemas.generated";
|
||||
import { DailyLocationType } from "@calcom/app-store/locations";
|
||||
import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes";
|
||||
import {
|
||||
allowDisablingAttendeeConfirmationEmails,
|
||||
@@ -11,11 +12,13 @@ import tasker from "@calcom/features/tasker";
|
||||
import { validateIntervalLimitOrder } from "@calcom/lib/intervalLimits/validateIntervalLimitOrder";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { CalVideoSettingsRepository } from "@calcom/lib/server/repository/calVideoSettings";
|
||||
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { WorkflowTriggerEvents } from "@calcom/prisma/client";
|
||||
import { SchedulingType, EventTypeAutoTranslatedField } from "@calcom/prisma/enums";
|
||||
import { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils";
|
||||
import { eventTypeLocations } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
@@ -83,6 +86,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
description: newDescription,
|
||||
title: newTitle,
|
||||
seatsPerTimeSlot,
|
||||
calVideoSettings,
|
||||
...rest
|
||||
} = input;
|
||||
|
||||
@@ -90,6 +94,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
where: { id },
|
||||
select: {
|
||||
title: true,
|
||||
locations: true,
|
||||
description: true,
|
||||
seatsPerTimeSlot: true,
|
||||
recurringEvent: true,
|
||||
@@ -115,6 +120,13 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
llmId: true,
|
||||
},
|
||||
},
|
||||
calVideoSettings: {
|
||||
select: {
|
||||
disableRecordingForOrganizer: true,
|
||||
disableRecordingForGuests: true,
|
||||
redirectUrlOnExit: true,
|
||||
},
|
||||
},
|
||||
children: {
|
||||
select: {
|
||||
userId: true,
|
||||
@@ -518,6 +530,24 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (calVideoSettings) {
|
||||
await CalVideoSettingsRepository.createOrUpdateCalVideoSettings({
|
||||
eventTypeId: id,
|
||||
calVideoSettings,
|
||||
});
|
||||
}
|
||||
|
||||
const parsedEventTypeLocations = eventTypeLocations.safeParse(eventType.locations ?? []);
|
||||
|
||||
const isCalVideoLocationActive = locations
|
||||
? locations.some((location) => location.type === DailyLocationType)
|
||||
: parsedEventTypeLocations.success &&
|
||||
parsedEventTypeLocations.data?.some((location) => location.type === DailyLocationType);
|
||||
|
||||
if (eventType.calVideoSettings && !isCalVideoLocationActive) {
|
||||
await CalVideoSettingsRepository.deleteCalVideoSettings(id);
|
||||
}
|
||||
|
||||
// Logic for updating `fieldTranslations`
|
||||
// user has no translations OR user is changing the field
|
||||
const hasNoDescriptionTranslations =
|
||||
|
||||
Reference in New Issue
Block a user