-
Notifications
You must be signed in to change notification settings - Fork 10.2k
fix: Custom Event Redirect in Routing Form with Dropdown Based fields #17870
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Hey there and thank you for opening this pull request! 👋🏼 We require pull request titles to follow the Conventional Commits specification and it looks like your proposed title needs to be adjusted. Details:
|
This PR is being marked as stale due to inactivity. |
This PR is being marked as stale due to inactivity. |
This PR is being marked as stale due to inactivity. |
@hariombalhara is this still relevant? |
…lity - Remove unused packages/app-store/routing-forms/trpc/report.handler.ts - Remove packages/app-store/routing-forms/jsonLogicToPrisma.ts implementation - Remove packages/app-store/routing-forms/__tests__/jsonLogicToPrisma.test.ts test file These files were no longer being used in the routing-forms package. Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>
- Remove unused report.schema.ts file - Remove buildResponsesForReporting test file - Remove report endpoint from router - Update comment referencing jsonLogicToPrisma All references to the removed jsonLogicToPrisma and report handler functionality have been cleaned up. Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>
Yeah It should be . Retesting it |
…rect-routing-forms
…f field labels - Fix custom URL redirects to substitute selected option labels (e.g., 'India') instead of field labels (e.g., 'Country') - Fix preview to show substituted variables instead of placeholders - Add getHumanReadableFieldResponseValue service to handle field value resolution - Move getLabelsFromOptionIds and ensureStringOrStringArray to centralized service location - Add comprehensive tests for variable substitution with select, text, and number fields - Fix TestForm to not call team member matching for custom URLs (eventTypeId check)
The latest updates on your projects. Learn more about Vercel for Git ↗︎ |
WalkthroughCentralizes response-to-label normalization by adding getHumanReadableFieldResponseValue and using it in substituteVariables and the form responses API. substituteVariables now uses human-readable option labels (slugified) for route placeholders. TestForm UI shows substituted internal event-type redirect URLs (UI-only) and guards team-members matching by eventTypeId. Adds unit tests for substituteVariables, getHumanReadableFieldResponseValue, and TestFormDialog URL-substitution cases. Removes routing-forms reporting utilities, jsonLogic-to-Prisma translator, TRPC report route/schema, report handler, and related reporting tests. Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (5)
💤 Files with no reviewable changes (4)
✅ Files skipped from review due to trivial changes (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
6a0834f
to
f7bbb69
Compare
e7d9ef6
to
33e966d
Compare
@@ -50,7 +35,10 @@ async function* getResponses(formId: string, fields: Fields) { | |||
fields.forEach((field) => { | |||
const fieldResponse = fieldResponses[field.id]; | |||
const value = fieldResponse?.value || ""; | |||
const readableValues = getHumanReadableFieldResponseValue({ field, value }); | |||
const humanReadableResponseValue = getHumanReadableFieldResponseValue({ field, value }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It now returns array only if the input value was an array, which is a better way and keeps the type unchanged
9561f88
to
5ca07ce
Compare
eventTypeRedirectUrl = getAbsoluteEventTypeRedirectUrl({ | ||
eventTypeRedirectUrl: route.action.value, | ||
form, | ||
allURLSearchParams: new URLSearchParams(), | ||
}); | ||
setEventTypeUrlWithoutParams(eventTypeRedirectUrl); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was unused as eventTypeUrlWithoutParams is also unused
5ca07ce
to
d022fb1
Compare
@alishaz-polymath Opened it for review |
Graphite Automations"Add consumer team as reviewer" took an action on this PR • (08/13/25)1 reviewer was added to this PR based on Keith Williams's automation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🔭 Outside diff range comments (1)
packages/app-store/routing-forms/api/responses/[formId].ts (1)
21-29
: Restrict Prisma selection to only required fields to reduce payload.Per repo guidelines, select only what you need. For the CSV loop below, only response and createdAt are used from FormResponse rows.
Apply this diff:
while ( (responses = await prisma.app_RoutingForms_FormResponse.findMany({ where: { formId, }, take: take, skip: skip, + select: { + response: true, + createdAt: true, + }, })) &&
🧹 Nitpick comments (7)
packages/app-store/routing-forms/lib/substituteVariables.ts (1)
26-41
: Precompute maps to avoid quadratic scans and repeated work.The nested loop (variables × response × fields.find) is O(V × R × F) and repeatedly computes identifiers/human-readable values. Pre-index fields by id and precompute identifier→value once from response, then substitute. This improves readability and performance.
Apply this refactor:
variables.forEach((variable) => { - for (const key in response) { - const field = fields.find((field) => field.id === key); - if (!field) { - continue; - } - const identifier = getFieldIdentifier(field); - if (identifier.toLowerCase() === variable.toLowerCase()) { - const humanReadableValues = getHumanReadableFieldResponseValue({ - field, - value: response[key].value, - }); - // ['abc', 'def'] ----toString---> 'abc,def' ----slugify---> 'abc-def' - const valueToSubstitute = slugify(humanReadableValues.toString()); - eventTypeUrl = eventTypeUrl.replace(`{${variable}}`, valueToSubstitute); - } - } + // Build maps once outside the variable loop for efficiency + // Note: hoist these outside variables.forEach if many variables are expected + const idToField = new Map(fields.map((f) => [f.id, f])); + const identifierToValue: Record<string, string> = {}; + for (const key in response) { + const field = idToField.get(key); + if (!field) continue; + const identifier = getFieldIdentifier(field).toLowerCase(); + const humanReadableValues = getHumanReadableFieldResponseValue({ + field, + value: response[key].value, + }); + identifierToValue[identifier] = slugify(humanReadableValues.toString()); + } + + const valueToSubstitute = identifierToValue[variable.toLowerCase()]; + if (valueToSubstitute !== undefined) { + eventTypeUrl = eventTypeUrl.replaceAll(`{${variable}}`, valueToSubstitute); + } });packages/app-store/routing-forms/components/_components/TestForm.tsx (1)
149-151
: Be explicit about eventTypeId gate to avoid truthiness pitfalls.The intent is to skip the mutation for custom event type (eventTypeId = 0) and only run when a valid, positive ID exists. Make the check explicit for readability.
- if (supportsTeamMembersMatchingLogic && route.action.eventTypeId) { + if ( + supportsTeamMembersMatchingLogic && + typeof route.action.eventTypeId === "number" && + route.action.eventTypeId > 0 + ) {packages/lib/server/service/routingForm/responseData/getHumanReadableFieldResponseValue.ts (4)
11-18
: Prefer Array.isArray and clarify the scalar-number behaviorUse Array.isArray for array detection. Also, the JSDoc implies numbers become strings, but here numbers become a single-element array of strings. Clarify that to avoid confusion for downstream consumers.
Apply this diff:
function ensureStringOrStringArray(value: string | number | (string | number)[]): string | string[] { if (typeof value === "string") { return value; - } else if (value instanceof Array) { + } else if (Array.isArray(value)) { return value.map((v) => v.toString()); } return [value.toString()]; }And update the JSDoc above to reflect the array-wrapping for non-string scalars:
/** - * Ensures a value is either a string or string array. - * Converts numbers to strings. + * Normalizes a response value to a string or string[]. + * Numbers are converted to strings; non-string scalars are wrapped in a single-element array + * to preserve downstream join/CSV semantics. * * @param value - The value to normalize - * @returns String or string array + * @returns string | string[] */
29-36
: Annotate return type and use Array.isArrayMake the return type explicit (string[]) and use Array.isArray for consistency and cross-realm safety.
Apply this diff:
function getLabelsFromOptionIds({ options, optionIds, }: { options: NonNullable<Field["options"]>; optionIds: string | string[]; -}) { - if (optionIds instanceof Array) { +}): string[] { + if (Array.isArray(optionIds)) { const labels = optionIds.map((optionId) => { const foundOption = options.find((option) => option.id === optionId); // It would mean that the optionId is actually a label which is why it isn't matching any option id. // Fallback to optionId(i.e. label) which was the case with legacy options if (!foundOption) { return optionId;Optional note: If options can be large and this runs in hot paths, consider precomputing a Map from id to label to reduce repeated linear scans.
56-64
: JSDoc return type mismatchThe comment says “Array of human-readable values” but the function can return string or string[]. Update the doc to avoid confusion.
Apply this diff:
/** * Get the human-readable value for a field response. * For select/multiselect/radio fields, this returns the label of the selected option(s). - * For other fields, it returns the value as-is. + * For other fields, it returns the normalized value (string or string[]). * * @param field - The field definition with options * @param value - The response value (typically an option ID for select fields) - * @returns Array of human-readable values + * @returns string | string[] */
72-79
: Consider normalizing to a single return shape (string[])Returning a union (string | string[]) forces callers to normalize again. Given the downstream usage relies on array join semantics (toString), you could simplify by always returning string[] (e.g., wrap single strings into an array). This would reduce branching in consumers.
Happy to provide a small refactor if you want to adopt a consistent string[] return shape.
packages/app-store/routing-forms/lib/substituteVariables.test.ts (1)
219-229
: Add tests for radio fields and legacy label fallbacksTwo high-signal additions:
- Radio-type field behaves like select.
- Legacy responses where value holds a label instead of an ID should still substitute correctly (you already support this in the utility).
Apply this diff to extend coverage:
it("should handle textarea fields like text fields", () => { const fields = [ { id: "field1", identifier: "description", label: "Description", type: "textarea" as const }, ]; const routeValue = "/ticket/{description}"; const response = createFormResponse("field1", "Bug Report Summary", "Description"); const result = substituteVariables(routeValue, response, fields); expect(result).toBe("/ticket/bug-report-summary"); }); + + it("should substitute radio field option labels", () => { + const fields: Field[] = [ + { + id: "fieldRadio", + identifier: "choice", + label: "Choice", + type: "radio" as const, + options: [ + { id: "opt-1", label: "First Option" }, + { id: "opt-2", label: "Second Option" }, + ], + }, + ]; + const routeValue = "/pick/{choice}"; + const response = createFormResponse("fieldRadio", "opt-1", "Choice"); + const result = substituteVariables(routeValue, response, fields); + expect(result).toBe("/pick/first-option"); + }); + + it("should fallback to labels when option IDs are not found (legacy)", () => { + const fields = [ + createSelectField("field1", "country", "Country", [{ id: "ind-001", label: "India" }]), + ]; + const routeValue = "/team/{country}/meeting"; + // Simulate legacy value already being the label instead of the ID + const response = createFormResponse("field1", "India", "Country"); + const result = substituteVariables(routeValue, response, fields); + expect(result).toBe("/team/india/meeting"); + }); });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
packages/app-store/routing-forms/__tests__/TestFormDialog.test.tsx
(2 hunks)packages/app-store/routing-forms/api/responses/[formId].ts
(2 hunks)packages/app-store/routing-forms/components/_components/TestForm.tsx
(2 hunks)packages/app-store/routing-forms/lib/reportingUtils.ts
(0 hunks)packages/app-store/routing-forms/lib/substituteVariables.test.ts
(1 hunks)packages/app-store/routing-forms/lib/substituteVariables.ts
(2 hunks)packages/app-store/routing-forms/trpc/_router.ts
(0 hunks)packages/app-store/routing-forms/trpc/report.handler.ts
(0 hunks)packages/lib/server/service/routingForm/responseData/getHumanReadableFieldResponseValue.test.ts
(1 hunks)packages/lib/server/service/routingForm/responseData/getHumanReadableFieldResponseValue.ts
(1 hunks)
💤 Files with no reviewable changes (3)
- packages/app-store/routing-forms/trpc/_router.ts
- packages/app-store/routing-forms/lib/reportingUtils.ts
- packages/app-store/routing-forms/trpc/report.handler.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
**/*.ts
: For Prisma queries, only select data you need; never useinclude
, always useselect
Ensure thecredential.key
field is never returned from tRPC endpoints or APIs
Files:
packages/lib/server/service/routingForm/responseData/getHumanReadableFieldResponseValue.test.ts
packages/lib/server/service/routingForm/responseData/getHumanReadableFieldResponseValue.ts
packages/app-store/routing-forms/lib/substituteVariables.test.ts
packages/app-store/routing-forms/lib/substituteVariables.ts
packages/app-store/routing-forms/api/responses/[formId].ts
**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()
in hot paths like loops
Files:
packages/lib/server/service/routingForm/responseData/getHumanReadableFieldResponseValue.test.ts
packages/lib/server/service/routingForm/responseData/getHumanReadableFieldResponseValue.ts
packages/app-store/routing-forms/lib/substituteVariables.test.ts
packages/app-store/routing-forms/lib/substituteVariables.ts
packages/app-store/routing-forms/__tests__/TestFormDialog.test.tsx
packages/app-store/routing-forms/api/responses/[formId].ts
packages/app-store/routing-forms/components/_components/TestForm.tsx
**/*.tsx
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Always use
t()
for text localization in frontend code; direct text embedding should trigger a warning
Files:
packages/app-store/routing-forms/__tests__/TestFormDialog.test.tsx
packages/app-store/routing-forms/components/_components/TestForm.tsx
🧠 Learnings (1)
📚 Learning: 2025-07-15T12:59:34.389Z
Learnt from: eunjae-lee
PR: calcom/cal.com#22106
File: packages/features/insights/components/FailedBookingsByField.tsx:65-71
Timestamp: 2025-07-15T12:59:34.389Z
Learning: In the FailedBookingsByField component (packages/features/insights/components/FailedBookingsByField.tsx), although routingFormId is typed as optional in useInsightsParameters, the system automatically enforces a routing form filter, so routingFormId is always present in practice. This means the data always contains only one entry, making the single-entry destructuring approach safe.
Applied to files:
packages/app-store/routing-forms/components/_components/TestForm.tsx
🧬 Code Graph Analysis (7)
packages/lib/server/service/routingForm/responseData/getHumanReadableFieldResponseValue.test.ts (2)
packages/app-store/routing-forms/types/types.d.ts (2)
Field
(30-30)FormResponse
(19-27)packages/lib/server/service/routingForm/responseData/getHumanReadableFieldResponseValue.ts (1)
getHumanReadableFieldResponseValue
(65-79)
packages/lib/server/service/routingForm/responseData/getHumanReadableFieldResponseValue.ts (1)
packages/app-store/routing-forms/types/types.d.ts (2)
Field
(30-30)FormResponse
(19-27)
packages/app-store/routing-forms/lib/substituteVariables.test.ts (2)
packages/app-store/routing-forms/types/types.d.ts (2)
Field
(30-30)FormResponse
(19-27)packages/app-store/routing-forms/lib/substituteVariables.ts (1)
substituteVariables
(16-46)
packages/app-store/routing-forms/lib/substituteVariables.ts (2)
packages/lib/server/service/routingForm/responseData/getHumanReadableFieldResponseValue.ts (1)
getHumanReadableFieldResponseValue
(65-79)packages/platform/libraries/index.ts (1)
slugify
(25-25)
packages/app-store/routing-forms/__tests__/TestFormDialog.test.tsx (1)
packages/app-store/routing-forms/components/_components/TestForm.tsx (1)
TestFormRenderer
(320-374)
packages/app-store/routing-forms/api/responses/[formId].ts (1)
packages/lib/server/service/routingForm/responseData/getHumanReadableFieldResponseValue.ts (1)
getHumanReadableFieldResponseValue
(65-79)
packages/app-store/routing-forms/components/_components/TestForm.tsx (1)
packages/app-store/routing-forms/lib/substituteVariables.ts (1)
substituteVariables
(16-46)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Tests / Unit
- GitHub Check: Type check / check-types
🔇 Additional comments (11)
packages/app-store/routing-forms/lib/substituteVariables.ts (1)
34-41
: Correct fix: substitute human-readable labels (not option UIDs).This uses getHumanReadableFieldResponseValue to resolve option labels and slugifies them before substitution. This directly addresses the root cause (UIDs vs labels) and aligns with the PR objective. Good job.
packages/app-store/routing-forms/__tests__/TestFormDialog.test.tsx (3)
54-55
: Good guard input for team-members matching logic.Setting eventTypeId in the mock ensures the mutation path is exercised only when appropriate. This matches the new guard in TestForm.
426-454
: Solid coverage: internal event redirect variables substituted in UI.This validates the UI-only substitution path for eventTypeRedirectUrl, including slugification (“Sales Team” → “sales-team”). Looks correct.
455-481
: Clear differentiation: external redirect URLs are left untouched.This test ensures externalRedirectUrl values aren’t substituted in the UI, which aligns with the intended behavior.
packages/app-store/routing-forms/api/responses/[formId].ts (2)
7-7
: Centralized human-readable conversion is correctly adopted.Importing and using getHumanReadableFieldResponseValue here removes duplication and ensures consistent behavior across the app.
38-41
: Correct normalization to array for CSV serialization.Wrapping non-array results and passing arrays through preserves original shape while simplifying downstream joining. Good.
packages/app-store/routing-forms/components/_components/TestForm.tsx (2)
19-21
: Right import: pulls in shared substitution logic.Importing substituteVariables keeps UI behavior consistent with core logic and avoids reimplementations.
131-145
: UI-only substitution is implemented cleanly.Creating displayRoute with a substituted value for eventTypeRedirectUrl (and leaving the original route untouched for backend logic) avoids side effects and clarifies intent. Nice.
packages/lib/server/service/routingForm/responseData/getHumanReadableFieldResponseValue.test.ts (1)
1-205
: Comprehensive and precise test coverage.Great set of cases across select, multiselect, radio, text/number/textarea, and edge conditions (numeric values, empty options, undefined options). This gives high confidence in the normalization logic.
packages/lib/server/service/routingForm/responseData/getHumanReadableFieldResponseValue.ts (1)
65-79
: Solid centralization; logic aligns with the bug fixThe approach to resolve option IDs to human-readable labels and reuse it across callers is correct and neatly scoped. Type-only imports avoid runtime coupling.
packages/app-store/routing-forms/lib/substituteVariables.test.ts (1)
1-229
: Excellent, comprehensive coverageThe tests thoroughly validate substitution for select, multiselect, text, number, special characters, case-insensitivity, and querystring preservation. They capture the intended label-based behavior and the slugification nuances well.
…hub.com/calcom/cal.com into fix-custom-event-redirect-routing-forms
d022fb1
to
54e1239
Compare
…er-1754987582' into fix-custom-event-redirect-routing-forms
E2E results are ready! |
What does this PR do?
Note: This PR has #23032 merged as well to avoid doing unnecessary work of fixing the files that were going to be deleted.
Fixes routing forms variable substitution to use selected option labels instead of field labels when substituting variables in custom event redirect URLs.
Loom Demo(Before and After)
Fixes PRI-304
Bug-1
When using variables like {Country} in routing form custom URLs (e.g., /team/{Country}/meeting), the system was incorrectly substituting the selected option's UID instead of the selected option's label (e.g., "India"). This made the feature unusable for dropdown fields.
Root Cause
The
substituteVariables
function was usingresponse[key].value
which contains the selected option's UID, not the selected option's label. For dropdown fields, we need to look up the option label using the option ID stored inresponse[key].value
.Bug-2
Preview of the Routing Form wasn't substituting the variables
Tech Debt Payment
Service Organization
getLabelsFromOptionIds
andensureStringOrStringArray
as internal dependency of getHumanReadableFieldResponseValuepackages/lib/server/service/routingForm/responseData/
Type of change
How should this be tested?
/team/{Country}/30min
)/team/india/30min
) instead of{Country}
Testing Coverage
Added comprehensive test coverage:
getHumanReadableFieldResponseValue
service (15 tests)substituteVariables
with select, multiselect, text, and number fields (15 tests)Mandatory Tasks