Skip to content

Commit 1fa8a5e

Browse files
feat: allow inserting webhook variables into custom payload template (#22835)
* feat: allow inserting webhook variables into custom payload template * localize UI and apply design system styling * add translations and minor UI fixes * add translations --------- Co-authored-by: Carina Wollendorfer <[email protected]>
1 parent 5a350ef commit 1fa8a5e

File tree

2 files changed

+247
-8
lines changed

2 files changed

+247
-8
lines changed

apps/web/public/static/locales/en/common.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3441,6 +3441,35 @@
34413441
"usage_based_expiration_description": "This link can be used for {{count}} booking",
34423442
"usage_based_generic_expiration_description": "This link can be configured to expire after a set number of bookings",
34433443
"usage_based_expiration_description_plural": "This link can be used for {{count}} bookings",
3444+
"webhook_trigger_event": "The name of the trigger event (e.g., BOOKING_CREATED, BOOKING_CANCELLED)",
3445+
"webhook_created_at": "The time of the webhook",
3446+
"webhook_type": "The event type slug",
3447+
"webhook_title": "The event type name",
3448+
"webhook_start_time": "The event's start time",
3449+
"webhook_end_time": "The event's end time",
3450+
"webhook_description": "The event's description as described in the event type settings",
3451+
"webhook_location": "Location of the event",
3452+
"webhook_uid": "The UID of the booking",
3453+
"webhook_reschedule_uid": "The UID for rescheduling",
3454+
"webhook_cancellation_reason": "Reason for cancellation",
3455+
"webhook_rejection_reason": "Reason for rejection",
3456+
"webhook_organizer_name": "Name of the organizer",
3457+
"webhook_organizer_email": "Email of the organizer",
3458+
"webhook_organizer_timezone": "Timezone of the organizer (e.g., 'America/New_York', 'Asia/Kolkata')",
3459+
"webhook_organizer_locale": "Locale of the organizer (e.g., 'en', 'fr')",
3460+
"webhook_attendee_name": "Name of the first attendee",
3461+
"webhook_attendee_email": "Email of the first attendee",
3462+
"webhook_attendee_timezone": "Timezone of the first attendee",
3463+
"webhook_attendee_locale": "Locale of the first attendee",
3464+
"webhook_team_name": "Name of the team booked",
3465+
"webhook_team_members": "Members of the team booked",
3466+
"webhook_video_call_url": "Video call URL for the meeting",
3467+
"webhook_hide_variables": "Hide variables",
3468+
"webhook_show_variable": "Show available variables",
3469+
"webhook_event_and_booking": "Event and Booking",
3470+
"webhook_people": "People",
3471+
"webhook_teams": "Teams",
3472+
"webhook_metadata": "Metadata",
34443473
"stats": "Stats",
34453474
"booking_status": "Booking status",
34463475
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"

packages/features/webhooks/components/WebhookForm.tsx

Lines changed: 218 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,154 @@ const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2: Record<string, WebhookTriggerEve
7373
],
7474
} as const;
7575

