From ce8e1d52da4689fd720b698eca1bacf40a56408f Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Tue, 7 Mar 2023 23:20:54 +0530 Subject: [PATCH] Fix "User Added Question" Label in Email and Calendar Invite (#7559) * Show label in email and calendar invite and send label as well as name in webhook * Make common code reusable --------- Co-authored-by: Peer Richelsen --- apps/web/playwright/webhook.e2e.ts | 11 +++++++- .../src/components/UserFieldsResponses.tsx | 13 +++++---- .../features/bookings/lib/handleNewBooking.ts | 28 ++++++++++++++----- packages/lib/CalEventParser.ts | 14 ++++++---- packages/lib/getLabelValueMapFromResponses.ts | 15 ++++++++++ packages/types/Calendar.d.ts | 16 +++++++++-- 6 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 packages/lib/getLabelValueMapFromResponses.ts diff --git a/apps/web/playwright/webhook.e2e.ts b/apps/web/playwright/webhook.e2e.ts index 3ed89f3a7d..23ba60e2a1 100644 --- a/apps/web/playwright/webhook.e2e.ts +++ b/apps/web/playwright/webhook.e2e.ts @@ -90,7 +90,16 @@ test("add webhook & test that creating an event triggers a webhook call", async timeZone: "[redacted/dynamic]", language: "[redacted/dynamic]", }, - responses: { email: "test@example.com", name: "Test Testson" }, + responses: { + email: { + value: "test@example.com", + label: "email_address", + }, + name: { + value: "Test Testson", + label: "your_name", + }, + }, userFieldsResponses: {}, attendees: [ { diff --git a/packages/emails/src/components/UserFieldsResponses.tsx b/packages/emails/src/components/UserFieldsResponses.tsx index fed4c41971..4df5a4bb81 100644 --- a/packages/emails/src/components/UserFieldsResponses.tsx +++ b/packages/emails/src/components/UserFieldsResponses.tsx @@ -1,16 +1,17 @@ +import getLabelValueMapFromResponses from "@calcom/lib/getLabelValueMapFromResponses"; import type { CalendarEvent } from "@calcom/types/Calendar"; import { Info } from "./Info"; export function UserFieldsResponses(props: { calEvent: CalendarEvent }) { - const { customInputs, userFieldsResponses } = props.calEvent; - const responses = userFieldsResponses || customInputs; - if (!responses) return null; + const labelValueMap = getLabelValueMapFromResponses(props.calEvent); + + if (!labelValueMap) return null; return ( <> - {Object.keys(responses).map((key) => - responses[key] !== "" ? ( - + {Object.keys(labelValueMap).map((key) => + labelValueMap[key] !== "" ? ( + ) : null )} diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index db41714499..85254195ca 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -363,11 +363,23 @@ function getBookingData({ const reqBody = bookingDataSchema.parse(req.body); if ("responses" in reqBody) { const responses = reqBody.responses; - const userFieldsResponses = {} as typeof responses; + const calEventResponses = {} as NonNullable; + const calEventUserFieldsResponses = {} as NonNullable; eventType.bookingFields.forEach((field) => { - if (field.editable === "user" || field.editable === "user-readonly") { - userFieldsResponses[field.name] = responses[field.name]; + const label = field.label || field.defaultLabel; + if (!label) { + throw new Error('Missing label for booking field "' + field.name + '"'); } + if (field.editable === "user" || field.editable === "user-readonly") { + calEventUserFieldsResponses[field.name] = { + label, + value: responses[field.name], + }; + } + calEventResponses[field.name] = { + label, + value: responses[field.name], + }; }); return { ...reqBody, @@ -377,8 +389,9 @@ function getBookingData({ location: responses.location?.optionValue || responses.location?.value || "", smsReminderNumber: responses.smsReminderNumber, notes: responses.notes || "", - userFieldsResponses, + calEventUserFieldsResponses, rescheduleReason: responses.rescheduleReason, + calEventResponses, }; } else { // Check if required custom inputs exist @@ -721,7 +734,8 @@ async function handler( } const responses = "responses" in reqBody ? reqBody.responses : null; - const userFieldsResponses = "userFieldsResponses" in reqBody ? reqBody.userFieldsResponses : null; + const calEventUserFieldsResponses = + "calEventUserFieldsResponses" in reqBody ? reqBody.calEventUserFieldsResponses : null; let evt: CalendarEvent = { type: eventType.title, title: getEventName(eventNameObject), //this needs to be either forced in english, or fetched for each attendee and organizer separately @@ -737,8 +751,8 @@ async function handler( timeZone: organizerUser.timeZone, language: { translate: tOrganizer, locale: organizerUser.locale ?? "en" }, }, - responses, - userFieldsResponses, + responses: "calEventResponses" in reqBody ? reqBody.calEventResponses : null, + userFieldsResponses: calEventUserFieldsResponses, attendees: attendeesList, location: bookingLocation, // Will be processed by the EventManager later. /** For team events & dynamic collective events, we will need to handle each member destinationCalendar eventually */ diff --git a/packages/lib/CalEventParser.ts b/packages/lib/CalEventParser.ts index 1413f77b8a..2a8f95f710 100644 --- a/packages/lib/CalEventParser.ts +++ b/packages/lib/CalEventParser.ts @@ -4,6 +4,7 @@ import { v5 as uuidv5 } from "uuid"; import type { CalendarEvent } from "@calcom/types/Calendar"; import { WEBAPP_URL } from "./constants"; +import getLabelValueMapFromResponses from "./getLabelValueMapFromResponses"; const translator = short(); @@ -72,17 +73,18 @@ ${calEvent.additionalNotes} }; export const getUserFieldsResponses = (calEvent: CalendarEvent) => { - const responses = calEvent.userFieldsResponses || calEvent.customInputs; - if (!responses) { + const labelValueMap = getLabelValueMapFromResponses(calEvent); + + if (!labelValueMap) { return ""; } - const responsesString = Object.keys(responses) + const responsesString = Object.keys(labelValueMap) .map((key) => { - if (!responses) return ""; - if (responses[key] !== "") { + if (!labelValueMap) return ""; + if (labelValueMap[key] !== "") { return ` ${key}: -${responses[key]} +${labelValueMap[key]} `; } }) diff --git a/packages/lib/getLabelValueMapFromResponses.ts b/packages/lib/getLabelValueMapFromResponses.ts new file mode 100644 index 0000000000..124c8af35c --- /dev/null +++ b/packages/lib/getLabelValueMapFromResponses.ts @@ -0,0 +1,15 @@ +import type { CalendarEvent } from "@calcom/types/Calendar"; + +export default function getLabelValueMapFromResponses(calEvent: CalendarEvent) { + const { customInputs, userFieldsResponses } = calEvent; + + let labelValueMap: Record = {}; + if (userFieldsResponses) { + for (const [, value] of Object.entries(userFieldsResponses)) { + labelValueMap[value.label] = value.value; + } + } else { + labelValueMap = customInputs as Record; + } + return labelValueMap; +} diff --git a/packages/types/Calendar.d.ts b/packages/types/Calendar.d.ts index d4db84dcc7..46e94dc7dd 100644 --- a/packages/types/Calendar.d.ts +++ b/packages/types/Calendar.d.ts @@ -165,10 +165,22 @@ export interface CalendarEvent { seatsPerTimeSlot?: number | null; // It has responses to all the fields(system + user) - responses?: Prisma.JsonObject | null; + responses?: Record< + string, + { + value: string | string[]; + label: string; + } + > | null; // It just has responses to only the user fields. It allows to easily iterate over to show only user fields - userFieldsResponses?: Prisma.JsonObject | null; + userFieldsResponses?: Record< + string, + { + value: string | string[]; + label: string; + } + > | null; } export interface EntryPoint {