Skip to content
29 changes: 29 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
Expand Down
226 changes: 218 additions & 8 deletions packages/features/webhooks/components/WebhookForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -94,6 +242,7 @@ const WebhookForm = (props: {
}) => {
const { apps = [], selectOnlyInstantMeetingOption = false, overrideTriggerOptions } = props;
const { t } = useLocale();
const webhookVariables = getWebhookVariables(t);

const triggerOptions = overrideTriggerOptions
? [...overrideTriggerOptions]
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
});
}}>
Comment on lines +539 to +549
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify template insertion functionality.

The variable insertion logic looks correct, but ensure that the JSON structure is maintained properly when multiple variables are added sequentially.


🏁 Script executed:

#!/bin/bash
# Description: Check if there are any existing tests for the insertVariableIntoTemplate function
# Expected: Find test files that verify the JSON template insertion functionality

echo "Searching for test files related to webhook template insertion..."
rg -l "insertVariableIntoTemplate|webhook.*template" --type test

echo "Searching for webhook form test files..."
fd -e test.ts -e test.tsx -e spec.ts -e spec.tsx | xargs rg -l "WebhookForm"

echo "Checking for existing webhook payload template tests..."
rg -A 5 -B 5 "payloadTemplate.*test" 

Length of output: 564


🏁 Script executed:

#!/bin/bash
set -e

echo "1. Locating definition of insertVariableIntoTemplate..."
rg -n "insertVariableIntoTemplate" -g '*.ts' -g '*.tsx'

echo
echo "2. Searching for tests referencing insertVariableIntoTemplate..."
rg -n "insertVariableIntoTemplate" -g '*.spec.ts*' -g '*.test.ts*' || true

echo
echo "3. Identifying directory of the function to look for nearby tests..."
func_file=$(rg -l "insertVariableIntoTemplate" -g '*.ts' -g '*.tsx' | head -n1)
echo "Function file: $func_file"

if [ -n "$func_file" ]; then
  search_dir=$(dirname "$func_file")
  echo
  echo "4. Listing any test files in $search_dir..."
  find "$search_dir" -type f \( -name '*.spec.ts' -o -name '*.spec.tsx' -o -name '*.test.ts' -o -name '*.test.tsx' \) -print

  echo
  echo "5. Searching those tests for payloadTemplate usage..."
  find "$search_dir" -type f \( -name '*.spec.ts' -o -name '*.spec.tsx' -o -name '*.test.ts' -o -name '*.test.tsx' \) -print0 \
    | xargs -0 grep -H "payloadTemplate" || true
fi

Length of output: 1946


Add unit tests for JSON template insertion
There’s currently no test coverage for the insertVariableIntoTemplate helper, which lives in packages/features/webhooks/components/WebhookForm.tsx. To ensure the JSON structure remains valid—especially when inserting multiple variables in sequence—please add focused tests that cover:

  • Inserting into an empty template ("{}")
  • Single-variable insertion
  • Multiple insertions back-to-back
  • Edge cases (e.g. reserved characters, nested braces)
  • Round-trip parsing (assert JSON.parse(insertedString) succeeds)

Suggested locations:

  • Create a new spec alongside the component, e.g.
    packages/features/webhooks/components/WebhookForm.spec.ts
  • Import and unit-test insertVariableIntoTemplate directly
🤖 Prompt for AI Agents
In packages/features/webhooks/components/WebhookForm.tsx around lines 526 to
536, there are no unit tests for the insertVariableIntoTemplate helper function.
To fix this, create a new test file named WebhookForm.spec.ts in the same
directory. Import insertVariableIntoTemplate into this test file and write
focused unit tests covering inserting into an empty template ("{}"),
single-variable insertion, multiple sequential insertions, edge cases like
reserved characters and nested braces, and verify that the output string can be
parsed by JSON.parse without errors.

<div className="text-emphasis font-mono">{variable}</div>
<div className="text-muted mt-1 text-xs">{description}</div>
</div>
))}
</div>
</div>
))}
</div>
)}
</div>
)}
</>
)}
Expand Down
Loading