Compare commits

...
160 Commits
Author SHA1 Message Date
hausgeist b34ef206f8 cc: gate on DB readiness + real healthchecks (start.sh has no set -e and wait-for-it passes mid-initdb)
- database: pg_isready healthcheck; calcom depends_on service_healthy
- calcom: healthcheck probes /auth/login with redirects (upstream wget --spider / fails on 307)
2026-09-28 08:16:12 -07:00
hausgeist 1c3c661726 cc: fix DB wiring — base compose hardcodes unicorn_user/magical_password and builds DATABASE_URL from raw creds (Prisma URL-parse crash with base64 pw)
- docker-compose.cc.yml: database env + calcom DATABASE_URL/DIRECT_URL from .env
- cc.env.example: require HEX (URL-safe) secrets; hex generation commands
2026-09-28 06:44:44 -07:00
hausgeist 3dbad3113d cc: build-based deploy — cal.diy publishes no prebuilt release images
Verified: Docker Hub calcom/cal.diy has 0 tags; scarf/ghcr mirrors serve none.
The upstream Dockerfile + compose build args are the deploy path.
2026-09-28 00:23:53 -07:00
hausgeist b1173fc8c3 cc: CC deployment source-of-truth pinned at v6.2.0 (issue #11)
- docker-compose.cc.yml: pin calcom v6.2.0 + postgres 16-alpine + redis 7-alpine;
  studio + calcom-api off (reserved profiles)
- cc.env.example: CC production env spec (webapp URL, telemetry off, branding)
- cc/README.md: mirror manifest + upstream-merge process
2026-09-27 23:36:33 -07:00
khushal-winnerandSahitya Chandra 1c193cca86 docs: document Windows setup fix for prisma .env symlink error (#28101)
Post release / release (push) Canceled after 0s
Release Docker / Prepare Release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
* docs: add Windows fix for prisma .env symlink issue

* Remove 'link-as-an-app' from redirect apps list

Removed 'link-as-an-app' from the list of apps.

* Update README.md

removed option 2

* Update README.md

* Update README.md

---------

Co-authored-by: Sahitya Chandra <sahityajb@gmail.com>
2026-03-01 12:42:42 +00:00
17af50bbdc feat: add per-host locations seed data in Acme Org for QA (#27832)
* fix: host location option

* feat: add per-host locations seed data for Round Robin event type

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: move per-host locations seed to Acme Org with dedicated event type

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: check app existence before creating credentials in per-host location seed

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: remove credential-based app seeding, use simple location types instead

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2026-02-26 18:51:11 +00:00
5242e419d9 feat: Calendar Sync (#24124)
* feat: calendar cache and sync - wip

* Add env.example

* refactor on CalendarCacheEventService

* remove test console.log

* Fix type checks errors

* chore: remove pt comment

* add route.ts

* chore: fix tests

* Improve cache impl

* chore: update recurring event id

* chore: small improvements

* calendar cache improvements

* Fix remove dynamic imports

* Add cleanup stale cache

* Fix tests

* add event update

* type fixes

* feat: add comprehensive tests for new calendar subscription API routes

- Add tests for /api/cron/calendar-subscriptions-cleanup route (9 tests)
- Add tests for /api/cron/calendar-subscriptions route (10 tests)
- Add tests for /api/webhooks/calendar-subscription/[provider] route (11 tests)
- Add missing feature flags for calendar-subscription-cache and calendar-subscription-sync
- All 30 tests pass with comprehensive coverage of authentication, feature flags, error handling, and service instantiation

Tests cover:
- Authentication scenarios (API key validation, Bearer tokens, query parameters)
- Feature flag combinations (cache/sync enabled/disabled states)
- Success and error handling (including non-Error exceptions)
- Service instantiation with proper dependency injection
- Provider validation for webhook endpoints

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* feat: add comprehensive tests for calendar subscription services, repositories, and adapters

- Add unit tests for CalendarSubscriptionService with subscription, webhook, and event processing
- Add unit tests for CalendarCacheEventService with cache operations and cleanup
- Add unit tests for CalendarSyncService with Cal.com event filtering and booking operations
- Add unit tests for CalendarCacheEventRepository with CRUD operations
- Add unit tests for SelectedCalendarRepository with calendar selection management
- Add unit tests for GoogleCalendarSubscriptionAdapter with subscription and event fetching
- Add unit tests for Office365CalendarSubscriptionAdapter with placeholder implementation
- Add unit tests for AdaptersFactory with provider management and adapter creation
- Fix lint issues by removing explicit 'any' type casting and unused variables
- All tests follow Cal.com conventions using Vitest framework with proper mocking

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: improve calendar-subscriptions-cleanup test performance by adding missing mocks

- Add comprehensive mocks for defaultResponderForAppDir, logger, performance monitoring, and Sentry
- Fix slow test execution (933ms -> <100ms) caused by missing dependency mocks
- Ensure consistent test performance across different environments

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* Fix tests

* Fix tests

* type fix

* Fix coderabbit comments

* Fix types

* Fix test

* Update apps/web/app/api/cron/calendar-subscriptions/route.ts

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Fixes by first review

* feat: add database migrations for calendar cache and sync fields

- Add CalendarCacheEventStatus enum with confirmed, tentative, cancelled values
- Add new fields to SelectedCalendar: channelId, channelKind, channelResourceId, channelResourceUri, channelExpiration, syncSubscribedAt, syncToken, syncedAt, syncErrorAt, syncErrorCount
- Create CalendarCacheEvent table with foreign key to SelectedCalendar
- Add necessary indexes and constraints for performance and data integrity

Fixes database schema issues causing e2e test failures with 'column does not exist' errors.

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* only google-calendar for now

* docs: add Calendar Cache and Sync feature documentation

- Add comprehensive feature overview and motivation
- Document feature flags with SQL examples
- Include SQL examples for enabling features for users and teams
- Reference technical documentation files

Addresses PR #23876 documentation requirements

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* docs: update calendar subscription README with comprehensive documentation

- Undo incorrect changes to main README.md
- Update packages/features/calendar-subscription/README.md with:
  - Feature overview and motivation
  - Environment variables section
  - Complete feature flags documentation with SQL examples
  - SQL examples for enabling features for users and teams
  - Detailed architecture documentation

Addresses PR #23876 documentation requirements

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix docs

* Fix test to available calendars

* Fix test to available calendars

* add migration and sync boilerplate

* fix typo

* remove double log

* sync boilerplate

* remove console.log

* only subscribe for google calendar

* adjust for 3 months fetch

* only subscribe for teams that have feature enabled

* adjust tests

* chore: safe increment error count

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* calendar sync

* test: add comprehensive tests for CalendarSyncService

- Add comprehensive test coverage for CalendarSyncService methods
- Test handleEvents, cancelBooking, and rescheduleBooking functionality
- Cover edge cases like missing UIDs, malformed UIDs, and error handling
- Fix dynamic import usage in CalendarSyncService to use .default
- Remove invalid properties from handleNewBooking call to fix type errors

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: add skipCalendarSyncTaskCreation flag to handleCancelBooking to prevent infinite loops

- Add skipCalendarSyncTaskCreation field to bookingCancelSchema in zod-utils.ts
- Update handleCancelBooking to skip EventManager.cancelEvent when flag is true
- Update CalendarSyncService.cancelBooking to pass skipCalendarSyncTaskCreation: true
- Update CalendarSyncService.rescheduleBooking to pass skipCalendarSyncTaskCreation: true
- Update tests to verify the flags are passed correctly

This prevents infinite loops when calendar events are cancelled/rescheduled from
external calendars (Google/Office365) which trigger webhooks to Cal.com, which
would otherwise try to update the external calendar again.

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* refactor: rename flag to skipCalendarSyncTaskCancellation and use static imports

- Rename skipCalendarSyncTaskCreation to skipCalendarSyncTaskCancellation in handleCancelBooking
- Convert dynamic imports to static imports in CalendarSyncService
- Update tests to use vi.hoisted for proper mock hoisting with static imports

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* feat: add error handling and static import for handleNewBooking

- Create index.ts entry point for handleNewBooking directory
- Add try-catch error handling to cancelBooking and rescheduleBooking
- Log errors but don't block calendar sync operations
- Update tests to verify errors are caught and logged, not thrown

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* refactor: use RegularBookingService directly and fix safeStringify usage

- Remove handleNewBooking/index.ts and call getRegularBookingService().createBooking() directly in CalendarSyncService
- Fix safeStringify usage: apply to error itself, not wrapper object
- Update tests to mock getRegularBookingService instead of handleNewBooking

Addresses PR comments from Volnei and Cubic-dev-ai

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: restore handleNewBooking/index.ts and fix bookingData structure for calendar sync

- Restore handleNewBooking/index.ts as entry point for static imports
- Fix CalendarSyncService.rescheduleBooking to use correct bookingData structure
  with required fields (eventTypeId, start, end, timeZone, language, metadata)
- Use rescheduleUid to indicate this is a reschedule operation
- Fix safeStringify usage in error logging (wrap only the error, not the whole object)
- Update tests to match new bookingData structure

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* refactor: address PR comments - use RegularBookingService directly, remove unnecessary flags, fix booking reference update

- Remove handleNewBooking/index.ts wrapper and use getRegularBookingService().createBooking() directly in CalendarSyncService
- Remove allRemainingBookings and cancelSubsequentBookings flags from cancelBooking (only cancel the specific booking, not the entire series)
- Move bookingReference update outside skipCalendarSyncTaskCancellation block for data consistency
- Update tests to match new implementation

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* chore: remove unnecessary comment from zod-utils.ts

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* feat: add Sentry metrics telemetry and fix null assertions in CalendarSyncService

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: use dynamic import for getRegularBookingService to avoid RAQB import in server context

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: use dynamic import for findTeamMembersMatchingAttributeLogic to avoid RAQB import in server context

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* improve booking update

* add more tests and edge cases

* fix: add required actionSource and Sentry mocks to calendar sync

Add actionSource: "SYSTEM" to handleCancelBooking call after it became
required, and mock @sentry/nextjs in test files.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-26 13:12:52 -03:00
5d65a0f091 fix: hide cal branding for orgs/teams (#27643)
* fix: hide branding for teams

* fix: remove unused organizationId and username fields from profiles select

Addresses Cubic AI review feedback (confidence 9/10) to select only
the profile fields that are actually used. The organizationId and
username fields were fetched but never referenced in this function.

Co-Authored-By: unknown <>

* fix: unit tests

* fix: add prisma named export to test mock

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

* Add tests: packages/features/profile/lib/hideBranding.test.ts

Generated by Paragon from proposal for PR #27643

* chore: implement cubic feedback

* fix: merge conflicts

* fix: unit tests

* fixup

* refactor: implement DI pattern for event type service

* fix: atoms build

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-25 20:53:01 +09:00
4081d11fbe feat: workflow auto translation (#27087)
* feat: workflow auto translation

* tests: add unit tests

* refactor: tests and workflow

* fix: type err

* fix: type err

* fix: remove redundant index on WorkflowStepTranslation

The @@index on [workflowStepId, field, targetLocale] duplicates the @@unique
constraint on the same columns. A unique index already provides efficient
lookups, so the separate @@index adds storage overhead and write latency
without benefit.

Addresses Cubic AI review feedback (confidence 9/10).

Co-Authored-By: unknown <>

* fix: correct locale mapping when translation API returns null

Map translations with their corresponding locales before filtering to
preserve correct locale-to-translation associations. Previously, filtering
out null translations would reindex the array, causing incorrect locale
mappings when any translation in the batch failed.

Also fixes pre-existing lint warnings:
- Move exports to end of file
- Add explicit return type to processTranslations
- Replace ternary with if-else for upsertMany selection

Co-Authored-By: udit@cal.com <udit222001@gmail.com>

* fix: address review feedback for workflow auto-translation

- Add change detection before creating translation tasks
- Rename userLocale to sourceLocale in task props for clarity
- Show source language in UI with new translation key
- Extract SUPPORTED_LOCALES to shared translationConstants.ts
- Fix locale mapping bug in translateEventTypeData.ts
- Add WhatsApp translation support
- Abstract translation lookup into shared translationLookup.ts helper
- Restore if-else readability for SCANNING_WORKFLOW_STEPS

Co-authored-by: Udit Takkar <udit.takkar@cal.com>
Co-Authored-By: unknown <>

* fix: update test to use sourceLocale instead of userLocale

Co-Authored-By: unknown <>

* refactor: feedback

* fix: handle first time

* fix: tests

* fix: tests

* fix: address Cubic AI review feedback (confidence 9/10 issues)

- WhatsApp translation: Apply variable substitution using getSMSMessageWithVariables
  and clear contentSid when using translated body to ensure Twilio uses the
  translated text instead of the original template

- update.handler.ts: Change sourceLocale assignment from ?? to || for consistency
  with tasker payload behavior (line 481)

- ITranslationService.ts: Rename methods from plural to singular naming:
  - getWorkflowStepTranslations -> getWorkflowStepTranslation
  - getEventTypeTranslations -> getEventTypeTranslation
  Updated all call sites and tests accordingly

Co-Authored-By: unknown <>

* fix: address Cubic AI review feedback (confidence 9/10+ issues)

- Fix getSMSMessageWithVariables to handle WHATSAPP_ATTENDEE action for
  locale and timezone (confidence 9/10)
- Remove WhatsApp translation feature that set contentSid to undefined
  since Twilio ignores body parameter for WhatsApp and requires
  pre-approved Message Templates (confidence 10/10)

Co-Authored-By: unknown <>

* fix: translatio

* Add tests: packages/features/eventTypeTranslation/repositories/EventTypeTranslationRepository.test.ts

Generated by Paragon from proposal for PR #27087

* Add tests: packages/features/tasker/tasks/translateWorkflowStepData.test.ts

Generated by Paragon from proposal for PR #27087

* chore: nit

* chore: verfied atg

* fix: set sourceLocale for new steps, add shouldDirty to checkbox, remove spec docs

- Set sourceLocale fallback in addedSteps mapping to fix stale detection mismatch
- Add { shouldDirty: true } to autoTranslateEnabled checkbox onChange
- Remove specs/workflow-translation/ directory (planning docs, not for repo)

Co-authored-by: Udit Takkar <udit.07.takkar@gmail.com>
Co-Authored-By: unknown <>

* chore: add specs back

* fix: type error

* fix: type error

* fix: type err

* fix: tests

* refactor: feedback

* fix: type err

* refactor

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.takkar@cal.com>
Co-authored-by: Udit Takkar <udit.07.takkar@gmail.com>
2026-02-25 01:03:55 +05:30
f883dee084 perf: improve getTotalBookingDuration query (#27383)
* perf: improve getTotalBookingDuration query

* perf: improve getTotalBookingDuration query

* test: add unit tests for getTotalBookingDurationForUsers and yearly duration pre-fetching

Co-Authored-By: udit@cal.com <udit222001@gmail.com>

* fix: integration tests

* fix: update tests

* fix: use vi.stubEnv instead of direct process.env modification for test isolation

Replace direct deletion of process.env variables with vi.stubEnv() to ensure
automatic restoration after tests, preventing flaky tests and side effects
in parallel execution.

Issue identified by Cubic AI (confidence: 9/10)

Co-Authored-By: unknown <>

* chore: add log

* refactor: imports

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-24 22:55:33 +05:30
e3a9f54ba5 feat: Configure cancellation reason (#26872)
* feat: Configure cancellation reason

* fix: use enums

* tests: add unit tests

* fix: type error

* chore: remove duplicate dialog

* fix: type erro

* refator: improvements

* refator: improvements

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-24 13:35:51 -03:00
c9abc556ba fix: improve getIP header resolution for CF → Vercel setup (#28152)
Co-Authored-By: unknown <>

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-24 14:47:14 +00:00
Benny Joo 9855176948 refactor: Remove all TrpcSessionUser usages in @calcom/features (#27853)
* sessionUser in features

* update sessionMiddleware

* update

* format changes

* update import paths

* fix

* fix ts errors

* refactor

* fix

* fix

* fix

* fix

* rename

* rename

* rename

* use error with code objs
2026-02-24 09:29:54 +00:00
48cbb94af2 feat: add webhook trigger when routing form fallback route is hit (#27588)
* feat: add no users found fallback action for routing forms

- Add NoUsersFoundFallbackActionType enum with CustomPageMessage and ExternalRedirectUrl options
- Add noUsersFoundFallbackAction field to zodNonRouterRoute schema
- Add NoUsersFoundFallback UI component in RouteBuilder for configuring fallback action
- Update handleResponse to return noUsersFoundFallbackAction when no team members match
- Add i18n translation keys for new UI strings

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* refactor: change fallback to use same action options as main route

- Replace noUsersFoundFallbackAction with fallbackAction that has same structure as main action
- Add action dropdown and input fields to fallback attributes query builder section
- Support Event redirect, External URL, and Custom page options in fallback
- Remove separate NoUsersFoundFallback toggle component
- Update handleResponse to return fallbackAction

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: ensure fallbackAction type is always defined in onChange handlers

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: add guards for route.fallbackAction in onChange handlers

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: only return fallbackAction when no users are found

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* refactor: restructure fallback section to mirror main route structure

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: ensure backwards compatibility for existing routes with fallbackAttributesQueryValue

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: initialize fallbackAction with main event type for backwards compatibility

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: include fallbackAction in getRoutesToSave to persist changes

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: use fallbackAction in getRoutedUrl when no team members found

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: use i18n for fallback label and add tests for fallback action

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: do not trigger fallback action when CRM contact owner is found

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: prioritize fallbackAction over fallbackAttributesQueryValue

When a route has a fallbackAction configured, skip the
fallbackAttributesQueryValue to ensure the fallbackAction is triggered
when no team members are found. This maintains backwards compatibility
by only using fallbackAttributesQueryValue when fallbackAction is not set.

Also treats teamMemberIdsMatchingAttributeLogic being null (routing couldn't
run, e.g., missing orgId) the same as an empty array for the purpose of
triggering the fallbackAction.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: extract RouteActionSelector shared component for action selection UI

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: add i18n string for default custom page message

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* feat: add webhook trigger when routing form fallback route is hit

- Add ROUTING_FORM_FALLBACK_HIT to WebhookTriggerEvents enum in Prisma schema
- Add new trigger to routing-forms webhook triggers in constants
- Add translation string for the new webhook trigger
- Implement triggerFallbackWebhook function in formSubmissionUtils.ts
- Call webhook trigger from handleResponse.ts when fallback action is used

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: remove webhook object from error log to avoid exposing secrets

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* revert: remove webhook trigger changes (to be moved to separate PR)

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* feat: add webhook trigger when routing form fallback route is hit

- Add ROUTING_FORM_FALLBACK_HIT to WebhookTriggerEvents enum in Prisma schema
- Add new trigger to routing-forms webhook triggers in constants
- Add translation string for the new webhook trigger
- Implement triggerFallbackWebhook function in formSubmissionUtils.ts
- Call webhook trigger from handleResponse.ts when fallback action is used

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: remove webhook object from error log to avoid exposing secrets

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: add ROUTING_FORM_FALLBACK_HIT to PayloadBuilderFactory trigger mapping

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* style: apply biome formatting fixes

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: restore removed comments in RouteBuilder

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* chore: add prisma migration for ROUTING_FORM_FALLBACK_HIT enum value

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: add ROUTING_FORM_FALLBACK_HIT to FormTriggerEvents type union

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* test: add test coverage for triggerFallbackWebhook function

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* refactor: consolidate fallback webhook firing with form submission webhooks

Move ROUTING_FORM_FALLBACK_HIT webhooks into the same Promise.all() batch
as FORM_SUBMITTED / FORM_SUBMITTED_NO_EVENT, eliminating redundant
getWebhooks/getWebhookTargetEntity/getOrgIdFromMemberOrTeamId calls.

- Add fallbackAction parameter to _onFormSubmission and onSubmissionOfFormResponse
- Remove triggerFallbackWebhook function and all its usages/imports
- Move getFallbackAction() earlier in handleResponse.ts and pass result through
- Update all tests to verify batched webhook behavior

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* Add UI option for trigger

* feat: persist fallbackAction on queued form responses

When a routing form response is queued, the fallbackAction determined
during routing is now stored on the QueuedFormResponse record. When
the queued response is later converted to a real FormResponse, the
stored fallbackAction is passed to onSubmissionOfFormResponse so the
ROUTING_FORM_FALLBACK_HIT webhook fires correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add coverage for fallbackAction in queued form response flow

Verify that fallbackAction is persisted when queuing and passed through
to onSubmissionOfFormResponse when the queued response is converted.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-23 16:44:41 -05:00
8238d4ffcd fix: use WEBAPP_URL for booking confirmation redirects to fix localhost behind proxy (#28144)
Replace request.url with WEBAPP_URL from @calcom/lib/constants as the base URL
for NextResponse.redirect() in booking confirmation API routes. Behind a reverse
proxy, request.url resolves to http://localhost:3000 instead of the public domain.

Fixes #20358

Co-Authored-By: unknown <>

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-23 14:19:54 -03:00
0a84ce5c73 fix: use smsReminderNumber fallback for attendee phone in SMS reminders (#27942)
* SMS reminders not trigger

* addressed devin review

* Add tests: packages/features/ee/workflows/lib/service/scheduleLazySMSWorkflow.test.ts

Generated by Paragon from proposal for PR #27942

* Add tests: packages/features/ee/workflows/lib/reminders/reminderScheduler.smsLazy.test.ts

Generated by Paragon from proposal for PR #27942

* Add tests: packages/features/tasker/tasks/sendWorkflowSMS.test.ts

Generated by Paragon from proposal for PR #27942

* Add tests: packages/features/ee/workflows/lib/reminders/smsReminderManager.test.ts

Generated by Paragon from proposal for PR #27942

* Delete packages/features/ee/workflows/lib/reminders/reminderScheduler.smsLazy.test.ts

* fix: update sendWorkflowSMS tests to match refactored implementation

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* fix: use smsReminderNumber fallback for attendee phone in SMS workflows

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* revert: keep only scheduleSMSReminders.ts changes, revert all other files

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* fix: restore reminderScheduler.ts and smsReminderManager.ts to original branch state

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* fix: use smsReminderNumber fallback for attendee phone in WhatsApp reminders

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-23 17:35:51 +01:00
7e73d675d7 refactor: improve invoice.upcoming logging and add strategyName to billing strategies (#28143)
- Add strategyName property to ISeatBillingStrategy interface and all implementations
- Log which strategy handled the invoice.upcoming webhook (not just whether it applied)
- Always log strategy result (not just when applied=true) for better observability
- Rename highWaterMarkApplied to strategyApplied since applied covers multiple strategies

Co-Authored-By: unknown <>

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-23 15:57:13 +00:00
Benny Joo 648ad72a54 refactor: extract dedicated @calcom/i18n package (#28141) 2026-02-23 13:30:12 +00:00
f00be08e8c fix: order custom questions by event type bookingFields in BookingDetailsSheet (#28140)
Co-Authored-By: unknown <>

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-23 22:25:02 +09:00
Yuvraj Angad SinghandAnik Dhabal Babu f1e8e3fa58 fix(caldav): consistent UIDs and VTIMEZONE in iCalendar output (#28115)
* fix(caldav): use consistent UIDs and inject VTIMEZONE in iCalendar output

Two remaining CalDAV interop issues from #9485:

1. UID consistency: use the booking's canonical UID (event.uid) instead
   of always generating a new random UUID. CalDAV servers use UID as the
   event identifier — different UIDs cause duplicate calendar entries.

2. VTIMEZONE injection: the ics library generates UTC times with no
   VTIMEZONE block. CalDAV servers like Fastmail read this as UTC and
   send scheduling emails with wrong times. Per RFC 5545 §3.6.5,
   DTSTART with TZID requires a matching VTIMEZONE component. We now
   build a proper VTIMEZONE using binary-searched DST transitions for
   the event's year, handling Northern/Southern hemisphere correctly.

* fix: use pre-transition offset for VTIMEZONE DTSTART per RFC 5545

The DTSTART in VTIMEZONE components must represent the local time
interpreted with the pre-transition offset (TZOFFSETFROM), not the
post-transition offset. For example, US Eastern spring forward DTSTART
should be 02:00 (EST), not 03:00 (EDT).

* Remove comments on UID handling in createEvent

Removed comments about UID handling for calendar events.

* Revise injectVTimezone documentation

Update injectVTimezone function documentation to clarify UTC handling.

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2026-02-21 14:38:56 +00:00
Anirban SinghaandRomit 4dbe044ecc refactor: remove unused imports and stale TODO comment from caleventparser.ts (#28096)
* refactor: replace manual provider logic with getAppFromLocationValue

* chore: revert changes and remove stale comment

---------

Co-authored-by: Romit <85230081+romitg2@users.noreply.github.com>
2026-02-21 19:38:14 +05:30
Romit f1ae5f727a fix: update fast-xml-parser and ignore audit 1113407 (#28117) 2026-02-21 12:22:57 +00:00
Romit 7ee01c0487 fix (#28119) 2026-02-21 09:16:13 -03:00
05537780b8 fix: add biome overrides for embed-core to prevent auto-import of node:process (#27387)
* fix: use import.meta.env instead of process.env in embed.ts

Using import.meta.env is Vite's native way to handle environment variables
in browser bundles. This prevents biome from auto-adding 'import process
from node:process' which breaks the embed.js in browsers.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Remove unncessar comment

* fix: migrate remaining process.env to import.meta.env in embed.ts

Complete the migration started by @hariombalhara by also converting
process.env usages at lines 1681, 1682, and 1700 to import.meta.env.
Update env.d.ts to include EMBED_PUBLIC_EMBED_FINGER_PRINT,
EMBED_PUBLIC_EMBED_VERSION, and INTEGRATION_TEST_MODE types.

This ensures no process.env references remain in embed.ts, fully
preventing Biome from auto-adding import process from node:process.

Co-Authored-By: unknown <>

* fix: add biome overrides for embed-core to prevent auto-import of node:process

Reverts import.meta.env back to process.env since API V2's tsconfig uses
module: commonjs which doesn't support import.meta. Instead, adds biome
overrides for embed-core to disable useNodejsImportProtocol, noProcessEnv,
and noProcessGlobal rules that were causing biome to auto-add
'import process from node:process' on save.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-20 14:38:11 -03:00
Hariom BalharaandDevin AI 3e29904d7d test: add tests for __reloadInitiated behavior to ensure correct bookerViewed vs bookerReloaded events (#27379)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-20 14:35:18 -03:00
06f4b25080 fix: update routing form identifier hint text with learn more link (#27837)
* fix: update routing form identifier hint text with learn more link

Co-Authored-By: carina@cal.com <c.wollendorfer@me.com>

* UI improvements

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2026-02-20 13:09:09 +01:00
Deepanshu 79fc0c8437 fix: toolbar dropdown toggle (#28097) 2026-02-20 11:20:38 +00:00
a2b25fa615 fix: prevent deselecting active option in booker layout toggle (#27748)
* fix: prevent deselecting active option in ToggleGroup

Radix UI's single-type ToggleGroup allows deselection by default,
calling onValueChange("") when the active item is clicked.

Fix by converting to controlled mode with proper dual-mode support:
- Controlled (value prop): parent owns state, component just filters
  empty values from onValueChange. Parent can still reject changes.
- Uncontrolled (defaultValue prop): internal useState prevents
  deselection by only updating state for non-empty values.

* Update ToggleGroup.tsx

---------

Co-authored-by: Sahitya Chandra <sahityajb@gmail.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2026-02-20 10:41:54 +00:00
14c151be76 fix: add CSRF protection to OAuth callback via HMAC-signed nonce (#28083)
* fix: add CSRF protection to OAuth callback via HMAC-signed nonce

The OAuth state parameter was used only for passing application data
(returnTo, teamId) with no cryptographic binding to the user session.
An attacker could authorize their own account on a provider, capture the
authorization code, and trick a logged-in user into visiting the callback
URL to link the attacker's account to the victim's Cal.com profile.

Changes:
- encodeOAuthState: generate a random nonce and HMAC-sign it with
  NEXTAUTH_SECRET + userId, injecting both into the OAuth state
- decodeOAuthState: verify the HMAC on callback using timingSafeEqual;
  skip verification when nonce is absent (backwards compatible with apps
  that don't yet use encodeOAuthState)
- Stripe callback: replace raw state.returnTo redirect with
  getSafeRedirectUrl to prevent open redirect, remove redundant
  getReturnToValueFromQueryState, add missing return on access_denied

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: make CSRF nonce verification mandatory with allowlist for exempt apps

Makes nonce/HMAC verification mandatory by default in decodeOAuthState,
preventing attackers from bypassing CSRF protection by omitting nonce
fields from the state parameter.

Apps not yet migrated to encodeOAuthState (stripe, basecamp3, dub,
webex, tandem) are explicitly allowlisted and pass their slug to
decodeOAuthState to skip verification.

Addresses review feedback (identified by cubic) about the conditional
check being trivially bypassable.

Co-Authored-By: unknown <>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-20 10:35:01 +00:00
1aae57daae refactor(booking-audit): discriminated union for displayFields and i18n param support (#27373)
* feat: Add infrastructure for no-show audit integration

- Add Prisma migrations for SYSTEM source and NO_SHOW_UPDATED audit action
- Add NoShowUpdatedAuditActionService with array-based attendeesNoShow schema
- Update BookingAuditActionServiceRegistry to include NO_SHOW_UPDATED
- Update BookingAuditTaskConsumer and BookingAuditViewerService
- Add AttendeeRepository methods for no-show queries
- Update IAuditActionService interface with values array support
- Update locales with no-show audit translation keys

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Add NO_SHOW_UPDATED to BookingAuditAction and SYSTEM to ActionSource types

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Remove HOST_NO_SHOW_UPDATED and ATTENDEE_NO_SHOW_UPDATED from BookingAuditAction type to match Prisma schema

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Update BookingAuditActionSchema to use NO_SHOW_UPDATED instead of HOST_NO_SHOW_UPDATED and ATTENDEE_NO_SHOW_UPDATED

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: Remove deprecated no-show audit services and unify to NoShowUpdatedAuditActionService

- Delete HostNoShowUpdatedAuditActionService and AttendeeNoShowUpdatedAuditActionService
- Update BookingAuditProducerService.interface.ts to use queueNoShowUpdatedAudit
- Update BookingAuditTaskerProducerService.ts to use queueNoShowUpdatedAudit
- Update BookingEventHandlerService.ts to use onNoShowUpdated
- Add integration tests for NoShowUpdatedAuditActionService

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Add data migration step for deprecated no-show enum values

Addresses Cubic AI review feedback (confidence 9/10): The migration now
includes an UPDATE statement to convert existing records using the
deprecated 'host_no_show_updated' or 'attendee_no_show_updated' enum
values to the new unified 'no_show_updated' value before the type cast.
This prevents migration failures if any existing data uses the old values.

Co-Authored-By: unknown <>

* fix: Use CASE expression in USING clause for enum migration

Fixes PostgreSQL error 'unsafe use of new value of enum type' by avoiding
the ADD VALUE statement and instead using a CASE expression in the ALTER
TABLE USING clause to convert deprecated enum values (host_no_show_updated,
attendee_no_show_updated) to the new unified value (no_show_updated) during
the type conversion.

Co-Authored-By: unknown <>

* fix: Replace hardcoded color with semantic text-success class

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Remove color class completely from display fields

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* feat: Add valuesWithParams support for translatable complex field values

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor(booking-audit): use discriminated union for displayFields and update consumers

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: add explicit return type to getBookingHistoryHandler to bust stale tRPC build cache

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: replace $t() nested interpolation with separate translation keys and add translationsWithParams tests

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-20 14:34:12 +05:30
Abir RoyandSahitya Chandra 12e95a1f47 feat: Add meta pixel conversion events (#25638)
* feat(meta-pixel): add trackingEvent to zod schema

* fix: do not allow trackingEvent to be optional

* feat(meta-pixel): add trackingEvent selection dropdown to UI

* feat(meta-pixel): add trackingEvent to Event Type App Data

* fix(meta-pixel): implement history API hook for SPA conversion tracking

* fix(meta-pixel): check for cancel in URL and prevent Re-wrapping of pushState

* Update EventTypeAppSettingsInterface.tsx

* fix: make trackingEvent optional in zod schema to fix type-check

---------

Co-authored-by: Sahitya Chandra <sahityajb@gmail.com>
2026-02-20 00:17:42 +05:30
60b6086221 feat: add keyboard shortcuts and tooltips to booking slideover buttons (#27428)
* feat: add keyboard shortcuts and tooltips to booking slideover buttons

Co-Authored-By: peer@cal.com <peer@cal.com>

* fix: remove duplicate useBookingLocation import and fix import ordering

- Removed duplicate import of useBookingLocation from non-existent
  @calcom/web/modules/bookings/hooks/useBookingLocation path
- Fixed import ordering to satisfy biome organizeImports rules
- Removed unnecessary code comment
- Original feature by @PeerRich via Devin AI

Co-Authored-By: unknown <>

* use same style arrows for both button and remove bydefault focus from them

* fix join button tooltip hover

* fix: disable keyboard shortcuts when overlays/dialogs are open on BookingDetailsSheet

Co-Authored-By: unknown <>

* fix: use focus-based detection instead of selector-based overlay detection for keyboard shortcuts

Co-Authored-By: unknown <>

* fix: allow keyboard shortcuts when focus is on sheet ancestors

Co-Authored-By: unknown <>

* fix: use capture phase for keyboard handler to prevent Enter from activating focused buttons

Co-Authored-By: unknown <>

* fix: allow shortcuts when focus is on page elements outside any Radix portal

Co-Authored-By: unknown <>

* fix: handle calendar event clicks in onInteractOutside to prevent sheet close/reopen

Co-Authored-By: unknown <>

* fix: stop arrow key propagation to prevent Radix dropdown from opening during booking navigation

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: always stop arrow key propagation when sheet is active, even at first/last booking

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: extract keyboard handler into testable utility with tests

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: resolve type errors in keyboard handler config and test mocks

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* test: add e2e tests for booking sheet keyboard shortcuts

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: replace text= locators with data-testid selectors in e2e tests

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* made 3 fixes: Fix 1 — Stabilize handleNext/handlePrevious/handleClose with useCallback
In BookingDetailsSheet.tsx, all three handler functions were plain arrow functions recreated on every render, causing the useEffect to tear down and re-attach the document keydown listener unnecessarily. Wrapped all three in useCallback with proper dependency arrays (the Zustand store functions they call).
Fix 2 — data-booking-list-item verified (no change needed)
Confirmed that data-booking-list-item is rendered on BookingListItem.tsx and data-booking-calendar-event is rendered on Event.tsx. The onInteractOutside handler in the final merged state correctly checks both selectors. No code change required.
Fix 3 — Removed dead code from JoinMeetingButton
Reverted JoinMeetingButton back to a plain function component:
Removed forwardRef wrapping (no caller passes a ref)
Removed showTooltip prop (unused — tooltip is handled by the parent BookingDetailsSheet)
Removed ref prop from the inner Button
Removed unused Tod forwardRef imports

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Dhairyashil <dhairyashil10101010@gmail.com>
Co-authored-by: eunjae@cal.com <hey@eunjae.dev>
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2026-02-19 22:32:43 +05:30
5d65df9c05 chore: migrate booking requested webhook trigger (#27546)
* init

* wiring up

* fix type

* feat: implement DI pattern for webhook producer in API v2

- Export IWebhookProducerService and getWebhookProducer from platform-libraries
- Add WEBHOOK_PRODUCER token and useFactory provider in RegularBookingModule
- Inject webhookProducer in RegularBookingService and pass to base class

This follows the composition root pattern where only the NestJS module
knows about getWebhookProducer(), and all consumers depend only on the
IWebhookProducerService interface via constructor injection.

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* test: migrate BOOKING_REQUESTED tests to new webhook architecture

- Remove failing BOOKING_REQUESTED tests from fresh-booking.test.ts (4 tests)
- Remove failing BOOKING_REQUESTED tests from reschedule.test.ts (2 tests)
- Remove failing BOOKING_REQUESTED test from collective-scheduling.test.ts (1 test)
- Replace WebhookTaskConsumer.test.ts with placeholder (constructor changed)
- Create new webhook architecture test suite:
  - producer/WebhookTaskerProducerService.test.ts (14 tests)
  - consumer/WebhookTaskConsumer.test.ts (8 tests)
  - consumer/triggers/booking-requested.test.ts (8 tests)

The new test suite is organized by trigger type for extensibility as more
triggers are migrated to the producer/consumer pattern.

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* test: remove paid events BOOKING_REQUESTED test (moved to new architecture)

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* wrap webhook in own try-catch

* wire datafetcher

* fix

* fix v2

* fix circular dependency

* --

* merge-conflict-resolve

* mreg-conflict-resolve

* remove early return

* test: add integration tests for BOOKING_REQUESTED webhook producer invocation

Cover all 8 scenarios verifying the booking flow correctly invokes
the webhook producer for BOOKING_REQUESTED:

1. Basic confirmation → queueBookingRequestedWebhook called
2. Booker-is-organizer + confirmation → still called
3. Confirmation threshold NOT met → not called (BOOKING_CREATED instead)
4. Confirmation threshold IS met → called
5. Paid event + confirmation → called after payment succeeds
6. Reschedule + confirmation (non-organizer) → called (not BOOKING_RESCHEDULED)
7. Reschedule + confirmation (organizer) → not called (BOOKING_RESCHEDULED instead)
8. Collective scheduling + confirmation → called

Adds reusable MockWebhookProducer helper in @calcom/testing for
extendable use as more webhook triggers migrate to the new architecture.

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* fix bug

* fix conditional check

* remove unnecessary comment

* add missing expect

* remove empty test

* clean up

* tasker config

* --

* fix missing metadata

* remove faulty if else

* test: add payload content verification tests for BOOKING_REQUESTED webhook

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* remove unnecessary tests

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Hariom Balhara <1780212+hariombalhara@users.noreply.github.com>
2026-02-19 20:21:26 +04:00
Peer RichelsenandDevin AI 66ce20245c docs: declare credentials and environment variables in calcom-api skill (#28047)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-19 14:32:41 +00:00
c321a6c07a feat: owner can test non accepted OAuth client (#27525)
* refactor: combine exchange and refresh into token endpoint

* refactor: controller error handling

* refactor: use snake_case

* refactor: use snake_case

* refactor: use snake case

* refactor: token endpoint accepts application/x-www-form-urlencoded

* refactor: token endpoint accepts application/x-www-form-urlencoded

* refactor: flat token response data

* refactor: error structure

* refactor: client_id in the body

* fix: address Cubic AI review feedback on OAuth2 endpoints

- Fix getClient endpoint to use proper REST API error format instead of OAuth token error format (confidence 9/10)
- Add missing space after comma in error format string in token.input.pipe.ts (confidence 9/10)
- Support both camelCase and snake_case inputs in authorize endpoint for backward compatibility (confidence 10/10)
- Restore legacy /exchange and /refresh endpoints alongside new /token endpoint for backward compatibility (confidence 10/10)
- Add OAuth2TokensResponseDto for legacy endpoint wrapped responses
- Add OAuth2LegacyExchangeInput and OAuth2LegacyRefreshInput for legacy endpoints

Co-Authored-By: unknown <>

* fix: address additional Cubic AI feedback on OAuth2 endpoints

- Log errors when status code >= 500 in handleClientError (confidence 9/10)
- Add Cache-Control: no-store and Pragma: no-cache headers to legacy /exchange and /refresh endpoints (confidence 9/10)

Co-Authored-By: unknown <>

* docs

* Revert "fix: address additional Cubic AI feedback on OAuth2 endpoints"

This reverts commit 39cc4aa3ebb9e59a171541d7010398425995ed89.

* Revert "fix: address Cubic AI review feedback on OAuth2 endpoints"

This reverts commit 97bf593186db04c0859f9ca30950c9e3e524019d.

* docs

* fix: address Cubic AI review feedback on OAuth2 endpoints

- Fix getClient to use handleClientError instead of handleTokenError (confidence 10)
- Restore legacy /exchange and /refresh endpoints for backward compatibility (confidence 9)
- Fix RFC 6749 error format: use human-readable messages in error_description (confidence 9)
- Fix errorDescription in OAuthService to use OAUTH_ERROR_REASONS mapping (confidence 9)

Co-Authored-By: unknown <>

* fix: address additional Cubic AI feedback on OAuth2 endpoints

- Fix security issue: Replace 'CALENDSO_ENCRYPTION_KEY is not set' with generic 'Internal server configuration error' message (confidence 10/10)
- Fix backward compatibility: Create OAuth2LegacyTokensDto with camelCase properties for legacy /exchange and /refresh endpoints (confidence 9/10)
- Skipped: RFC 6749 error field issue (confidence 8/10, below threshold)

Co-Authored-By: unknown <>

* e2e

* Revert "fix: address additional Cubic AI feedback on OAuth2 endpoints"

This reverts commit a080e93f07aaf5a7dcf81fe605012cb7ebcdc192.

* Revert "fix: address Cubic AI review feedback on OAuth2 endpoints"

This reverts commit 04986a16c981521ca97069152457bf521a9ee45f.

* fix: re-apply Cubic AI review feedback on OAuth2 endpoints

- Restore OAuth2LegacyExchangeInput and OAuth2LegacyRefreshInput classes
- Restore legacy /exchange and /refresh endpoints in OAuth2Controller
- Restore OAuth2LegacyTokensDto and OAuth2TokensResponseDto classes
- Restore OAUTH_ERROR_DESCRIPTIONS mapping in oauth2-error.service.ts
- Restore OAUTH_ERROR_REASONS lookup in OAuthService.ts mapErrorToOAuthError
- Fix encryption_key_missing error to not expose internal env var name

Addresses Cubic AI feedback with confidence >= 9/10:
- Comment 32 (9/10): Legacy endpoints and input classes
- Comment 34 (9/10): Error description mapping in OAuthService
- Comment 35 (10/10): OAUTH_ERROR_DESCRIPTIONS in error service

Skipped (confidence < 9/10):
- Comment 33 (8/10): getClient handleTokenError vs handleClientError

Co-Authored-By: unknown <>

* Revert "fix: re-apply Cubic AI review feedback on OAuth2 endpoints"

This reverts commit 416bef9c931d9a7ed78c65a70a3425550d61b151.

* delete unused file

* fix: e2e tests

* address cubic review

* fix: address Cubic AI review feedback on OAuth2 exception filter

- Fix header case sensitivity: use lowercase 'x-request-id' instead of 'X-Request-Id' since Express lowercases all request headers
- Redact request body in error logs to prevent exposing sensitive OAuth2 credentials like client_secret, password, and refresh_token

Co-Authored-By: unknown <>

* docs: api v2 oauth controller docs

* chore: remove authorize endpoint

* feat: owner can test non accepted OAuth client

* fix: remove sensitive data from OAuth2 exception logs

Remove Authorization header and userEmail from error logs in
OAuth2HttpExceptionFilter to avoid logging sensitive information.

Addresses Cubic AI review feedback (confidence 9/10).

Co-Authored-By: unknown <>

* fix: e2e

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-19 08:11:46 -03:00
0eb2c15b39 fix: handle JSON string name field from URL prefill for firstAndLastName variant (#28039)
* fix: handle JSON string name field from URL prefill for firstAndLastName variant

* refactoring logic

* improve test coverage

* remove unnecessary comment

---------

Co-authored-by: Maks Pikov <mixelburg@users.noreply.github.com>
Co-authored-by: Romit <romitgabani1.work@gmail.com>
Co-authored-by: Romit <85230081+romitg2@users.noreply.github.com>
2026-02-19 05:14:51 +05:30
9d29055808 feat: replace admin API settings page with link to API docs (#28046)
* feat: replace admin API settings page with link to API docs

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* chore: remove unused AdminAPIView component

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* feat: rename admin API tab to API Docs

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-18 17:12:24 -05:00
74f3608b42 fix: Billings page reorganised (#28053)
* billings page reorganised

* Update redirect-apps.generated.ts

* small border update

* udpate for organization billing page

---------

Co-authored-by: Romit <romitgabani1.work@gmail.com>
Co-authored-by: Romit <85230081+romitg2@users.noreply.github.com>
2026-02-18 21:50:38 +00:00
9bfa4165b9 fix: prevent sending raw errors in responses (#24282)
* fix: prevent sending raw errors in responses

* correct already existing typo

* style: fix import ordering per biome lint

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-19 02:43:14 +05:30
cal-com-ci[bot]andLingo.dev 8a96a45424 feat: update translations via @LingoDotDev (#27639)
Co-authored-by: Lingo.dev <support@lingo.dev>
2026-02-18 16:32:10 -03:00
Deepanshu 0787c4890e fix: add toggle in workflow form (#27983) 2026-02-18 18:39:11 +01:00
Eunjae Lee 51e902f03e chore: pause syncing rules to devin (#28041)
* chore: pause syncing rules to devin

* Revert "chore: pause syncing rules to devin"

This reverts commit dd0e55322fe079b62207c9a4b58c9d24ce1c0dda.

* disable temporarily
2026-02-18 16:35:45 +00:00
Anirban SinghaandSahitya Chandra 9b1bf29ecf chore: remove stale Vitest workspace TODO (#27667)
Co-authored-by: Sahitya Chandra <sahityajb@gmail.com>
2026-02-18 14:50:58 +00:00
Hariom BalharaandDevin AI ad65fbbd81 fix: change URL prefill behavior to skip only invalid fields (#26982)
* fix: change URL prefill behavior to skip only invalid fields

Previously, when prefilling booking form fields from URL query parameters,
if any single field failed validation, all prefilling was skipped and an
empty object was returned.

This change modifies the behavior so that only invalid fields are skipped,
while valid fields are still prefilled. The implementation:

- Adds validateSingleFieldForPrefill() to validate individual fields
- Adds parseFieldByField() to collect valid fields when full parse fails
- Modifies getBookingResponsesPartialSchema to use field-by-field parsing
  as a fallback when full schema validation fails
- Logs which specific fields were skipped and why

This improves user experience by preserving valid prefill data even when
some fields have invalid values.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: reduce code duplication in partial prefill validation

Simplify the implementation by integrating field-by-field error handling
directly into the existing preprocess() function instead of using separate
helper functions.

Changes:
- Remove validateSingleFieldForPrefill() and parseFieldByField() functions
- Simplify getBookingResponsesPartialSchema to directly use preprocess()
- Add try-catch wrapper in preprocess() when isPartialSchema=true
- Continue processing other fields even if one fails during preprocessing
- Log skipped fields with reasons at debug level

This reduces code duplication while maintaining the same behavior of
skipping only invalid fields during URL prefill.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fixes

* refactor: rename getRefinedValue to superRefineField and add edge case tests

- Rename getRefinedValue to superRefineField for clarity
- Remove duplicate multiselect test block
- Add test for empty object name field during partial prefill
- Add test for invalid URL format during partial prefill
- Add test for null responses during partial prefill

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Simplifu

* Simplif more

* Enhance booking responses schema tests to validate required fields and name variants. Updated error handling for empty names and added tests for first and last name requirements. Refactored schema validation logic to improve clarity and maintainability.

* fix types

* test: add test for email validation behavior during partial prefill

Documents that email validation is relaxed to z.string() during partial prefill,
consistent with phone field behavior where partial values like '+91' are accepted.
This addresses Cubic AI feedback (confidence 9/10) to verify email handling.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix

* Add tests: packages/features/bookings/lib/getBookingResponsesSchema.test.ts

Generated by Paragon from proposal for PR #26982

* Revert "Add tests: packages/features/bookings/lib/getBookingResponsesSchema.test.ts"

This reverts commit fc2b7ad7ca8099bc4f68e97b383ea16b41cdd772.

* Simplify

* fix: sanitize partial prefill log to avoid exposing user input data

Replaced raw Zod issues array in console.warn with a count of
validation errors to prevent sensitive user data from being logged.

Issue identified by cubic (confidence 9/10).

Co-Authored-By: unknown <>

* fix

* Remove throw Error

* Refactor booking responses schema tests to use helper functions for assertions

- Introduced `expectResponsesToBe` and `expectParsingToFail` helper functions to streamline test assertions.
- Replaced direct assertions with these helper functions for improved readability and maintainability.
- Removed redundant error handling code to simplify test logic.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-18 20:01:23 +05:30
eef47ddefd feat: add duplicate functionality for managed event types (#26792)
* feat: add duplicate functionality for managed event types

Co-Authored-By: peer@cal.com <peer@cal.com>

* feat: add disclaimer and redirect for managed event type duplication

- Add info alert disclaimer in duplicate modal for managed event types
- Pass schedulingType to duplicate dialog via query params
- Redirect to assignment tab after duplicating managed event types
- Add translation key for managed event type duplicate disclaimer

Co-Authored-By: peer@cal.com <peer@cal.com>

* test: add tests for managed event type duplication

Co-Authored-By: peer@cal.com <peer@cal.com>

* refactor: replace PrismaMock unit test with Prisma integration test for duplicate handler (#27973)

* refactor: replace PrismaMock unit test with Prisma integration test for duplicate handler

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* fix: re-throw TRPCErrors in duplicate handler catch block and assert specific error codes in tests

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Hariom Balhara <1780212+hariombalhara@users.noreply.github.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
2026-02-18 16:49:23 +04:00
8908a66c23 fix: allow booking rejection via GET for email clients that do not support forms (#28029)
* fix: allow booking rejection via GET for email clients that do not support forms

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* Refactor test cases for GET handler in route.test.ts

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-18 17:23:42 +05:30
Alex van AndelandDevin AI a78a3ffe39 refactor: optimize team booking limits query with in-app filtering (#28021)
Replace 3 sequential queries (with expensive attendee email IN clauses)
with 2 parallel queries using indexed eventType.teamId filtering and
in-application user filtering with Sets for O(1) lookups.

Fix managed events booking-limit behavior: managed bookings now only
match by organizer userId (not attendee email), preserving the original
query semantics where managed bookings used userId-only matching.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-18 11:47:30 +00:00
a6a428c8cb feat: make actionSource required with ValidActionSource type across booking handlers (#27869)
* feat: make actionSource required with ValidActionSource type across booking handlers

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: update getAuditActionSource to return ValidActionSource, default to SYSTEM

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: revert getAuditActionSource, handleSeats, RegularBookingService back to ActionSource

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2026-02-18 07:34:25 -03:00
Pasquale Vitiello 217c6e6a76 chore: re-pull coss-ui components (#28032) 2026-02-18 10:08:35 +00:00
b1eb5a2809 feat: api v2 POST booking attendees endpoint (#27759)
* feat: add booking attendee endpoint

* chore: add attendee added email for add guests handler

* cleanup

* fix: restore BookingPbacGuard to prevent IDOR vulnerability in booking attendees endpoint

Co-Authored-By: unknown <>

* chore: implement review feedback, add a core service for booking attendees endpoint

* chore: implement PR feedback

* fix: dont reuse add guests handler instead add logic to add attendee function

* fix: revert previous changes

* chore: implement cubic feedback

* fix: e2e tests

* fix: e2e tests

* fix: remove convertTRPCErrorToErrorWithCode not needed

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-17 20:46:18 +00:00
Anirban Singha bd5d94a4ba fix: localize remaining hardcoded strings in PayPal setup (#27908)
* chore: add en locale key-value for paypal setup

* fix: add translation function for paypal setup

* chore: avoid merge conflicts
2026-02-17 17:36:45 +00:00
Shrey-SutariyaandRomit 3cfe295ed5 Commit 1 (#28011)
Co-authored-by: Romit <85230081+romitg2@users.noreply.github.com>
2026-02-17 16:51:21 +00:00
20dcef6680 fix: validate schedule title input to block invalid characters (#27818)
* fix(availability): validate schedule title input to block invalid characters

* fix: add english translation for invalid_characters_in_name

* fix(schedule): allow Unicode characters in schedule name validation

* fix(schedule): update validation message to match regex

* fix: tsconfig target

* fix(schedule): add required validation in NewScheduleButton using translation

* fix(schedule): allow ASCII apostrophe in schedule name validation

* Update ScheduleListItem.tsx

* fix: remove unused @ts-expect-error directives

* fix: remove tsconfig target change

* Revert "fix: remove tsconfig target change"

This reverts commit d4992caf9b71cdfe32ac9db9a509128548097a30.

* Revert "fix: remove unused @ts-expect-error directives"

This reverts commit 913eda500aeccdd67b15cf1898f6db376bf3dda9.

* fix: update validation regex to support unicode characters

* fix: remove es6 from tsconfig

* Update NewScheduleButton.tsx

---------

Co-authored-by: Deepanshu Verma <deepanshuverma186@email.com>
Co-authored-by: Sahitya Chandra <sahityajb@gmail.com>
2026-02-17 22:12:38 +05:30
11b65b295b test: add missing negation operator tests for TEXT, NUMBER, and compound rules (#27690)
* Fix exclusion filter - include all team members

* Fix display when members aren't saved in the DB

* Update tests

* test: add missing negation operator tests for TEXT, NUMBER, and compound rules

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: revert non-intentional changes to AddMembersWithSwitch.tsx

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

---------

Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2026-02-17 15:45:36 +00:00
4c73695d3a fix: refresh slots on timezone change for booker timezone restrictions (#27491)
* fix: refresh slots on timezone change for booker timezone restrictions

* refactor: use useMemo for timezone change detection

* revert: remove unnecessary formatting changes

* feat: add timezone refresh for platform components

Add timezone change detection and slot refresh to BookerPlatformWrapper
and EventTypeCalendarViewComponent to handle restriction schedules with
useBookerTimezone enabled.

* refactor: extract timezone slot refresh logic into reusable hook

* Update packages/platform/atoms/calendar-view/EventTypeCalendarViewComponent.tsx

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>

* Update packages/platform/atoms/calendar-view/EventTypeCalendarViewComponent.tsx

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>

* fix

* fix: prevent unnecessary getSchedule calls when useBookerTimezone is disabled

* fix: add timezone fields to BookerEvent type

* fix: add missing properties to BookerEvent and BookerEventProfile types

* trying to fix type errors

* Add restrictionScheduleId and useBookerTimezone fields

* fix: correct import path for useBookerTime hook

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* fix: explicitly include restrictionScheduleId and useBookerTimezone in getPublicEvent return

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* chore: trigger fresh CI build

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* fix: cast event.data to BookerEvent for timezone fields access

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* refactor: address review feedback for timezone slot refresh

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* refactor: add explicit return type to event handler to ensure type propagation

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* refactor: extract useStableTimezone hook and remove dead timezone detection code

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

---------

Co-authored-by: hackice20 <yashkam431@gmail.com>
Co-authored-by: Yash <116657771+hackice20@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-17 07:46:40 -03:00
Raihanullah Shamsi ab4eff1fe1 fix: translate hardcoded 'mins' on payment page (#27998) 2026-02-17 09:26:25 +00:00
38b43f75ba refactor: remove circular dependencies from CalendarView atom (#27850)
* fix: decouple large calendar from features

* fix: move calendar to features since this is the better approach

* chore: move OOO slots to features

* feat: add DefaultOutOfOfficeSlot fallback for calendar OOO rendering

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

* chore: cleanup unused LargeCalendar component

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-17 08:14:33 +00:00
Hariom Balhara 9603831def refactor: update BookingHistory component to use date-fns for timestamp formatting (#27851)
Replaced dayjs with date-fns for better performance and consistency in date handling. Added Tooltip for displaying formatted timestamps and improved search functionality by translating field labels and values. Cleaned up code for better readability.
2026-02-17 13:22:16 +05:30
2bc17313fd fix: deep link reschedule audit log to booking drawer history tab (#27709)
* fix: deep link reschedule audit log to booking drawer history tab

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: make booking drawer tab-agnostic for cross-tab deep links

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: use status-agnostic /bookings URL for audit log deep links

- Update audit service URLs from /bookings/upcoming?uid=... to /bookings?uid=...
- Add /bookings/page.tsx redirect that routes to /bookings/upcoming preserving query params
- Update tests to expect new URL format

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fixes

* refactor: use client-side replaceState instead of server redirect for booking deep links

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: use router.replace instead of replaceState to update tab and booking list

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* improvements

* refactor: extract deep link logic from BookingListContainer into usePreSelectedBooking hook

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: use proper BookingOutput status type in test helper

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: decouple getTabForBooking from BookingOutput type for simpler testing

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: accept Date | string for endTime in BookingForTabResolution interface

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: eliminate initialBookingUid prop drilling and revert formatting-only changes

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: use AuditDeepLink wrapper to preserve target=_blank through ServerTrans cloneElement

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Improve code organization

* refactor: rename usePreSelectedBooking to useSwitchToCorrectStatusTab

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: push preSelectedBooking into store so drawer opens on direct navigation

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fixes

* fixes

* fixes

* fix

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2026-02-17 13:20:06 +05:30
3a7122d613 fix: revert assignmentReason breaking change in webhook payloads (#27891)
* fix: revert assignmentReason breaking change in webhook payloads

Remove the new { category, details } format from EventPayloadType to
maintain backward compatibility for webhook consumers. The new format
is stripped at all webhook payload construction sites by destructuring
assignmentReason out of CalendarEvent before spreading into the payload.

A sanitizeAssignmentReasonForWebhook function provides an additional
safety net in sendPayload itself. Emails and booking single view
continue to use the new format via CalendarEvent.

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* fix: strip assignmentReason from handlePaymentSuccess webhook payload

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* fix: strip assignmentReason from triggerWebhooks and handleSeats webhook payloads

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* refactor: use zod safeParse instead of type assertion in sanitizeAssignmentReasonForWebhook

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-16 21:55:10 -03:00
Eunjae LeeandClaude Opus 4.6 21d28c9747 refactor: apply biome formatting to packages/trpc (#27928)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 12:46:42 -03:00
Volnei MunhozandDevin AI f8a93414a5 chore: remove companion app (moved to calcom/companion) (#27957)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-16 10:08:24 -03:00
DeepanshuandDeepanshu Verma 85e1ed3ab5 refactor: move cancel logic from inline onClick handler to dedicated function (#27972)
* refactor: move cancel booking logic into handleCancel function

* chore: remove unused comments per review feedback

---------

Co-authored-by: Deepanshu Verma <deepanshuverma186@email.com>
2026-02-16 09:46:42 +00:00
emrysal 4647333839 chore: release v6.2.0 2026-02-16 01:36:08 +00:00
Aritra Dey 9d4cb08c55 fix: Correct hours-to-days conversion in convertToNewDurationType (#27964)
Signed-off-by: Aritra Dey <adey01027@gmail.com>
2026-02-15 21:01:00 +05:30
Peer RichelsenandDevin AI 773fab858f fix: hide bookings opt-in banner on mobile viewport (#27965)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-15 14:24:21 +00:00
Aritra DeyandRomit ea0c92a267 fix: use maxLength parameter in truncateOnWord instead of hardcoded value (#27961)
Signed-off-by: Aritra Dey <adey01027@gmail.com>
Co-authored-by: Romit <85230081+romitg2@users.noreply.github.com>
2026-02-15 09:51:53 +00:00
Dhairyashil Shinde 2abefc412c update version (#27952) 2026-02-14 14:36:43 +00:00
Volnei MunhozandDevin AI 9d4b12b8d3 fix: lazy-load companion iframe to avoid requests on every page visit (#27946)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-14 11:41:43 +01:00
Anik Dhabal Babu c21281a55c preserve customReplyToEmail (#27941) 2026-02-13 18:09:39 +00:00
sean-brydon f5a813f531 Revert "revert (#27933)" (#27938)
This reverts commit 227ed64b49.
2026-02-13 23:36:50 +05:30
Dhairyashil Shinde fa53f54815 feat(companion): add configurable landing page (#27267)
* feat(companion): add configurable landing page feature

- Add useUserPreferences hook for persistent storage of landing page preference
- Add LandingPagePicker component for both iOS and Android/web platforms
- Update tabs index to redirect based on user preference
- Update bookings index to accept initial filter from URL params
- Add App Settings section in More screen with landing page selector
- Clear user preferences on logout for fresh state

* fix(companion): remove try-finally for React Compiler compatibility

* fix(companion): use router.replace for dynamic landing page redirect

Replace Redirect component with router.replace() to fix TypeScript
strict typing issue with expo-router's Href type for dynamic routes.

* fix(companion): use literal route strings for TypeScript strict typing

Use switch statement with literal route strings instead of dynamic
string variable to satisfy expo-router's strict Href type checking.

* working fix

* better css
2026-02-13 11:28:42 -03:00
Pedro Castro f07bed1a2a chore(deps): bump axios to 1.13.5 (#27864)
* chore: bump axios to 1.13.5

* chore: bump axios in apps/api/v2

* chore: dedupe follow-redirects to 1.15.11
2026-02-13 22:08:49 +09:00
sean-brydon 227ed64b49 revert (#27933) 2026-02-13 12:14:08 +00:00
Alex van AndelandDevin AI 237d7e95c4 fix: inline package.json descriptions in app-store _metadata.ts to fix Vitest RPC errors (#27931)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-13 08:41:39 -03:00
Eunjae LeeandDevin AI 896dfd50fd fix: prevent BookingDetailsSheet flicker when switching bookings (#27894)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-13 11:53:22 +01:00
Eunjae LeeandClaude Opus 4.6 e2119879ae refactor: apply biome formatting to packages/sms, prisma, emails, lib (#27880)
* refactor: apply biome formatting to small packages + packages/lib

Format packages/sms, packages/prisma, packages/platform/libraries,
packages/platform/examples, packages/platform/types, packages/emails,
and packages/lib.

Excludes packages/platform/examples/base/src/pages/[bookingUid].tsx
due to pre-existing lint errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* revert: remove packages/platform formatting changes

Revert biome formatting for packages/platform as requested.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 07:39:01 -03:00
7aefefc31f feat: enable onboarding-v3 globally and update e2e tests (#27922)
* feat: enable onboarding-v3 globally and update e2e tests

- Add migration to set onboarding-v3 feature flag enabled=true
- Rewrite onboarding.e2e.ts for v3 flow (Plan Selection → Personal Settings → Calendar)
- Update URL assertions in signup, team-invitation, org-invitation, ab-tests-redirect, and auth tests to accept both old and new onboarding paths

Co-Authored-By: sean@cal.com <Sean@brydon.io>

* fix: use pathname-only matching in waitForURL to prevent false matches on callbackUrl query param

Co-Authored-By: sean@cal.com <Sean@brydon.io>

* use test IDs

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2026-02-13 10:37:47 +00:00
1b6b91b0ad fix: display phone numbers and localized timezone in BookingDetailsSheet (#27909)
* fix: display phone numbers and localized timezone in BookingDetailsSheet

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: replace selectAll with explicit column selects for Attendee query

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* Update apps/web/modules/bookings/components/BookingDetailsSheet.tsx

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Add tests: packages/lib/dayjs/formatToLocalizedTimezone.test.ts

Generated by Paragon from proposal for PR #27909

* Revert "refactor: replace selectAll with explicit column selects for Attendee query"

This reverts commit f810ba801c900c8f065e5e1ce1d02a1700322257.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-02-13 10:27:01 +00:00
50c7210d6b fix: resolve signup watchlist review issues and auto-unlock on SIGNUP entry removal (#27923)
* fix: resolve signup watchlist review issues with deleteEntry, email verification ordering, and unlock flow

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* fix: scope sendEmailVerification to non-invite signups only

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* feat: auto-unlock users when SIGNUP-source watchlist entries are removed

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* fix: remove PII from error logging in unlockSignupUser

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-13 10:13:33 +00:00
Eunjae LeeandDevin AI 1bb4b206c6 fix: default icon size to 16px in createIcon wrapper and fix download button alignment (#27924)
* fix: resolve insights page UI regressions for plus button size and download alignment

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: default icon size to 16px in createIcon wrapper

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-13 10:00:16 +00:00
Anik Dhabal Babu 6386b2ec95 fix: add e2e-spec pattern to CODEOWNERS test file exclusions (#27926)
* add e2e-spec pattern to CODEOWNERS

* Add pattern for test-suite files in CODEOWNERS
2026-02-13 15:18:32 +05:30
RomitandAnik Dhabal Babu 68abc63fe4 fix (#27902)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2026-02-13 09:46:37 +00:00
Syed Ali ShahbazandDevin AI bdeb4b1dc9 fix: use randomString() for booking uid in slots e2e tests to avoid unique constraint flake (#27916)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-13 06:18:52 -03:00
Devanshu SharmaandSahitya Chandra 1541730886 fix(app-store): preserve existing enabled state during seed (#27918)
* fix(app-store): preserve existing enabled state during seed

* Update seed-app-store.ts

---------

Co-authored-by: Sahitya Chandra <sahityajb@gmail.com>
2026-02-13 08:34:06 +00:00
4c6af5659a fix: exclude test files from CODEOWNERS foundation approval (#27919)
* fix: exclude test files from CODEOWNERS foundation approval

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: add *.test.* to CODEOWNERS test file exclusions

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: add *.integration-test.* to CODEOWNERS test file exclusions

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-13 14:01:51 +05:30
Anik Dhabal Babu a9951df501 show invite link settings modal (#27917) 2026-02-13 04:13:17 +00:00
emrysal 68663231d8 chore: release v6.1.16
Post release / release (push) Canceled after 0s
Release Docker / Prepare Release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2026-02-12 23:27:31 +00:00
d3bbed01f6 feat: add signup watchlist review mode (#27912)
* feat: add signup watchlist review feature flag and handler logic

- Add 'signup-watchlist-review' global feature flag
- Add SIGNUP to WatchlistSource enum in Prisma schema
- When flag enabled, lock new signups and add email to watchlist
- Show 'account under review' message on signup page
- Add i18n strings for review UI
- Create seed migration for the feature flag

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

* test: add isAccountUnderReview tests to fetchSignup test suite

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

* fix: address Cubic AI review feedback (confidence >= 9/10)

- Remove 'import process from node:process' in signup-view.tsx (P0 bug in 'use client' component)
- Move watchlist review check before checkoutSessionId early return in calcomSignupHandler (P1 premium bypass)
- Revert selfHostedHandler to original state (out of scope per user request)
- Add test mocks for FeaturesRepository and GlobalWatchlistRepository

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

* fix: remove node:process import from useFlags.ts (client-side file)

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

* fix: remove !token condition from watchlist review check

Token is present in normal email-verified signups, so the !token
condition was incorrectly skipping watchlist review for verified users.

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

* Apply suggestion from @cubic-dev-ai[bot]

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* refactor: move user lock to UserRepository.lockByEmail

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

* refactor: use cached getFeatureRepository() instead of deprecated FeaturesRepository

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

* refactor: remove user locking, keep only watchlist addition on signup review

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

* feat: lock user on signup review, remove watchlist entry on unlock

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-02-12 23:14:31 +00:00
Volnei MunhozandDevin AI 9dfcb0d482 fix: include locale-prefixed paths in botid client-side protection (#27910)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-12 20:13:01 +00:00
Madhav KumarandAnik Dhabal Babu 240405f849 fix: Trimmed License Section in README (#27903)
* Pruned the 'License' section and Pushed it before 'Acknowledgement' to avoid confusion and Clarity

* Improves README file by pruning License Section and Aligning Properly

* Remove License section from README.md

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2026-02-12 17:46:20 +00:00
bc9a9c0c24 fix: Organised the OOO page (#27865)
* fixed text issue

* matched style of api keys page

* organised the out of office page

* Clean up unused imports in OutOfOfficeEntriesList

Removed unused imports from OutOfOfficeEntriesList.tsx

* fixed import issues

* remove unused imports

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Romit <romitgabani1.work@gmail.com>
Co-authored-by: Romit <85230081+romitg2@users.noreply.github.com>
2026-02-12 22:35:43 +05:30
Eunjae LeeandClaude Opus 4.6 f8abce44cb fix: update data-table hook imports missed in #27833 refactor (#27900)
* fix: update data-table hook imports after refactor (#27833)

The data-table hooks (useDataTable, useFilterValue) were moved from
@calcom/features/data-table to ~/data-table/hooks/ in #27833 but this
file was missed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update DataTableProvider import path in insights-wrong-routing-view

Another import missed in #27833 refactor — DataTableProvider moved from
@calcom/features/data-table/ to ~/data-table/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:05:12 +00:00
Eunjae LeeandDevin AI 1368ffe55d refactor: move data-table hooks/contexts/provider from features to web modules (#27833)
* refactor: move data-table hooks/contexts/provider from features to web modules

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: update broken useColumnResizing import path in DataTable.tsx

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: update remaining broken import paths for moved hooks

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* docs: update GUIDE.md import paths and file references for moved modules

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-13 00:00:40 +09:00
Carina WollendorferandCarinaWolli 397bf3cc0c fix: use personal credits if no more team credits (#27518)
* use personal credits if team has no more credits

* clean up

* adjust comment

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2026-02-12 20:02:12 +05:30
sean-brydon 98ec38aeaa feat: add annual plans for teams/organizations (#27896)
* wip: add annual plan priceId support

* fix type errors

* type cast
2026-02-12 14:05:24 +00:00
d52e2d9fad feat: active user billing (#27867)
* factory and statergie

* chore: use correct method of DI

* feat: add onchagne

* add logic to HWM stat

* add webhook resolver methods to each statergy

* move seat tracking + webhooks over to own statergy

* Move to factory base approach

* move logic to correct class

* rename create -> createByTeamId

* fix: remove debug `true ||` overrides from IS_STRIPE_ENABLED and IS_TEAM_BILLING_ENABLED

Remove accidentally committed debug overrides that short-circuited
IS_STRIPE_ENABLED and IS_TEAM_BILLING_ENABLED to always be true,
bypassing Stripe credential checks. This would break self-hosted
instances without Stripe configured.

Identified by cubic (https://cubic.dev)

Co-Authored-By: unknown <>

* feat: active user billing

* add tests

* UI side for users on active billing

* use correct period of stripe sub

* feat: claude feedback

* fix: skip Stripe sync for canceled/expired subscriptions to prevent repeated API calls

Co-Authored-By: unknown <>

* feat: feedback

* feat: only render when active users mode is set

* fix type error

* fix: constants + feature flags

* fix: default to null in tests

* chore: use seats in test

* fix: use node:crypto protocol for Node.js builtin imports

Co-Authored-By: sean@cal.com <Sean@brydon.io>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-12 13:57:32 +00:00
bf348ee209 refactor: migrate Icon to component icons from @coss/ui/icons (#27458)
* feat: add @coss/ui/icons package and migrate static icon usages

Create a new icon system in @coss/ui/icons that wraps lucide-react icons
with controlled props (size and className only). Migrate 44 component
files from <Icon name="xxx" /> to <XxxIcon /> pattern.

This is Phase 1 of the icon migration, covering only static icon names.
Dynamic icon usages (e.g., name={variable}) are skipped for now.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate more static icons to @coss/ui/icons (batch 2)

Migrated 18 files from <Icon name="xxx" /> to <XxxIcon /> pattern:
- Event meta, insights, shell, user table bulk actions
- Webhooks, blocklist, form-builder, form components

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate more static icons to @coss/ui/icons (batch 3)

Migrated 7 files:
- CancelBooking, AddGuestsDialog, ChargeCardDialog
- ReassignDialog, RescheduleDialog
- Categories, Slider

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate more static icons to @coss/ui/icons (batch 4)

Migrated 6 files:
- Organizations: AboutOrganizationForm, PaymentStatusView, profile
- Workflows: CallDetailsSheet, SkeletonLoaderList, TimeTimeUnitInput

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate more static icons to @coss/ui/icons (batch 5)

Migrated 4 files:
- TwoFactorModalHeader
- App setups: alby, make, btcpayserver

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 6)

- confirmDialog.tsx: clock icon
- ics-feedcalendar/Setup.tsx: trash, plus icons
- hitpay/Setup.tsx: info icon
- paypal/Setup.tsx: circle-alert icons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 7)

- webhook-edit-view.tsx: external-link icon
- webhook-new-view.tsx: external-link icon
- UserDropdown.tsx: chevron-down, user, settings, moon, map, circle-help, download, blocks, log-out icons
- TopNav.tsx: settings icon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 8)

- EmailInviteForm.tsx: x icon
- InviteOptions.tsx: mail, upload, link icons
- SkeletonLoader.tsx: search, clock, user icons
- Embed.tsx: sun, arrow-left icons
- CalendarSwitch.tsx: arrow-left, rotate-cw icons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 9)

- apps-view.tsx: search icon
- categories-view.tsx: arrow-left, arrow-right icons
- DatePicker.tsx: calendar icon
- EditLocationDialog.tsx: map-pin icon
- AppPage.tsx: circle-alert, book-open, external-link, mail, file, shield, flag icons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 10)

- LicenseSelection.tsx: loader, check icons
- AppConnectionItem.tsx: circle-alert, arrow-right icons
- oauth-client-form/index.tsx: info icons (x2)
- auth/error/page.tsx: x icon
- troubleshoot/layout.tsx: loader icon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 11)

- UpgradeTipWrapper.tsx: users, refresh-ccw, user-plus icons
- PbacOptInModal.tsx: shield-check, lock, users, shield icons
- AdvancedPermissionGroup.tsx: chevron-right, info icons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 12)

- AdminAppsList.tsx: pencil icon
- AvailableTimes.tsx: calendar-x-2 icon
- BookingDetailsSheet.tsx: external-link (x2), repeat icons
- BookingCalendarContainer.tsx: chevron-left, chevron-right icons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 13)

- videos-single-view.tsx: chevron-right icon
- TroubleshooterSidebar.tsx: arrow-left icon
- RuleBuilder.tsx: filter icon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 14)

- FieldMappingBuilder.tsx: arrow-right, link icons
- IntegrationAttributeSyncCard.tsx: key icon
- onboarding-view.tsx: loader icon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 15)

- team-profile-view.tsx: copy icon
- DateRangeFilter.tsx: check icon
- ColumnVisibilityButton.tsx: check icon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static Icon to @coss/ui/icons (batch 16)

- EventWorkflowsTab.tsx: lock
- event-types-listing-view.tsx: search
- onboarding-migrate-members-browser-view.tsx: arrow-left, arrow-right, rotate-cw, lock, ellipsis-vertical, check, user-plus
- onboarding-migrate-teams-browser-view.tsx: arrow-left, arrow-right, rotate-cw, lock, ellipsis-vertical, check, arrow-right, users
- EventAvailabilityTab.tsx: globe, user
- onboarding-teams-browser-view.tsx: arrow-left, arrow-right, rotate-cw, lock, ellipsis-vertical, check, arrow-right, users
- FormBuilder.tsx: mail, phone, arrow-up, arrow-down
- AIEventController.tsx: info
- EventTeamAssignmentTab.tsx: x

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static Icon to @coss/ui/icons (batch 17)

- EventAdvancedTab.tsx: pencil, info
- HostLocations.tsx: triangle-alert, loader
- EventTypeLayout.tsx: loader
- Locations.tsx: x, corner-down-right, check
- EditWeightsForAllTeamMembers.tsx: upload, search, info, chevron-down
- WorkflowPage.tsx: pencil

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static Icon to @coss/ui/icons (batch 18)

- TestPhoneCallDialog.tsx: phone
- WorkflowListPage.tsx: link
- UsersTable.tsx: building, lock
- TeamList.tsx: building, paintbrush, chart-line, user-plus, users, pencil
- bookings-single-view.tsx: chevron-left, check, calendar, x, external-link

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static Icon to @coss/ui/icons (batch 19)

- TeamsListing.tsx: users, refresh-ccw, user-plus, mail, video, eye-off, info
- ConnectionInfo.tsx: clipboard
- IntercomContactForm.tsx: message-circle, loader, send

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: migrate static Icon to @coss/ui/icons in teams components

- EventTypesList.tsx: check → CheckIcon
- MemberInvitationModal.tsx: user, users, building → UserIcon, UsersIcon, BuildingIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: migrate static Icon to @coss/ui/icons in workflow components

- ImportNumberDialog.tsx: info → InfoIcon
- VoiceSelector.tsx: user → UserIcon
- IncomingCallsTab.tsx: info, phone-incoming → InfoIcon, PhoneIncomingIcon
- PhoneNumberTab.tsx: loader, phone → LoaderIcon, PhoneIcon
- VoiceSelectionDialog.tsx: pause, play, user → PauseIcon, PlayIcon, UserIcon
- WebCallDialog.tsx: loader, phone, phone-off, triangle-alert, trash, message-circle → new icons (kept Icon import for dynamic mic icon)
- WorkflowDetailsPage.tsx: zap, arrow-right → ZapIcon, ArrowRightIcon
- AddActionDialog.tsx: info → InfoIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: migrate static Icon to @coss/ui/icons in WorkflowStepContainer

- info → InfoIcon (2 instances)
- phone → PhoneIcon (2 instances)
- circle-help → CircleHelpIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: migrate static Icon to @coss/ui/icons in data-table and org components

- attributes-list-view.tsx: tags → TagsIcon
- AddFilterButton.tsx: plus, eye-off → PlusIcon, EyeOffIcon
- BaseSelectFilterOptions.tsx: check → CheckIcon
- DataTable.tsx: arrow-up, arrow-down, check, chevrons-up-down, eye-off → new icons
- booking-dry-run-success-view.tsx: check → CheckIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: migrate static Icon to @coss/ui/icons in username and routing components

- UsernameTextfield.tsx: check → CheckIcon
- PremiumTextfield.tsx: check, external-link → CheckIcon, ExternalLinkIcon (kept Icon for star with fill prop)
- Header.tsx (routing-forms): pencil, menu, waypoints → PencilIcon, MenuIcon, WaypointsIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate Icon to component icons in routing-forms pages

Batch 25: Migrate static Icon usages to @coss/ui/icons components.

Files changed:
- Forms.tsx: file-text, shuffle, chart-bar, circle-check, mail, download → FileTextIcon, ShuffleIcon, ChartBarIcon, CircleCheckIcon, MailIcon, DownloadIcon
- FormEdit.tsx: chevron-down, menu → ChevronDownIcon, MenuIcon
- IncompleteBooking.tsx: globe → GlobeIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate Icon to component icons in UI package

Batch 26: Migrate static Icon usages to @coss/ui/icons components.

Files changed:
- ProgressToast.tsx: file-down, x → FileDownIcon, XIcon
- showToast.tsx: check, info, x → CheckIcon, InfoIcon, XIcon
- InfoBadge.tsx: info → InfoIcon
- MeetingTimeInTimezones.tsx: globe → GlobeIcon
- InputError.tsx: info → InfoIcon
- ConfirmationDialogContent.tsx: circle-alert, check → CircleAlertIcon, CheckIcon
- CalendarSwitch.tsx: arrow-left, rotate-cw → ArrowLeftIcon, RotateCwIcon
- ArrowButton.tsx: arrow-up, arrow-down → ArrowUpIcon, ArrowDownIcon
- FileUploader.tsx: upload → UploadIcon (kept Icon for dynamic)
- Checkbox.tsx: check → CheckIcon
- EditableHeading.tsx: pencil → PencilIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate Icon to component icons in UI and app-store packages

Batch 27: Migrate static Icon usages to @coss/ui/icons components.

Files changed:
- MultiEmail.tsx: x → XIcon (converted width to size)
- AppListCard.tsx: circle-alert → CircleAlertIcon
- SplitButton.tsx: chevron-down → ChevronDownIcon (kept Icon for dynamic)
- Calendar.tsx: chevron-left, chevron-right → ChevronLeftIcon, ChevronRightIcon
- AppDependencyComponent.tsx: check, circle-x, arrow-right → CheckIcon, CircleXIcon, ArrowRightIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate Icon to component icons in form input components

Batch 28: Migrate static Icon usages to @coss/ui/icons components.

Files changed:
- TextField.tsx: info → InfoIcon (kept Icon for x with onClick)
- Input.tsx: eye, eye-off, search → EyeIcon, EyeOffIcon, SearchIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use @coss/ui/src/icons in packages with legacy moduleResolution

Packages like packages/ui and packages/app-store use a legacy moduleResolution
setting that doesn't support subpath exports. This changes the import path from
@coss/ui/icons to @coss/ui/src/icons for these packages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate Icon to component icons in UI form and editor components

Migrates static Icon usages:
- MultiOptionInput: x (2x), arrow-up, arrow-down
- components (select): check
- AddVariablesDropdown: chevron-down (3x)
- ToolbarPlugin: chevron-down

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate Icon to component icons in settings and platform atoms

Migrates static Icon usages:
- SettingsLayoutAppDirClient: chevron-down (2x), chevron-right (2x), arrow-left
- platform/atoms dialog: x
- platform/atoms toast: x

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: resolve @coss/ui/icons module resolution for legacy moduleResolution

Add typesVersions to @coss/ui package.json to help TypeScript resolve
subpath exports even with legacy moduleResolution: "node" setting.

Change imports from @coss/ui/src/icons to @coss/ui/icons to avoid
double-src path issue when Next.js applies import maps.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: localize 'Select field type' fallback label in FormEdit

Address Cubic AI review feedback (confidence 9/10) by using t() for
the hardcoded 'Select field type' string to ensure proper i18n support.

Co-Authored-By: unknown <>

* fix: add typesVersions wildcards for @coss/ui subpath imports

Add lib/*, components/*, and hooks/* to typesVersions to support
legacy moduleResolution with all package subpath patterns.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: resolve missing Icon references in settings and workflows

- Add Icon import for dynamic icon usages in SettingsLayoutAppDirClient
- Replace static Icon usages with component icons (InfoIcon, PhoneIcon)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: resolve icon component type errors

- Add onClick prop support to IconProps
- Fix size prop type (use number instead of string)
- Rename local MailOpenIcon to MailOpenIconWrapper to avoid shadowing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add data-testid to icon components for test compatibility

- Add testId parameter to createIcon function
- Auto-generate data-testid for all icons (e.g., "check-icon", "search-icon")
- Remove outdated mock for @calcom/ui/components/icon in tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update package.json

* Update package.json

* fix: migrate Icon to CalendarDaysIcon in videos-single-view.tsx

Co-Authored-By: unknown <>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-12 14:30:27 +01:00
Hariom Balhara 635c1febb0 fix: Booking Drawer - text overlap in footer (#27897)
## What does this PR do?

Fixes footer text overlap in the Booking Details drawer where action buttons covered the last content items on smaller screens.

**Root cause:** `sm:-my-6` on `SheetFooter` applied both negative top and bottom margins. The negative top margin pulled the footer up into the body content area, causing the overlap.

**Fix:**
- Removed `-mb-4` and `sm:-my-6` from `SheetFooter`, keeping only horizontal negative margins (`-mx-4` / `sm:-mx-6`) for edge-to-edge stretch
- Added `pb-0` to `SheetContent` to remove bottom padding so the footer sits flush at the bottom on all breakpoints

<img width="1098" height="1362" alt="image" src="https://github.com/user-attachments/assets/0418df56-a432-4edf-8e5b-664a5e8c18ac" />

- Fixes #XXXX (GitHub issue number)
- Fixes CAL-XXXX (Linear issue number - should be visible at the bottom of the GitHub issue description)

## Visual Demo (For contributors especially)

#### Image Demo (if applicable):

See screenshot above showing the overlap bug. The footer actions ("..." button) overlap with the last booking history entry.

## Mandatory Tasks (DO NOT REMOVE)

- [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [ ] I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). If N/A, write N/A here and check the checkbox.
- [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works.

## How should this be tested?

1. Open any booking in the bookings list to open the details drawer
2. Scroll to the bottom of the drawer content
3. Verify the footer actions (buttons, dropdown) do not overlap with the last content item
4. Test on both mobile and desktop viewport sizes — footer should sit flush at the bottom with no extra gap on either

## Human Review Checklist

- [ ] Verify footer appearance on **mobile** (below `sm` breakpoint) — no gap below footer, no overlap with content
- [ ] Verify footer appearance on **desktop** (`sm+` breakpoint) — same checks
- [ ] Confirm the footer still stretches edge-to-edge horizontally on both breakpoints

## Checklist

- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- My code doesn't follow the style guidelines of this project
- I haven't commented my code, particularly in hard-to-understand areas
- I haven't checked if my changes generate no new warnings
- My PR is too large (>500 lines or >10 files) and should be split into smaller PRs

---

Link to Devin run: https://app.devin.ai/sessions/b9d130b8a7a54f039b90b53921b19c78
Requested by: @hariombalhara
2026-02-12 18:50:33 +05:30
emrysal 8beb629b07 chore: release v6.1.15
Post release / release (push) Canceled after 0s
Release Docker / Prepare Release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2026-02-12 13:09:31 +00:00
7cfb053026 feat: add wrong routing tab under Insights for reviewing assignment reports (#27423)
* Add DB table for wrong assignment reports

* When report is submitted write to the db

* Prevent duplicate reportings

* test: add migration and tests for WrongAssignmentReport table

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: add unique constraint on bookingUid and booking access check for hasWrongAssignmentReport

- Add @unique constraint on bookingUid in WrongAssignmentReport model to prevent duplicate reports at DB level
- Add booking ownership check using BookingAccessService in hasWrongAssignmentReport endpoint
- Refactor hasWrongAssignmentReport into separate handler and schema files

Addresses Cubic AI review feedback on PR #27405

Co-Authored-By: unknown <>

* feat: add routingFormId to WrongAssignmentReport and fix Select clearing

- Add routingFormId field to WrongAssignmentReport model in schema.prisma
- Add relation to App_RoutingForms_Form with SetNull on delete
- Update WrongAssignmentReportRepository.createReport to accept routingFormId
- Update BookingRepository.findByUidIncludeEventTypeAndTeamAndAssignmentReason to include routedFromRoutingFormReponse
- Extract routingFormId from booking in reportWrongAssignment handler
- Fix Select clearing issue: handle null case when user clears team member selection
- Update tests to include routingFormId field

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* chore: add migration for routingFormId in WrongAssignmentReport

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* feat: add wrong assignment reports dashboard under routing tab

- Add reviewedById and reviewedAt fields to WrongAssignmentReport model
- Add repository methods for listing reports by status and updating status
- Create tRPC endpoints for fetching reports and updating status
- Create dashboard UI with pending/reviewed tabs showing routing form name
- Add translation keys for dashboard UI
- Integrate dashboard into routing insights page

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: move hooks before early return and fix indentation

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: address Udit's review comments

- hasWrongAssignmentReport: throw UNAUTHORIZED error instead of returning false
- reportWrongAssignment: add try-catch for Prisma P2002 unique constraint error
- WrongAssignmentReport: add Team relation to teamId field
- WrongAssignmentReportRepository: use findUnique instead of findFirst
- reportWrongAssignment: use i18n for error messages

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: revert hasWrongAssignmentReport to return false when user lacks access

Per PR checklist, hasWrongAssignmentReport should return { hasReport: false }
when user lacks access to booking, not throw an error. This allows the UI
to gracefully treat 'no access' as 'no report exists'.

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* test: update mocks for findUnique and i18n in unit tests

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: throw UNAUTHORIZED in hasWrongAssignmentReport and squash migrations

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* feat: move wrong assignment reports to its own tab under Insights

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* Use data table for wrong reports

* Add option to view routing trace

* feat: add view routing form submission action to wrong assignment reports

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* feat: add RoutingFormResponseSheet component for viewing form submissions

- Create slide-out sheet to display routing form responses
- Map option IDs to display labels for select/multiselect fields
- Handle both legacy and modern option formats
- Add i18n strings: form_submission, no_responses_found

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: improve wrong assignment reports UX

- Integrate RoutingFormResponseSheet as slide-out panel instead of new tab
- Fix dropdown padding by using StartIcon prop instead of manual Icon
- Allow direct status changes for reviewed reports (no need to reopen first)
- Remove unused Icon import

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: change User relation onDelete from Cascade to SetNull in WrongAssignmentReport

Address Hariom's review feedback:
- Changed reportedById from Int to Int? (nullable)
- Changed reportedBy relation from onDelete: Cascade to onDelete: SetNull
- Updated migration SQL to reflect these changes

This preserves wrong assignment reports even when the reporting user is deleted,
as the data is still useful for analysis.

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* feat: add missing i18n strings for wrong assignment reports dashboard

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* refactor: move form response display value resolution server-side

Replace client-side option ID to label resolution in
RoutingFormResponseSheet with a new lean tRPC endpoint
(getFormResponseDisplay) that resolves values server-side using
the existing getHumanReadableFieldResponseValue utility. This
enforces DTO boundaries by returning a clean pre-resolved payload
instead of leaking internal option format details to the client.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add report wrong assignment button to routing trace sheet

Wire up the WrongAssignmentDialog from the routing trace sheet header
so users can flag wrong assignments directly while viewing the trace.
The report button is disabled with a tooltip when a report already
exists.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: reuse existing WrongAssignmentDialog from parent

Replace the duplicate WrongAssignmentDialog in RoutingTraceSheet with
a callback to the existing instance in BookingActionsDropdown. This
reduces the prop surface from a 6-field reportContext object to an
onReport callback and hasExistingReport boolean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: use repository instead of direct Prisma in getFormResponseDisplay

Replace direct Prisma query with PrismaRoutingFormResponseRepository's
findByIdIncludeForm method. Extend the method to also select form name,
description, userId, and teamId needed for display and auth checks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: replace direct Prisma calls with repository methods

Use MembershipRepository.hasMembership() for auth checks and
TeamRepository.findAllByParentId() for child team queries instead
of direct Prisma calls. Replace direct user query with
UserRepository.getTimeZoneAndDefaultScheduleId(). Remove unused
seed script.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: use PBAC services for wrong assignment report auth

Replace manual MembershipRepository.hasMembership() checks with
PBAC-aware permission checking. getWrongAssignmentReports uses
createTeamPbacProcedure middleware since teamId is in input.
updateWrongAssignmentReportStatus uses PermissionCheckService
directly since teamId is discovered from the report entity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve type errors in RoutingFormResponseSheet and wrong-routing view

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* feat: add seed script for wrong assignment reports test data

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Revert "feat: add seed script for wrong assignment reports test data"

This reverts commit 0bd60e9661858a59aab1573d14d57d81733b7991.

* Only update reviewed fields when not pending

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor: wrap handleStatusChange in useCallback to fix useMemo recalculation

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* Fix routing sheet UI

* fix: use appropriate error message in getFormResponseDisplay handler

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* refactor: extract WrongAssignmentReportService from tRPC handlers

Move business logic (booking lookup, duplicate check, report creation,
webhook dispatch, org-level team resolution) into a dedicated service
in packages/features. Handlers become thin controllers that only handle
auth checks and delegate to the service.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: restrict report status updates to admin/owner roles

Remove MembershipRole.MEMBER from fallbackRoles in
updateWrongAssignmentReportStatus permission check. Updating report
status is an administrative action that should be limited to team
admins and owners.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: localize hard-coded success message in WrongAssignmentReportService

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: rename reviewed tab to handled and add missing translations

Rename the "Reviewed" tab to "Handled" since it groups three distinct
statuses (Reviewed, Resolved, Dismissed). Also add missing translation
keys for "resolved" and "dismissed" status badges.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* merge: resolve conflicts with main branch

Merge main into devin/1769747741-wrong-assignment-dashboard, resolving conflicts in:
- BookingActionsDropdown.tsx: use main's booking prop with PR's fragment structure
- reportWrongAssignment.handler.ts: keep PR's service-based approach
- reportWrongAssignment.handler.test.ts: use main's class-based mocks with PR's additions
- WrongAssignmentReportService.ts: align with main's repo method and field names

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* Add guard when accessing assignmentReasonSortedByCreatedAt

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* fix: address Cubic AI review feedback - select projection and useLocale refactor

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: remove stale TRPCError assertion in test (service throws ErrorWithCode)

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* refactor: remove unused findByTeamIdAndStatus and findByTeamIdAndStatuses methods

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

* perf: add composite index on WrongAssignmentReport and narrow findByIdIncludeForm select

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

* perf: use lightweight findTeamIdById in update-status handler instead of findById

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: hariom@cal.com <hariombalhara@gmail.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: alex@cal.com <me@alexvanandel.com>
2026-02-12 13:02:33 +00:00
Hariom Balhara 50c5423ba7 fix: support underscores in workflow template variables with backward compatibility (#27571)
## What does this PR do?

Fixes an issue where underscores in form field identifiers were being stripped when converting to workflow template variables. According to the documentation, users should be able to use variables like `{COMPANY_NAME}` or `{MONTHLY_INFRASTRUCTURE_SPEND}`, but the regex in `formatIdentifierToVariable` was removing underscores, causing these variables to not match.

**The problem:**
- Form field identifier: `Company_Name` or `Monthly_Infrastructure_Spend`
- Expected variable: `{COMPANY_NAME}` or `{MONTHLY_INFRASTRUCTURE_SPEND}`
- Actual variable (before fix): `{COMPANYNAME}` or `{MONTHLYINFRASTRUCTURESPEND}`

**The fix:**
- Updated the regex from `[^a-zA-Z0-9 ]` to `[^a-zA-Z0-9_ ]` to preserve underscores in `formatIdentifierToVariable`
- Added a private `formatIdentifierToVariableLegacy` function that maintains the old behavior (strips underscores)
- Added `getVariableFormats` helper that returns both current and legacy formats for backward compatibility
- Added `convertResponsesToVariableFormats` helper in `executeAIPhoneCall.ts` to convert form responses to both variable formats
- Updated `customTemplate.ts` matching logic to support both variable formats
- Updated `executeAIPhoneCall.ts` to include both variable formats when building dynamic variables

This ensures existing templates using `{COMPANYNAME}` continue to work while new templates can use the documented `{COMPANY_NAME}` format.

## Mandatory Tasks (DO NOT REMOVE)

- [x] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [x] I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. N/A - no docs changes needed.
- [x] I confirm automated tests are in place that prove my fix is effective or that my feature works.

## How should this be tested?

1. Create a routing form with a field identifier containing underscores (e.g., `Company_Name`)
2. Create a workflow with a template using `{COMPANY_NAME}` (with underscore)
3. Submit the form and verify the variable is replaced correctly
4. Also test with `{COMPANYNAME}` (without underscore) to verify backward compatibility

**Automated tests added:**
- `customTemplate.test.ts` - 15 tests covering `formatIdentifierToVariable`, `getVariableFormats`, and template variable replacement
- `executeAIPhoneCall.test.ts` - 6 tests covering `convertResponsesToVariableFormats` function for form responses

## Human Review Checklist

- [ ] Verify the regex change `[^a-zA-Z0-9_ ]` correctly preserves underscores
- [ ] Verify backward compatibility: both `{COMPANY_NAME}` and `{COMPANYNAME}` should work for the same form field
- [ ] Verify `convertResponsesToVariableFormats` correctly generates both variable formats
- [ ] Review test coverage for edge cases (empty responses, undefined values)

---

Link to Devin run: https://app.devin.ai/sessions/9d5d90178714493086d94691865c3e07
Requested by: @hariombalhara
<!-- devin-review-badge-begin -->

---

<a href="https://app.devin.ai/review/calcom/cal.com/pull/27571">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1">
    <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin">
  </picture>
</a>
<!-- devin-review-badge-end -->
2026-02-12 17:21:42 +05:30
Anirban Singha 7716f8c0a1 feat: add list formatting options to profile and event description editor (#27838) 2026-02-12 10:42:03 +00:00
8a3944f987 chore: Remove /api/ routes from proxy (#27883)
* chore: Remove /api/ routes from proxy

* Update apps/web/proxy.ts

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Add tests: apps/web/proxy.test.ts

Generated by Paragon from proposal for PR #27883

* Update proxy.test.ts

* fix: update proxy tests to account for /api/auth/signup in matcher

Co-Authored-By: unknown <>

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-12 07:33:47 -03:00
Dhairyashil Shinde be0b707296 style: take feedback dialog little right (#27892) 2026-02-12 14:32:36 +05:30
fad64a012a fix(form-builder): correct value of selection box behavior in Edit of new Added Question dialog (#27890)
* fix/SelectionBox Issue

* comment removed

* remove empty lines

---------

Co-authored-by: Romit <85230081+romitg2@users.noreply.github.com>
Co-authored-by: Romit <romitgabani1.work@gmail.com>
2026-02-12 13:36:05 +05:30
Eunjae LeeandDevin AI 14563f664e feat: re-render bookings page after feature opt-in without full refresh (#27873)
* feat: re-render bookings page after feature opt-in without full refresh

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: make onOptInSuccess a required prop

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-12 10:41:40 +05:30
c7e66a3aa9 fix: maintain uid parameter in BookingDetailsSheet URL (#27872)
* fix: maintain uid parameter in BookingDetailsSheet URL

Move the isSyncedFromUrlToStoreRef guard from the effect level to inside
the Zustand subscription callback. This ensures the Store→URL subscription
is always created while still preventing premature URL overwrites during
URL→Store sync.

Also adds an E2E test verifying the uid parameter appears in the URL
after clicking a booking item in the v3 bookings view.

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* Update apps/web/playwright/bookings-list.e2e.ts

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* fix: clean up bookings-v3 feature flag after E2E test

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-02-12 10:39:08 +05:30
Pedro Castro 0d55939d13 chore(deps): resolve fast-xml-parser to 5.3.4 for @lingo.dev/_compiler (#27887) 2026-02-12 01:26:59 +00:00
06bb994145 fix: Improve wrong assignment report (#27162)
* Pass already fetched booking

* Fetch the assignment reason

* Update tests

* Label as first assignment reason

* Pass single booking object

* Remove unused repository method

* Only pass bookingUid to handler

* Rename booking `assignmentReason` to `assignmentReasonSortedByCreatedAt`

* fix: use renamed assignmentReasonSortedByCreatedAt property

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* Remove take statement

* fix: address Udit's review comments

- Restore BookingAccessService for access control checks
- Rename repository method from findByUidForWrongAssignmentReport to findByUidIncludeUserAndEventTypeTeamAndAttendeesAndAssignmentReason

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* test: update tests to use BookingAccessService mock

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: use latest assignment reason in BookingDetailsSheet for consistency

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-11 13:12:21 -05:00
Udit Takkar b185cd231b feat: login overlay re design (#27800) 2026-02-11 15:12:10 +00:00
Anik Dhabal Babu 4c804cdb00 fix flakes (#27849) 2026-02-11 14:51:34 +00:00
Eunjae LeeandClaude Opus 4.6 98b6d63164 refactor: apply biome formatting to packages/features (#27844)
* refactor: apply biome formatting to packages/features (batch 1 - small subdirs)

Format small subdirectories in packages/features: di, flags, holidays, oauth,
settings, users, assignment-reason, selectedCalendar, hashedLink, host, form,
form-builder, availability, data-table, pbac, schedules, troubleshooter,
eventtypes, calendar-subscription, and root-level files.

Also includes straggler apps/web BookEventForm.tsx.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: apply biome formatting to packages/features (batch 2 - medium subdirs)

Format medium subdirectories in packages/features: auth, credentials,
calendars, routing-forms, routing-trace, attributes, watchlist, calAIPhone,
tasker, and webhooks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: apply biome formatting to packages/features (batch 3 - bookings + insights)

Format bookings and insights subdirectories in packages/features.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: apply biome formatting to packages/features (batch 4 - ee)

Format packages/features/ee subdirectory covering billing, workflows,
organizations, teams, managed-event-types, round-robin, dsync,
integration-attribute-sync, and payments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: apply biome formatting to packages/features (batch 5 - booking-audit part 1)

Format booking-audit di, actions, common, dto, repository, and types
subdirectories in packages/features/booking-audit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: apply biome formatting to packages/features (batch 6 - booking-audit part 2)

Format booking-audit service subdirectory in packages/features/booking-audit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 15:47:14 +01:00
Keith WilliamsandDevin AI bc8ebb9213 perf: use UNION ALL with DISTINCT for bookings query optimization (#27841)
* perf: use UNION ALL with DISTINCT for bookings query optimization

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: add unionAll and distinct to kysely test mocks

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: add integration tests for getBookings query

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: add totalCount deduplication test for multi-branch union scenario

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: fix totalCount assertion to be exact instead of gte

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* Apply suggestion from @keithwillcode

* perf: restore COUNT(DISTINCT id) for totalCount query

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* Add tests: packages/trpc/server/routers/viewer/bookings/get.handler.integration-test.ts

Generated by Paragon from proposal for PR #27841

* Add tests: packages/trpc/server/routers/viewer/bookings/get.handler.test.ts

Generated by Paragon from proposal for PR #27841

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-11 11:33:11 -03:00
6089f47d24 feat: add fallback action options to routing form fallback section (#27449)
* feat: add no users found fallback action for routing forms

- Add NoUsersFoundFallbackActionType enum with CustomPageMessage and ExternalRedirectUrl options
- Add noUsersFoundFallbackAction field to zodNonRouterRoute schema
- Add NoUsersFoundFallback UI component in RouteBuilder for configuring fallback action
- Update handleResponse to return noUsersFoundFallbackAction when no team members match
- Add i18n translation keys for new UI strings

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* refactor: change fallback to use same action options as main route

- Replace noUsersFoundFallbackAction with fallbackAction that has same structure as main action
- Add action dropdown and input fields to fallback attributes query builder section
- Support Event redirect, External URL, and Custom page options in fallback
- Remove separate NoUsersFoundFallback toggle component
- Update handleResponse to return fallbackAction

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: ensure fallbackAction type is always defined in onChange handlers

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: add guards for route.fallbackAction in onChange handlers

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: only return fallbackAction when no users are found

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* refactor: restructure fallback section to mirror main route structure

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: ensure backwards compatibility for existing routes with fallbackAttributesQueryValue

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: initialize fallbackAction with main event type for backwards compatibility

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: include fallbackAction in getRoutesToSave to persist changes

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: use fallbackAction in getRoutedUrl when no team members found

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: use i18n for fallback label and add tests for fallback action

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: do not trigger fallback action when CRM contact owner is found

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: prioritize fallbackAction over fallbackAttributesQueryValue

When a route has a fallbackAction configured, skip the
fallbackAttributesQueryValue to ensure the fallbackAction is triggered
when no team members are found. This maintains backwards compatibility
by only using fallbackAttributesQueryValue when fallbackAction is not set.

Also treats teamMemberIdsMatchingAttributeLogic being null (routing couldn't
run, e.g., missing orgId) the same as an empty array for the purpose of
triggering the fallbackAction.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: extract RouteActionSelector shared component for action selection UI

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: add i18n string for default custom page message

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* feat: add webhook trigger when routing form fallback route is hit

- Add ROUTING_FORM_FALLBACK_HIT to WebhookTriggerEvents enum in Prisma schema
- Add new trigger to routing-forms webhook triggers in constants
- Add translation string for the new webhook trigger
- Implement triggerFallbackWebhook function in formSubmissionUtils.ts
- Call webhook trigger from handleResponse.ts when fallback action is used

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: remove webhook object from error log to avoid exposing secrets

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* revert: remove webhook trigger changes (to be moved to separate PR)

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: only show fallback query builder when fallbackAction is explicitly set

The fallback attributes query builder was being shown for new routes
when fallbackAction was not set, causing an extra 'Add rule' button
to appear. This broke e2e tests that relied on the button index.

Now the fallback query builder is only shown when fallbackAction is
explicitly set to EventTypeRedirectUrl, which happens after the route
is saved and reloaded (via auto-initialization in useRoutes).

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: use data-testid for attribute query builder selectors in e2e tests

- Add data-testid='attributes-query-builder' to the main attributes query builder div
- Update addAttributeRoutingRule and addAttributeRoutingRuleWithOperator to use the data-testid selector instead of nth() indices
- This makes the tests more robust and independent of DOM structure changes

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: initialize fallbackAction for new routes in useCreateRoute hook

- New routes created via useCreateRoute now initialize fallbackAction
- This ensures new routes have a valid fallbackAction when saved
- Matches the initialization logic for existing routes in useRoutes

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: initialize fallbackAction at save time when action is EventTypeRedirectUrl

- When saving routes, if fallbackAction is undefined but action is EventTypeRedirectUrl,
  initialize fallbackAction from the main action
- This handles the case where user creates a new route and changes action to an event type
- Fixes e2e test failures where form save was failing due to undefined fallbackAction

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: initialize fallbackAction when main action changes to EventTypeRedirectUrl

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: only show fallback section when main action has valid event type selected

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: check for valid fallbackAction event type before preserving existing value

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* Give more structure to main and fallback routes

* Update comment

* Rename fallbackAttributesQueryBuilder to matchingMembersFallbackRoute

* Display custom event redirect input

* fix: address Cubic AI review feedback for routing form fallback

- Simplify getFallbackAction logic to use checkedFallback flag instead of
  manually parsing attributesQueryValue (confidence 9/10)
- Add i18n translation for 'And connect with specific team members' string
  using t('connect_with_specific_team_members') (confidence 9/10)

Co-Authored-By: unknown <>

* fix: update getFallbackAction to use checkedFallback and handle missing orgId case

- Use checkedFallback flag as primary indicator for triggering fallback action
- Also handle edge case where attribute routing was configured but couldn't run
  due to missing orgId (teamMemberIdsMatchingAttributeLogic is null)
- Update test to mock checkedFallback: true when expecting fallback action

Co-Authored-By: unknown <>

* Address UI comment

* fix: Use fallbackAttributesQueryValue when fallback action is event redirect

Only use fallbackAttributesQueryValue when:
1. fallbackAction is EventTypeRedirectUrl (attribute routing applies)
2. fallbackAction is not set (backwards compatibility)

Skip it for CustomPageMessage and ExternalRedirectUrl fallback actions
since attribute routing doesn't apply to those action types.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: correct JSX syntax error in matchingMembersFallbackRoute

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* refactor: extract getCustomSlug helper to deduplicate custom event type slug logic

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: only show fallback attribute filtering for teams under orgs

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: show fallback action for all team forms, gate attribute filtering on isOrganization

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2026-02-11 09:15:16 -05:00
saptarshi-bose f6fa24de2b adjusted zoho Token expiry time to miliseconds. (#27855)
The Date.now() method returns time in epoch miliseconds, adding 60 * 60 = 3.6seconds leads to a bug that results in expiring the access token in 3.6 seconds, although it been valid. It creates unnecessary api calls to the zoho servers before most the api calls.
2026-02-11 12:49:44 +00:00
Shrey-SutariyaandRomit b75000ea7b feat(form-builder): format field labels with spaces (#27858)
Co-authored-by: Romit <85230081+romitg2@users.noreply.github.com>
2026-02-11 11:37:45 +00:00
Benny Joo dc7b9d91ae chore: add agent rules to prevent circular dependencies between packages (#27860) 2026-02-11 12:00:57 +01:00
36713fc12e feat: add UTM parameter data to insights bookings CSV download (#27854)
* feat: add UTM parameter data to insights bookings CSV download

Co-Authored-By: carina@cal.com <c.wollendorfer@me.com>

* test: add UTM data tests and update snapshots for csvDataTransformer

Co-Authored-By: carina@cal.com <c.wollendorfer@me.com>

* fix: use Tracking relation for UTM data instead of flat fields on Booking

Co-Authored-By: carina@cal.com <c.wollendorfer@me.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-11 15:28:54 +05:30
Anik Dhabal Babu 285f6d54a3 fix: unable to add and remove org banner (#27848) 2026-02-11 06:58:20 +00:00
Peer Richelsen 1e0cf3d0f6 feat: caretta.so app (#27840)
* new caretta.so app

* added logo and screenshots

* nit
2026-02-11 06:45:34 +00:00
681d191537 fix: ensure metadata key is always present in BOOKING_CREATED webhook payload (#27300)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: ali@cal.com <ali@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2026-02-11 06:05:51 +00:00
Anik Dhabal BabuandDevin AI 269ed87db4 fix: use unique UIDs in seated slots E2E tests to prevent flaky uid collisions (#27847)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-11 00:55:38 -03:00
Madhav KumarandVolnei Munhoz 0191f54519 Populated Contributing Section with a welcoming summary (#27830)
Co-authored-by: Volnei Munhoz <volnei@cal.com>
2026-02-11 03:02:54 +00:00
Everton Resende dc3e681e01 fix: hide cancel/reschedule links in emails when disabled (#27637)
When disableCancelling or disableRescheduling is enabled on an event type,
the confirmation email still showed non-functional cancel/reschedule links.

The ManageLink email component already had correct conditional logic to
hide these links, but two flows constructed the CalendarEvent manually
without including the flags: the booking confirmation handler and the
payment booking flow. This caused the flags to be undefined, which
ManageLink interpreted as enabled.

Added disableCancelling and disableRescheduling to the Prisma select and
CalendarEvent object in both confirm.handler.ts and getBooking.ts.

Closes #22906
2026-02-10 18:57:54 -03:00
Romit 30bba3da15 fix (#27478) 2026-02-10 18:56:45 -03:00
Udit Takkar cfa0783ebc feat: add claude specs (#26852)
* feat: claude templates

* refactor: new file

* feat: symlink
2026-02-10 19:06:31 +00:00
81e4241f85 refactor: apply biome formatting to apps/web (#27692)
* refactor: apply biome formatting to apps/web (batch 1)

Formats apps/web non-modules directories:
- apps/web/app
- apps/web/lib
- apps/web/pages
- apps/web/styles
- apps/web/server
- apps/web/test
- Root-level .ts and .mjs files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/modules (batch 2)

Formats smaller apps/web/modules directories:
- onboarding, data-table, users, apps, auth
- integration-attribute-sync, shell, availability
- feature-flags, team, webhooks, getting-started
- feature-opt-in, troubleshooter, schedules, notifications
- signup-view.tsx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/modules (batch 3)

Formats medium apps/web/modules directories:
- bookings
- event-types
- insights
- embed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/modules/ee (batch 4)

Formats apps/web/modules/ee directory.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web (batch 5)

Formats remaining apps/web directories:
- apps/web/modules/settings
- apps/web/modules/booking-audit
- apps/web/playwright

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/components (batch 6)

Formats remaining apps/web/components files.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to new apps/web files from main

Formats newly added/modified files from main merge:
- WorkflowStepContainer.tsx (resolved merge conflicts)
- Newly moved files (blocklist, form-builder, schedules, etc.)
- Other files modified in main

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to new apps/web files from main

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 23:25:24 +05:30
f4abbb2de1 feat: refactor billing to strategy implemention (#27828)
* factory and statergie

* chore: use correct method of DI

* feat: add onchagne

* add logic to HWM stat

* add webhook resolver methods to each statergy

* move seat tracking + webhooks over to own statergy

* Move to factory base approach

* move logic to correct class

* rename create -> createByTeamId

* fix: remove debug `true ||` overrides from IS_STRIPE_ENABLED and IS_TEAM_BILLING_ENABLED

Remove accidentally committed debug overrides that short-circuited
IS_STRIPE_ENABLED and IS_TEAM_BILLING_ENABLED to always be true,
bypassing Stripe credential checks. This would break self-hosted
instances without Stripe configured.

Identified by cubic (https://cubic.dev)

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 13:11:36 -03:00
3908cda372 perf: optimize getBusyTimesForLimitChecks with batched parallel queries (#27399)
* perf: optimize getBusyTimesForLimitChecks with batched parallel queries and raw SQL

Co-Authored-By: udit@cal.com <udit222001@gmail.com>

* test: add unit tests for getBusyTimesForLimitChecks batched query optimization

Co-Authored-By: udit@cal.com <udit222001@gmail.com>

* refactor: use Prisma findMany instead of raw SQL for better testability

The batching (50 user chunks) and parallel execution (Promise.all) provide
the main performance optimization. Using findMany instead of $queryRaw
maintains the same performance characteristics while being compatible
with prismock for testing.

Co-Authored-By: udit@cal.com <udit222001@gmail.com>

* fix: type error

* refactor: add integration test

* chore: remove log

* chore: remove log

* fix: avoid logging PII (userId) in busyTimes debug log

Replace JSON.stringify(busyTimes) with count to avoid exposing userId
in logs. Addresses Cubic AI review feedback (confidence 9/10).

Co-Authored-By: unknown <>

* chore: undo

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 15:56:30 +00:00
Rajiv Sahal db76980644 fix: API v2 @GetWebhook() decorator doesn't generate OpenAPI path (#27612)
* fix: openapi spec for GetWebhook decorator

* fix: openapi spec
2026-02-10 11:56:55 -03:00
sean-brydon d8e6ecc7a0 fix: add i18n + migration (#27836) 2026-02-10 14:44:19 +00:00
16eb96e27d fix: update booking page terms text to use APP_NAME instead of "our" (#27834)
* fix: update booking page terms text to use APP_NAME instead of 'our'

Co-Authored-By: carina@cal.com <c.wollendorfer@me.com>

* remove changes for platform booker

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2026-02-10 14:41:00 +00:00
Pedro Castro 83c42cfb2b chore: resolve @modelcontextprotocol/sdk to 1.26.0 (#27640) 2026-02-10 22:28:21 +09:00
Udit TakkarandHariom Balhara 589260a7c7 fix: host location option (#27807)
Post release / release (push) Canceled after 0s
Release Docker / Prepare Release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
Co-authored-by: Hariom Balhara <1780212+hariombalhara@users.noreply.github.com>
2026-02-10 13:21:57 +00:00
emrysal ff076d2394 chore: release v6.1.14 2026-02-10 13:18:45 +00:00
Alex van Andel ee1748dbee Revert "fix: prevent layout shift in sticky navbar (#27256)"
This reverts commit 38492d5839.
2026-02-10 13:09:04 +00:00
Alex van Andel 0a68a189f2 Revert "fixed header (#27794)"
This reverts commit 6f6c27b47d.
2026-02-10 13:09:01 +00:00
Alex van Andel abbc8bbb7b Revert "fix: availability page UI (#27825)"
This reverts commit 267686704d.
2026-02-10 13:05:50 +00:00
Hariom Balhara 11f336811b feat: add non-interactive mode to app-store CLI (#27831) 2026-02-10 13:59:00 +01:00
Keith Williams 3fd922664f chore: Update CODEOWNERS for Shell.tsx (#27835) 2026-02-10 18:18:25 +05:30
ae31e05bec refactor: decouple @calcom/features from @calcom/trpc/server [2] (#27821)
* inviteMemberUtils in featuers

* update imports

* wip

* wip

* fix: remove PII from log statements in inviteMemberUtils

- Remove email (usernameOrEmail) from error log in sendSignupToOrganizationEmail
- Replace full user object with userId in debug log in sendExistingUserTeamInviteEmails

Co-Authored-By: unknown <>

* revert: restore original code for pure relocation (no formatting/logic changes)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix

* fix: update test mocks to use new import path for inviteMemberUtils

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* revert: restore original formatting in proration test and handleGroupEvents

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 09:29:32 -03:00
Benny JooandDevin AI 431fbe44fc refactor: decouple @calcom/features from @calcom/trpc/server (#27751)
* make GetPublicEventInput

* space

* mv feature flags router to trpc

* remove trpc imports

* wip

* mv OrgMembershipLookup to features

* fix

* fix

* update

* fix

* revert formatting-only changes to improve PR reviewability

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 09:28:37 -03:00
c0504993a6 feat: Write wrong assignment reports to the database (#27405)
* Add DB table for wrong assignment reports

* When report is submitted write to the db

* Prevent duplicate reportings

* test: add migration and tests for WrongAssignmentReport table

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: add unique constraint on bookingUid and booking access check for hasWrongAssignmentReport

- Add @unique constraint on bookingUid in WrongAssignmentReport model to prevent duplicate reports at DB level
- Add booking ownership check using BookingAccessService in hasWrongAssignmentReport endpoint
- Refactor hasWrongAssignmentReport into separate handler and schema files

Addresses Cubic AI review feedback on PR #27405

Co-Authored-By: unknown <>

* feat: add routingFormId to WrongAssignmentReport and fix Select clearing

- Add routingFormId field to WrongAssignmentReport model in schema.prisma
- Add relation to App_RoutingForms_Form with SetNull on delete
- Update WrongAssignmentReportRepository.createReport to accept routingFormId
- Update BookingRepository.findByUidIncludeEventTypeAndTeamAndAssignmentReason to include routedFromRoutingFormReponse
- Extract routingFormId from booking in reportWrongAssignment handler
- Fix Select clearing issue: handle null case when user clears team member selection
- Update tests to include routingFormId field

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* chore: add migration for routingFormId in WrongAssignmentReport

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: address Udit's review comments

- hasWrongAssignmentReport: throw UNAUTHORIZED error instead of returning false
- reportWrongAssignment: add try-catch for Prisma P2002 unique constraint error
- WrongAssignmentReport: add Team relation to teamId field
- WrongAssignmentReportRepository: use findUnique instead of findFirst
- reportWrongAssignment: use i18n for error messages

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: revert hasWrongAssignmentReport to return false when user lacks access

Per PR checklist, hasWrongAssignmentReport should return { hasReport: false }
when user lacks access to booking, not throw an error. This allows the UI
to gracefully treat 'no access' as 'no report exists'.

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* test: update mocks for findUnique and i18n in unit tests

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: throw UNAUTHORIZED in hasWrongAssignmentReport and squash migrations

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: change User relation onDelete from Cascade to SetNull in WrongAssignmentReport

Address Hariom's review feedback:
- Changed reportedById from Int to Int? (nullable)
- Changed reportedBy relation from onDelete: Cascade to onDelete: SetNull
- Updated migration SQL to reflect these changes

This preserves wrong assignment reports even when the reporting user is deleted,
as the data is still useful for analysis.

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 08:00:01 -03:00
c3a8301b0c fix: hide feature opt-in feedback dialog during impersonation (#27802)
* fix: hide feature opt-in banner and feedback forms during impersonation

Co-Authored-By: sean@cal.com <Sean@brydon.io>

* fix: keep opt-in banner visible during impersonation, only hide feedback forms

Co-Authored-By: sean@cal.com <Sean@brydon.io>

* fix: allow feedback form during impersonation in development mode

Co-Authored-By: sean@cal.com <Sean@brydon.io>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 10:28:03 +00:00
c58e1f4ae5 feat: add filter segment validation with useActiveFiltersValidator hook and refactor DataTableProvider (#27208)
* feat: add activeFilters validator to DataTableProvider for filter segment validation

- Add validateActiveFilters prop to DataTableProvider to filter out inaccessible values
- Create useActiveFiltersValidator hook in bookings module to validate filter values
- Integrate validator in bookings-view.tsx to validate userId, eventTypeId, teamId filters
- Add comprehensive tests for the filter validation logic

This provides a scalable solution for handling stale filter segment data by validating
filters at the caller level using already-fetched accessible resource IDs.

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: add type guard for eventTypeIds to ensure number[] type

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: delay segment filter application until validator is ready

- Change useActiveFiltersValidator to return 'loading' state while data is fetching
- Add pending segment handling in DataTableProvider to delay filter application
- Expose isValidatorPending in context for consumers to disable queries
- Update BookingListContainer to disable bookings query while validator is pending

This prevents both errors from invalid filters and flash from fetching without filters.

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: extract useSegmentManagement hook from DataTableProvider

- Extract segment management logic into dedicated useSegmentManagement hook
- Reduces DataTableProvider from ~500 lines to ~330 lines
- Use NuqsSetter type to match nuqs useQueryState setter signatures
- Maintains all existing functionality and type safety

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: export ActiveFiltersValidator types from data-table lib/types

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: import ActiveFiltersValidatorState from lib/types instead of hook

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* Revert "refactor: extract useSegmentManagement hook from DataTableProvider"

This reverts commit dd588ac4cfb1d709c71d1f621994a7e8f3118f33.

* refactor: split DataTableProvider into fine-grained context providers

- Create DataTableStateContext for raw nuqs state management
- Create DataTableSegmentContext for segment management
- Create DataTableFiltersContext for filter manipulation
- DataTableProvider now composes all providers with a bridge component
- Maintains backward compatibility via combined DataTableContext

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: apply segment filters when navigating back to page with existing URL state

When navigating back to a page with a segment already selected in the URL,
the segment was being marked as selected but its filters were not being
applied. This caused non-deterministic behavior where the filter segment
would sometimes not take effect.

The fix ensures that when segmentIdRaw exists in the URL and segments are
fetched, we also call applySegmentFilters() (with validator timing support)
instead of just setting the selectedSegment state.

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: simplify filter validator to only handle multi-select

These filters are always multi-select, so remove unnecessary
single-select handling code and tests.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: cancel debounced search term on unmount

Cancel the debounced search term on unmount/dependency change to avoid
queued updates firing after the provider unmounts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 10:38:12 +01:00
Udit TakkarandDevin AI 267686704d fix: availability page UI (#27825)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 09:06:21 +00:00
83354fbe18 fix(readme): correct 'therefor' typo to 'therefore' (#27816)
Co-authored-by: Dhairyashil <dhairyashil10101010@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 09:04:35 +00:00
sean-brydon 892d53917d fix: pass onboardingfalse (#27806) 2026-02-10 09:03:17 +00:00
Dhairyashil Shinde 6f6c27b47d fixed header (#27794) 2026-02-10 14:10:13 +05:30
sean-brydonandDevin AI 9424649d6f fix: add isCompanyEmail check to organization upgrade path (#27813)
Users with personal emails (e.g. Gmail) could enter the org creation flow
at /settings/organizations/new but only hit the company email error at
checkout, with no way to fix it from the form. After changing their email
in settings, the cached store email was still used.

- Add server-side isCompanyEmail check in page.tsx to redirect early
- Add client-side check in CreateANewOrganizationForm with clear messaging
- Always use current session email for non-admin users (not cached store)
- Add tests for isCompanyEmail utility

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 06:15:28 +00:00
Carina WollendorferandDevin AI b03c00aded fix: remove duplicate label on team name input in team creation page (#27803)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 04:48:49 +00:00
Keith Williams e2e1ba096c chore: Use IF NOT EXISTS for index of App_RoutingForms_FormResponse (#27814) 2026-02-09 22:56:55 +00:00
Keith WilliamsandDevin AI 6a9b1ec3a8 perf: add composite index on (formId, createdAt) for App_RoutingForms_FormResponse (#27252)
* perf: add composite index on (formId, createdAt) for App_RoutingForms_FormResponse

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* chore: add migration for formId, createdAt composite index

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* Update migration.sql

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-09 15:49:35 -03:00
Keith WilliamsandDevin AI 8b17df4621 refactor: Stop using Unkey for IP-based rate limiting (#27674)
* refactor: migrate IP-based rate limiting from Unkey to Cloudflare

Remove Unkey rate limiting for IP-based endpoints that will now be
handled by Cloudflare Enterprise Advanced Rate Limiting:

- Regular booking creation (createBooking:{hashedIP})
- Recurring booking creation (createRecurringBooking:{hashedIP})
- Instant meeting creation (instant.event-{hashedIP})
- Booking cancellation for unauthenticated users (api:cancel-ip:{hashedIP})
- Forgot password (forgotPassword:{hashedIP})
- Reset password (api:reset-password:{hashedIP})
- Signup (api:signup:{hashedIP})
- API v1 requests ({userId} with auto-lock)
- Global proxy rate limiting (common namespace)

Rate limiting that remains in Unkey (user/entity-based):
- Login (hashedEmail)
- Booking cancellation for authenticated users (api:cancel-user:{userId})
- 2FA setup/enable/disable (api:totp-*:{userId})
- SMS sending (team/org/user based)
- Email verification (various patterns)
- Team member operations (userId based)
- Routing forms (formId:responseHash)
- AI phone calls (userId based)

Also includes Cloudflare configuration proposal document with
recommended rules using JA4 fingerprinting for enhanced protection.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* chore: remove cloudflare proposal doc from PR

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* chore: remove cloudflare comments and keep common rate limiting type

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor: revert global middleware from PR #25080 and restore core rate limits

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor: restore instantMeeting rate limiting

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: restore email fallback in forgot-password rate limiting

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* Remove the static file check

* refactor: add POST_METHODS_ALLOWED_API_ROUTES to proxy matcher

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* revert: restore API v1 rate limiting to original state

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* revert: restore reset-password, cancel, book/event, book/recurring-event to original state

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor: use POST_METHODS_ALLOWED_API_ROUTES spread in matcher instead of hardcoded routes

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* revert: restore signup route to original state

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: update proxy matcher tests for POST_METHODS_ALLOWED_API_ROUTES spread

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* Rename var

* refactor: revert matcher to static strings, add sync-check test for POST_METHODS_ALLOWED_API_ROUTES

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* removed dead routing forms rewrite code

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-09 15:49:26 -03:00
Lauris Skraucis edf9cd70dd fix: hide cal branding on platform workflows (#27385)
* fix: hide cal branding on platform workflows

* refactor: rely on existing with platform variables code

* revert: comment

* fix e2e

* chore: remove unit results
2026-02-09 13:46:52 -03:00
f66fffd13b feat: add routing trace seeding to seed-insights (#27782)
* feat: add routing trace seeding to seed-insights and standalone seed script

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* chore: remove standalone seed-routing-trace.ts (now in seed-insights)

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: track skipped routing trace creates for accurate log output

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-09 21:46:25 +05:30
82a4780145 feat: show routing trace dialog on assignment reason badge click (#27629)
* feat: show routing trace dialog on assignment reason badge click

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: remove duplicate RoutingTraceSheet rendering

- Remove RoutingTraceSheet from BookingListItem.tsx to avoid duplicate rendering
- Move RoutingTraceSheet in BookingActionsDropdown.tsx outside of isBookingFromRoutingForm condition
- Use booking.assignmentReason.length > 0 condition for RoutingTraceSheet rendering
- Remove unused isOpenRoutingTraceSheet getter and RoutingTraceSheet import from BookingListItem.tsx

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-09 10:39:34 -05:00
0a2d0854e7 fix: allow unmarking host as no-show (#27694)
* fix: allow unmarking host as no-show and filter invalid attendees in updateAttendees

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* revert: remove updateAttendees change per review feedback

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-09 13:14:53 +00:00
e04a394e1c fix: (booking-audit) Remove IS_PRODUCTION gate and add feature flag check in producer (#26524)
* fix: remove IS_PRODUCTION gate from BookingAuditProducer

Remove the IS_PRODUCTION check that was preventing booking audits from
being queued in production. Audits are still properly gated by:

1. Organization check: Audits are skipped for non-organization bookings
   (organizationId === null)
2. Feature flag: The BookingAuditTaskConsumer checks if the 'booking-audit'
   feature is enabled for the organization via featuresRepository.checkIfTeamHasFeature()

The IS_PRODUCTION gate was intentionally added to prevent logs from being
created in production while the action data versioning was being actively
reviewed and finalized. Without proper versioning handling, the Booking
History UI could crash when encountering unversioned data. Now that the
versioning system is in place, this gate can be safely removed.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: revert formatting, keep only IS_PRODUCTION removal

Reverts the unintended formatting changes from the previous commit.
Only removes the IS_PRODUCTION gate without changing indentation.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* feat: add booking-audit feature flag check in producer to avoid unnecessary task creation

- Query booking-audit and booking-email-sms-tasker flags in parallel before fireBookingEvents
- Pass isBookingAuditEnabled through BookingEventHandler to producer's queueTask method
- Add conditional check in queueTask with debug log when skipping audit
- Reuse pre-queried isBookingEmailSmsTaskerEnabled flag instead of querying again

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* feat: make isBookingAuditEnabled required and pass it through all booking audit flows

- Make isBookingAuditEnabled a required property in BookingAuditProducerService interface
- Update all BookingEventHandler methods to require isBookingAuditEnabled
- Add feature flag check in all flows that call booking audit:
  - handleSeats (seat booking/rescheduling)
  - RecurringBookingService (bulk bookings)
  - handleCancelBooking (booking cancellation)
  - handleConfirmation (booking acceptance)
  - roundRobinReassignment (automatic reassignment)
  - roundRobinManualReassignment (manual reassignment)
  - trpc handlers: addGuests, confirm, editLocation, requestReschedule
- Skip queueing audit tasks when feature is disabled with debug logging

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: add featuresRepository dependency to RecurringBookingService DI module

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: make isBookingAuditEnabled optional for non-main flows

Keep feature flag check only in main flows (handleSeats, RegularBookingService, RecurringBookingService) which are frequently triggered. For other flows (handleCancelBooking, handleConfirmation, roundRobinReassignment, etc.), rely on the existing consumer-level check.

Changes:
- Revert feature flag check from non-main flows
- Make isBookingAuditEnabled optional in interface for non-main flow methods
- Keep isBookingAuditEnabled required for main flow methods (queueCreatedAudit, queueRescheduledAudit, queueSeatBookedAudit, queueSeatRescheduledAudit, queueBulkCreatedAudit, queueBulkRescheduledAudit)
- Update BookingEventHandlerService to use required params for main flows and optional for non-main flows

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: remove isBookingAuditEnabled from non-main flow methods

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* feat: make isBookingAuditEnabled required in all BookingEventHandlerService methods

- Add isBookingAuditEnabled as required parameter in all BookingAuditProducerService interface methods
- Update BookingAuditTaskerProducerService to use simplified check (!params.isBookingAuditEnabled)
- Update BookingEventHandlerService to require isBookingAuditEnabled in all methods
- Update all callers to query booking-audit feature flag and pass isBookingAuditEnabled:
  - handleCancelBooking
  - handleConfirmation
  - roundRobinReassignment
  - roundRobinManualReassignment
  - addGuests.handler
  - confirm.handler
  - editLocation.handler
  - requestReschedule.handler
- Inject featuresRepository in API V2's booking-location.service.ts

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* test: update roundRobinReassignment tests to include isBookingAuditEnabled

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* test: update roundRobinManualReassignment tests to include isBookingAuditEnabled

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: inject featuresRepository in API V2 RecurringBookingService

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: import PrismaWorkerModule for PrismaFeaturesRepository dependency

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: use booking's organization for feature flag check in addGuests handler

Use booking.user?.profiles?.[0]?.organizationId instead of user.organizationId
to check the booking-audit feature flag. This ensures the feature flag is
checked against the booking's organization rather than the actor's organization,
which is consistent with other handlers in this PR.

Addresses Cubic AI review feedback (confidence 9/10).

Co-Authored-By: unknown <>

* Add comment

* fix: use user.organizationId for feature flag check in addGuests handler

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: use booking's organizationId for feature flag check in addGuests handler

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: revert to user.organizationId for feature flag check in addGuests handler

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: add isBookingAuditEnabled to onNoShowUpdated calls after main merge

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: add isBookingAuditEnabled to onNoShowUpdated calls and update tests

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-09 09:25:33 -03:00
1921 changed files with 47628 additions and 80298 deletions
-57
View File
@@ -1,57 +0,0 @@
# EAS Build ignore file for monorepo
# Only upload the companion app folder
# ===========================================
# IGNORE EVERYTHING AT ROOT LEVEL
# ===========================================
/*
# ===========================================
# EXCEPT THE COMPANION APP
# ===========================================
!companion
# ===========================================
# BUT IGNORE THESE INSIDE COMPANION
# ===========================================
# Native folders - EAS generates these during builds (~2.1 GB)
companion/ios
companion/android
# Dependencies - EAS installs these during builds (~1.6 GB)
companion/node_modules
# Expo build cache
companion/.expo
companion/dist
companion/web-build
# Metro
companion/.metro-health-check*
# WXT Chrome Extension outputs (not needed for mobile builds)
companion/.output
companion/.wxt
companion/dev
# Kotlin cache
companion/.kotlin
# Build artifacts
companion/*.jks
companion/*.p8
companion/*.p12
companion/*.key
companion/*.mobileprovision
# Debug logs
companion/*.log
# TypeScript build info
companion/*.tsbuildinfo
# Environment files
companion/.env
companion/.env.*
!companion/.env.example
+2
View File
@@ -209,11 +209,13 @@ NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE=
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
STRIPE_TEAM_MONTHLY_PRICE_ID=
STRIPE_TEAM_ANNUAL_PRICE_ID=
NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID=
ORG_MONTHLY_CREDITS=
STRIPE_TEAM_PRODUCT_ID=
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
STRIPE_ORG_MONTHLY_PRICE_ID=
STRIPE_ORG_ANNUAL_PRICE_ID=
STRIPE_ORG_PRODUCT_ID=
# Used to pass trial days for orgs during the Stripe checkout session
STRIPE_ORG_TRIAL_DAYS=
+9
View File
@@ -9,6 +9,7 @@
/apps/web/lib/core/**/* @calcom/Foundation
/apps/web/lib/booking.ts @calcom/Foundation
/apps/web/lib/handleOrgRedirect.ts @calcom/Foundation
/apps/web/modules/shell/Shell.tsx @calcom/Foundation
/apps/web/proxy.ts @calcom/Foundation
/deploy/**/* @calcom/Foundation
/packages/app-store/applecalendar/**/* @calcom/Foundation
@@ -36,3 +37,11 @@
/packages/trpc/server/routers/viewer/bookings/get.handler.ts @calcom/Foundation
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
/packages/ui/**/* @calcom/Consumer @calcom/Foundation
# Exclude test files from requiring foundation/consumer approval
**/*.spec.*
**/*.e2e.*
**/*.e2e-spec.*
**/*.test.*
**/*.test-suite.*
**/*.integration-test.*
-120
View File
@@ -1,120 +0,0 @@
---
name: Companion (iOS/Android/Extension)
about: Report bugs or request features for mobile apps and browser extension
title: ""
labels: ["companion", "🚨 needs approval"]
assignees: ""
---
### Issue Type
<!--
Select one:
- Bug Report
- Feature Request
-->
- [ ] 🐛 Bug Report
- [ ] ✨ Feature Request
### Platform
<!--
Select all that apply:
-->
- [ ] 📱 iOS App
- [ ] 🤖 Android App
- [ ] 🧩 Browser Extension
### Platform Details
<!--
Fill in the details for your selected platform(s).
Delete sections that don't apply.
-->
#### iOS
- **iOS Version**: (e.g., 17.2)
- **Device Model**: (e.g., iPhone 15 Pro)
- **App Version**: (e.g., 1.0.0)
#### Android
- **Android Version**: (e.g., 14)
- **Device Model**: (e.g., Pixel 8)
- **App Version**: (e.g., 1.0.0)
#### Browser Extension
- **Browser & Version**: (e.g., Chrome 120, Firefox 121)
- **Extension Version**: (e.g., 1.0.0)
---
### Description
<!--
Provide a clear and concise description of the issue or feature.
-->
(Write your description here.)
### Steps to Reproduce (Bug Reports Only)
<!--
If this is a bug report, provide steps to reproduce the issue.
Delete this section for feature requests.
-->
1. Go to...
2. Click on...
3. See error...
### Expected Behavior
<!--
What did you expect to happen?
-->
(Describe what you expected.)
### Actual Behavior (Bug Reports Only)
<!--
What actually happened? Delete this section for feature requests.
-->
(Describe what actually happened.)
### Proposed Solution (Feature Requests Only)
<!--
If this is a feature request, describe your proposed solution.
Delete this section for bug reports.
-->
(Describe your proposed solution.)
### Screenshots / Evidence
<!--
Add screenshots, screen recordings, or console logs if applicable.
You can use tools like Bird Eats Bug for recordings.
-->
(Attach evidence here.)
### Additional Context
<!--
Any other information that might be helpful.
-->
(Add any additional context here.)
---
##### House rules
- If this issue has a `🚨 needs approval` label, don't start coding yet. Wait until a core member approves the request by removing this label.
- Your ideas are invaluable! However, they undergo review to ensure alignment with the product's direction.
- Follow best practices in our [Contributor Docs](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
+8 -1
View File
@@ -7,4 +7,11 @@ GET /v2/bookings/{bookingUid} added 'subschema #3, subschema #6' to the 'data' r
POST /v2/bookings/{bookingUid}/cancel added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
POST /v2/bookings/{bookingUid}/confirm added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
POST /v2/bookings/{bookingUid}/decline added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
POST /v2/bookings/{bookingUid}/guests added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
POST /v2/bookings/{bookingUid}/guests added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
DELETE /v2/event-types/{eventTypeId}/webhooks/{webhookId} added the new path request parameter 'webhookId'
GET /v2/event-types/{eventTypeId}/webhooks/{webhookId} added the new path request parameter 'webhookId'
DELETE /v2/oauth-clients/{clientId}/webhooks/{webhookId} added the new path request parameter 'webhookId'
GET /v2/oauth-clients/{clientId}/webhooks/{webhookId} added the new path request parameter 'webhookId'
DELETE /v2/teams/{teamId}/event-types/{eventTypeId}/webhooks/{webhookId} added the new path request parameter 'webhookId'
GET /v2/teams/{teamId}/event-types/{eventTypeId}/webhooks/{webhookId} added the new path request parameter 'webhookId'
GET /v2/webhooks/{webhookId} added the new path request parameter 'webhookId'
-44
View File
@@ -1,44 +0,0 @@
name: Companion Builds
on:
workflow_call:
permissions:
contents: read
env:
EXPO_PUBLIC_CAL_API_KEY: ${{ secrets.CI_EXPO_PUBLIC_CAL_API_KEY || vars.CI_EXPO_PUBLIC_CAL_API_KEY || vars.EXPO_PUBLIC_CAL_API_KEY }}
jobs:
build:
name: Build Companion App & Extension
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.3.0
cache: true
cache-dependency-path: companion/bun.lock
- name: Install dependencies
working-directory: companion
run: bun install --frozen-lockfile
- name: Build Expo web app
working-directory: companion
env:
EXPO_NO_TELEMETRY: 1
CI: true
run: bunx expo export --platform web
- name: Build browser extension
working-directory: companion
run: bun run ext:build
-33
View File
@@ -1,33 +0,0 @@
name: Companion Lint
on:
workflow_call:
permissions:
contents: read
jobs:
lint:
name: Lint Companion App
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.3.0
cache: false
- name: Install dependencies
working-directory: companion
run: bun install --frozen-lockfile
- name: Run lint
working-directory: companion
run: bun run lint:all
-34
View File
@@ -1,34 +0,0 @@
name: Companion Type Check
on:
workflow_call:
permissions:
contents: read
jobs:
typecheck:
name: Type Check Companion App
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.3.0
cache: true
cache-dependency-path: companion/bun.lock
- name: Install dependencies
working-directory: companion
run: bun install --frozen-lockfile
- name: Type check
working-directory: companion
run: bun run typecheck:all
+1 -37
View File
@@ -167,7 +167,6 @@ jobs:
pull-requests: read
outputs:
has-files-requiring-all-checks: ${{ steps.filter-exclusions.outputs.has-files-requiring-all-checks }}
has-companion: ${{ steps.filter-inclusions.outputs.has-companion }}
has-api-v2-changes: ${{ steps.filter-inclusions.outputs.has-api-v2-changes }}
has-prisma-changes: ${{ steps.filter-inclusions.outputs.has-prisma-changes }}
has-agents-changes: ${{ steps.filter-inclusions.outputs.has-agents-changes }}
@@ -197,14 +196,13 @@ jobs:
filters: |
has-files-requiring-all-checks:
- '**'
- '!companion/**'
- '!.vscode/**'
- '!**/*.md'
- '!**/*.mdx'
- '!.github/CODEOWNERS'
- '!docs/**'
- '!help/**'
- '!apps/web/public/static/locales/**/common.json'
- '!packages/i18n/locales/**/common.json'
- '!i18n.lock'
- name: Check for specific path changes
uses: dorny/paths-filter@v3
@@ -213,8 +211,6 @@ jobs:
filters: |
has-agents-changes:
- "agents/**"
has-companion:
- "companion/**"
has-api-v2-changes:
- "apps/api/v2/**"
- "packages/platform-constants/**"
@@ -387,27 +383,6 @@ jobs:
uses: ./.github/workflows/docs-build.yml
secrets: inherit
build-companion:
name: Companion builds
needs: [prepare]
if: needs.prepare.outputs.has-companion == 'true'
uses: ./.github/workflows/companion-build.yml
secrets: inherit
typecheck-companion:
name: Type Checks
needs: [prepare]
if: needs.prepare.outputs.has-companion == 'true'
uses: ./.github/workflows/companion-typecheck.yml
secrets: inherit
lint-companion:
name: Linters
needs: [prepare]
if: needs.prepare.outputs.has-companion == 'true'
uses: ./.github/workflows/companion-lint.yml
secrets: inherit
build:
name: Production builds
needs: [prepare]
@@ -489,9 +464,6 @@ jobs:
build-api-v2,
build-atoms,
build-docs,
build-companion,
typecheck-companion,
lint-companion,
setup-db,
e2e,
e2e-api-v2,
@@ -540,14 +512,6 @@ jobs:
needs.e2e-app-store.result != 'success'
)
) ||
(
needs.prepare.outputs.has-companion == 'true' &&
(
needs.build-companion.result != 'success' ||
needs.typecheck-companion.result != 'success' ||
needs.lint-companion.result != 'success'
)
) ||
(
needs.prepare.outputs.has-agents-changes == 'true' &&
needs.validate-agents-format.result != 'success'
@@ -12,6 +12,7 @@ permissions:
jobs:
sync:
if: false
name: Sync to Devin Knowledge
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
+3
View File
@@ -108,6 +108,9 @@ packages/**/.yarn/ci-cache/
.claude/plans/
.claude/settings.local.json
# Spec drafts (rough work, not ready for review)
specs/**/_drafts/
# trigger.dev
.trigger
agents/devin-knowledge.json
+6
View File
@@ -4,4 +4,10 @@ enableGlobalCache: false
nodeLinker: node-modules
# fast-xml-parser 4.4.1 via @boxyhq/saml-jackson → @aws-sdk/core@3.816.0 (transitive).
# Only parses trusted AWS API responses, not user input. No practical attack vector.
# Upstream fix pending: ory/polis (saml-jackson) has bumped to @aws-sdk@3.994.0 on main but hasn't released yet.
npmAuditIgnoreAdvisories:
- "1113407"
yarnPath: .yarn/releases/yarn-4.12.0.cjs
+10 -2
View File
@@ -12,7 +12,7 @@ You are a senior Cal.com engineer working in a Yarn/Turbo monorepo. You prioriti
- Create PRs in draft mode by default
- Run `yarn type-check:ci --force` before concluding CI failures are unrelated to your changes
- Import directly from source files, not barrel files (e.g., `@calcom/ui/components/button` not `@calcom/ui`)
- Add translations to `apps/web/public/static/locales/en/common.json` for all UI strings
- Add translations to `packages/i18n/locales/en/common.json` for all UI strings
- Use `date-fns` or native `Date` instead of Day.js when timezone awareness isn't needed
- Put permission checks in `page.tsx`, never in `layout.tsx`
- Use `ast-grep` for searching if available; otherwise use `rg` (ripgrep), then fall back to `grep`
@@ -126,7 +126,7 @@ packages/lib/ # Shared utilities
- Routes: `apps/web/app/` (App Router)
- Database schema: `packages/prisma/schema.prisma`
- tRPC routers: `packages/trpc/server/routers/`
- Translations: `apps/web/public/static/locales/en/common.json`
- Translations: `packages/i18n/locales/en/common.json`
- Workflow constants: `packages/features/ee/workflows/lib/constants.ts`
## Tech Stack
@@ -226,6 +226,14 @@ import { ProfileRepository } from "@calcom/features/profile/repositories/Profile
- Fix type errors before test failures - they're often the root cause
- Run `yarn prisma generate` if you see missing enum/type errors
## Spec-Driven Development (Opt-In)
For complex features, you can use spec-driven development when explicitly requested.
**To enable:** Tell the AI "use spec-driven development" or "follow the spec workflow"
See [SPEC-WORKFLOW.md](SPEC-WORKFLOW.md) for the full workflow documentation.
## Extended Documentation
For detailed information, see the `agents/` directory:
+14 -10
View File
@@ -149,6 +149,13 @@ Here is what you need to be able to run Cal.com.
- Duplicate `.env.example` to `.env`
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
> **Windows users:** Replace the `packages/prisma/.env` symlink with a real copy to avoid a Prisma error (`unexpected character / in variable name`):
>
> ```sh
> # Git Bash / WSL
> rm packages/prisma/.env && cp .env packages/prisma/.env
> ```
5. Setup Node
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
@@ -671,7 +678,7 @@ Certain versions may have trouble creating a user if the field `metadata` is emp
##### CLIENT_FETCH_ERROR
If you experience this error, it may be the way the default Auth callback in the server is using the WEBAPP_URL as a base url. The container does not necessarily have access to the same DNS as your local machine, and therefor needs to be configured to resolve to itself. You may be able to correct this by configuring `NEXTAUTH_URL=http://localhost:3000/api/auth`, to help the backend loop back to itself.
If you experience this error, it may be the way the default Auth callback in the server is using the WEBAPP_URL as a base url. The container does not necessarily have access to the same DNS as your local machine, and therefore needs to be configured to resolve to itself. You may be able to correct this by configuring `NEXTAUTH_URL=http://localhost:3000/api/auth`, to help the backend loop back to itself.
```
docker-calcom-1 | @calcom/web:start: [next-auth][error][CLIENT_FETCH_ERROR]
@@ -767,11 +774,14 @@ Cal.com, Inc. is a commercial open source company, which means some parts of thi
<img width="100%" src="https://repobeats.axiom.co/api/embed/6bfca2f20f39738048b6e70ca205efde46352c3d.svg" />
<!-- CONTRIBUTING -->
## Contributing
We ❤️ contributions! Whether it’s fixing a typo, improving documentation, or building new features, your help makes Cal.com better.
Please see our [contributing guide](/CONTRIBUTING.md).
- Check out our [Contributing Guide](./CONTRIBUTING.md) for detailed steps.
- Join the discussion on [GitHub Discussions](https://github.com/calcom/cal.com/discussions) or our community channels.
- Please follow our coding standards and commit message conventions to keep the project consistent.
Even small improvements matter — thank you for helping us grow!
### Good First Issues
@@ -984,12 +994,6 @@ An example of good readme is [atoms readme](https://github.com/calcom/cal.com/bl
1. Follow semantic versioning when using changesets.
2. Mark breaking changes using `❗️Breaking change`
<!-- LICENSE -->
## License
Distributed under the [AGPLv3 License](https://github.com/calcom/cal.com/blob/main/LICENSE). See `LICENSE` for more information.
<!-- ACKNOWLEDGEMENTS -->
## Acknowledgements
+105
View File
@@ -0,0 +1,105 @@
# Spec-Driven Development Workflow
This workflow is **opt-in**. Use it when explicitly requested by saying "use spec-driven development" or "follow the spec workflow".
## Design Documents
The `specs/` folder contains design documents for features in development.
Each spec follows this structure:
```
specs/{feature}/
├── CLAUDE.md # Feature-specific instructions (read this first)
├── design.md # The specification
├── implementation.md # Current status and what's done
├── decisions.md # Why decisions were made
├── prompts.md # Reusable prompts
├── future-work.md # What's deferred
└── docs/ # Documentation with screenshots
```
**Workflow:**
1. Read the spec's `CLAUDE.md` for specific instructions
2. Read `design.md` to understand what we're building
3. Check `implementation.md` for current status
4. Find the relevant code in the codebase
5. Implement in small pieces, update `implementation.md` after each
---
## When Implementing Features (Spec Mode)
1. **Check for design doc** in `specs/` — if it exists, follow it
2. **If no spec exists** — ask if you should create one first
3. **Look at existing patterns** — find similar code and follow conventions
4. **Update implementation.md** — mark what's done after each piece
5. **Update decisions.md** — when choosing between approaches
---
## Creating a New Spec
When user asks to build a new feature:
1. Copy the template: `cp -r specs/_templates specs/{feature-name}`
2. Explore the codebase to understand existing patterns
3. Write `design.md` with technical spec
4. Write `CLAUDE.md` with feature-specific instructions
5. Initialize `implementation.md` with "not-started" status
6. Ask user to review before implementing
---
## Updating Spec Files
### implementation.md — After completing each piece:
```markdown
## Status: in-progress
## Completed
- [x] Database schema
- [x] Migration file
## In Progress
- tRPC endpoints
## Next Steps
1. UI components
2. Tests
## Session Notes
### 2024-01-15
- Done: Added schema, created migration
- Next: Implement tRPC router
```
### decisions.md — When choosing between approaches:
```markdown
## ADR-001: Use Separate Table for Custom Locations
### Context
Need to store user-defined locations.
### Options
1. JSON field — simpler, but harder to query
2. Separate table — more flexible, better indexing
### Decision
Separate table for better querying.
### Consequences
- Need migration
- Need new tRPC router
```
---
## Don't (When Using Spec-Driven Development)
- Don't implement features without checking for a design doc first
- Don't skip updating implementation.md after completing work
- Don't make architectural decisions without recording them in decisions.md
@@ -0,0 +1,193 @@
---
title: Prevent Circular Dependencies Between Packages
impact: CRITICAL
impactDescription: Prevents build failures, type errors, ensures honest implementation, and maintains clean dependency graph
tags: architecture, dependencies, packages, imports, circular
---
## Prevent Circular Dependencies Between Packages
**Impact: CRITICAL**
Circular dependencies between packages cause build failures, type errors, and make the codebase unmaintainable. The dependency graph must be acyclic, with clear layers from low-level utilities up to high-level features.
The dependency hierarchy (from lowest to highest):
```
packages/lib (lowest - no feature dependencies)
↓
packages/app-store
↓
packages/features
↓
packages/trpc
↓
apps/web (highest - can depend on all packages)
```
**Benefits:**
- **Honest implementation**: The dependency graph accurately reflects what code depends on what, with no hidden or circular relationships that create false expectations
- **Predictable behavior**: Code behaves as the dependency structure suggests - no surprises from circular imports
- **Clear mental model**: Developers can reason about the system without tracking complex circular relationships
- **Build reliability**: No circular dependency errors during compilation or bundling
- **Easier testing**: Lower-level packages can be tested independently without pulling in the entire codebase
### `packages/lib`
**Rules:**
1. No files in `packages/lib` import from `@calcom/app-store` or `../app-store/**`
2. No files in `packages/lib` import from `@calcom/features` or `../features/**`
3. No files in `packages/lib` import from `@calcom/trpc` or `../trpc/**`
4. No files in `packages/lib` import from `@trpc/server`
5. **BONUS:** No repository files allowed in `packages/lib` (repositories belong in features or app-store)
**Incorrect:**
```typescript
// Bad - lib importing from features
import { getBooking } from "@calcom/features/bookings";
import { EventRepository } from "@calcom/features/events/repositories";
// Bad - lib importing from app-store
import { googleCalendarHandler } from "@calcom/app-store/googlecalendar";
// Bad - lib importing from trpc
import { router } from "@calcom/trpc/server/trpc";
```
**Correct:**
```typescript
// Good - lib only imports from other lib files or external packages
import { logger } from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
import type { User } from "@prisma/client";
```
### `packages/app-store`
**Rules:**
6. No files in `packages/app-store` import from `@calcom/features` or `../features/**`
7. No files in `packages/app-store` import from `@calcom/trpc` or `../trpc/**`
**Incorrect:**
```typescript
// Bad - app-store importing from features
import { BookingService } from "@calcom/features/bookings/services";
// Bad - app-store importing from trpc
import { publicProcedure } from "@calcom/trpc/server/trpc";
```
**Correct:**
```typescript
// Good - app-store imports from lib and prisma
import { logger } from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
import type { Credential } from "@prisma/client";
```
### `packages/features`
**Rules:**
8. No files in `packages/features` import from `@calcom/trpc` or `../trpc/**`
9. No files in `packages/features` import from `@calcom/web` or `@calcom/web/**`
10. No files in `packages/features` import from `../../apps/web/**`
**Incorrect:**
```typescript
// Bad - features importing from trpc
import { router } from "@calcom/trpc/server/trpc";
// Bad - features importing from web app
import { getServerSession } from "@calcom/web/lib/auth";
import { WebComponent } from "../../apps/web/components/WebComponent";
```
**Correct:**
```typescript
// Good - features import from lib, app-store, and prisma
import { logger } from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
import { googleCalendarHandler } from "@calcom/app-store/googlecalendar";
```
### `packages/trpc`
**Rules:**
11. No files in `packages/trpc` import from `../apps/web/**`
**Incorrect:**
```typescript
// Bad - trpc importing from web app
import { getServerSession } from "../../apps/web/lib/auth";
```
**Correct:**
```typescript
// Good - trpc imports from features, lib, and prisma
import { BookingService } from "@calcom/features/bookings/services";
import { logger } from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
```
### `packages/testing`
**Rules:**
12. No files in `packages/testing` import from `@calcom/web` or `@calcom/web/**`
13. No files in `packages/testing` import from `@calcom/features` or `@calcom/features/**`
14. No files in `packages/testing` import from `../../apps/web/**`
15. No files in `packages/testing` import from `../features/**`
**Incorrect:**
```typescript
// Bad - testing importing from web or features
import { WebComponent } from "@calcom/web/components";
import { BookingService } from "@calcom/features/bookings";
```
**Correct:**
```typescript
// Good - testing imports from lib and prisma
import { prisma } from "@calcom/prisma";
import { logger } from "@calcom/lib/logger";
```
### `packages/platform/atoms`
**Rules:**
16. No files in `packages/platform/atoms` import from `@calcom/trpc` or `@calcom/trpc/**`
17. No files in `packages/platform/atoms` import from `../../trpc` or `../../trpc/**`
18. No files in `packages/platform/atoms` import from `@calcom/web`
**Incorrect:**
```typescript
// Bad - platform/atoms importing from trpc or web
import { router } from "@calcom/trpc/server/trpc";
import { trpc } from "../../trpc";
import { WebComponent } from "@calcom/web";
```
**Correct:**
```typescript
// Good - platform/atoms imports from lib, features, and ui
import { logger } from "@calcom/lib/logger";
import { Button } from "@calcom/ui/components/button";
import type { Booking } from "@calcom/features/bookings/types";
```
## Enforcement
These rules should be enforced through:
- ESLint rules that detect forbidden import paths
- CI checks that fail on circular dependencies
- Code review guidelines that flag violations
+1 -1
View File
@@ -33,7 +33,7 @@ tags: reference, navigation, file-locations
## Translations
- English: `apps/web/public/static/locales/en/common.json`
- English: `packages/i18n/locales/en/common.json`
## App Store
+22
View File
@@ -1,6 +1,19 @@
---
name: calcom-api
description: Interact with the Cal.com API v2 to manage scheduling, bookings, event types, availability, and calendars. Use this skill when building integrations that need to create or manage bookings, check availability, configure event types, or sync calendars with Cal.com's scheduling infrastructure.
env:
CAL_API_KEY:
description: "Cal.com API key (prefixed with cal_live_ or cal_test_). Required for all API requests."
required: true
CAL_CLIENT_ID:
description: "OAuth client ID for platform integrations managing users on behalf of others. Sent as x-cal-client-id header."
required: false
CAL_SECRET_KEY:
description: "OAuth client secret for platform integrations. Sent as x-cal-secret-key header."
required: false
CAL_WEBHOOK_SECRET:
description: "Secret used to verify webhook payload signatures via X-Cal-Signature-256 header."
required: false
---
# Cal.com API v2
@@ -14,6 +27,15 @@ All API requests should be made to:
https://api.cal.com/v2
```
## Required Credentials
| Environment Variable | Required | Description |
|---------------------|----------|-------------|
| `CAL_API_KEY` | Yes | Cal.com API key (prefixed with `cal_live_` or `cal_test_`). Used as Bearer token for all API requests. Generate from Settings > Developer > API Keys. |
| `CAL_CLIENT_ID` | No | OAuth client ID for platform integrations that manage users on behalf of others. Sent as `x-cal-client-id` header. |
| `CAL_SECRET_KEY` | No | OAuth client secret for platform integrations. Sent as `x-cal-secret-key` header. |
| `CAL_WEBHOOK_SECRET` | No | Secret for verifying webhook payload signatures via the `X-Cal-Signature-256` header. |
## Authentication
All API requests require authentication via Bearer token:
-10
View File
@@ -1,10 +0,0 @@
const path = require("node:path");
const i18nConfig = require("@calcom/config/next-i18next.config");
/** @type {import("next-i18next").UserConfig} */
const config = {
...i18nConfig,
localePath: path.resolve("../../web/public/static/locales"),
};
module.exports = config;
@@ -82,6 +82,7 @@ async function handler(req: NextApiRequest) {
return await handleCancelBooking({
bookingData: { id, allRemainingBookings, cancellationReason, cancelledBy },
userId: req.userId,
actionSource: "API_V1",
});
}
+1 -2
View File
@@ -1,4 +1,3 @@
// TODO: Fix tests (These test were never running due to the vitest workspace config)
import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock";
import type { Request, Response } from "express";
@@ -174,7 +173,7 @@ vi.mock("@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType", () =>
getAllWorkflowsFromEventType: vi.fn().mockResolvedValue([]),
}));
vi.mock("@calcom/lib/server/i18n", () => {
vi.mock("@calcom/i18n/server", () => {
const mockT = (key: string, options?: any) => {
if (key === "event_between_users") {
return `${options?.eventName} between ${options?.host} and ${options?.attendeeName}`;
+1 -1
View File
@@ -14,7 +14,7 @@ import handler from "../../../pages/api/users/_post";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
vi.mock("@calcom/lib/server/i18n", () => {
vi.mock("@calcom/i18n/server", () => {
return {
getTranslation: (key: string) => {
return () => key;
+1 -1
View File
@@ -1 +1 @@
export const TRIGGER_VERSION = '20260209.2';
export const TRIGGER_VERSION = '20260216.1';
+1 -1
View File
@@ -27,7 +27,7 @@ CALENDSO_ENCRYPTION_KEY="Lmqy57DlHKPvwwrtkwjL/8QtxJWxC/OV"
# Service Account Encryption Key for encrypting/decrypting service account keys
CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY="LDBFYLEVICRZBYQZUZCSTZRJFZUKZMEI"
# url to redis instance running in Docker and used to cache data. Redis is setup in apps/api/v2/docker-compose.yaml
REDIS_URL="rediss://localhost:6379"
REDIS_URL="redis://localhost:6379"
# KEEP THIS EMPTY, DISABLE SENTRY CLIENT INSIDE OF LIBRARIES USED BY APIv2
NEXT_PUBLIC_SENTRY_DSN=
-11
View File
@@ -1,11 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require("node:path");
const i18nConfig = require("@calcom/config/next-i18next.config");
/** @type {import("next-i18next").UserConfig} */
const config = {
...i18nConfig,
localePath: path.resolve("../../web/public/static/locales"),
};
module.exports = config;
+1 -1
View File
@@ -62,7 +62,7 @@
"@sentry/node": "9.46.0",
"@sentry/profiling-node": "9.46.0",
"@snyk/protect": "latest",
"axios": "1.13.2",
"axios": "1.13.5",
"body-parser": "1.20.3",
"bull": "4.15.1",
"class-transformer": "0.5.1",
@@ -279,6 +279,7 @@ export class BookingsController_2024_04_15 {
platformCancelUrl: bookingRequest.platformCancelUrl,
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
actionSource: "API_V2",
});
if (!res.onlyRemovedAttendee && res.isPlatformManagedUserBooking) {
void (await this.billingService.cancelUsageByBookingUid(res.bookingUid));
@@ -611,6 +612,10 @@ export class BookingsController_2024_04_15 {
...clone.body,
noEmail: oAuthParams === undefined ? false : !oAuthParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
metadata: {
...(clone.body.metadata || {}),
...(oAuthClientId && { platformClientId: oAuthClientId }),
},
};
if (oAuthClientId) {
await this.setPlatformAttendeesEmails(clone.body, oAuthClientId);
@@ -1,9 +1,12 @@
import { Module } from "@nestjs/common";
import { BookingAttendeesController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/booking-attendees.controller";
import { BookingGuestsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/booking-guests.controller";
import { BookingLocationController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/booking-location.controller";
import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller";
import { BookingPbacGuard } from "@/ee/bookings/2024-08-13/guards/booking-pbac.guard";
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
import { BookingAttendeesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-attendees.service";
import { BookingGuestsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-guests.service";
import { BookingLocationService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-location.service";
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
@@ -12,20 +15,22 @@ import { CalVideoOutputService } from "@/ee/bookings/2024-08-13/services/cal-vid
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { BookingEventHandlerModule } from "@/lib/modules/booking-event-handler.module";
import { InstantBookingModule } from "@/lib/modules/instant-booking.module";
import { RecurringBookingModule } from "@/lib/modules/recurring-booking.module";
import { RegularBookingModule } from "@/lib/modules/regular-booking.module";
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { BillingModule } from "@/modules/billing/billing.module";
@@ -49,8 +54,6 @@ import { TeamsModule } from "@/modules/teams/teams/teams.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
import { BookingEventHandlerModule } from "@/lib/modules/booking-event-handler.module";
@Module({
imports: [
@@ -80,6 +83,7 @@ import { BookingEventHandlerModule } from "@/lib/modules/booking-event-handler.m
OAuthClientRepository,
OAuthClientUsersService,
BookingsService_2024_08_13,
BookingAttendeesService_2024_08_13,
BookingGuestsService_2024_08_13,
InputBookingsService_2024_08_13,
OutputBookingsService_2024_08_13,
@@ -105,9 +109,11 @@ import { BookingEventHandlerModule } from "@/lib/modules/booking-event-handler.m
CalVideoOutputService,
BookingPbacGuard,
BookingLocationService_2024_08_13,
PrismaFeaturesRepository,
],
controllers: [
BookingsController_2024_08_13,
BookingAttendeesController_2024_08_13,
BookingGuestsController_2024_08_13,
BookingLocationController_2024_08_13,
],
@@ -0,0 +1,71 @@
import { BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import { AddAttendeeInput_2024_08_13 } from "@calcom/platform-types";
import { Body, Controller, HttpCode, HttpStatus, Param, Post, UseGuards } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { BookingPbacGuard } from "@/ee/bookings/2024-08-13/guards/booking-pbac.guard";
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
import { AddAttendeeOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/add-attendee.output";
import { BookingAttendeesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-attendees.service";
import { VERSION_2024_08_13, VERSION_2024_08_13_VALUE } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { Throttle } from "@/lib/endpoint-throttler-decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
@Controller({
path: "/v2/bookings/:bookingUid/attendees",
version: VERSION_2024_08_13_VALUE,
})
@UseGuards(PermissionsGuard)
@DocsTags("Bookings / Attendees")
@ApiHeader({
name: "cal-api-version",
description: `Must be set to ${VERSION_2024_08_13}. This header is required as this endpoint does not exist in older API versions.`,
example: VERSION_2024_08_13,
required: true,
})
export class BookingAttendeesController_2024_08_13 {
constructor(private readonly bookingAttendeesService: BookingAttendeesService_2024_08_13) {}
@Post("/")
@HttpCode(HttpStatus.CREATED)
@Permissions([BOOKING_WRITE])
@UseGuards(ApiAuthGuard, BookingUidGuard, BookingPbacGuard)
@Throttle({
limit: 5,
ttl: 60000,
blockDuration: 60000,
name: "booking_attendees_add",
})
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary: "Add an attendee to a booking",
description: `Add a new attendee to an existing booking by its UID.
**Side effects:**
- The booking's attendee list is updated in the database
- The calendar event is updated on connected calendars (Google Calendar, Outlook, etc.) to include the new attendee
- An email notification is sent to the new attendee with the booking details
**Permissions:**
- The authenticated user must be either the booking organizer, an existing attendee, or have the \`booking.update\` permission for the team
<Note>The cal-api-version header is required for this endpoint. Without it, the request will fail with a 404 error.</Note>
`,
})
async addAttendee(
@Param("bookingUid") bookingUid: string,
@Body() body: AddAttendeeInput_2024_08_13,
@GetUser() user: ApiAuthGuardUser
): Promise<AddAttendeeOutput_2024_08_13> {
const attendee = await this.bookingAttendeesService.addAttendee(bookingUid, body, user);
return {
status: SUCCESS_STATUS,
data: attendee,
};
}
}
@@ -0,0 +1,415 @@
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
AttendeeAddGuestsEmail,
AttendeeScheduledEmail,
OrganizerAddGuestsEmail,
} from "@calcom/platform-libraries/emails";
import type { BookingOutput_2024_08_13, CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
import type { Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import request from "supertest";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { mockThrottlerGuard } from "test/utils/withNoThrottler";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { AddAttendeeOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/add-attendee.output";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
const attendeeAddGuestsEmailSpy = jest
.spyOn(AttendeeAddGuestsEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
const organizerAddGuestsEmailSpy = jest
.spyOn(OrganizerAddGuestsEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
const attendeeScheduledEmailSpy = jest
.spyOn(AttendeeScheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
type TestUser = {
user: User;
accessToken: string;
refreshToken: string;
};
type TestSetup = {
organizer: TestUser;
unrelatedUser: TestUser;
eventTypeId: number;
bookingUid: string;
bookingId: number;
};
describe("Bookings Endpoints 2024-08-13 add attendee", () => {
let app: INestApplication;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let bookingsRepositoryFixture: BookingsRepositoryFixture;
let schedulesService: SchedulesService_2024_04_15;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let tokensRepositoryFixture: TokensRepositoryFixture;
let testSetup: TestSetup;
beforeAll(async () => {
mockThrottlerGuard();
const moduleRef = await Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
})
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({
name: `add-attendee-2024-08-13-organization-${randomString()}`,
});
await setupTestData();
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
async function setupTestData() {
const oAuthClient = await createOAuthClient(organization.id, true);
const organizerUser = await userRepositoryFixture.create({
email: `add-attendee-2024-08-13-organizer-${randomString()}@api.com`,
platformOAuthClients: {
connect: { id: oAuthClient.id },
},
});
const unrelatedUserData = await userRepositoryFixture.create({
email: `add-attendee-2024-08-13-unrelated-${randomString()}@api.com`,
platformOAuthClients: {
connect: { id: oAuthClient.id },
},
});
const organizerTokens = await tokensRepositoryFixture.createTokens(organizerUser.id, oAuthClient.id);
const unrelatedUserTokens = await tokensRepositoryFixture.createTokens(
unrelatedUserData.id,
oAuthClient.id
);
const schedule: CreateScheduleInput_2024_04_15 = {
name: `add-attendee-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(organizerUser.id, schedule);
const eventType = await eventTypesRepositoryFixture.create(
{
title: `add-attendee-2024-08-13-event-type-${randomString()}`,
slug: `add-attendee-2024-08-13-event-type-${randomString()}`,
length: 60,
},
organizerUser.id
);
testSetup = {
organizer: {
user: organizerUser,
accessToken: organizerTokens.accessToken,
refreshToken: organizerTokens.refreshToken,
},
unrelatedUser: {
user: unrelatedUserData,
accessToken: unrelatedUserTokens.accessToken,
refreshToken: unrelatedUserTokens.refreshToken,
},
eventTypeId: eventType.id,
bookingUid: "",
bookingId: 0,
};
}
async function createOAuthClient(organizationId: number, emailsEnabled: boolean) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["http://localhost:5555"],
permissions: 32,
areEmailsEnabled: emailsEnabled,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
beforeEach(async () => {
attendeeAddGuestsEmailSpy.mockClear();
organizerAddGuestsEmailSpy.mockClear();
attendeeScheduledEmailSpy.mockClear();
});
describe("POST /v2/bookings/:bookingUid/attendees", () => {
beforeAll(async () => {
const createBookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 10, 13, 0, 0)).toISOString(),
eventTypeId: testSetup.eventTypeId,
attendee: {
name: "Original Attendee",
email: "original.attendee@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
const createBookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(createBookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const createBookingResponseBody: CreateBookingOutput_2024_08_13 = createBookingResponse.body;
expect(createBookingResponseBody.status).toEqual(SUCCESS_STATUS);
if (!responseDataIsBooking(createBookingResponseBody.data)) {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
testSetup.bookingUid = createBookingResponseBody.data.uid;
testSetup.bookingId = createBookingResponseBody.data.id;
});
describe("Authentication", () => {
it("should return 401 when adding attendee without authentication", async () => {
const addAttendeeBody = {
email: "unauthenticated.attendee@example.com",
name: "Unauthenticated Attendee",
timeZone: "Europe/London",
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings/${testSetup.bookingUid}/attendees`)
.send(addAttendeeBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(response.status).toBe(401);
});
});
describe("Authorization", () => {
it("should allow booking organizer to add an attendee", async () => {
const addAttendeeBody = {
email: "new.attendee@example.com",
name: "New Attendee",
timeZone: "Europe/London",
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings/${testSetup.bookingUid}/attendees`)
.send(addAttendeeBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
.expect(201);
const responseBody: AddAttendeeOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.email).toEqual(addAttendeeBody.email);
expect(responseBody.data.name).toEqual(addAttendeeBody.name);
expect(responseBody.data.timeZone).toEqual(addAttendeeBody.timeZone);
expect(responseBody.data.bookingId).toEqual(testSetup.bookingId);
expect(responseBody.data.id).toBeDefined();
});
it("should return 403 when unrelated user tries to add attendee", async () => {
const addAttendeeBody = {
email: "unauthorized.attendee@example.com",
name: "Unauthorized Attendee",
timeZone: "America/New_York",
};
await request(app.getHttpServer())
.post(`/v2/bookings/${testSetup.bookingUid}/attendees`)
.send(addAttendeeBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.unrelatedUser.accessToken}`)
.expect(403);
});
});
describe("Response format", () => {
it("should return attendee with correct structure", async () => {
const addAttendeeBody = {
email: "structure.test@example.com",
name: "Structure Test Attendee",
timeZone: "Asia/Tokyo",
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings/${testSetup.bookingUid}/attendees`)
.send(addAttendeeBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
.expect(201);
const responseBody: AddAttendeeOutput_2024_08_13 = response.body;
expect(responseBody).toHaveProperty("status", SUCCESS_STATUS);
expect(responseBody).toHaveProperty("data");
expect(responseBody.data).toHaveProperty("id");
expect(responseBody.data).toHaveProperty("bookingId");
expect(responseBody.data).toHaveProperty("name");
expect(responseBody.data).toHaveProperty("email");
expect(responseBody.data).toHaveProperty("timeZone");
expect(typeof responseBody.data.id).toBe("number");
expect(typeof responseBody.data.bookingId).toBe("number");
expect(typeof responseBody.data.name).toBe("string");
expect(typeof responseBody.data.email).toBe("string");
expect(typeof responseBody.data.timeZone).toBe("string");
});
});
describe("Validation", () => {
it("should return 400 when email is missing", async () => {
const addAttendeeBody = {
name: "No Email Attendee",
timeZone: "Europe/London",
};
await request(app.getHttpServer())
.post(`/v2/bookings/${testSetup.bookingUid}/attendees`)
.send(addAttendeeBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
.expect(400);
});
it("should return 400 when email is invalid", async () => {
const addAttendeeBody = {
email: "invalid-email",
name: "Invalid Email Attendee",
timeZone: "Europe/London",
};
await request(app.getHttpServer())
.post(`/v2/bookings/${testSetup.bookingUid}/attendees`)
.send(addAttendeeBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
.expect(400);
});
it("should return 400 when name is missing", async () => {
const addAttendeeBody = {
email: "no.name@example.com",
timeZone: "Europe/London",
};
await request(app.getHttpServer())
.post(`/v2/bookings/${testSetup.bookingUid}/attendees`)
.send(addAttendeeBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
.expect(400);
});
it("should return 400 when timeZone is missing", async () => {
const addAttendeeBody = {
email: "no.timezone@example.com",
name: "No TimeZone Attendee",
};
await request(app.getHttpServer())
.post(`/v2/bookings/${testSetup.bookingUid}/attendees`)
.send(addAttendeeBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
.expect(400);
});
it("should return 400 when timeZone is invalid", async () => {
const addAttendeeBody = {
email: "invalid.timezone@example.com",
name: "Invalid TimeZone Attendee",
timeZone: "Invalid/TimeZone",
};
await request(app.getHttpServer())
.post(`/v2/bookings/${testSetup.bookingUid}/attendees`)
.send(addAttendeeBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
.expect(400);
});
it("should return 403 when booking does not exist", async () => {
const addAttendeeBody = {
email: "nonexistent.booking@example.com",
name: "Nonexistent Booking Attendee",
timeZone: "Europe/London",
};
await request(app.getHttpServer())
.post(`/v2/bookings/non-existent-booking-uid/attendees`)
.send(addAttendeeBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
.expect(403);
});
});
});
afterAll(async () => {
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(testSetup.organizer.user.email);
await userRepositoryFixture.deleteByEmail(testSetup.unrelatedUser.user.email);
await bookingsRepositoryFixture.deleteAllBookings(
testSetup.organizer.user.id,
testSetup.organizer.user.email
);
await app.close();
});
function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data !== null && "id" in data;
}
});
@@ -1053,7 +1053,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(400);
expect(response.body.message).toEqual("Cancellation reason is required when you are the host");
expect(response.body.message).toEqual("Cancellation reason is required");
});
it("should cancel seated booking", async () => {
@@ -515,7 +515,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(updatedAtDate?.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
expect(updatedAtDate?.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
expect(data.metadata).toEqual(body.metadata);
expect(data.metadata).toEqual({...body.metadata, platformClientId: oAuthClient.id});
createdBooking = data;
} else {
throw new Error(
@@ -2115,7 +2115,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(updatedAtDate?.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
expect(updatedAtDate?.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
expect(data.metadata).toEqual(body.metadata);
expect(data.metadata).toEqual({...body.metadata, platformClientId: oAuthClient.id});
createdBooking = data;
} else {
throw new Error(
@@ -2662,16 +2662,22 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
describe("calendar events", () => {
// Type assertion to access private methods for test spying
const eventManagerProto = EventManager.prototype as unknown as Record<
"createAllCalendarEvents" | "createAllCRMEvents",
(...args: unknown[]) => Promise<unknown[]>
>;
beforeEach(() => {
jest.restoreAllMocks();
jest
.spyOn(EventManager.prototype, "create")
.mockImplementation(() => Promise.resolve({ results: [], referencesToCreate: [] }));
jest
.spyOn(EventManager.prototype, "createAllCalendarEvents")
.spyOn(eventManagerProto, "createAllCalendarEvents")
.mockImplementation(() => Promise.resolve([]));
jest
.spyOn(EventManager.prototype, "createAllCRMEvents")
.spyOn(eventManagerProto, "createAllCRMEvents")
.mockImplementation(() => Promise.resolve([]));
});
@@ -2843,8 +2849,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(EventManager.prototype.createAllCalendarEvents).toHaveBeenCalledTimes(0);
expect(EventManager.prototype.createAllCRMEvents).toHaveBeenCalledTimes(0);
expect(eventManagerProto.createAllCalendarEvents).toHaveBeenCalledTimes(0);
expect(eventManagerProto.createAllCRMEvents).toHaveBeenCalledTimes(0);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
@@ -2874,8 +2880,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(EventManager.prototype.createAllCalendarEvents).toHaveBeenCalledTimes(0);
expect(EventManager.prototype.createAllCRMEvents).toHaveBeenCalledTimes(0);
expect(eventManagerProto.createAllCalendarEvents).toHaveBeenCalledTimes(0);
expect(eventManagerProto.createAllCRMEvents).toHaveBeenCalledTimes(0);
});
});
@@ -2909,8 +2915,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(EventManager.prototype.createAllCalendarEvents).toHaveBeenCalledTimes(0);
expect(EventManager.prototype.createAllCRMEvents).toHaveBeenCalledTimes(0);
expect(eventManagerProto.createAllCalendarEvents).toHaveBeenCalledTimes(0);
expect(eventManagerProto.createAllCRMEvents).toHaveBeenCalledTimes(0);
bookingThatRequiresConfirmationUid = data.uid;
} else {
throw new Error(
@@ -2935,8 +2941,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(EventManager.prototype.createAllCalendarEvents).toHaveBeenCalledTimes(0);
expect(EventManager.prototype.createAllCRMEvents).toHaveBeenCalledTimes(0);
expect(eventManagerProto.createAllCalendarEvents).toHaveBeenCalledTimes(0);
expect(eventManagerProto.createAllCRMEvents).toHaveBeenCalledTimes(0);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
@@ -0,0 +1,28 @@
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
import { BookingAttendee } from "@calcom/platform-types";
import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsEnum, IsNumber, ValidateNested } from "class-validator";
export class BookingAttendeeOutput_2024_08_13 extends BookingAttendee {
@ApiProperty({ type: Number, example: 251 })
@IsNumber()
@Expose()
id!: number;
@ApiProperty({ type: Number, example: 313 })
@IsNumber()
@Expose()
bookingId!: number;
}
export class AddAttendeeOutput_2024_08_13 {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({ type: BookingAttendeeOutput_2024_08_13 })
@ValidateNested()
@Type(() => BookingAttendeeOutput_2024_08_13)
data!: BookingAttendeeOutput_2024_08_13;
}
@@ -0,0 +1,76 @@
import { BookingAttendeesService } from "@calcom/platform-libraries/bookings";
import type { AddAttendeeInput_2024_08_13 } from "@calcom/platform-types";
import { HttpException, Injectable, NotFoundException } from "@nestjs/common";
import { plainToClass } from "class-transformer";
import { BookingAttendeeOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/add-attendee.output";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import type { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
@Injectable()
export class BookingAttendeesService_2024_08_13 {
private bookingAttendeesService: BookingAttendeesService;
constructor(
private readonly bookingsRepository: BookingsRepository_2024_08_13,
private readonly platformBookingsService: PlatformBookingsService
) {
this.bookingAttendeesService = new BookingAttendeesService();
}
async addAttendee(
bookingUid: string,
input: AddAttendeeInput_2024_08_13,
user: ApiAuthGuardUser
): Promise<BookingAttendeeOutput_2024_08_13> {
const booking = await this.bookingsRepository.getByUidWithEventType(bookingUid);
if (!booking) {
throw new NotFoundException(`Booking with uid ${bookingUid} not found`);
}
const platformClientParams = booking.eventTypeId
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
const createdAttendee = await this.bookingAttendeesService.addAttendee({
bookingId: booking.id,
attendee: {
email: input.email,
name: input.name,
timeZone: input.timeZone,
phoneNumber: input.phoneNumber,
language: input.language,
},
user: {
id: user.id,
email: user.email,
organizationId: user.organizationId,
uuid: user.uuid,
},
emailsEnabled,
actionSource: "API_V2",
});
return plainToClass(
BookingAttendeeOutput_2024_08_13,
{
id: createdAttendee.id,
bookingId: createdAttendee.bookingId,
name: createdAttendee.name,
email: createdAttendee.email,
displayEmail: this.getDisplayEmail(createdAttendee.email),
timeZone: createdAttendee.timeZone,
language: createdAttendee.locale ?? undefined,
absent: false,
phoneNumber: createdAttendee.phoneNumber ?? undefined,
},
{ strategy: "excludeAll" }
);
}
private getDisplayEmail(email: string): string {
return email.replace(/\+[a-zA-Z0-9]{25}/, "");
}
}
@@ -10,6 +10,7 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/reposito
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service";
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service";
@@ -26,7 +27,8 @@ export class BookingLocationService_2024_08_13 {
private readonly inputService: InputBookingsService_2024_08_13,
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly eventTypeAccessService: EventTypeAccessService,
private readonly bookingEventHandlerService: BookingEventHandlerService
private readonly bookingEventHandlerService: BookingEventHandlerService,
private readonly featuresRepository: PrismaFeaturesRepository
) {}
async updateBookingLocation(
@@ -99,10 +101,15 @@ export class BookingLocationService_2024_08_13 {
responses: updatedBookingResponses,
});
const organizationId = existingBookingHost.organizationId ?? null;
const isBookingAuditEnabled = organizationId
? await this.featuresRepository.checkIfTeamHasFeature(organizationId, "booking-audit")
: false;
await this.bookingEventHandlerService.onLocationChanged({
bookingUid: existingBooking.uid,
actor: makeUserActor(user.uuid),
organizationId: existingBookingHost.organizationId ?? null,
organizationId,
source: "API_V2",
auditData: {
location: {
@@ -110,6 +117,7 @@ export class BookingLocationService_2024_08_13 {
new: bookingLocation,
},
},
isBookingAuditEnabled,
});
return this.bookingsService.getBooking(updatedBooking.uid, user);
@@ -190,7 +190,10 @@ export class InputBookingsService_2024_08_13 {
eventTypeId: inputBooking.eventTypeId,
timeZone: inputBooking.attendee.timeZone,
language: inputBooking.attendee.language || "en",
metadata: inputBooking.metadata || {},
metadata: {
...(inputBooking.metadata || {}),
...(platformClientId && { platformClientId }),
},
hasHashedBookingLink: false,
guests,
verificationCode: inputBooking.emailVerificationCode,
@@ -7,8 +7,10 @@ import { Scope } from "@nestjs/common";
import { BookingAuditProducerService } from "@/lib/services/booking-audit-producer.service";
import { HashedLinkService } from "@/lib/services/hashed-link.service";
import { TaskerService } from "@/lib/services/tasker.service";
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
import { PrismaWorkerModule } from "@/modules/prisma/prisma-worker.module";
@Module({
imports: [RegularBookingModule],
imports: [RegularBookingModule, PrismaWorkerModule],
providers: [
RecurringBookingService,
BookingEventHandlerService,
@@ -24,6 +26,8 @@ import { TaskerService } from "@/lib/services/tasker.service";
BookingAuditProducerService,
TaskerService,
/** Required by BookingEventHandlerService - Ends **/
/** Required by RecurringBookingService **/
PrismaFeaturesRepository,
],
exports: [RecurringBookingService],
})
@@ -21,6 +21,10 @@ import { TaskerService } from "@/lib/services/tasker.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { Module, Scope } from "@nestjs/common";
import { getWebhookProducer } from "@calcom/platform-libraries/bookings";
import { WEBHOOK_PRODUCER } from "./regular-booking.tokens";
@Module({
imports: [PrismaModule],
providers: [
@@ -37,6 +41,10 @@ import { Module, Scope } from "@nestjs/common";
},
scope: Scope.TRANSIENT,
},
{
provide: WEBHOOK_PRODUCER,
useFactory: () => getWebhookProducer(),
},
BookingAuditProducerService,
BookingEventHandlerService,
CheckBookingAndDurationLimitsService,
@@ -51,6 +59,6 @@ import { Module, Scope } from "@nestjs/common";
TaskerService,
RegularBookingService,
],
exports: [RegularBookingService],
exports: [RegularBookingService, WEBHOOK_PRODUCER],
})
export class RegularBookingModule {}
@@ -0,0 +1,8 @@
/**
* Injection token for IWebhookProducerService.
* Used to bridge the Cal.com DI container into NestJS.
*
* Defined in a separate file to avoid a circular import between
* regular-booking.module.ts and regular-booking.service.ts.
*/
export const WEBHOOK_PRODUCER = "WEBHOOK_PRODUCER";
@@ -1,5 +1,6 @@
import { RegularBookingService } from "@/lib/services/regular-booking.service";
import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service";
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
import { Injectable } from "@nestjs/common";
import { RecurringBookingService as BaseRecurringBookingService } from "@calcom/platform-libraries/bookings";
@@ -8,11 +9,13 @@ import { RecurringBookingService as BaseRecurringBookingService } from "@calcom/
export class RecurringBookingService extends BaseRecurringBookingService {
constructor(
regularBookingService: RegularBookingService,
bookingEventHandler: BookingEventHandlerService
bookingEventHandler: BookingEventHandlerService,
featuresRepository: PrismaFeaturesRepository
) {
super({
regularBookingService,
bookingEventHandler,
featuresRepository,
});
}
}
@@ -1,3 +1,10 @@
import {
RegularBookingService as BaseRegularBookingService,
type IWebhookProducerService,
} from "@calcom/platform-libraries/bookings";
import type { PrismaClient } from "@calcom/prisma";
import { Inject, Injectable } from "@nestjs/common";
import { WEBHOOK_PRODUCER } from "@/lib/modules/regular-booking.tokens";
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
@@ -7,10 +14,6 @@ import { HashedLinkService } from "@/lib/services/hashed-link.service";
import { LuckyUserService } from "@/lib/services/lucky-user.service";
import { BookingEmailAndSmsTasker } from "@/lib/services/tasker/booking-emails-sms-tasker.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { RegularBookingService as BaseRegularBookingService } from "@calcom/platform-libraries/bookings";
import type { PrismaClient } from "@calcom/prisma";
@Injectable()
export class RegularBookingService extends BaseRegularBookingService {
@@ -23,7 +26,8 @@ export class RegularBookingService extends BaseRegularBookingService {
userRepository: PrismaUserRepository,
bookingEmailAndSmsTasker: BookingEmailAndSmsTasker,
featuresRepository: PrismaFeaturesRepository,
bookingEventHandler: BookingEventHandlerService
bookingEventHandler: BookingEventHandlerService,
@Inject(WEBHOOK_PRODUCER) webhookProducer: IWebhookProducerService
) {
super({
checkBookingAndDurationLimitsService,
@@ -35,6 +39,7 @@ export class RegularBookingService extends BaseRegularBookingService {
bookingEmailAndSmsTasker,
featuresRepository,
bookingEventHandler,
webhookProducer,
});
}
}
@@ -1,7 +1,6 @@
import { getTasker, type Tasker } from "@calcom/platform-libraries/tasker";
import { Injectable } from "@nestjs/common";
import { getTasker, type Tasker } from "@calcom/platform-libraries";
@Injectable()
export class TaskerService {
private readonly tasker: Tasker;
@@ -14,5 +13,3 @@ export class TaskerService {
return this.tasker;
}
}
@@ -1,11 +1,12 @@
import { generateSecret } from "@calcom/platform-libraries";
import type { Membership, Team, User } from "@calcom/prisma/client";
import { AccessScope, OAuthClientType } from "@calcom/prisma/enums";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test, TestingModule } from "@nestjs/testing";
import { getToken } from "next-auth/jwt";
import { AccessScope, OAuthClientStatus, OAuthClientType } from "@calcom/prisma/enums";
import type { INestApplication } from "@nestjs/common";
import type { NestExpressApplication } from "@nestjs/platform-express";
import type { TestingModule } from "@nestjs/testing";
import { Test } from "@nestjs/testing";
import request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuth2ClientRepositoryFixture } from "test/fixtures/repository/oauth2-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
@@ -21,20 +22,16 @@ import { AuthModule } from "@/modules/auth/auth.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
// Mock next-auth/jwt getToken for ApiAuthStrategy NEXT_AUTH authentication
// Mock next-auth/jwt getToken so the NextAuth fallback in ApiAuthStrategy doesn't crash in tests
jest.mock("next-auth/jwt", () => ({
getToken: jest.fn(),
getToken: jest.fn().mockResolvedValue(null),
}));
const mockGetToken = getToken as jest.MockedFunction<typeof getToken>;
describe("OAuth2 Controller Endpoints", () => {
describe("User Not Authenticated", () => {
let appWithoutAuth: INestApplication;
beforeAll(async () => {
// Mock getToken to return null for unauthenticated tests
mockGetToken.mockResolvedValue(null);
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter, ZodExceptionFilter],
imports: [AppModule, UsersModule, AuthModule, PrismaModule],
@@ -69,20 +66,23 @@ describe("OAuth2 Controller Endpoints", () => {
});
});
describe("User Authenticated", () => {
describe("User Authenticated (non-owner)", () => {
let app: INestApplication;
let moduleRef: TestingModule;
let userRepositoryFixture: UserRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let membershipRepositoryFixture: MembershipRepositoryFixture;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let oAuthClientFixture: OAuth2ClientRepositoryFixture;
let oAuthService: OAuthService;
let user: User;
let clientOwner: User;
let authenticatedUser: User;
let team: Team;
let membership: Membership;
let oAuthClient: { clientId: string };
let apiKeyString: string;
let refreshToken: string;
const testClientId = `test-oauth-client-${randomString()}`;
@@ -96,7 +96,7 @@ describe("OAuth2 Controller Endpoints", () => {
): Promise<string> {
const result = await oAuthService.generateAuthorizationCode(
testClientId,
user.id,
authenticatedUser.id,
testRedirectUri,
scopes,
undefined,
@@ -107,12 +107,6 @@ describe("OAuth2 Controller Endpoints", () => {
}
beforeAll(async () => {
const userEmail = `oauth2-e2e-user-${randomString()}@api.com`;
// Mock getToken to return the user email for authenticated tests
// This is needed because ApiAuthGuard uses ApiAuthStrategy which calls getToken from next-auth/jwt
mockGetToken.mockResolvedValue({ email: userEmail });
moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter, ZodExceptionFilter],
imports: [AppModule, UsersModule, AuthModule, PrismaModule],
@@ -125,20 +119,28 @@ describe("OAuth2 Controller Endpoints", () => {
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
oAuthClientFixture = new OAuth2ClientRepositoryFixture(moduleRef);
oAuthService = moduleRef.get(OAuthService);
user = await userRepositoryFixture.create({
email: userEmail,
clientOwner = await userRepositoryFixture.create({
email: `oauth2-e2e-owner-${randomString()}@api.com`,
});
authenticatedUser = await userRepositoryFixture.create({
email: `oauth2-e2e-user-${randomString()}@api.com`,
});
const { keyString } = await apiKeysRepositoryFixture.createApiKey(authenticatedUser.id, null);
apiKeyString = keyString;
team = await teamRepositoryFixture.create({
name: `oauth2-e2e-team-${randomString()}`,
slug: `oauth2-e2e-team-${randomString()}`,
});
membership = await membershipRepositoryFixture.create({
user: { connect: { id: user.id } },
user: { connect: { id: authenticatedUser.id } },
team: { connect: { id: team.id } },
role: "OWNER",
accepted: true,
@@ -151,6 +153,7 @@ describe("OAuth2 Controller Endpoints", () => {
redirectUri: testRedirectUri,
clientSecret: hashedSecret,
clientType: OAuthClientType.CONFIDENTIAL,
userId: clientOwner.id,
});
});
@@ -159,6 +162,7 @@ describe("OAuth2 Controller Endpoints", () => {
it("should return OAuth client info for valid client ID", async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/auth/oauth2/clients/${testClientId}`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(200);
expect(response.body.status).toBe("success");
@@ -174,6 +178,7 @@ describe("OAuth2 Controller Endpoints", () => {
it("should return 404 for non-existent client ID", async () => {
await request(app.getHttpServer())
.get("/api/v2/auth/oauth2/clients/non-existent-client-id")
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(404);
});
});
@@ -434,7 +439,238 @@ describe("OAuth2 Controller Endpoints", () => {
await oAuthClientFixture.delete(oAuthClient.clientId);
await membershipRepositoryFixture.delete(membership.id);
await teamRepositoryFixture.delete(team.id);
await userRepositoryFixture.delete(user.id);
await userRepositoryFixture.delete(authenticatedUser.id);
await userRepositoryFixture.delete(clientOwner.id);
await app.close();
});
});
describe("Owner can use non-approved OAuth client", () => {
let app: INestApplication;
let moduleRef: TestingModule;
let userRepositoryFixture: UserRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let membershipRepositoryFixture: MembershipRepositoryFixture;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let oAuthClientFixture: OAuth2ClientRepositoryFixture;
let oAuthService: OAuthService;
let owner: User;
let team: Team;
let membership: Membership;
let pendingClient: { clientId: string };
let rejectedClient: { clientId: string };
let apiKeyString: string;
const pendingClientId = `test-pending-client-${randomString()}`;
const rejectedClientId = `test-rejected-client-${randomString()}`;
const testClientSecret = "test-secret-456";
const testRedirectUri = "https://example.com/callback";
/** Generate a user-scoped authorization code (no teamSlug) so the owner's userId is in the token. */
async function generateAuthCodeForClient(
clientId: string,
scopes: AccessScope[] = [AccessScope.READ_BOOKING]
): Promise<string> {
const result = await oAuthService.generateAuthorizationCode(
clientId,
owner.id,
testRedirectUri,
scopes
);
const redirectUrl = new URL(result.redirectUrl);
return redirectUrl.searchParams.get("code") as string;
}
beforeAll(async () => {
moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter, ZodExceptionFilter],
imports: [AppModule, UsersModule, AuthModule, PrismaModule],
}).compile();
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
oAuthClientFixture = new OAuth2ClientRepositoryFixture(moduleRef);
oAuthService = moduleRef.get(OAuthService);
owner = await userRepositoryFixture.create({
email: `oauth2-owner-e2e-${randomString()}@api.com`,
});
const { keyString } = await apiKeysRepositoryFixture.createApiKey(owner.id, null);
apiKeyString = keyString;
team = await teamRepositoryFixture.create({
name: `oauth2-owner-team-${randomString()}`,
slug: `oauth2-owner-team-${randomString()}`,
});
membership = await membershipRepositoryFixture.create({
user: { connect: { id: owner.id } },
team: { connect: { id: team.id } },
role: "OWNER",
accepted: true,
});
const [hashedSecret] = generateSecret(testClientSecret);
pendingClient = await oAuthClientFixture.create({
clientId: pendingClientId,
name: "Pending OAuth Client",
redirectUri: testRedirectUri,
clientSecret: hashedSecret,
clientType: OAuthClientType.CONFIDENTIAL,
status: OAuthClientStatus.PENDING,
userId: owner.id,
});
rejectedClient = await oAuthClientFixture.create({
clientId: rejectedClientId,
name: "Rejected OAuth Client",
redirectUri: testRedirectUri,
clientSecret: hashedSecret,
clientType: OAuthClientType.CONFIDENTIAL,
status: OAuthClientStatus.REJECTED,
userId: owner.id,
});
});
it("should return PENDING client info when authenticated as owner", async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/auth/oauth2/clients/${pendingClientId}`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(200);
expect(response.body.status).toBe("success");
expect(response.body.data.client_id).toBe(pendingClientId);
expect(response.body.data.name).toBe("Pending OAuth Client");
});
it("should exchange authorization code for tokens with PENDING client owned by user", async () => {
const code = await generateAuthCodeForClient(pendingClientId);
const response = await request(app.getHttpServer())
.post("/api/v2/auth/oauth2/token")
.type("form")
.send({
client_id: pendingClientId,
grant_type: "authorization_code",
code,
client_secret: testClientSecret,
redirect_uri: testRedirectUri,
})
.expect(200);
expect(response.body.access_token).toBeDefined();
expect(response.body.refresh_token).toBeDefined();
expect(response.body.token_type).toBe("bearer");
});
it("should refresh tokens with PENDING client owned by user", async () => {
const code = await generateAuthCodeForClient(pendingClientId);
const tokenResponse = await request(app.getHttpServer())
.post("/api/v2/auth/oauth2/token")
.type("form")
.send({
client_id: pendingClientId,
grant_type: "authorization_code",
code,
client_secret: testClientSecret,
redirect_uri: testRedirectUri,
})
.expect(200);
const response = await request(app.getHttpServer())
.post("/api/v2/auth/oauth2/token")
.type("form")
.send({
client_id: pendingClientId,
grant_type: "refresh_token",
refresh_token: tokenResponse.body.refresh_token,
client_secret: testClientSecret,
})
.expect(200);
expect(response.body.access_token).toBeDefined();
expect(response.body.refresh_token).toBeDefined();
expect(response.body.token_type).toBe("bearer");
});
it("should reject authorization code generation for REJECTED client even as owner", async () => {
await expect(generateAuthCodeForClient(rejectedClientId)).rejects.toThrow();
});
it("should reject token exchange when client becomes rejected", async () => {
const code = await generateAuthCodeForClient(pendingClientId);
await oAuthClientFixture.updateStatus(pendingClientId, OAuthClientStatus.REJECTED);
const response = await request(app.getHttpServer())
.post("/api/v2/auth/oauth2/token")
.type("form")
.send({
client_id: pendingClientId,
grant_type: "authorization_code",
code,
client_secret: testClientSecret,
redirect_uri: testRedirectUri,
})
.expect(401);
expect(response.body.error).toBe("unauthorized_client");
expect(response.body.error_description).toBe("client_rejected");
await oAuthClientFixture.updateStatus(pendingClientId, OAuthClientStatus.PENDING);
});
it("should reject token refresh when client becomes rejected", async () => {
const code = await generateAuthCodeForClient(pendingClientId);
const tokenResponse = await request(app.getHttpServer())
.post("/api/v2/auth/oauth2/token")
.type("form")
.send({
client_id: pendingClientId,
grant_type: "authorization_code",
code,
client_secret: testClientSecret,
redirect_uri: testRedirectUri,
})
.expect(200);
await oAuthClientFixture.updateStatus(pendingClientId, OAuthClientStatus.REJECTED);
const response = await request(app.getHttpServer())
.post("/api/v2/auth/oauth2/token")
.type("form")
.send({
client_id: pendingClientId,
grant_type: "refresh_token",
refresh_token: tokenResponse.body.refresh_token,
client_secret: testClientSecret,
})
.expect(401);
expect(response.body.error).toBe("unauthorized_client");
expect(response.body.error_description).toBe("client_rejected");
await oAuthClientFixture.updateStatus(pendingClientId, OAuthClientStatus.PENDING);
});
afterAll(async () => {
await oAuthClientFixture.delete(pendingClient.clientId);
await oAuthClientFixture.delete(rejectedClient.clientId);
await membershipRepositoryFixture.delete(membership.id);
await teamRepositoryFixture.delete(team.id);
await userRepositoryFixture.delete(owner.id);
await app.close();
});
});
@@ -16,15 +16,16 @@ export class OAuth2HttpExceptionFilter implements ExceptionFilter<OAuth2HttpExce
const requestId = request.headers["x-request-id"] ?? "unknown-request-id";
response.setHeader("X-Request-Id", requestId.toString());
const userContext = extractUserContext(request);
const { userEmail: _userEmail, ...safeUserContext } = extractUserContext(request);
const { Authorization: _auth, ...safeHeaders } = filterReqHeaders(request.headers);
this.logger.error(`OAuth2 Http Exception: ${exception.oAuthErrorData.error}`, {
exception,
body: "[REDACTED]",
headers: filterReqHeaders(request.headers),
headers: safeHeaders,
url: request.url,
method: request.method,
requestId,
...userContext,
...safeUserContext,
});
response.status(exception.getStatus()).json(exception.oAuthErrorData);
@@ -26,7 +26,7 @@ import {
Patch,
ParseIntPipe,
} from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -84,6 +84,7 @@ export class EventTypeWebhooksController {
@Get("/:webhookId")
@ApiOperation({ summary: "Get a webhook" })
@ApiParam({ name: "webhookId", type: String, required: true })
async getEventTypeWebhook(@GetWebhook() webhook: Webhook): Promise<EventTypeWebhookOutputResponseDto> {
return {
status: SUCCESS_STATUS,
@@ -116,6 +117,7 @@ export class EventTypeWebhooksController {
@Delete("/:webhookId")
@ApiOperation({ summary: "Delete a webhook" })
@ApiParam({ name: "webhookId", type: String, required: true })
async deleteEventTypeWebhook(@GetWebhook() webhook: Webhook): Promise<EventTypeWebhookOutputResponseDto> {
await this.webhooksService.deleteWebhook(webhook.id);
return {
@@ -3,7 +3,7 @@ import { MembershipRole } from "@calcom/platform-libraries";
import { SkipTakePagination } from "@calcom/platform-types";
import type { Webhook } from "@calcom/prisma/client";
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
@@ -94,6 +94,7 @@ export class OAuthClientWebhooksController {
summary: "Get a webhook",
description: `<Warning>These endpoints are deprecated and will be removed in the future.</Warning>`,
})
@ApiParam({ name: "webhookId", type: String, required: true })
@UseGuards(IsOAuthClientWebhookGuard)
async getOAuthClientWebhook(@GetWebhook() webhook: Webhook): Promise<OAuthClientWebhookOutputResponseDto> {
return {
@@ -135,6 +136,7 @@ export class OAuthClientWebhooksController {
summary: "Delete a webhook",
description: `<Warning>These endpoints are deprecated and will be removed in the future.</Warning>`,
})
@ApiParam({ name: "webhookId", type: String, required: true })
@UseGuards(IsOAuthClientWebhookGuard)
async deleteOAuthClientWebhook(
@GetWebhook() webhook: Webhook
@@ -515,7 +515,7 @@ describe("Slots 2024-04-15 Endpoints", () => {
it("should do a booking for seated event and slot should show attendees count and bookingUid", async () => {
const startTime = "2050-09-05T11:00:00.000Z";
const booking = await bookingsRepositoryFixture.create({
uid: `booking-uid-${seatedEventTypeId}`,
uid: `booking-uid-seated-${seatedEventTypeId}-${randomString()}`,
title: "booking title",
startTime,
endTime: "2050-09-05T12:00:00.000Z",
@@ -549,7 +549,7 @@ describe("Slots 2024-04-15 Endpoints", () => {
});
bookingSeatsRepositoryFixture.create({
referenceUid: "100",
referenceUid: `seat-${randomString()}`,
data: {},
booking: {
connect: {
@@ -598,7 +598,7 @@ describe("Slots 2024-04-15 Endpoints", () => {
it("should do a booking for seated event and slot should show attendees count and bookingUid in range format", async () => {
const startTime = "2050-09-05T11:00:00.000Z";
const booking = await bookingsRepositoryFixture.create({
uid: `booking-uid-${seatedEventTypeId}`,
uid: `booking-uid-seated-${seatedEventTypeId}-${randomString()}`,
title: "booking title",
startTime,
endTime: "2050-09-05T12:00:00.000Z",
@@ -632,7 +632,7 @@ describe("Slots 2024-04-15 Endpoints", () => {
});
bookingSeatsRepositoryFixture.create({
referenceUid: "100",
referenceUid: `seat-${randomString()}`,
data: {},
booking: {
connect: {
@@ -832,7 +832,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
it("should do a booking for seated event and slot should show attendees count and bookingUid", async () => {
const startTime = "2050-09-05T11:00:00.000Z";
const booking = await bookingsRepositoryFixture.create({
uid: `booking-uid-${seatedEventType.id}`,
uid: `booking-uid-seated-${seatedEventType.id}-${randomString()}`,
title: "booking title",
startTime,
endTime: "2050-09-05T12:00:00.000Z",
@@ -866,7 +866,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
});
bookingSeatsRepositoryFixture.create({
referenceUid: "100",
referenceUid: `seat-${randomString()}`,
data: {},
booking: {
connect: {
@@ -961,7 +961,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
it("should do a booking for seated event and slot should show attendees count and bookingUid and return range format", async () => {
const startTime = "2050-09-05T11:00:00.000Z";
const booking = await bookingsRepositoryFixture.create({
uid: `booking-uid-${seatedEventType.id}`,
uid: `booking-uid-seated-range-${seatedEventType.id}-${randomString()}`,
title: "booking title",
startTime,
endTime: "2050-09-05T12:00:00.000Z",
@@ -995,7 +995,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
});
bookingSeatsRepositoryFixture.create({
referenceUid: "100",
referenceUid: `seat-${randomString()}`,
data: {},
booking: {
connect: {
@@ -1348,7 +1348,8 @@ describe("Slots 2024-09-04 Endpoints", () => {
advanceTo(newDate);
const slotDuration = 60;
const slotStartTime = "2050-09-05T10:00:00.000Z";
// Use 2050-09-06 to avoid overlap with other tests that reserve 2050-09-05
const slotStartTime = "2050-09-06T09:00:00.000Z";
const reserveResponse = await request(app.getHttpServer())
.post(`/v2/slots/reservations`)
.send({
@@ -1377,7 +1378,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
const response = await request(app.getHttpServer())
.get(
`/v2/slots?eventTypeId=${variableLengthEventType.id}&start=2050-09-05&end=2050-09-09&duration=60`
`/v2/slots?eventTypeId=${variableLengthEventType.id}&start=2050-09-05&end=2050-09-10&duration=60`
)
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
.expect(200);
@@ -1390,10 +1391,10 @@ describe("Slots 2024-09-04 Endpoints", () => {
const days = Object.keys(slots);
expect(days.length).toEqual(5);
const expectedSlotsUTC2050_09_05 = expectedSlotsUTC["2050-09-05"].filter(
const expectedSlotsUTC2050_09_06 = expectedSlotsUTC["2050-09-06"].filter(
(slot) => slot.start !== slotStartTime
);
expect(slots).toEqual({ ...expectedSlotsUTC, "2050-09-05": expectedSlotsUTC2050_09_05 });
expect(slots).toEqual({ ...expectedSlotsUTC, "2050-09-06": expectedSlotsUTC2050_09_06 });
const dbSlot = await selectedSlotRepositoryFixture.getByUid(
responseReservedVariableSlot.reservationUid
@@ -1409,7 +1410,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
});
it("request slot contains already existing reserved slot", async () => {
// Try to reserve 9:45-12:45 when 10:00-11:00 is taken
// Try to reserve 8:45-11:45 when 09:00-10:00 on 2050-09-06 is taken
const newSlotStart = DateTime.fromISO(responseReservedVariableSlot.slotStart)
.minus({ minutes: 15 })
.toISO();
@@ -13,7 +13,7 @@ import {
Query,
UseGuards,
} from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_HEADER } from "@/lib/docs/headers";
@@ -87,6 +87,7 @@ export class TeamsEventTypesWebhooksController {
@Get("/:webhookId")
@ApiOperation({ summary: "Get a webhook for a team event type" })
@ApiParam({ name: "webhookId", type: String, required: true })
@Roles("TEAM_MEMBER")
async getTeamEventTypeWebhook(@GetWebhook() webhook: Webhook): Promise<EventTypeWebhookOutputResponseDto> {
return {
@@ -122,6 +123,7 @@ export class TeamsEventTypesWebhooksController {
@Delete("/:webhookId")
@Roles("TEAM_ADMIN")
@ApiOperation({ summary: "Delete a webhook for a team event type" })
@ApiParam({ name: "webhookId", type: String, required: true })
async deleteTeamEventTypeWebhook(
@GetWebhook() webhook: Webhook
): Promise<EventTypeWebhookOutputResponseDto> {
@@ -16,7 +16,7 @@ import { WebhookOutputPipe } from "@/modules/webhooks/pipes/WebhookOutputPipe";
import { UserWebhooksService } from "@/modules/webhooks/services/user-webhooks.service";
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
import { Controller, Post, Body, UseGuards, Get, Param, Query, Delete, Patch } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -75,6 +75,7 @@ export class WebhooksController {
@Get("/:webhookId")
@ApiOperation({ summary: "Get a webhook" })
@ApiParam({ name: "webhookId", type: String, required: true })
@UseGuards(IsUserWebhookGuard)
async getWebhook(@GetWebhook() webhook: Webhook): Promise<UserWebhookOutputResponseDto> {
return {
@@ -114,7 +114,11 @@ export class WorkflowsRepository {
) {
await updateWorkflow({
ctx: {
user: { ...user, locale: user?.locale ?? "en" },
user: {
...user,
locale: user?.locale ?? "en",
organizationId: user.profiles?.[0]?.organization?.id ?? null,
},
prisma: this.dbWrite.prisma as unknown as PrismaClient,
},
input: data,
@@ -132,7 +136,11 @@ export class WorkflowsRepository {
) {
await updateWorkflow({
ctx: {
user: { ...user, locale: user?.locale ?? "en" },
user: {
...user,
locale: user?.locale ?? "en",
organizationId: user.profiles?.[0]?.organization?.id ?? null,
},
prisma: this.dbWrite.prisma as unknown as PrismaClient,
},
input: data,
@@ -21,6 +21,7 @@ export class OAuth2ClientRepositoryFixture {
status?: OAuthClientStatus;
logo?: string;
isTrusted?: boolean;
userId?: number;
}) {
return this.prismaWriteClient.oAuthClient.create({
data: {
@@ -32,10 +33,18 @@ export class OAuth2ClientRepositoryFixture {
status: data.status || OAuthClientStatus.APPROVED,
logo: data.logo,
isTrusted: data.isTrusted || false,
...(data.userId && { user: { connect: { id: data.userId } } }),
},
});
}
async updateStatus(clientId: string, status: OAuthClientStatus) {
return this.prismaWriteClient.oAuthClient.update({
where: { clientId },
data: { status },
});
}
async delete(clientId: string) {
return this.prismaWriteClient.oAuthClient.delete({
where: { clientId },
+1 -1
View File
@@ -1 +1 @@
export const TRIGGER_VERSION = '20260209.2';
export const TRIGGER_VERSION = '20260216.1';
@@ -3,7 +3,7 @@ import withEmbedSsrAppDir from "app/WithEmbedSSR";
import type { PageProps as ServerPageProps } from "app/_types";
import { cookies, headers } from "next/headers";
import { loadTranslations } from "@calcom/lib/server/i18n";
import { loadTranslations } from "@calcom/i18n/server";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
@@ -5,7 +5,7 @@ import { generateMeetingMetadata } from "app/_utils";
import { headers, cookies } from "next/headers";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { loadTranslations } from "@calcom/lib/server/i18n";
import { loadTranslations } from "@calcom/i18n/server";
import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx";
@@ -3,7 +3,7 @@ import withEmbedSsrAppDir from "app/WithEmbedSSR";
import type { PageProps as ServerPageProps } from "app/_types";
import { cookies, headers } from "next/headers";
import { loadTranslations } from "@calcom/lib/server/i18n";
import { loadTranslations } from "@calcom/i18n/server";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
@@ -5,7 +5,7 @@ import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { loadTranslations } from "@calcom/lib/server/i18n";
import { loadTranslations } from "@calcom/i18n/server";
import { BookingStatus } from "@calcom/prisma/enums";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
@@ -3,7 +3,7 @@ import withEmbedSsrAppDir from "app/WithEmbedSSR";
import type { PageProps as ServerPageProps } from "app/_types";
import { cookies, headers } from "next/headers";
import { loadTranslations } from "@calcom/lib/server/i18n";
import { loadTranslations } from "@calcom/i18n/server";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
@@ -5,7 +5,7 @@ import { generateMeetingMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { loadTranslations } from "@calcom/lib/server/i18n";
import { loadTranslations } from "@calcom/i18n/server";
import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
@@ -3,7 +3,7 @@ import withEmbedSsrAppDir from "app/WithEmbedSSR";
import type { PageProps as ServerPageProps } from "app/_types";
import { cookies, headers } from "next/headers";
import { loadTranslations } from "@calcom/lib/server/i18n";
import { loadTranslations } from "@calcom/i18n/server";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
@@ -6,7 +6,7 @@ import { cookies, headers } from "next/headers";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { loadTranslations } from "@calcom/lib/server/i18n";
import { loadTranslations } from "@calcom/i18n/server";
import { prisma } from "@calcom/prisma";
import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx";
@@ -11,7 +11,7 @@ import { getOrgFullOrigin, orgDomainConfig } from "@calcom/features/ee/organizat
import { getOrganizationSEOSettings } from "@calcom/features/ee/organizations/lib/orgSettings";
import type { TeamData } from "@calcom/features/ee/teams/lib/getTeamData";
import { shouldHideBrandingForTeamEvent } from "@calcom/features/profile/lib/hideBranding";
import { loadTranslations } from "@calcom/lib/server/i18n";
import { loadTranslations } from "@calcom/i18n/server";
import slugify from "@calcom/lib/slugify";
import { BookingStatus, RedirectType } from "@calcom/prisma/enums";
@@ -1,6 +1,6 @@
// Added as a separate route for now to ease the testing of the audit logs feature
// It partially matches the figma design - https://www.figma.com/design/wleA2SR6rn60EK7ORxAfMy/Cal.com-New-Features?node-id=5641-6732&p=f
// TOOD: Move it to the booking page side bar later
// TOOD: Move it to the booking page side bar later
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
import type { PageProps } from "app/_types";
import { _generateMetadata, getTranslate } from "app/_utils";
@@ -14,35 +14,34 @@ import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { BookingHistoryPage } from "@calcom/web/modules/booking-audit/components/BookingHistoryPage";
export const generateMetadata = async ({ params }: { params: Promise<{ uid: string }> }) =>
await _generateMetadata(
(t) => t("booking_history"),
(t) => t("booking_history_description"),
undefined,
undefined,
`/booking/${(await params).uid}/logs`
);
await _generateMetadata(
(t) => t("booking_history"),
(t) => t("booking_history_description"),
undefined,
undefined,
`/booking/${(await params).uid}/logs`
);
const Page = async ({ params }: PageProps) => {
const resolvedParams = await params;
const bookingUid = resolvedParams.uid;
const resolvedParams = await params;
const bookingUid = resolvedParams.uid;
if (!bookingUid || typeof bookingUid !== "string") {
redirect("/bookings/upcoming");
}
if (!bookingUid || typeof bookingUid !== "string") {
redirect("/bookings/upcoming");
}
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
if (!session?.user?.id) {
redirect("/auth/login");
}
if (!session?.user?.id) {
redirect("/auth/login");
}
return (
<ShellMainAppDir heading={t("booking_history")} subtitle={t("booking_history_description")}>
<BookingHistoryPage bookingUid={bookingUid} />
</ShellMainAppDir>
);
return (
<ShellMainAppDir heading={t("booking_history")} subtitle={t("booking_history_description")}>
<BookingHistoryPage bookingUid={bookingUid} />
</ShellMainAppDir>
);
};
export default Page;
@@ -5,11 +5,7 @@ import type { RouterOutputs } from "@calcom/trpc/react";
import { eventTypesRouter } from "@calcom/trpc/server/routers/viewer/eventTypes/_router";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { createRouterCaller, getTRPCContext } from "app/_trpc/context";
import type {
PageProps,
ReadonlyHeaders,
ReadonlyRequestCookies,
} from "app/_types";
import type { PageProps, ReadonlyHeaders, ReadonlyRequestCookies } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { unstable_cache } from "next/cache";
import { cookies, headers } from "next/headers";
@@ -26,26 +22,25 @@ const getCachedEventGroups: (
userIds?: number[] | undefined;
upIds?: string[] | undefined;
}
) => Promise<RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"]> =
unstable_cache(
async (
headers: ReadonlyHeaders,
cookies: ReadonlyRequestCookies,
filters?: {
teamIds?: number[] | undefined;
userIds?: number[] | undefined;
upIds?: string[] | undefined;
}
): Promise<RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"]> => {
const eventTypesCaller = await createRouterCaller(
eventTypesRouter,
await getTRPCContext(headers, cookies)
);
return await eventTypesCaller.getUserEventGroups({ filters });
},
["viewer.eventTypes.getUserEventGroups"],
{ revalidate: 3600 } // seconds
);
) => Promise<RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"]> = unstable_cache(
async (
headers: ReadonlyHeaders,
cookies: ReadonlyRequestCookies,
filters?: {
teamIds?: number[] | undefined;
userIds?: number[] | undefined;
upIds?: string[] | undefined;
}
): Promise<RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"]> => {
const eventTypesCaller = await createRouterCaller(
eventTypesRouter,
await getTRPCContext(headers, cookies)
);
return await eventTypesCaller.getUserEventGroups({ filters });
},
["viewer.eventTypes.getUserEventGroups"],
{ revalidate: 3600 } // seconds
);
const Page = async ({ searchParams }: PageProps): Promise<ReactElement> => {
const _searchParams = await searchParams;
@@ -71,23 +66,12 @@ const Page = async ({ searchParams }: PageProps): Promise<ReactElement> => {
}
const filters = getTeamsFiltersFromQuery(_searchParams);
const userEventGroupsData = await getCachedEventGroups(
_headers,
_cookies,
filters
);
const userEventGroupsData = await getCachedEventGroups(_headers, _cookies, filters);
return (
<EventTypesWrapper
userEventGroupsData={userEventGroupsData}
user={session.user}
/>
);
return <EventTypesWrapper userEventGroupsData={userEventGroupsData} user={session.user} />;
};
export const generateMetadata = async (): Promise<
ReturnType<typeof _generateMetadata>
> =>
export const generateMetadata = async (): Promise<ReturnType<typeof _generateMetadata>> =>
await _generateMetadata(
(t) => t("event_types_page_title"),
(t) => t("event_types_page_subtitle"),
@@ -15,7 +15,7 @@ export const TeamsCTA = () => {
size="sm"
type="button"
onClick={() => {
posthog.capture("add_team_button_clicked")
posthog.capture("add_team_button_clicked");
}}
href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}>
{t("new")}
@@ -38,12 +38,15 @@ const ServerPage = async () => {
const categoryQuery = appStore.map(({ categories }) => ({
categories: categories || [],
}));
const categories = categoryQuery.reduce((c, app) => {
for (const category of app.categories) {
c[category] = c[category] ? c[category] + 1 : 1;
}
return c;
}, {} as Record<string, number>);
const categories = categoryQuery.reduce(
(c, app) => {
for (const category of app.categories) {
c[category] = c[category] ? c[category] + 1 : 1;
}
return c;
},
{} as Record<string, number>
);
const props = {
categories: Object.entries(categories)
@@ -2,6 +2,7 @@
import { FieldTypes } from "@calcom/app-store/routing-forms/lib/FieldTypes";
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
import { LearnMoreLink } from "@calcom/features/eventtypes/components/LearnMoreLink";
import { getFieldIdentifier } from "@calcom/features/form-builder/utils/getFieldIdentifier";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@calcom/ui/classNames";
@@ -14,10 +15,10 @@ import {
SelectField,
TextField,
} from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { Tooltip } from "@calcom/ui/components/tooltip";
import type { getServerSidePropsForSingleFormView as getServerSideProps } from "@calcom/web/lib/apps/routing-forms/[...pages]/getServerSidePropsSingleForm";
import SingleForm from "@components/apps/routing-forms/SingleForm";
import { ChevronDownIcon, MenuIcon } from "@coss/ui/icons";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { UseFormReturn } from "react-hook-form";
@@ -93,8 +94,7 @@ function Field({
}
: null
}
deleteField={router ? null : deleteField}
>
deleteField={router ? null : deleteField}>
<FormCardBody>
<div className="mb-3 w-full">
<TextField
@@ -114,17 +114,12 @@ function Field({
hookForm.setValue(`${hookFieldNamespace}.label`, newLabel, {
shouldDirty: true,
});
const currentIdentifier = hookForm.getValues(
`${hookFieldNamespace}.identifier`
);
const currentIdentifier = hookForm.getValues(`${hookFieldNamespace}.identifier`);
// Only auto-update identifier if it was auto-generated from the previous label
// This preserves manual identifier changes
const isIdentifierGeneratedFromPreviousLabel =
currentIdentifier === getFieldIdentifier(previousLabel).toLowerCase();
if (
!currentIdentifier ||
isIdentifierGeneratedFromPreviousLabel
) {
if (!currentIdentifier || isIdentifierGeneratedFromPreviousLabel) {
hookForm.setValue(
`${hookFieldNamespace}.identifier`,
getFieldIdentifier(newLabel).toLowerCase(),
@@ -138,23 +133,21 @@ function Field({
<TextField
disabled={!!router}
label={t("identifier_url_parameter")}
hint={t("identifier_url_parameter_hint")}
hint={
<LearnMoreLink
t={t}
i18nKey="identifier_url_parameter_hint"
href="https://cal.com/help/routing/connect-routing-form-to-booking-questions"
/>
}
name={`${hookFieldNamespace}.identifier`}
required
placeholder={t("identifies_name_field")}
value={
identifier ||
routerField?.identifier ||
label ||
routerField?.label ||
""
}
value={identifier || routerField?.identifier || label || routerField?.label || ""}
onChange={(e) => {
hookForm.setValue(
`${hookFieldNamespace}.identifier`,
e.target.value.toLowerCase(),
{ shouldDirty: true }
);
hookForm.setValue(`${hookFieldNamespace}.identifier`, e.target.value.toLowerCase(), {
shouldDirty: true,
});
}}
/>
</div>
@@ -164,9 +157,7 @@ function Field({
control={hookForm.control}
defaultValue={routerField?.type}
render={({ field: { value, onChange } }) => {
const defaultValue = FieldTypes.find(
(fieldType) => fieldType.value === value
);
const defaultValue = FieldTypes.find((fieldType) => fieldType.value === value);
if (disableTypeChange) {
return (
<div className="data-testid-field-type">
@@ -179,15 +170,9 @@ function Field({
className={classNames(
"h-8 w-full justify-between text-left text-sm",
!!router && "bg-subtle cursor-not-allowed"
)}
>
<span className="text-default">
{defaultValue?.label || "Select field type"}
</span>
<Icon
name="chevron-down"
className="text-default h-4 w-4"
/>
)}>
<span className="text-default">{defaultValue?.label || t("select_field_type")}</span>
<ChevronDownIcon className="text-default h-4 w-4" />
</Button>
</Tooltip>
</div>
@@ -304,9 +289,7 @@ const FormEdit = ({
<div className="w-full py-4 lg:py-8">
<div ref={animationRef} className="flex w-full flex-col rounded-md">
{hookFormFields.map((field, key) => {
const existingField = Boolean(
(form.fields || []).find((f) => f.id === field.id)
);
const existingField = Boolean((form.fields || []).find((f) => f.id === field.id));
const hasFormResponses = (form._count?.responses ?? 0) > 0;
return (
<Field
@@ -343,13 +326,7 @@ const FormEdit = ({
</div>
{hookFormFields.length ? (
<div className={classNames("flex")}>
<Button
data-testid="add-field"
type="button"
StartIcon="plus"
color="secondary"
onClick={addField}
>
<Button data-testid="add-field" type="button" StartIcon="plus" color="secondary" onClick={addField}>
Add question
</Button>
</div>
@@ -363,7 +340,7 @@ const FormEdit = ({
{/* Icon card - Top */}
<div className="bg-default border-subtle z-30 col-start-1 col-end-1 row-start-1 row-end-1 h-10 w-10 transform rounded-md border shadow-sm">
<div className="text-emphasis flex h-full items-center justify-center">
<Icon name="menu" className="text-emphasis h-4 w-4" />
<MenuIcon className="text-emphasis h-4 w-4" />
</div>
</div>
{/* Left fanned card */}
@@ -389,12 +366,7 @@ const FormEdit = ({
Fields are the form fields that the booker would see.
</p>
</div>
<Button
data-testid="add-field"
onClick={addField}
StartIcon="plus"
className="mt-6"
>
<Button data-testid="add-field" onClick={addField} StartIcon="plus" className="mt-6">
Add question
</Button>
</div>
@@ -414,9 +386,7 @@ export default function FormEditPage({
{...props}
appUrl={appUrl}
permissions={permissions}
Page={({ hookForm, form }) => (
<FormEdit appUrl={appUrl} hookForm={hookForm} form={form} />
)}
Page={({ hookForm, form }) => <FormEdit appUrl={appUrl} hookForm={hookForm} form={form} />}
/>
</>
);
@@ -1,10 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import type z from "zod";
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
import { WhenToWriteToRecord, SalesforceFieldType } from "@calcom/app-store/salesforce/lib/enums";
import { SalesforceFieldType, WhenToWriteToRecord } from "@calcom/app-store/salesforce/lib/enums";
import type { writeToRecordDataSchema as salesforceWriteToRecordDataSchema } from "@calcom/app-store/salesforce/zod";
import { routingFormIncompleteBookingDataSchema as salesforceRoutingFormIncompleteBookingDataSchema } from "@calcom/app-store/salesforce/zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -12,15 +9,13 @@ import { IncompleteBookingActionType } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Button } from "@calcom/ui/components/button";
import { Switch } from "@calcom/ui/components/form";
import { InputField } from "@calcom/ui/components/form";
import { Select } from "@calcom/ui/components/form";
import { Label } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { InputField, Label, Select, Switch } from "@calcom/ui/components/form";
import { showToast } from "@calcom/ui/components/toast";
import type { getServerSidePropsForSingleFormView as getServerSideProps } from "@calcom/web/lib/apps/routing-forms/[...pages]/getServerSidePropsSingleForm";
import SingleForm from "@components/apps/routing-forms/SingleForm";
import { GlobeIcon } from "@coss/ui/icons";
import { useEffect, useState } from "react";
import type z from "zod";
function Page({ form }: { form: RoutingFormWithResponseCount }) {
const { t } = useLocale();
@@ -88,8 +83,8 @@ function Page({ form }: { form: RoutingFormWithResponseCount }) {
setSelectedCredential(
credentialOptions
? credentialOptions.find((option) => option.value === salesforceAction?.credentialId) ??
selectedCredential
? (credentialOptions.find((option) => option.value === salesforceAction?.credentialId) ??
selectedCredential)
: selectedCredential
);
}
@@ -105,7 +100,7 @@ function Page({ form }: { form: RoutingFormWithResponseCount }) {
<div className="flex items-center justify-between">
<div className="flex items-center gap-0.5">
<div className="border-subtle rounded-lg border p-1">
<Icon name="globe" className="text-subtle h-4 w-4" />
<GlobeIcon className="text-subtle h-4 w-4" />
</div>
<div className="flex flex-col">
<span className="text-emphasis ml-2 text-sm font-medium">
@@ -128,17 +123,17 @@ function Page({ form }: { form: RoutingFormWithResponseCount }) {
<div className="bg-default mt-2 rounded-xl px-2 py-2">
<Label>Credential to use</Label>
<Select
size="sm"
options={credentialOptions}
value={selectedCredential}
onChange={(option) => {
if (!option) {
return;
}
setSelectedCredential(option);
}}
/>
</div>
size="sm"
options={credentialOptions}
value={selectedCredential}
onChange={(option) => {
if (!option) {
return;
}
setSelectedCredential(option);
}}
/>
</div>
</>
)}
@@ -1,43 +1,34 @@
"use client";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import Link from "next/link";
import React, { useCallback, useState, useEffect } from "react";
import { Query, Builder, Utils as QbUtils } from "react-awesome-query-builder";
import type { ImmutableTree, BuilderProps, Config } from "react-awesome-query-builder";
import type { JsonTree } from "react-awesome-query-builder";
import type { UseFormReturn } from "react-hook-form";
import { Toaster } from "sonner";
import { buildEmptyQueryValue } from "@calcom/app-store/_utils/raqb/raqbUtils.client";
import { raqbQueryValueUtils } from "@calcom/app-store/_utils/raqb/raqbUtils.server";
import { routingFormAppComponents } from "@calcom/app-store/routing-forms/appComponents";
import DynamicAppComponent from "@calcom/app-store/routing-forms/components/DynamicAppComponent";
import { EmptyState } from "@calcom/app-store/routing-forms/components/_components/EmptyState";
import { RoutingSkeleton } from "@calcom/app-store/routing-forms/components/_components/RoutingSkeleton";
import DynamicAppComponent from "@calcom/app-store/routing-forms/components/DynamicAppComponent";
import {
withRaqbSettingsAndWidgets,
ConfigFor,
withRaqbSettingsAndWidgets,
} from "@calcom/app-store/routing-forms/components/react-awesome-query-builder/config/uiConfig";
import { RoutingPages } from "@calcom/app-store/routing-forms/lib/RoutingPages";
import { createFallbackRoute } from "@calcom/app-store/routing-forms/lib/createFallbackRoute";
import getEventTypeAppMetadata from "@calcom/app-store/routing-forms/lib/getEventTypeAppMetadata";
import {
getQueryBuilderConfigForFormFields,
getQueryBuilderConfigForAttributes,
type FormFieldsQueryBuilderConfigWithRaqbFields,
type AttributesQueryBuilderConfigWithRaqbFields,
type FormFieldsQueryBuilderConfigWithRaqbFields,
getQueryBuilderConfigForAttributes,
getQueryBuilderConfigForFormFields,
isDynamicOperandField,
} from "@calcom/app-store/routing-forms/lib/getQueryBuilderConfig";
import isRouter from "@calcom/app-store/routing-forms/lib/isRouter";
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
import { RoutingPages } from "@calcom/app-store/routing-forms/lib/RoutingPages";
import type {
Attribute,
AttributeRoutingConfig,
EditFormRoute,
GlobalRoute,
LocalRoute,
RoutingFormWithResponseCount,
SerializableRoute,
Attribute,
EditFormRoute,
AttributeRoutingConfig,
} from "@calcom/app-store/routing-forms/types/types";
import { RouteActionType } from "@calcom/app-store/routing-forms/zod";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
@@ -52,15 +43,25 @@ import classNames from "@calcom/ui/classNames";
import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { FormCard } from "@calcom/ui/components/card";
import { SelectWithValidation as Select, TextArea } from "@calcom/ui/components/form";
import { TextField } from "@calcom/ui/components/form";
import { SelectField } from "@calcom/ui/components/form";
import { Switch } from "@calcom/ui/components/form";
import {
SelectWithValidation as Select,
SelectField,
Switch,
TextArea,
TextField,
} from "@calcom/ui/components/form";
import type { IconName } from "@calcom/ui/components/icon";
import { Icon } from "@calcom/ui/components/icon";
import type { getServerSidePropsForSingleFormView as getServerSideProps } from "@calcom/web/lib/apps/routing-forms/[...pages]/getServerSidePropsSingleForm";
import SingleForm from "@components/apps/routing-forms/SingleForm";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import Link from "next/link";
import type React from "react";
import { useCallback, useEffect, useState } from "react";
import type { BuilderProps, Config, ImmutableTree, JsonTree } from "react-awesome-query-builder";
import { Builder, Utils as QbUtils, Query } from "react-awesome-query-builder";
import type { UseFormReturn } from "react-hook-form";
import { Toaster } from "sonner";
type Form = inferSSRProps<typeof getServerSideProps>["form"];
@@ -342,6 +343,165 @@ const WeightedAttributesSelector = ({
) : null;
};
type RouteActionSelectorProps = {
action: LocalRoute["action"] | undefined;
onActionChange: (action: LocalRoute["action"]) => void;
onEventTypeChange?: (action: LocalRoute["action"]) => void;
eventTypeOptions: { label: string; value: string; eventTypeId: number }[];
selectedEventTypeOption:
| { label: string; value: string; eventTypeId: number }
| { label: string; value: "custom"; eventTypeId: 0 }
| undefined;
disabled?: boolean;
className?: string;
showCustomEventTypeInput?: boolean;
customEventTypeSlug?: string;
onCustomEventTypeSlugChange?: (slug: string) => void;
eventTypePrefix?: string;
fieldIdentifiers?: string[];
};
const RouteActionSelector = ({
action,
onActionChange,
onEventTypeChange,
eventTypeOptions,
selectedEventTypeOption,
disabled = false,
className = "",
showCustomEventTypeInput = false,
customEventTypeSlug = "",
onCustomEventTypeSlugChange,
eventTypePrefix = "",
fieldIdentifiers = [],
}: RouteActionSelectorProps) => {
const { t } = useLocale();
return (
<div className={classNames("flex w-full flex-col gap-2 text-sm lg:flex-row", className)}>
<div className="flex grow items-center gap-2">
<Select
size="sm"
isDisabled={disabled}
className="data-testid-select-routing-action block w-full grow"
required
value={RoutingPages.find((page) => page.value === action?.type)}
onChange={(item) => {
if (!item) {
return;
}
const newAction: LocalRoute["action"] = {
type: item.value,
value: "",
};
if (newAction.type === "customPageMessage") {
newAction.value = t("default_custom_page_message");
} else {
newAction.value = "";
}
onActionChange(newAction);
}}
options={RoutingPages}
/>
</div>
{action?.type ? (
action?.type === "customPageMessage" ? (
<TextArea
required
disabled={disabled}
name="customPageMessage"
className="border-default flex grow lg:w-fit"
style={{
minHeight: "38px",
}}
value={action.value}
onChange={(e) => {
onActionChange({ ...action, value: e.target.value });
}}
/>
) : action?.type === "externalRedirectUrl" ? (
<TextField
size="sm"
disabled={disabled}
name="externalRedirectUrl"
className="border-default flex grow text-sm"
containerClassName="grow"
type="url"
required
labelSrOnly
value={action.value}
onChange={(e) => {
onActionChange({ ...action, value: e.target.value });
}}
placeholder="https://example.com"
/>
) : (
<div className="grow">
<Select
size="sm"
required
className="data-testid-eventTypeRedirectUrl-select"
isDisabled={disabled}
options={eventTypeOptions}
onChange={(option) => {
if (!option) {
return;
}
const newAction = {
...action,
value: option.value,
eventTypeId: option.eventTypeId,
};
if (onEventTypeChange) {
onEventTypeChange(newAction);
} else {
onActionChange(newAction);
}
}}
value={selectedEventTypeOption}
/>
{showCustomEventTypeInput &&
eventTypeOptions.length !== 0 &&
action.value !== "" &&
(action.value === "custom" ||
!eventTypeOptions.find((eventOption) => eventOption.value === action.value) ||
customEventTypeSlug.length) ? (
<>
<TextField
disabled={disabled}
className="border-default flex w-full grow text-sm"
containerClassName="grow mt-2"
addOnLeading={eventTypePrefix}
required
value={customEventTypeSlug}
onChange={(e) => {
onCustomEventTypeSlugChange?.(e.target.value);
onActionChange({
...action,
value: `${eventTypePrefix}${e.target.value}`,
});
}}
placeholder="event-url"
/>
<div className="mt-2 ">
<p className="text-subtle text-xs">
{fieldIdentifiers.length
? t("field_identifiers_as_variables_with_example", {
variable: `{${fieldIdentifiers[0]}}`,
})
: t("field_identifiers_as_variables")}
</p>
</div>
</>
) : null}
</div>
)
) : null}
</div>
);
};
const Route = ({
form,
route,
@@ -395,10 +555,20 @@ const Route = ({
? eventOptions[0].value.substring(0, eventOptions[0].value.lastIndexOf("/") + 1)
: "";
const getCustomSlug = (actionValue: string | undefined) => {
if (!actionValue) return "";
const isCustom = !eventOptions.find((eventOption) => eventOption.value === actionValue);
return isCustom ? (actionValue.split("/").pop() ?? "") : "";
};
const [customEventTypeSlug, setCustomEventTypeSlug] = useState<string>(() => {
const isCustom =
!isRouter(route) && !eventOptions.find((eventOption) => eventOption.value === route.action.value);
return isCustom && !isRouter(route) ? route.action.value.split("/").pop() ?? "" : "";
if (isRouter(route)) return "";
return getCustomSlug(route.action.value);
});
const [customFallbackEventTypeSlug, setCustomFallbackEventTypeSlug] = useState<string>(() => {
if (isRouter(route)) return "";
return getCustomSlug(route.fallbackAction?.value);
});
useEnsureEventTypeIdInRedirectUrlAction({
@@ -558,7 +728,7 @@ const Route = ({
const attributesQueryBuilder =
// team member attributes are only available for organization teams
route.action?.type === RouteActionType.EventTypeRedirectUrl && isTeamForm && isOrganization ? (
<div className="mt-4">
<div>
{/* TODO: */}
{eventTypeRedirectUrlSelectedOption?.eventTypeAppMetadata &&
"salesforce" in eventTypeRedirectUrlSelectedOption.eventTypeAppMetadata ? (
@@ -573,13 +743,13 @@ const Route = ({
</div>
) : null}
<div className="bg-default border-subtle cal-query-builder-container mt-2 rounded-2xl border p-2">
<div className="cal-query-builder-container mt-4" data-testid="attributes-query-builder">
<div className="ml-2 flex items-center gap-0.5">
<div className="border-subtle rounded-lg border p-1">
<Icon name="user-check" className="text-subtle h-4 w-4" />
</div>
<span className="text-emphasis ml-2 text-sm font-medium">
And connect with specific team members
{t("connect_with_specific_team_members")}
</span>
</div>
{route.attributesQueryBuilderState && attributesQueryBuilderConfigWithRaqbSettingsAndWidgets && (
@@ -600,30 +770,70 @@ const Route = ({
</div>
) : null;
const fallbackAttributesQueryBuilder =
route.action?.type === RouteActionType.EventTypeRedirectUrl && isTeamForm ? (
const fallbackActionOptions = eventTypeRedirectUrlOptions;
const fallbackActionSelectedOption = route.fallbackAction?.eventTypeId
? eventTypeRedirectUrlOptions.find((option) => option.eventTypeId === route.fallbackAction?.eventTypeId)
: eventTypeRedirectUrlOptions.find((option) => option.value === route.fallbackAction?.value);
// Only show fallback section when main action has a valid event type selected
// This prevents showing an empty fallback action selector before user selects an event type
const hasValidEventTypeSelected =
route.action?.type === RouteActionType.EventTypeRedirectUrl &&
(route.action?.eventTypeId || route.action?.value);
const matchingMembersFallbackRoute =
hasValidEventTypeSelected && isTeamForm ? (
<div className="bg-default border-subtle cal-query-builder-container mt-2 rounded-2xl border p-2">
<div className="ml-2 flex items-center gap-0.5">
<div className="border-subtle rounded-lg border p-1">
<Icon name="blocks" className="text-subtle h-4 w-4" />
</div>
<span className="text-emphasis ml-2 text-sm font-medium">Fallback</span>
<span className="text-emphasis ml-2 text-sm font-medium">{t("fallback_action")}</span>
</div>
{route.fallbackAttributesQueryBuilderState &&
attributesQueryBuilderConfigWithRaqbSettingsAndWidgets && (
<Query
{...attributesQueryBuilderConfigWithRaqbSettingsAndWidgets}
value={route.fallbackAttributesQueryBuilderState.tree}
onChange={(immutableTree, attributesQueryBuilderConfig) => {
onChangeFallbackTeamMembersQuery(
route,
immutableTree,
attributesQueryBuilderConfig as unknown as AttributesQueryBuilderConfigWithRaqbFields
);
}}
renderBuilder={renderBuilder}
/>
)}
<div className="bg-cal-muted mt-2 rounded-xl p-2">
<RouteActionSelector
action={route.fallbackAction}
onActionChange={(newAction) => setRoute(route.id, { fallbackAction: newAction })}
eventTypeOptions={fallbackActionOptions}
selectedEventTypeOption={fallbackActionSelectedOption}
disabled={disabled}
showCustomEventTypeInput
customEventTypeSlug={customFallbackEventTypeSlug}
onCustomEventTypeSlugChange={setCustomFallbackEventTypeSlug}
eventTypePrefix={eventTypePrefix}
fieldIdentifiers={fieldIdentifiers}
/>
</div>
{isOrganization && (
<div className="mt-4">
{route.fallbackAction?.type === RouteActionType.EventTypeRedirectUrl &&
route.fallbackAttributesQueryBuilderState &&
attributesQueryBuilderConfigWithRaqbSettingsAndWidgets && (
<div>
<div className="ml-2 flex items-center gap-0.5">
<div className="border-subtle rounded-lg border p-1">
<Icon name="user-check" className="text-subtle h-4 w-4" />
</div>
<span className="text-emphasis ml-2 text-sm font-medium">
{t("connect_with_specific_team_members")}
</span>
</div>
<Query
{...attributesQueryBuilderConfigWithRaqbSettingsAndWidgets}
value={route.fallbackAttributesQueryBuilderState.tree}
onChange={(immutableTree, attributesQueryBuilderConfig) => {
onChangeFallbackTeamMembersQuery(
route,
immutableTree,
attributesQueryBuilderConfig as unknown as AttributesQueryBuilderConfigWithRaqbFields
);
}}
renderBuilder={renderBuilder}
/>
</div>
)}
</div>
)}
</div>
) : null;
@@ -659,283 +869,85 @@ const Route = ({
{formFieldsQueryBuilder}
<div>
{route.isFallback ? (
<div className="flex w-full flex-col gap-2 text-sm lg:flex-row">
<div className="flex grow items-center gap-2">
{/* <div className="flex grow-0 whitespace-nowrap">
<span>{t("send_booker_to")}</span>
</div> */}
<Select
size="sm"
isDisabled={disabled}
className="data-testid-select-routing-action block w-full grow"
required
value={RoutingPages.find((page) => page.value === route.action?.type)}
onChange={(item) => {
if (!item) {
return;
}
const action: LocalRoute["action"] = {
type: item.value,
value: "",
};
if (action.type === "customPageMessage") {
action.value = "We are not ready for you yet :(";
} else {
action.value = "";
}
setRoute(route.id, { action });
}}
options={RoutingPages}
/>
</div>
{route.action?.type ? (
route.action?.type === "customPageMessage" ? (
<TextArea
required
disabled={disabled}
name="customPageMessage"
className="border-default flex grow lg:w-fit"
style={{
minHeight: "38px",
}}
value={route.action.value}
onChange={(e) => {
setRoute(route.id, { action: { ...route.action, value: e.target.value } });
}}
/>
) : route.action?.type === "externalRedirectUrl" ? (
<TextField
disabled={disabled}
name="externalRedirectUrl"
className="border-default flex grow text-sm"
containerClassName="grow"
type="url"
required
labelSrOnly
value={route.action.value}
onChange={(e) => {
setRoute(route.id, { action: { ...route.action, value: e.target.value } });
}}
placeholder="https://example.com"
/>
) : (
<div className="grow">
<Select
size="sm"
required
className="data-testid-eventTypeRedirectUrl-select"
isDisabled={disabled}
options={eventTypeRedirectUrlOptions}
onChange={(option) => {
if (!option) {
return;
}
if (option.value !== "custom") {
setRoute(route.id, {
action: {
...route.action,
value: option.value,
eventTypeId: option.eventTypeId,
},
attributeRoutingConfig: {},
});
setCustomEventTypeSlug("");
} else {
setRoute(route.id, {
action: { ...route.action, value: "custom", eventTypeId: 0 },
attributeRoutingConfig: {},
});
setCustomEventTypeSlug("");
}
}}
value={eventTypeRedirectUrlSelectedOption}
/>
{eventOptions.length !== 0 &&
route.action.value !== "" &&
(!eventOptions.find((eventOption) => eventOption.value === route.action.value) ||
customEventTypeSlug.length) ? (
<>
<TextField
disabled={disabled}
className="border-default flex w-full grow text-sm"
containerClassName="grow mt-2"
addOnLeading={eventTypePrefix}
required
value={customEventTypeSlug}
onChange={(e) => {
setCustomEventTypeSlug(e.target.value);
setRoute(route.id, {
action: { ...route.action, value: `${eventTypePrefix}${e.target.value}` },
});
}}
placeholder="event-url"
/>
<div className="mt-2 ">
<p className="text-subtle text-xs">
{fieldIdentifiers.length
? t("field_identifiers_as_variables_with_example", {
variable: `{${fieldIdentifiers[0]}}`,
})
: t("field_identifiers_as_variables")}
</p>
</div>
</>
) : (
<></>
)}
</div>
)
) : null}
</div>
<RouteActionSelector
action={route.action}
onActionChange={(newAction) => setRoute(route.id, { action: newAction })}
onEventTypeChange={(newAction) => {
// Initialize fallbackAction with the same event type if not already set with a valid event type
// Check if fallbackAction has a valid event type (eventTypeId or non-empty value)
const hasValidFallbackEventType =
route.fallbackAction?.eventTypeId || route.fallbackAction?.value;
const fallbackAction = hasValidFallbackEventType
? route.fallbackAction
: newAction.type === RouteActionType.EventTypeRedirectUrl
? {
type: newAction.type,
value: newAction.value,
eventTypeId: newAction.eventTypeId,
}
: undefined;
setRoute(route.id, { action: newAction, attributeRoutingConfig: {}, fallbackAction });
setCustomEventTypeSlug("");
}}
eventTypeOptions={eventTypeRedirectUrlOptions}
selectedEventTypeOption={eventTypeRedirectUrlSelectedOption}
disabled={disabled}
showCustomEventTypeInput
customEventTypeSlug={customEventTypeSlug}
onCustomEventTypeSlugChange={setCustomEventTypeSlug}
eventTypePrefix={eventTypePrefix}
fieldIdentifiers={fieldIdentifiers}
/>
) : (
<div className="bg-default border-subtle my-3 rounded-xl border p-2">
<div className="mb-2 ml-2 flex items-center gap-0.5">
<div className="border-subtle rounded-lg border p-1">
<Icon name="arrow-right" className="text-subtle h-4 w-4" />
</div>
<span className="text-emphasis ml-2 text-sm font-medium">Send booker to</span>
<span className="text-emphasis ml-2 text-sm font-medium">{t("send_booker_to")}</span>
</div>
<div className="bg-cal-muted flex w-full flex-col gap-2 rounded-xl p-2 text-sm lg:flex-row">
<div className="flex grow items-center gap-2">
<Select
size="sm"
isDisabled={disabled}
className="data-testid-select-routing-action block w-full grow"
required
value={RoutingPages.find((page) => page.value === route.action?.type)}
onChange={(item) => {
if (!item) {
return;
}
const action: LocalRoute["action"] = {
type: item.value,
value: "",
};
if (action.type === "customPageMessage") {
action.value = "We are not ready for you yet :(";
} else {
action.value = "";
}
setRoute(route.id, { action });
}}
options={RoutingPages}
/>
</div>
{route.action?.type ? (
route.action?.type === "customPageMessage" ? (
<TextArea
required
disabled={disabled}
name="customPageMessage"
className="border-default flex grow lg:w-fit"
style={{
minHeight: "38px",
}}
value={route.action.value}
onChange={(e) => {
setRoute(route.id, { action: { ...route.action, value: e.target.value } });
}}
/>
) : route.action?.type === "externalRedirectUrl" ? (
<TextField
size="sm"
disabled={disabled}
name="externalRedirectUrl"
className="border-default flex grow text-sm"
containerClassName="grow"
type="url"
required
labelSrOnly
value={route.action.value}
onChange={(e) => {
setRoute(route.id, { action: { ...route.action, value: e.target.value } });
}}
placeholder="https://example.com"
/>
) : (
<div className="grow">
<Select
size="sm"
required
className="data-testid-eventTypeRedirectUrl-select"
isDisabled={disabled}
options={eventTypeRedirectUrlOptions}
onChange={(option) => {
if (!option) {
return;
<div className="bg-cal-muted rounded-xl p-2">
<RouteActionSelector
action={route.action}
onActionChange={(newAction) => setRoute(route.id, { action: newAction })}
onEventTypeChange={(newAction) => {
// Initialize fallbackAction with the same event type if not already set with a valid event type
// Check if fallbackAction has a valid event type (eventTypeId or non-empty value)
const hasValidFallbackEventType =
route.fallbackAction?.eventTypeId || route.fallbackAction?.value;
const fallbackAction = hasValidFallbackEventType
? route.fallbackAction
: newAction.type === RouteActionType.EventTypeRedirectUrl
? {
type: newAction.type,
value: newAction.value,
eventTypeId: newAction.eventTypeId,
}
if (option.value !== "custom") {
setRoute(route.id, {
action: {
...route.action,
value: option.value,
eventTypeId: option.eventTypeId,
},
attributeRoutingConfig: {},
});
setCustomEventTypeSlug("");
} else {
setRoute(route.id, {
action: { ...route.action, value: "custom", eventTypeId: 0 },
attributeRoutingConfig: {},
});
setCustomEventTypeSlug("");
}
}}
value={eventTypeRedirectUrlSelectedOption}
/>
{eventOptions.length !== 0 &&
route.action.value !== "" &&
(!eventOptions.find((eventOption) => eventOption.value === route.action.value) ||
customEventTypeSlug.length) ? (
<>
<TextField
disabled={disabled}
className="border-default flex w-full grow text-sm"
containerClassName="grow mt-2"
addOnLeading={eventTypePrefix}
required
value={customEventTypeSlug}
onChange={(e) => {
setCustomEventTypeSlug(e.target.value);
setRoute(route.id, {
action: { ...route.action, value: `${eventTypePrefix}${e.target.value}` },
});
}}
placeholder="event-url"
/>
<div className="mt-2 ">
<p className="text-subtle text-xs">
{fieldIdentifiers.length
? t("field_identifiers_as_variables_with_example", {
variable: `{${fieldIdentifiers[0]}}`,
})
: t("field_identifiers_as_variables")}
</p>
</div>
</>
) : (
<></>
)}
</div>
)
) : null}
: undefined;
setRoute(route.id, { action: newAction, attributeRoutingConfig: {}, fallbackAction });
setCustomEventTypeSlug("");
}}
eventTypeOptions={eventTypeRedirectUrlOptions}
selectedEventTypeOption={eventTypeRedirectUrlSelectedOption}
disabled={disabled}
showCustomEventTypeInput
customEventTypeSlug={customEventTypeSlug}
onCustomEventTypeSlugChange={setCustomEventTypeSlug}
eventTypePrefix={eventTypePrefix}
fieldIdentifiers={fieldIdentifiers}
/>
</div>
{attributesQueryBuilder}
</div>
)}
{attributesQueryBuilder}
<WeightedAttributesSelector
attributes={attributes}
route={route}
eventTypeRedirectUrlSelectedOption={eventTypeRedirectUrlSelectedOption}
setRoute={setRoute}
/>
{fallbackAttributesQueryBuilder ? <>{fallbackAttributesQueryBuilder}</> : null}
{matchingMembersFallbackRoute ? <>{matchingMembersFallbackRoute}</> : null}
</div>
</div>
</div>
@@ -952,7 +964,7 @@ const buildState = <
| {
queryValue: AttributesQueryValue;
config: AttributesQueryBuilderConfigWithRaqbFields;
}
},
>({
queryValue,
config,
@@ -1026,6 +1038,16 @@ function useRoutes({
if (!r.fallbackAttributesQueryValue) {
r.fallbackAttributesQueryValue = getEmptyQueryValue() as LocalRoute["fallbackAttributesQueryValue"];
}
// Initialize fallbackAction with the main action for backwards compatibility
// This ensures existing routes show the correct default (same event type as main route)
if (!r.fallbackAction && r.action?.type === RouteActionType.EventTypeRedirectUrl) {
r.fallbackAction = {
type: r.action.type,
value: r.action.value,
eventTypeId: r.action.eventTypeId,
};
}
});
return _routes;
};
@@ -1056,6 +1078,17 @@ function useRoutes({
if (isRouter(route)) {
return route;
}
// Initialize fallbackAction from action if not set and action is EventTypeRedirectUrl
// This handles the case where user creates a new route and changes action to an event type
const fallbackAction =
route.fallbackAction ??
(route.action?.type === RouteActionType.EventTypeRedirectUrl
? {
type: route.action.type,
value: route.action.value,
eventTypeId: route.action.eventTypeId,
}
: undefined);
return {
id: route.id,
name: route.name,
@@ -1066,6 +1099,7 @@ function useRoutes({
attributesQueryValue: route.attributesQueryValue,
fallbackAttributesQueryValue: route.fallbackAttributesQueryValue,
attributeIdForWeights: route.attributeIdForWeights,
fallbackAction,
};
});
}
@@ -1087,10 +1121,21 @@ const useCreateRoute = ({
}) => {
const createRoute = useCallback(() => {
const newEmptyRoute = getEmptyRoute();
// Initialize fallbackAction with the main action for new routes
// This ensures new routes have a valid fallbackAction when saved
const fallbackAction =
newEmptyRoute.action?.type === RouteActionType.EventTypeRedirectUrl
? {
type: newEmptyRoute.action.type,
value: newEmptyRoute.action.value,
eventTypeId: newEmptyRoute.action.eventTypeId,
}
: undefined;
const newRoutes = [
...routes,
{
...newEmptyRoute,
fallbackAction,
formFieldsQueryBuilderState: buildState({
queryValue: newEmptyRoute.queryValue,
config: formFieldsQueryBuilderConfig,
@@ -10,7 +10,7 @@ import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { routingFormsComponents } from "./app-routing.client-config";
const normalizePages = (pages: string[] | string | undefined) => {
const normalizedPages = Array.isArray(pages) ? pages : pages?.split("/") ?? [];
const normalizedPages = Array.isArray(pages) ? pages : (pages?.split("/") ?? []);
return normalizedPages[0];
};
@@ -1,13 +1,7 @@
"use client";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import posthog from "posthog-js";
import { useEffect, useState } from "react";
import { useFormContext } from "react-hook-form";
import { isFallbackRoute } from "@calcom/app-store/routing-forms/lib/isFallbackRoute";
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
import LicenseRequired from "~/ee/common/components/LicenseRequired";
import { FilterResults } from "~/filters/components/FilterResults";
import { TeamsFilter } from "~/filters/components/TeamsFilter";
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
@@ -21,22 +15,33 @@ import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { Icon } from "@calcom/ui/components/icon";
import { List, ListLinkItem } from "@calcom/ui/components/list";
import { Tooltip } from "@calcom/ui/components/tooltip";
import type {
SetNewFormDialogState,
NewFormDialogState,
SetNewFormDialogState,
} from "@calcom/web/components/apps/routing-forms/FormActions";
import {
FormAction,
FormActionsDropdown,
FormActionsProvider,
} from "@calcom/web/components/apps/routing-forms/FormActions";
import {
ChartBarIcon,
CircleCheckIcon,
DownloadIcon,
FileTextIcon,
MailIcon,
ShuffleIcon,
} from "@coss/ui/icons";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import posthog from "posthog-js";
import { useEffect, useState } from "react";
import { useFormContext } from "react-hook-form";
import { useHasPaidPlan } from "~/billing/hooks/useHasPaidPlan";
import SkeletonLoaderTeamList from "~/ee/teams/components/SkeletonloaderTeamList";
import LicenseRequired from "~/ee/common/components/LicenseRequired";
import { CreateButtonWithTeamsList } from "~/ee/teams/components/createButton/CreateButtonWithTeamsList";
import SkeletonLoaderTeamList from "~/ee/teams/components/SkeletonloaderTeamList";
import { ShellMain } from "~/shell/Shell";
import { UpgradeTip } from "~/shell/UpgradeTip";
@@ -92,32 +97,32 @@ export default function RoutingForms({ appUrl }: { appUrl: string }) {
const forms = queryRes.data?.filtered;
const features = [
{
icon: <Icon name="file-text" className="h-5 w-5 text-orange-500" />,
icon: <FileTextIcon className="h-5 w-5 text-orange-500" />,
title: t("create_your_first_form"),
description: t("create_your_first_form_description"),
},
{
icon: <Icon name="shuffle" className="h-5 w-5 text-lime-500" />,
icon: <ShuffleIcon className="h-5 w-5 text-lime-500" />,
title: t("create_your_first_route"),
description: t("route_to_the_right_person"),
},
{
icon: <Icon name="chart-bar" className="h-5 w-5 text-blue-500" />,
icon: <ChartBarIcon className="h-5 w-5 text-blue-500" />,
title: t("reporting"),
description: t("reporting_feature"),
},
{
icon: <Icon name="circle-check" className="h-5 w-5 text-teal-500" />,
icon: <CircleCheckIcon className="h-5 w-5 text-teal-500" />,
title: t("test_routing_form"),
description: t("test_preview_description"),
},
{
icon: <Icon name="mail" className="h-5 w-5 text-yellow-500" />,
icon: <MailIcon className="h-5 w-5 text-yellow-500" />,
title: t("routing_forms_send_email_owner"),
description: t("routing_forms_send_email_owner_description"),
},
{
icon: <Icon name="download" className="h-5 w-5 text-violet-500" />,
icon: <DownloadIcon className="h-5 w-5 text-violet-500" />,
title: t("download_responses"),
description: t("download_responses_description"),
},
@@ -5,7 +5,7 @@ import { z } from "zod";
import { IdentityProvider } from "@calcom/prisma/enums";
import { Button } from "@calcom/ui/components/button";
import { Icon } from "@calcom/ui/components/icon";
import { XIcon } from "@coss/ui/icons";
import AuthContainer from "@components/ui/AuthContainer";
@@ -42,10 +42,10 @@ const ServerPage = async ({ searchParams }: PageProps) => {
provider === IdentityProvider.GOOGLE
? "Google"
: provider === IdentityProvider.CAL
? "Email and Password"
: provider === IdentityProvider.SAML
? "SAML (like Okta)"
: "your original login method";
? "Email and Password"
: provider === IdentityProvider.SAML
? "SAML (like Okta)"
: "your original login method";
return t("account_managed_by_identity_provider_error", { provider: providerName });
} else if (error === "saml-idp-not-authoritative") {
return t("saml_idp_not_authoritative_error");
@@ -61,7 +61,7 @@ const ServerPage = async ({ searchParams }: PageProps) => {
<AuthContainer>
<div>
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
<Icon name="x" className="h-6 w-6 text-red-600" />
<XIcon className="h-6 w-6 text-red-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
@@ -3,14 +3,14 @@
import React, { Suspense } from "react";
import { ErrorBoundary } from "@calcom/ui/components/errorBoundary";
import { Icon } from "@calcom/ui/components/icon";
import { LoaderIcon } from "@coss/ui/icons";
export default function TroubleshooterLayout({ children }: { children: React.ReactNode }) {
return (
<>
<div className="flex flex-1 *:flex-1">
<ErrorBoundary>
<Suspense fallback={<Icon name="loader" />}>{children}</Suspense>
<Suspense fallback={<LoaderIcon />}>{children}</Suspense>
</ErrorBoundary>
</div>
</>
@@ -7,7 +7,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui/components/button";
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
import { Icon } from "@calcom/ui/components/icon";
import { RefreshCcwIcon, UserPlusIcon, UsersIcon } from "@coss/ui/icons";
import { UpgradeTip } from "~/shell/UpgradeTip";
@@ -22,17 +22,17 @@ export default function UpgradeTipWrapper({ children }: { children: React.ReactN
const features = [
{
icon: <Icon name="users" className="h-5 w-5 text-red-500" />,
icon: <UsersIcon className="h-5 w-5 text-red-500" />,
title: t("view_bookings_across"),
description: t("view_bookings_across_description"),
},
{
icon: <Icon name="refresh-ccw" className="h-5 w-5 text-blue-500" />,
icon: <RefreshCcwIcon className="h-5 w-5 text-blue-500" />,
title: t("identify_booking_trends"),
description: t("identify_booking_trends_description"),
},
{
icon: <Icon name="user-plus" className="h-5 w-5 text-green-500" />,
icon: <UserPlusIcon className="h-5 w-5 text-green-500" />,
title: t("spot_popular_event_types"),
description: t("spot_popular_event_types_description"),
},
@@ -0,0 +1,26 @@
import { _generateMetadata } from "app/_utils";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import prisma from "@calcom/prisma";
import InsightsWrongRoutingPage from "~/insights/views/insights-wrong-routing-view";
import { checkInsightsPagePermission } from "../checkInsightsPagePermission";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("wrong_routing"),
(t) => t("wrong_routing_description"),
undefined,
undefined,
"/insights/wrong-routing"
);
export default async function Page() {
const session = await checkInsightsPagePermission();
const userRepository = new UserRepository(prisma);
const user = await userRepository.getTimeZoneAndDefaultScheduleId({ userId: session?.user.id ?? -1 });
return <InsightsWrongRoutingPage timeZone={user?.timeZone ?? "UTC"} />;
}
@@ -27,7 +27,9 @@ const ServerPage = async () => {
// If user has any team membership (pending or accepted), redirect them directly to personal onboarding
// This handles the case where users sign up with an invite token (membership is auto-accepted)
const hasTeamMembership = await MembershipRepository.hasAnyTeamMembershipByUserId({ userId: session.user.id });
const hasTeamMembership = await MembershipRepository.hasAnyTeamMembershipByUserId({
userId: session.user.id,
});
if (hasTeamMembership) {
return redirect("/onboarding/personal/settings");
}
@@ -32,4 +32,3 @@ const ServerPage = async () => {
};
export default ServerPage;
@@ -32,4 +32,3 @@ const ServerPage = async () => {
};
export default ServerPage;
@@ -31,7 +31,9 @@ const ServerPage = async (props: { searchParams: Promise<{ fromTeamOnboarding?:
const searchParams = await props.searchParams;
const fromTeamOnboarding = searchParams?.fromTeamOnboarding === "true";
return <PersonalSettingsView userEmail={userEmail} userName={userName} fromTeamOnboarding={fromTeamOnboarding} />;
return (
<PersonalSettingsView userEmail={userEmail} userName={userName} fromTeamOnboarding={fromTeamOnboarding} />
);
};
export default ServerPage;
@@ -32,4 +32,3 @@ const ServerPage = async () => {
};
export default ServerPage;
@@ -124,7 +124,7 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
<div className="col-span-2 mb-6">
{date.locale(i18n.language ?? "en").format("dddd, DD MMMM YYYY")}
<br />
{date.format(is24h ? "H:mm" : "h:mma")} - {props.eventType.length} mins{" "}
{date.format(is24h ? "H:mm" : "h:mma")} - {props.eventType.length} {t("minute_timeUnit")}{" "}
<span className="text-subtle">({timezone})</span>
</div>
{props.booking.location && (
@@ -42,10 +42,7 @@ const Page = async ({ params }: { params: Params }) => {
const org = await organizationRepository.adminFindById({ id: input.data.id });
const t = await getTranslate();
return (
<SettingsHeader
title={`${t("editing_org")}: ${org.name}`}
description={t("admin_orgs_edit_description")}
>
<SettingsHeader title={`${t("editing_org")}: ${org.name}`} description={t("admin_orgs_edit_description")}>
<LicenseRequired>
<OrgForm org={org} />
</LicenseRequired>
@@ -17,10 +17,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
return (
<SettingsHeader
title={t("organizations")}
description={t("orgs_page_description")}
>
<SettingsHeader title={t("organizations")} description={t("orgs_page_description")}>
<LicenseRequired>
<AdminOrgTable />
</LicenseRequired>
@@ -3,7 +3,8 @@
import { useReactTable, getCoreRowModel, createColumnHelper } from "@tanstack/react-table";
import { useMemo } from "react";
import { ColumnFilterType, DataTableProvider } from "@calcom/features/data-table";
import { ColumnFilterType } from "@calcom/features/data-table";
import { DataTableProvider } from "~/data-table/DataTableProvider";
import type { DateRangeFilterOptions } from "@calcom/features/data-table/lib/types";
import { DateRangeFilter } from "~/data-table/components/filters/DateRangeFilter";
@@ -3,7 +3,7 @@
import { useState } from "react";
import dayjs from "@calcom/dayjs";
import { Calendar } from "@calcom/web/modules/calendars/weeklyview/components/Calendar";
import { Calendar } from "@calcom/features/calendars/weeklyview/components/Calendar";
import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events";
import type { CalendarComponentProps, Hours } from "@calcom/features/calendars/weeklyview/types/state";
@@ -16,10 +16,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
return (
<SettingsHeader
title={t("workspace_platforms")}
description={t("workspace_platforms_description")}
>
<SettingsHeader title={t("workspace_platforms")} description={t("workspace_platforms_description")}>
<LicenseRequired>
<WorkspacePlatformsPage />
</LicenseRequired>
@@ -31,6 +31,7 @@ import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { ErrorBoundary } from "@calcom/ui/components/errorBoundary";
import { Icon } from "@calcom/ui/components/icon";
import { ArrowLeftIcon, ChevronDownIcon, ChevronRightIcon } from "@coss/ui/icons";
import type { VerticalTabItemProps } from "@calcom/ui/components/navigation";
import { VerticalTabItem } from "@calcom/ui/components/navigation";
import { Skeleton } from "@calcom/ui/components/skeleton";
@@ -153,9 +154,10 @@ const getTabs = (orgBranding: OrganizationBranding | null) => {
trackingMetadata: { section: "developer", page: "api_keys" },
},
{
name: "admin_api",
href: "/settings/organizations/admin-api",
trackingMetadata: { section: "developer", page: "admin_api" },
name: "api_docs",
href: "https://cal.com/docs/api-reference/v2/introduction",
isExternalLink: true,
trackingMetadata: { section: "developer", page: "api_docs" },
},
// TODO: Add profile level for embeds
// { name: "embeds", href: "/v2/settings/developer/embeds" },
@@ -205,10 +207,10 @@ const getTabs = (orgBranding: OrganizationBranding | null) => {
trackingMetadata: { section: "organization", page: "directory_sync" },
},
{
name: "admin_api",
href: "https://cal.com/docs/enterprise-features/api/api-reference/bookings#admin-access",
name: "api_docs",
href: "https://cal.com/docs/api-reference/v2/introduction",
isExternalLink: true,
trackingMetadata: { section: "organization", page: "admin_api" },
trackingMetadata: { section: "organization", page: "api_docs" },
},
...(HAS_ORG_OPT_IN_FEATURES
? [
@@ -449,7 +451,7 @@ const useTabs = ({
return { ...tab, children: filtered };
} else if (tab.href === "/settings/developer") {
const filtered = tab?.children?.filter(
(childTab) => permissions?.canUpdateOrganization || childTab.name !== "admin_api"
(childTab) => permissions?.canUpdateOrganization || childTab.name !== "api_docs"
);
return { ...tab, children: filtered };
}
@@ -475,10 +477,7 @@ const BackButtonInSidebar = ({ name }: { name: string }) => {
href="/event-types"
className="hover:bg-subtle todesktop:mt-10 [&[aria-current='page']]:bg-emphasis [&[aria-current='page']]:text-emphasis group-hover:text-default text-emphasis group my-6 flex h-6 max-h-6 w-full flex-row items-center rounded-md px-3 py-2 text-sm font-medium leading-4 transition"
data-testid={`vertical-tab-${name}`}>
<Icon
name="arrow-left"
className="h-4 w-4 stroke-[2px] ltr:mr-[10px] rtl:ml-[10px] rtl:rotate-180 md:mt-0"
/>
<ArrowLeftIcon className="h-4 w-4 stroke-[2px] ltr:mr-[10px] rtl:ml-[10px] rtl:rotate-180 md:mt-0" />
<Skeleton title={name} as="p" className="min-h-4 max-w-36 truncate" loadingClassName="ms-3">
{name}
</Skeleton>
@@ -602,9 +601,9 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record<number, T
}`}>
<div className="me-3">
{teamMenuState[index].teamMenuOpen ? (
<Icon name="chevron-down" className="h-4 w-4" />
<ChevronDownIcon className="h-4 w-4" />
) : (
<Icon name="chevron-right" className="h-4 w-4" />
<ChevronRightIcon className="h-4 w-4" />
)}
</div>
{}
@@ -826,7 +825,7 @@ const SettingsSidebarContainer = ({
href={child.href || "/"}
trackingMetadata={child.trackingMetadata}
textClassNames="text-emphasis font-medium text-sm"
className={`px-2! py-1! min-h-7 h-auto w-fit ${
className={`px-2! py-1! min-h-7 h-auto w-full ${
tab.children && index === tab.children?.length - 1 && "mb-3!"
}`}
disableChevron
@@ -949,9 +948,9 @@ const SettingsSidebarContainer = ({
}`}>
<div className="me-3">
{otherTeamMenuState[index].teamMenuOpen ? (
<Icon name="chevron-down" className="h-4 w-4" />
<ChevronDownIcon className="h-4 w-4" />
) : (
<Icon name="chevron-right" className="h-4 w-4" />
<ChevronRightIcon className="h-4 w-4" />
)}
</div>
{}
@@ -1036,7 +1035,7 @@ const MobileSettingsContainer = (props: { onSideContainerOpen?: () => void }) =>
<button
className="hover:bg-emphasis flex items-center space-x-2 rounded-md px-3 py-1 rtl:space-x-reverse"
onClick={() => router.back()}>
<Icon name="arrow-left" className="text-default h-4 w-4" />
<ArrowLeftIcon className="text-default h-4 w-4" />
<p className="text-emphasis font-semibold">{t("settings")}</p>
</button>
</div>
@@ -1052,12 +1051,7 @@ type SettingsLayoutProps = {
permissions?: SettingsPermissions;
} & ComponentProps<typeof Shell>;
function SettingsLayoutAppDirClient({
children,
teamFeatures,
permissions,
...rest
}: SettingsLayoutProps) {
function SettingsLayoutAppDirClient({ children, teamFeatures, permissions, ...rest }: SettingsLayoutProps) {
const pathname = usePathname();
const state = useState(false);
const [sideContainerOpen, setSideContainerOpen] = state;
@@ -21,7 +21,7 @@ const Page = async () => {
<SettingsHeader
title={t("billing")}
description={t("manage_billing_description")}
borderInShellHeader={false}>
borderInShellHeader={true}>
<BillingView />
</SettingsHeader>
);
@@ -25,9 +25,7 @@ const Page = async () => {
redirect("/auth/login?callbackUrl=/settings/developer/oauth");
}
return (
<OAuthClientsView />
);
return <OAuthClientsView />;
};
export default Page;
@@ -29,7 +29,7 @@ const Page = async () => {
<SettingsHeader
title={t("billing")}
description={t("manage_billing_description")}
borderInShellHeader={false}>
borderInShellHeader={true}>
<BillingView />
</SettingsHeader>
);
@@ -1,33 +1,7 @@
import { getTranslate, _generateMetadata } from "app/_utils";
import { redirect } from "next/navigation";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { AdminAPIView } from "~/ee/organizations/admin-api";
import { validateUserHasOrg } from "../actions/validateUserHasOrg";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => `${t("admin")} ${t("api_reference")}`,
(t) => t("leverage_our_api"),
undefined,
undefined,
"/settings/organizations/admin-api"
);
const Page = async () => {
const t = await getTranslate();
await validateUserHasOrg();
return (
<SettingsHeader
title={`${t("admin")} ${t("api_reference")}`}
description={t("leverage_our_api")}
borderInShellHeader={false}>
<AdminAPIView />
</SettingsHeader>
);
const Page = () => {
redirect("https://cal.com/docs/api-reference/v2/introduction");
};
export default Page;
@@ -11,8 +11,8 @@ import {
import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@calcom/ui/classNames";
import { Checkbox, Label } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { Tooltip } from "@calcom/ui/components/tooltip";
import { ChevronRightIcon, InfoIcon } from "@coss/ui/icons";
import { usePermissions } from "./usePermissions";
@@ -54,10 +54,10 @@ export function AdvancedPermissionGroup({
const allPermissions = isAllResources
? ["*.*"]
: resourceConfig
? Object.entries(resourceConfig)
.filter(([action]) => action !== INTERNAL_DATAACCESS_KEY)
.map(([action]) => `${resource}.${action}`)
: [];
? Object.entries(resourceConfig)
.filter(([action]) => action !== INTERNAL_DATAACCESS_KEY)
.map(([action]) => `${resource}.${action}`)
: [];
// Check if all permissions for this resource are selected
const isAllSelected = isAllResources
@@ -98,8 +98,7 @@ export function AdvancedPermissionGroup({
}
}}>
<div className="flex items-center gap-1.5" onClick={() => setIsExpanded(!isExpanded)}>
<Icon
name="chevron-right"
<ChevronRightIcon
className={classNames(
"h-4 w-4 transition-transform",
isExpanded && !isAllResources ? "rotate-90" : ""
@@ -173,7 +172,7 @@ export function AdvancedPermissionGroup({
</span>
{isAutoEnabled && (
<Tooltip content={t("read_permission_auto_enabled_tooltip")}>
<Icon name="info" className="text-muted-foreground h-3 w-3" />
<InfoIcon className="text-muted-foreground h-3 w-3" />
</Tooltip>
)}
</div>
@@ -7,8 +7,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { Dialog, DialogContent } from "@calcom/ui/components/dialog";
import { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
import { LockIcon, ShieldCheckIcon, ShieldIcon, UsersIcon } from "@coss/ui/icons";
import { FingerprintAnimation } from "./FingerprintAnimation";
@@ -59,7 +59,7 @@ export function PbacOptInModal({ open, onOpenChange, revalidateRolesPath }: Pbac
<div className="bg-subtle border-subtle w-full stack-y-px overflow-hidden rounded-xl border">
<div className="bg-default flex gap-3 p-3">
<div className="bg-subtle flex h-8 w-8 shrink-0 items-center justify-center rounded-md">
<Icon name="shield-check" className="text-subtle h-5 w-5" />
<ShieldCheckIcon className="text-subtle h-5 w-5" />
</div>
<div className="flex flex-col gap-1">
<p className="text-emphasis text-sm font-semibold">{t("pbac_opt_in_custom_roles_title")}</p>
@@ -69,7 +69,7 @@ export function PbacOptInModal({ open, onOpenChange, revalidateRolesPath }: Pbac
<div className="bg-default flex gap-3 p-3">
<div className="bg-subtle flex h-8 w-8 shrink-0 items-center justify-center rounded-md">
<Icon name="lock" className="text-subtle h-5 w-5" />
<LockIcon className="text-subtle h-5 w-5" />
</div>
<div className="flex flex-col gap-1">
<p className="text-emphasis text-sm font-semibold">
@@ -81,7 +81,7 @@ export function PbacOptInModal({ open, onOpenChange, revalidateRolesPath }: Pbac
<div className="bg-default flex gap-3 p-3">
<div className="bg-subtle flex h-8 w-8 shrink-0 items-center justify-center rounded-md">
<Icon name="users" className="text-subtle h-5 w-5" />
<UsersIcon className="text-subtle h-5 w-5" />
</div>
<div className="flex flex-col gap-1">
<p className="text-emphasis text-sm font-semibold">
@@ -93,7 +93,7 @@ export function PbacOptInModal({ open, onOpenChange, revalidateRolesPath }: Pbac
<div className="bg-default flex gap-3 p-3">
<div className="bg-subtle flex h-8 w-8 shrink-0 items-center justify-center rounded-md">
<Icon name="shield" className="text-subtle h-5 w-5" />
<ShieldIcon className="text-subtle h-5 w-5" />
</div>
<div className="flex flex-col gap-1">
<p className="text-emphasis text-sm font-semibold">
@@ -27,9 +27,9 @@ const Page = async () => {
const organizationRepository = getOrganizationRepository();
const otherTeams = organizationId
? await organizationRepository.findTeamsInOrgIamNotPartOf({
userId: session?.user.id,
parentId: organizationId,
})
userId: session?.user.id,
parentId: organizationId,
})
: [];
return (
@@ -1,8 +1,6 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import LegacyPage, {
TeamMembersCTA,
} from "~/ee/organizations/other-team-members-view";
import LegacyPage, { TeamMembersCTA } from "~/ee/organizations/other-team-members-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { validateUserHasOrg } from "../../../../actions/validateUserHasOrg";
@@ -23,9 +23,7 @@ const getData = withAppDirSsr<inferSSRProps<typeof getServerSideProps>>(getServe
const ServerPage = async ({ params, searchParams }: PageProps) => {
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
return (
<SettingsNewView />
);
return <SettingsNewView />;
};
export default ServerPage;
@@ -1,7 +1,11 @@
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { isCompanyEmail } from "@calcom/features/ee/organizations/lib/utils";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import LicenseRequired from "~/ee/common/components/LicenseRequired";
import LegacyPage, { LayoutWrapper } from "~/ee/organizations/new/create-new-view";
export const generateMetadata = async () =>
@@ -14,6 +18,19 @@ export const generateMetadata = async () =>
);
const ServerPage = async () => {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
if (!session?.user?.id) {
return redirect("/auth/login");
}
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
const userEmail = session.user.email || "";
if (!isAdmin && !isCompanyEmail(userEmail)) {
return redirect("/settings/my-account/profile");
}
return (
<LayoutWrapper>
<LicenseRequired>
+1 -7
View File
@@ -8,13 +8,7 @@ interface GeoContextValue {
const GeoContext = createContext<GeoContextValue | undefined>(undefined);
export function GeoProvider({
country,
children,
}: {
country: string;
children: React.ReactNode;
}) {
export function GeoProvider({ country, children }: { country: string; children: React.ReactNode }) {
return <GeoContext.Provider value={{ country }}>{children}</GeoContext.Provider>;
}
+2 -2
View File
@@ -35,8 +35,8 @@ const url =
typeof window !== "undefined"
? "/api/trpc"
: process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/trpc`
: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`;
? `https://${process.env.VERCEL_URL}/api/trpc`
: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`;
export const trpcClient = trpc.createClient({
links: [
+1 -1
View File
@@ -7,7 +7,7 @@ import { constructAppImage, constructGenericImage, constructMeetingImage } from
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants";
import { getCalcomUrl } from "@calcom/lib/getCalcomUrl";
import { buildCanonical } from "@calcom/lib/next-seo.config";
import { getTranslation } from "@calcom/lib/server/i18n";
import { getTranslation } from "@calcom/i18n/server";
import { truncateOnWord } from "@calcom/lib/text";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";

Some files were not shown because too many files have changed in this diff Show More