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 <30310907+CarinaWolli@users.noreply.github.com>
This commit is contained in:
keerthi vardhan
2025-08-11 07:32:52 +00:00
committed by GitHub
co-authored by Carina Wollendorfer
parent 5a350efb40
commit 1fa8a5ef25
2 changed files with 247 additions and 8 deletions
@@ -3441,6 +3441,35 @@
"usage_based_expiration_description": "This link can be used for {{count}} booking",
"usage_based_generic_expiration_description": "This link can be configured to expire after a set number of bookings",
"usage_based_expiration_description_plural": "This link can be used for {{count}} bookings",
"webhook_trigger_event": "The name of the trigger event (e.g., BOOKING_CREATED, BOOKING_CANCELLED)",
"webhook_created_at": "The time of the webhook",
"webhook_type": "The event type slug",
"webhook_title": "The event type name",
"webhook_start_time": "The event's start time",
"webhook_end_time": "The event's end time",
"webhook_description": "The event's description as described in the event type settings",
"webhook_location": "Location of the event",
"webhook_uid": "The UID of the booking",
"webhook_reschedule_uid": "The UID for rescheduling",
"webhook_cancellation_reason": "Reason for cancellation",
"webhook_rejection_reason": "Reason for rejection",
"webhook_organizer_name": "Name of the organizer",
"webhook_organizer_email": "Email of the organizer",
"webhook_organizer_timezone": "Timezone of the organizer (e.g., 'America/New_York', 'Asia/Kolkata')",
"webhook_organizer_locale": "Locale of the organizer (e.g., 'en', 'fr')",
"webhook_attendee_name": "Name of the first attendee",
"webhook_attendee_email": "Email of the first attendee",
"webhook_attendee_timezone": "Timezone of the first attendee",
"webhook_attendee_locale": "Locale of the first attendee",
"webhook_team_name": "Name of the team booked",
"webhook_team_members": "Members of the team booked",
"webhook_video_call_url": "Video call URL for the meeting",
"webhook_hide_variables": "Hide variables",
"webhook_show_variable": "Show available variables",
"webhook_event_and_booking": "Event and Booking",
"webhook_people": "People",
"webhook_teams": "Teams",
"webhook_metadata": "Metadata",
"stats": "Stats",
"booking_status": "Booking status",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
@@ -73,6 +73,154 @@ const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2: Record<string, WebhookTriggerEve
],
} as const;
function getWebhookVariables(t: (key: string) => string) {
return [
{
category: t("webhook_event_and_booking"),
variables: [
{
name: "triggerEvent",
variable: "{{triggerEvent}}",
type: "String",
description: t("webhook_trigger_event"),
},
{
name: "createdAt",
variable: "{{createdAt}}",
type: "Datetime",
description: t("webhook_created_at"),
},
{ name: "type", variable: "{{type}}", type: "String", description: t("webhook_type") },
{ name: "title", variable: "{{title}}", type: "String", description: t("webhook_title") },
{
name: "startTime",
variable: "{{startTime}}",
type: "Datetime",
description: t("webhook_start_time"),
},
{
name: "endTime",
variable: "{{endTime}}",
type: "Datetime",
description: t("webhook_end_time"),
},
{
name: "description",
variable: "{{description}}",
type: "String",
description: t("webhook_description"),
},
{
name: "location",
variable: "{{location}}",
type: "String",
description: t("webhook_location"),
},
{ name: "uid", variable: "{{uid}}", type: "String", description: t("webhook_uid") },
{
name: "rescheduleUid",
variable: "{{rescheduleUid}}",
type: "String",
description: t("webhook_reschedule_uid"),
},
{
name: "cancellationReason",
variable: "{{cancellationReason}}",
type: "String",
description: t("webhook_cancellation_reason"),
},
{
name: "rejectionReason",
variable: "{{rejectionReason}}",
type: "String",
description: t("webhook_rejection_reason"),
},
],
},
{
category: t("webhook_people"),
variables: [
{
name: "organizer.name",
variable: "{{organizer.name}}",
type: "String",
description: t("webhook_organizer_name"),
},
{
name: "organizer.email",
variable: "{{organizer.email}}",
type: "String",
description: t("webhook_organizer_email"),
},
{
name: "organizer.timezone",
variable: "{{organizer.timezone}}",
type: "String",
description: t("webhook_organizer_timezone"),
},
{
name: "organizer.language.locale",
variable: "{{organizer.language.locale}}",
type: "String",
description: t("webhook_organizer_locale"),
},
{
name: "attendees.0.name",
variable: "{{attendees.0.name}}",
type: "String",
description: t("webhook_attendee_name"),
},
{
name: "attendees.0.email",
variable: "{{attendees.0.email}}",
type: "String",
description: t("webhook_attendee_email"),
},
{
name: "attendees.0.timezone",
variable: "{{attendees.0.timezone}}",
type: "String",
description: t("webhook_attendee_timezone"),
},
{
name: "attendees.0.language.locale",
variable: "{{attendees.0.language.locale}}",
type: "String",
description: t("webhook_attendee_locale"),
},
],
},
{
category: t("webhook_teams"),
variables: [
{
name: "team.name",
variable: "{{team.name}}",
type: "String",
description: t("webhook_team_name"),
},
{
name: "team.members",
variable: "{{team.members}}",
type: "String[]",
description: t("webhook_team_members"),
},
],
},
{
category: t("webhook_metadata"),
variables: [
{
name: "metadata.videoCallUrl",
variable: "{{metadata.videoCallUrl}}",
type: "String",
description: t("webhook_video_call_url"),
},
],
},
];
}
export type WebhookFormValues = {
subscriberUrl: string;
active: boolean;
@@ -94,6 +242,7 @@ const WebhookForm = (props: {
}) => {
const { apps = [], selectOnlyInstantMeetingOption = false, overrideTriggerOptions } = props;
const { t } = useLocale();
const webhookVariables = getWebhookVariables(t);
const triggerOptions = overrideTriggerOptions
? [...overrideTriggerOptions]
@@ -141,6 +290,29 @@ const WebhookForm = (props: {
const [useCustomTemplate, setUseCustomTemplate] = useState(
props?.webhook?.payloadTemplate !== undefined && props?.webhook?.payloadTemplate !== null
);
function insertVariableIntoTemplate(current: string, name: string, value: string): string {
try {
const parsed = JSON.parse(current || "{}");
parsed[name] = value;
return JSON.stringify(parsed, null, 2);
} catch {
const trimmed = current.trim();
if (trimmed === "{}" || trimmed === "") {
return `{\n "${name}": "${value}"\n}`;
}
if (trimmed.endsWith("}")) {
const withoutClosing = trimmed.slice(0, -1);
const needsComma = withoutClosing.trim().endsWith('"') || withoutClosing.trim().endsWith("}");
return `${withoutClosing}${needsComma ? "," : ""}\n "${name}": "${value}"\n}`;
}
return `${current}\n"${name}": "${value}"`;
}
}
const [showVariables, setShowVariables] = useState(false);
const [newSecret, setNewSecret] = useState("");
const [changeSecret, setChangeSecret] = useState<boolean>(false);
const hasSecretKey = !!props?.webhook?.secret;
@@ -339,14 +511,52 @@ const WebhookForm = (props: {
/>
</div>
{useCustomTemplate && (
<TextArea
name="customPayloadTemplate"
rows={3}
value={value}
onChange={(e) => {
formMethods.setValue("payloadTemplate", e?.target.value, { shouldDirty: true });
}}
/>
<div className="space-y-3">
<TextArea
name="customPayloadTemplate"
rows={8}
value={value || ""}
placeholder={`{\n\n}`}
onChange={(e) =>
formMethods.setValue("payloadTemplate", e?.target.value, { shouldDirty: true })
}
/>
<Button type="button" color="secondary" onClick={() => setShowVariables(!showVariables)}>
{showVariables ? t("webhook_hide_variables") : t("webhook_show_variable")}
</Button>
{showVariables && (
<div className="border-muted max-h-80 overflow-y-auto rounded-md border p-3">
{webhookVariables.map(({ category, variables }) => (
<div key={category} className="mb-4">
<h4 className="mb-2 text-sm font-medium">{category}</h4>
<div className="space-y-2">
{variables.map(({ name, variable, description }) => (
<div
key={name}
className="hover:bg-muted cursor-pointer rounded p-2 text-sm transition-colors"
onClick={() => {
const currentValue = formMethods.getValues("payloadTemplate") || "{}";
const updatedValue = insertVariableIntoTemplate(
currentValue,
name,
variable
);
formMethods.setValue("payloadTemplate", updatedValue, {
shouldDirty: true,
});
}}>
<div className="text-emphasis font-mono">{variable}</div>
<div className="text-muted mt-1 text-xs">{description}</div>
</div>
))}
</div>
</div>
))}
</div>
)}
</div>
)}
</>
)}