feat: add toggle to opt out of booking title translation in instant meetings (#25547)
* feat: add toggle to opt out of booking title translation in instant meetings Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: add autoTranslateTitleEnabled to test builder Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: respect autoTranslateTitleEnabled toggle in InstantBookingCreateService Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: fetch autoTranslateTitleEnabled directly in InstantBookingCreateService Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * refactor: rename autoTranslateTitleEnabled to autoTranslateInstantMeetingTitleEnabled - Renamed field to clarify it only applies to instant meeting title translation - Moved Prisma query to EventTypeRepository.findInstantMeetingConfigById() - Removed title translation logic from update.handler.ts (flag only controls instant meeting title) - Updated all references across the codebase - Added new i18n translation keys for the renamed field Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: add autoTranslateInstantMeetingTitleEnabled to test destructuring patterns Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: use Prisma.TeamCreateInput type for metadata in test helper Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: add autoTranslateInstantMeetingTitleEnabled to mockUpdatedEventType in test Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * refactor: use generic findByIdMinimal instead of business-specific method - Add autoTranslateInstantMeetingTitleEnabled to eventTypeSelect constant - Remove findInstantMeetingConfigById from EventTypeRepository - Update InstantBookingCreateService to use findByIdMinimal Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * refactor: remove autoTranslateInstantMeetingTitleEnabled from eventTypeSelect (not needed for findByIdMinimal) Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * feat: add autoTranslateInstantMeetingTitleEnabled to event type form defaults - Rename shouldTranslateTitle to shouldAutoTranslateInstantMeetingTitle for clarity - Add autoTranslateInstantMeetingTitleEnabled to findById and findByIdForOrgAdmin selects - Add autoTranslateInstantMeetingTitleEnabled to useEventTypeForm defaultValues Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: only update autoTranslateInstantMeetingTitleEnabled when explicitly provided Fixes issue where omitting the field in update requests would overwrite the saved opt-out setting with the default value (true). Now the field is only included in the update payload when explicitly provided. Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
6ae2ce8ed9
commit
3a59cd41cc
@@ -3261,7 +3261,11 @@
|
||||
"filter_operator_is_empty": "Is empty",
|
||||
"filter_operator_not_empty": "Not empty",
|
||||
"translate_description_button": "Auto translate title and description",
|
||||
"translate_description_button_description": "Automatically translate titles and descriptions to the visitor’s browser language using AI.",
|
||||
"translate_description_button_description": "Automatically translate titles and descriptions to the visitor's browser language using AI.",
|
||||
"auto_translate_booking_title": "Auto translate booking title",
|
||||
"auto_translate_booking_title_description": "Automatically translate the booking title to the visitor's browser language. Disable this to keep the original title.",
|
||||
"auto_translate_instant_meeting_title": "Auto translate instant meeting title",
|
||||
"auto_translate_instant_meeting_title_description": "Automatically translate the instant meeting booking title to the visitor's browser language. Disable this to keep the original English title.",
|
||||
"rr_distribution_method": "Distribution",
|
||||
"rr_distribution_method_description": "Allows for optimising distribution for maximum availability or to aim for a more balanced assignment.",
|
||||
"rr_distribution_method_availability_title": "Maximize availability",
|
||||
|
||||
@@ -136,6 +136,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId,
|
||||
autoTranslateDescriptionEnabled,
|
||||
autoTranslateInstantMeetingTitleEnabled,
|
||||
includeNoShowInRRCalculation,
|
||||
instantMeetingScheduleId,
|
||||
...evType
|
||||
@@ -161,6 +162,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId,
|
||||
autoTranslateDescriptionEnabled,
|
||||
autoTranslateInstantMeetingTitleEnabled,
|
||||
includeNoShowInRRCalculation,
|
||||
instantMeetingScheduleId,
|
||||
};
|
||||
@@ -223,6 +225,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId,
|
||||
assignRRMembersUsingSegment,
|
||||
autoTranslateInstantMeetingTitleEnabled,
|
||||
includeNoShowInRRCalculation,
|
||||
instantMeetingScheduleId,
|
||||
...evType
|
||||
@@ -332,6 +335,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId,
|
||||
autoTranslateDescriptionEnabled,
|
||||
autoTranslateInstantMeetingTitleEnabled,
|
||||
includeNoShowInRRCalculation,
|
||||
instantMeetingScheduleId,
|
||||
assignRRMembersUsingSegment,
|
||||
@@ -358,6 +362,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId,
|
||||
autoTranslateDescriptionEnabled,
|
||||
autoTranslateInstantMeetingTitleEnabled,
|
||||
includeNoShowInRRCalculation,
|
||||
instantMeetingScheduleId,
|
||||
assignRRMembersUsingSegment,
|
||||
@@ -420,6 +425,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId,
|
||||
autoTranslateInstantMeetingTitleEnabled,
|
||||
includeNoShowInRRCalculation,
|
||||
instantMeetingScheduleId,
|
||||
assignRRMembersUsingSegment,
|
||||
@@ -487,6 +493,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId,
|
||||
autoTranslateDescriptionEnabled,
|
||||
autoTranslateInstantMeetingTitleEnabled,
|
||||
includeNoShowInRRCalculation,
|
||||
instantMeetingScheduleId,
|
||||
assignRRMembersUsingSegment,
|
||||
@@ -520,6 +527,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId,
|
||||
autoTranslateDescriptionEnabled,
|
||||
autoTranslateInstantMeetingTitleEnabled,
|
||||
includeNoShowInRRCalculation,
|
||||
instantMeetingScheduleId,
|
||||
assignRRMembersUsingSegment,
|
||||
@@ -533,6 +541,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
timeZone: "UTC",
|
||||
teamId: 1,
|
||||
autoTranslateDescriptionEnabled: false,
|
||||
autoTranslateInstantMeetingTitleEnabled: true,
|
||||
secondaryEmailId: null,
|
||||
schedulingType: SchedulingType.MANAGED,
|
||||
requiresBookerEmailVerification: false,
|
||||
|
||||
@@ -181,6 +181,8 @@ const getEventTypesFromDBSelect = {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
instantMeetingExpiryTimeOffsetInSeconds: true,
|
||||
autoTranslateInstantMeetingTitleEnabled: true,
|
||||
} satisfies Prisma.EventTypeSelect;
|
||||
|
||||
export const getEventTypesFromDB = async (eventTypeId: number) => {
|
||||
|
||||
@@ -197,9 +197,19 @@ export async function handler(
|
||||
const attendeeTimezone = reqBody.timeZone;
|
||||
const attendeeLanguage = reqBody.language;
|
||||
const tAttendees = await getTranslation(attendeeLanguage ?? "en", "common");
|
||||
const tEnglish = await getTranslation("en", "common");
|
||||
|
||||
const fullName = getFullName(bookerName);
|
||||
|
||||
// Determine whether to auto-translate the instant meeting title based on the event type setting
|
||||
// Default is true (opt-out), so we only skip translation when explicitly set to false
|
||||
const shouldAutoTranslateInstantMeetingTitle = eventType.autoTranslateInstantMeetingTitleEnabled;
|
||||
|
||||
// Get the booking title - either translated to attendee's language or in English
|
||||
const bookingTitle = shouldAutoTranslateInstantMeetingTitle
|
||||
? tAttendees("instant_meeting_with_title", { name: fullName })
|
||||
: tEnglish("instant_meeting_with_title", { name: fullName });
|
||||
|
||||
const invitee = [
|
||||
{
|
||||
email: bookerEmail,
|
||||
@@ -242,7 +252,7 @@ export async function handler(
|
||||
const newBookingData: Prisma.BookingCreateInput = {
|
||||
uid,
|
||||
responses: reqBody.responses === null ? Prisma.JsonNull : reqBody.responses,
|
||||
title: tAttendees("instant_meeting_with_title", { name: invitee[0].name }),
|
||||
title: bookingTitle,
|
||||
startTime: dayjs.utc(reqBody.start).toDate(),
|
||||
endTime: dayjs.utc(reqBody.end).toDate(),
|
||||
description: reqBody.notes,
|
||||
@@ -279,17 +289,7 @@ export async function handler(
|
||||
|
||||
const token = randomBytes(32).toString("hex");
|
||||
|
||||
const eventTypeWithExpiryTimeOffset = await prisma.eventType.findUniqueOrThrow({
|
||||
where: {
|
||||
id: bookingData.eventTypeId,
|
||||
},
|
||||
select: {
|
||||
instantMeetingExpiryTimeOffsetInSeconds: true,
|
||||
},
|
||||
});
|
||||
|
||||
const instantMeetingExpiryTimeOffsetInSeconds =
|
||||
eventTypeWithExpiryTimeOffset?.instantMeetingExpiryTimeOffsetInSeconds ?? 90;
|
||||
const instantMeetingExpiryTimeOffsetInSeconds = eventType.instantMeetingExpiryTimeOffsetInSeconds ?? 90;
|
||||
|
||||
const instantMeetingToken = await prisma.instantMeetingToken.create({
|
||||
data: {
|
||||
|
||||
@@ -245,6 +245,21 @@ export default function InstantEventController({
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="autoTranslateInstantMeetingTitleEnabled"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SettingsToggle
|
||||
labelClassName="text-sm"
|
||||
title={t("auto_translate_instant_meeting_title")}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
data-testid="auto-translate-instant-meeting-title-toggle"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6 mt-4"
|
||||
description={t("auto_translate_instant_meeting_title_description")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<InstantMeetingWebhooks eventType={eventType} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -145,6 +145,7 @@ const commons = {
|
||||
requiresConfirmationWillBlockSlot: false,
|
||||
canSendCalVideoTranscriptionEmails: false,
|
||||
instantMeetingExpiryTimeOffsetInSeconds: 0,
|
||||
autoTranslateInstantMeetingTitleEnabled: true,
|
||||
instantMeetingScheduleId: null,
|
||||
instantMeetingParameters: [],
|
||||
eventTypeColor: null,
|
||||
|
||||
@@ -135,6 +135,7 @@ export type FormValues = {
|
||||
seatsShowAvailabilityCount: boolean | null;
|
||||
seatsPerTimeSlotEnabled: boolean;
|
||||
autoTranslateDescriptionEnabled: boolean;
|
||||
autoTranslateInstantMeetingTitleEnabled: boolean;
|
||||
fieldTranslations: EventTypeTranslation[];
|
||||
scheduleName: string;
|
||||
minimumBookingNotice: number;
|
||||
|
||||
@@ -570,6 +570,7 @@ export class EventTypeRepository {
|
||||
requiresConfirmationWillBlockSlot: true,
|
||||
requiresBookerEmailVerification: true,
|
||||
autoTranslateDescriptionEnabled: true,
|
||||
autoTranslateInstantMeetingTitleEnabled: true,
|
||||
fieldTranslations: {
|
||||
select: {
|
||||
translatedText: true,
|
||||
@@ -868,6 +869,7 @@ export class EventTypeRepository {
|
||||
requiresConfirmationWillBlockSlot: true,
|
||||
requiresBookerEmailVerification: true,
|
||||
autoTranslateDescriptionEnabled: true,
|
||||
autoTranslateInstantMeetingTitleEnabled: true,
|
||||
fieldTranslations: {
|
||||
select: {
|
||||
translatedText: true,
|
||||
|
||||
@@ -155,6 +155,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
|
||||
assignRRMembersUsingSegment: false,
|
||||
rrSegmentQueryValue: null,
|
||||
autoTranslateDescriptionEnabled: false,
|
||||
autoTranslateInstantMeetingTitleEnabled: true,
|
||||
useEventLevelSelectedCalendars: false,
|
||||
allowReschedulingPastBookings: false,
|
||||
hideOrganizerEmail: false,
|
||||
|
||||
@@ -118,6 +118,7 @@ export const useEventTypeForm = ({
|
||||
})),
|
||||
seatsPerTimeSlotEnabled: eventType.seatsPerTimeSlot,
|
||||
autoTranslateDescriptionEnabled: eventType.autoTranslateDescriptionEnabled,
|
||||
autoTranslateInstantMeetingTitleEnabled: eventType.autoTranslateInstantMeetingTitleEnabled ?? true,
|
||||
rescheduleWithSameRoundRobinHost: eventType.rescheduleWithSameRoundRobinHost,
|
||||
assignAllTeamMembers: eventType.assignAllTeamMembers,
|
||||
assignRRMembersUsingSegment: eventType.assignRRMembersUsingSegment,
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."EventType" ADD COLUMN "autoTranslateInstantMeetingTitleEnabled" BOOLEAN NOT NULL DEFAULT true;
|
||||
@@ -182,6 +182,7 @@ model EventType {
|
||||
canSendCalVideoTranscriptionEmails Boolean @default(true)
|
||||
|
||||
autoTranslateDescriptionEnabled Boolean @default(false)
|
||||
autoTranslateInstantMeetingTitleEnabled Boolean @default(true)
|
||||
/// @zod.import(["import { recurringEventType } from '../../zod-utils'"]).custom.use(recurringEventType)
|
||||
recurringEvent Json?
|
||||
disableGuests Boolean @default(false)
|
||||
|
||||
@@ -93,6 +93,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
aiPhoneCallConfig,
|
||||
isRRWeightsEnabled,
|
||||
autoTranslateDescriptionEnabled,
|
||||
autoTranslateInstantMeetingTitleEnabled,
|
||||
description: newDescription,
|
||||
title: newTitle,
|
||||
seatsPerTimeSlot,
|
||||
@@ -231,7 +232,8 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
|
||||
const data: Prisma.EventTypeUpdateInput = {
|
||||
...rest,
|
||||
// autoTranslate feature is allowed for org users only
|
||||
// Only update autoTranslateInstantMeetingTitleEnabled when explicitly provided to avoid overwriting saved opt-out
|
||||
...(autoTranslateInstantMeetingTitleEnabled !== undefined && { autoTranslateInstantMeetingTitleEnabled }),
|
||||
// Only set if explicitly provided to avoid overwriting existing value with false
|
||||
...(autoTranslateDescriptionEnabled !== undefined && {
|
||||
autoTranslateDescriptionEnabled: Boolean(ctx.user.organizationId && autoTranslateDescriptionEnabled),
|
||||
|
||||
+2
-3
@@ -1,10 +1,9 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { Team, User, Membership, Profile } from "@calcom/prisma/client";
|
||||
import type { Prisma, Team, User, Membership, Profile } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
|
||||
import type { JsonValue } from "@calcom/types/Json";
|
||||
|
||||
import inviteMemberHandler, { inviteMembersWithNoInviterPermissionCheck } from "./inviteMember.handler";
|
||||
|
||||
@@ -66,7 +65,7 @@ async function createTestTeam(data: {
|
||||
slug: string;
|
||||
isOrganization?: boolean;
|
||||
parentId?: number;
|
||||
metadata?: JsonValue;
|
||||
metadata?: Prisma.TeamCreateInput["metadata"];
|
||||
organizationSettings?: {
|
||||
orgAutoAcceptEmail: string;
|
||||
isOrganizationVerified?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user