76+
function getWebhookVariables(t: (key: string) => string) {
77+
return [
78+
{
79+
category: t("webhook_event_and_booking"),
80+
variables: [
81+
{
82+
name: "triggerEvent",
83+
variable: "{{triggerEvent}}",
84+
type: "String",
85+
description: t("webhook_trigger_event"),
86+
},
87+
{
88+
name: "createdAt",
89+
variable: "{{createdAt}}",
90+
type: "Datetime",
91+
description: t("webhook_created_at"),
92+
},
93+
{ name: "type", variable: "{{type}}", type: "String", description: t("webhook_type") },
94+
{ name: "title", variable: "{{title}}", type: "String", description: t("webhook_title") },
95+
{
96+
name: "startTime",
97+
variable: "{{startTime}}",
98+
type: "Datetime",
99+
description: t("webhook_start_time"),
100+
},
101+
{
102+
name: "endTime",
103+
variable: "{{endTime}}",
104+
type: "Datetime",
105+
description: t("webhook_end_time"),
106+
},
107+
{
108+
name: "description",
109+
variable: "{{description}}",
110+
type: "String",
111+
description: t("webhook_description"),
112+
},
113+
{
114+
name: "location",
115+
variable: "{{location}}",
116+
type: "String",
117+
description: t("webhook_location"),
118+
},
119+
{ name: "uid", variable: "{{uid}}", type: "String", description: t("webhook_uid") },
120+
{
121+
name: "rescheduleUid",
122+
variable: "{{rescheduleUid}}",
123+
type: "String",
124+
description: t("webhook_reschedule_uid"),
125+
},
126+
{
127+
name: "cancellationReason",
128+
variable: "{{cancellationReason}}",
129+
type: "String",
130+
description: t("webhook_cancellation_reason"),
131+
},
132+
{
133+
name: "rejectionReason",
134+
variable: "{{rejectionReason}}",
135+
type: "String",
136+
description: t("webhook_rejection_reason"),
137+
},
138+
],
139+
},
140+
{
141+
category: t("webhook_people"),
142+
variables: [
143+
{
144+
name: "organizer.name",
145+
variable: "{{organizer.name}}",
146+
type: "String",
147+
description: t("webhook_organizer_name"),
148+
},
149+
{
150+
name: "organizer.email",
151+
variable: "{{organizer.email}}",
152+
type: "String",
153+
description: t("webhook_organizer_email"),
154+
},
155+
{
156+
name: "organizer.timezone",
157+
variable: "{{organizer.timezone}}",
158+
type: "String",
159+
description: t("webhook_organizer_timezone"),
160+
},
161+
{
162+
name: "organizer.language.locale",
163+
variable: "{{organizer.language.locale}}",
164+
type: "String",
165+
description: t("webhook_organizer_locale"),
166+
},
167+
{
168+
name: "attendees.0.name",
169+
variable: "{{attendees.0.name}}",
170+
type: "String",
171+
description: t("webhook_attendee_name"),
172+
},
173+
{
174+
name: "attendees.0.email",
175+
variable: "{{attendees.0.email}}",
176+
type: "String",
177+
description: t("webhook_attendee_email"),
178+
},
179+
{
180+
name: "attendees.0.timezone",
181+
variable: "{{attendees.0.timezone}}",
182+
type: "String",
183+
description: t("webhook_attendee_timezone"),
184+
},
185+
{
186+
name: "attendees.0.language.locale",
187+
variable: "{{attendees.0.language.locale}}",
188+
type: "String",
189+
description: t("webhook_attendee_locale"),
190+
},
191+
],
192+
},
193+
{
194+
category: t("webhook_teams"),
195+
variables: [
196+
{
197+
name: "team.name",
198+
variable: "{{team.name}}",
199+
type: "String",
200+
description: t("webhook_team_name"),
201+
},
202+
{
203+
name: "team.members",
204+
variable: "{{team.members}}",
205+
type: "String[]",
206+
description: t("webhook_team_members"),
207+
},
208+
],
209+
},
210+
{
211+
category: t("webhook_metadata"),
212+
variables: [
213+
{
214+
name: "metadata.videoCallUrl",
215+
variable: "{{metadata.videoCallUrl}}",
216+
type: "String",
217+
description: t("webhook_video_call_url"),
218+
},
219+
],
220+
},
221+
];
222+
}
223+
76224
export type WebhookFormValues = {
77225
subscriberUrl: string;
78226
active: boolean;
@@ -94,6 +242,7 @@ const WebhookForm = (props: {
94242
}) => {
95243
const { apps = [], selectOnlyInstantMeetingOption = false, overrideTriggerOptions } = props;
96244
const { t } = useLocale();
245+
const webhookVariables = getWebhookVariables(t);
97246

98247
const triggerOptions = overrideTriggerOptions
99248
? [...overrideTriggerOptions]
@@ -141,6 +290,29 @@ const WebhookForm = (props: {
141290
const [useCustomTemplate, setUseCustomTemplate] = useState(
142291
props?.webhook?.payloadTemplate !== undefined && props?.webhook?.payloadTemplate !== null
143292
);
293+
294+
function insertVariableIntoTemplate(current: string, name: string, value: string): string {
295+
try {
296+
const parsed = JSON.parse(current || "{}");
297+
parsed[name] = value;
298+
return JSON.stringify(parsed, null, 2);
299+
} catch {
300+
const trimmed = current.trim();
301+
if (trimmed === "{}" || trimmed === "") {
302+
return `{\n "${name}": "${value}"\n}`;
303+
}
304+
305+
if (trimmed.endsWith("}")) {
306+
const withoutClosing = trimmed.slice(0, -1);
307+
const needsComma = withoutClosing.trim().endsWith('"') || withoutClosing.trim().endsWith("}");
308+
return `${withoutClosing}${needsComma ? "," : ""}\n "${name}": "${value}"\n}`;
309+
}
310+
311+
return `${current}\n"${name}": "${value}"`;
312+
}
313+
}
314+
315+
const [showVariables, setShowVariables] = useState(false);
144316
const [newSecret, setNewSecret] = useState("");
145317
const [changeSecret, setChangeSecret] = useState<boolean>(false);
146318
const hasSecretKey = !!props?.webhook?.secret;
@@ -339,14 +511,52 @@ const WebhookForm = (props: {
339511
/>
340512
</div>
341513
{useCustomTemplate && (
342-
<TextArea
343-
name="customPayloadTemplate"
344-
rows={3}
345-
value={value}
346-
onChange={(e) => {
347-
formMethods.setValue("payloadTemplate", e?.target.value, { shouldDirty: true });
348-
}}
349-
/>
514+
<div className="space-y-3">
515+
<TextArea
516+
name="customPayloadTemplate"
517+
rows={8}
518+
value={value || ""}
519+
placeholder={`{\n\n}`}
520+
onChange={(e) =>
521+
formMethods.setValue("payloadTemplate", e?.target.value, { shouldDirty: true })
522+
}
523+
/>
524+
525+
<Button type="button" color="secondary" onClick={() => setShowVariables(!showVariables)}>
526+
{showVariables ? t("webhook_hide_variables") : t("webhook_show_variable")}
527+
</Button>
528+
529+
{showVariables && (
530+
<div className="border-muted max-h-80 overflow-y-auto rounded-md border p-3">
531+
{webhookVariables.map(({ category, variables }) => (
532+
<div key={category} className="mb-4">
533+
<h4 className="mb-2 text-sm font-medium">{category}</h4>
534+
<div className="space-y-2">
535+
{variables.map(({ name, variable, description }) => (
536+
<div
537+
key={name}
538+
className="hover:bg-muted cursor-pointer rounded p-2 text-sm transition-colors"
539+
onClick={() => {
540+
const currentValue = formMethods.getValues("payloadTemplate") || "{}";
541+
const updatedValue = insertVariableIntoTemplate(
542+
currentValue,
543+
name,
544+
variable
545+
);
546+
formMethods.setValue("payloadTemplate", updatedValue, {
547+
shouldDirty: true,
548+
});
549+
}}>
550+
<div className="text-emphasis font-mono">{variable}</div>
551+
<div className="text-muted mt-1 text-xs">{description}</div>
552+
</div>
553+
))}
554+
</div>
555+
</div>
556+
))}
557+
</div>
558+
)}
559+
</div>
350560
)}
351561
</>
352562
)}

0 commit comments

Comments
 (0)