Compare commits

...
66 Commits
Author SHA1 Message Date
Alex van Andel 14edd6c42f chore: release v6.0.4
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-12-23 20:55:12 +00:00
Keith WilliamsandDevin AI 1c5f05638f fix: search by branch instead of SHA in E2E report workflow_dispatch (#26168)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-23 20:51:14 +00:00
fe4017f302 docs: clarify tRPC architecture - packages/features vs apps/web/modules (#26155)
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>
2025-12-23 19:38:09 +00:00
BetoandDhairyashil Shinde 2abfae7f13 feat(companion): Booking details screen navigation header improvements (#26163)
* feat(companion): restructure booking detail components and enhance type safety

- Migrate BookingDetail component to a new file structure under (tabs)/(bookings) for better organization.
- Introduce a new BookingDetailScreen with improved header and menu options.
- Update app.json to enable typed routes and enhance TypeScript configuration in tsconfig.json.
- Remove the old booking-detail.tsx file to clean up the codebase.
- Adjust imports and routing to reflect the new structure, ensuring all components are properly linked.

This refactor aims to improve maintainability and clarity in the booking detail flow.

* add header options

* refactor(booking-detail): rename "Standalone Actions" menu to "Danger Zone" for clarity

---------

Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2025-12-23 19:35:55 +00:00
Dhairyashil Shinde 75d8300635 fix/companion-consider-all-profile-links (#26158) 2025-12-23 19:24:26 +00:00
Volnei Munhoz 67f20f4503 fix: Change matcher params for CI changes matcher (#26162)
* Change matcher params

* upgrade dorny

* Fix predicated-quantifier

* Fix predicated-quantifier
2025-12-23 19:20:06 +00:00
Keith Williams d6376c6d70 chore: CODEOWNERS Change Platform -> Consumer (#26160) 2025-12-23 19:18:42 +00:00
Keith WilliamsandDevin AI 9256d306b9 ci: make E2E report jobs non-blocking by moving to separate workflow (#26157)
* ci: make E2E report jobs non-blocking by moving to separate workflow

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* ci: minimize permissions in e2e-report workflow

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* ci: refactor to reuse existing workflow files via workflow_call

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* ci: add workflow_dispatch trigger for manual testing of E2E report workflow

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* ci: simplify workflow_dispatch to only require pr_number input

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* ci: remove secrets inheritance from merge-reports for fork PR security

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-23 19:16:36 +00:00
Anik Dhabal Babu b62493235c fix: all the e2e flakes visible (#26161)
* fix: all th e2e flakes

* check

* fix
2025-12-23 18:55:37 +00:00
Anik Dhabal Babu f35b36fafa add appId fallback for appName in event results (#26146) 2025-12-23 17:20:28 +00:00
Anik Dhabal Babu 2689cdfdff perf: pass app data to OmniInstallAppButton to avoid redundant API calls (#26156)
* perf: pass app data to OmniInstallAppButton to avoid redundant API calls

* Clean up comments in OmniInstallAppButton component

Removed comment about pre-fetched app data and fetching logic.
2025-12-23 16:49:34 +00:00
Dhairyashil ShindeandPallav efcf68869d fix: Routing page horizontal scroll for small mobile devices (#24663)
* fix: horizontal scroll for small mobile devices

* address cubics comments

* fix disable form toggle

---------

Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-12-23 16:23:50 +00:00
Benny JooandDevin AI 8afe87ff75 refactor: Move trpc-dependent components from features to web [1] (#25859)
* refactor: migrate UnconfirmedBookingBadge from features to apps/web

Move UnconfirmedBookingBadge.tsx from packages/features/bookings/ to
apps/web/modules/bookings/components/ as part of the architectural
refactor to remove trpc client imports from the features layer.

Also removes unused preserveBookingsQueryParams function from Navigation.tsx.

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: move shell navigation and badges to apps/web

Move shell navigation components and trpc-using badges from
packages/features to apps/web/modules to fix circular dependency:

- Move navigation folder to apps/web/modules/shell/navigation/
- Move TeamInviteBadge.tsx to apps/web/modules/shell/
- Create Shell wrapper in apps/web that provides MobileNavigationContainer
- Update all Shell imports in apps/web to use the new wrapper
- Remove MobileNavigationContainer default from features Shell.tsx
- Fix pre-existing lint warnings in touched files

This establishes the pattern for migrating React components that use
trpc hooks from the features layer to the web app layer, ensuring
proper dependency direction: apps/web imports from packages/features,
never the reverse.

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: move SideBar.tsx to apps/web to fix build error

SideBar.tsx was importing Navigation from the moved navigation folder,
causing a build error. Moving SideBar.tsx to apps/web and updating the
features Shell to not have a default SidebarContainer fixes this.

The web Shell wrapper now provides both the default SidebarContainer
and MobileNavigationContainer, maintaining the injection pattern.

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* revert

* revert

* revert

* wip

* wip

* wip

* wip

* wip

* wip

* not used anywhere

* wip

* wip

* wip

* wip

* fix

* fix

* wip

* wip

* wip

* wip

* wip

* fix

* fix

* fix

* fix

* migrate

* migrate admin-adpi

* wip

* feat: migrate organization settings components from packages/features to apps/web/modules

- Migrate profile.tsx, appearance.tsx, general.tsx, privacy.tsx, guest-notifications.tsx, delegationCredential.tsx, other-team-members-view.tsx, other-team-profile-view.tsx
- Migrate attributes directory (AttributesForm.tsx, DeleteAttributeModal.tsx, ListSkeleton.tsx, attributes-create-view.tsx, attributes-edit-view.tsx, attributes-list-view.tsx)
- Migrate admin directory (AdminOrgEditPage.tsx, AdminOrgPage.tsx, WorkspacePlatformPage.tsx)
- Update all page imports to use new paths from ~/settings/organizations/
- Update relative imports in migrated files to use @calcom/features paths
- Fix lint warnings in migrated files

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: update test import path after migration

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: remove unnecessary test-setup import (already in vitest config)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* wip

* refactor: delete original files after migration to apps/web/modules

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* fix

* fix

* fix

* fix

* wip

* refactor more

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* mv

* update import paths

* wip

* wip

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* mv

* mv

* mv

* fix

* wip

* wip

* fix

* fix

* fix

* fix: make test mocks resilient to vi.resetAllMocks()

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: fix AttributeForm test failures

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix

* fix

* refactor: move ee files to apps/web/modules/ee/ folder

- Move teams, workflows, and organizations folders to apps/web/modules/ee/
- Add LICENSE file to apps/web/modules/ee/
- Update all import paths from ~/teams/ to ~/ee/teams/
- Update all import paths from ~/settings/organizations/ to ~/ee/organizations/
- Remove duplicate MemberInvitationModal copy.tsx file

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: move useHasPaidPlan and dependent files to apps/web/modules

- Move useHasPaidPlan.ts from packages/features/billing/hooks to apps/web/modules/billing/hooks
- Move intercom files from packages/features/ee/support to apps/web/modules/ee/support
- Move ContactMenuItem.tsx and dependencies (freshchat, helpscout, zendesk) to apps/web/modules/ee/support
- Move ViewRecordingsDialog.tsx and RecordingListSkeleton to apps/web/modules/ee/video
- Move CalVideoSettings.tsx to apps/web/modules/eventtypes/components/locations
- Move CreateOrEditOutOfOfficeModal.tsx to apps/web/modules/settings/outOfOffice
- Refactor UpgradeTeamsBadge to accept props and create wrapper in apps/web/modules/billing
- Update all callers to use new file locations
- Add eslint-disable comments for pre-existing lint warnings

This fixes the tRPC server build failure caused by circular dependency where
the server build was traversing into packages/features and pulling in React
hooks that depend on @calcom/trpc/react types.

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: correct UpgradeTeamsBadge import path to use package export

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix: pass plan state through SelectProps to UpgradeTeamsBadge

- Add upgradeTeamsBadgeProps field to ExtendedOption type in Select component
- Update OptionComponent to spread upgradeTeamsBadgeProps to UpgradeTeamsBadge
- Update getOptions.ts to accept PlanState object and include upgradeTeamsBadgeProps
- Update WorkflowStepContainer.tsx to pass planState to getWorkflowTriggerOptions/getWorkflowTemplateOptions
- Update WorkflowDetailsPage.tsx to pass upgradeTeamsBadgeProps in transformed action options
- Update AddActionDialog.tsx interface and mapping to include upgradeTeamsBadgeProps
- Add eslint-disable comments for pre-existing React Hook dependency warnings

This fixes the UpgradeTeamsBadge refactoring issue where the badge was always showing
'upgrade' text instead of the correct text based on plan state (trial_mode, inactive_team_plan, etc.)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: update import paths to use /ee/ folder for workflows and organizations

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: update workflow component imports to use /ee/ folder

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add missing types.ts for LocationInput.tsx

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: extract BookingRedirectForm type to shared location

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix

* wip

* wip

* fix: update BookingRedirectForm import to use local types file

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* wip

* fix

* fix

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-23 12:22:26 -03:00
Pedro CastroandKeith Williams 7847177e8b fix(api): accept avatar URLs and use email for user creation (#25629)
* fix(api): fix user creation with avatarUrl and username

- avatarValidator: accept URLs in addition to base64 images
- organizations-users-service: use email for user creation instead of username

The avatarValidator was incorrectly rejecting valid URLs even though the API documentation shows URL examples and all e2e tests use URLs.

The user creation was failing when username was provided because createNewUsersConnectToOrgIfExists requires a valid email, not username. The username is correctly applied via updateOrganizationUser after creation.

Fixes user creation endpoint POST /v2/organizations/{orgId}/users

* test(api): add tests for avatar validator fixes

  Add comprehensive test coverage for avatarValidator changes:

  - Unit tests: 18 scenarios covering valid/invalid URLs and base64 images
  - E2E tests: user creation with username + avatarUrl (URL and base64)
  - Security validation: reject unsafe protocols and malformed data
  - Error message verification for validation failures

  Addresses testing requirements from code review

* fix(api): enhance avatar validator security per code review

  - Reject HTTP URLs, accept only HTTPS for security and browser compatibility
  - Reject empty strings and whitespace (use null to reset avatar)
  - Update validation message to clarify HTTPS requirement
  - Add test coverage for new security restrictions

 Addresses security feedback from code review regarding mixed content vulnerabilities and clearer API semantics for avatar reset

* refactor(api): simplify avatarValidator null check

Remove redundant null/undefined check since @IsOptional decorator already skips validation for undefined values. Updated test mock to include @IsOptional to match real DTO usage

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-12-23 15:07:14 +00:00
Hariom BalharaandDevin AI 866c8c046d fix: create host records when team membership transitions to accepted via PATCH (#26138)
The PATCH endpoint for /v2/teams/:teamId/memberships/:membershipId had a bug
where host records for assignAllTeamMembers event types were not created when
a membership transitioned from accepted=false to accepted=true.

The bug was caused by incorrect operation ordering:
1. Update membership (first update)
2. Get current membership (gets already-updated state)
3. Update membership again (second update)
4. Check if transition happened (condition never fires)

Fixed by reordering to match the org-scoped controller pattern:
1. Get current membership BEFORE any updates
2. Update membership
3. Compare and call updateNewTeamMemberEventTypes if transition occurred

Also added an e2e test to verify host records are created when membership
transitions from accepted=false to accepted=true via PATCH.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-23 10:46:14 +00:00
Kartik LabhshetwarandDhairyashil Shinde 96f8a89efa fix: replace console logging with logger across services and features (#26127)
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2025-12-23 08:24:23 +00:00
Kartik Labhshetwar a2cb529d62 fix: update CommandInput styling for focus and transition (#26055) 2025-12-23 13:16:55 +05:30
9c680838b6 fix(companion): improve code clarity and type safety in components (#26062)
- Remove 'as any' type assertion in LoginScreen.tsx, use Platform.OS check
- Fix malformed className string in LogoutButton.tsx (extra quotes)
- Remove @ts-ignore comments in Tooltip.tsx with proper web-specific types
- Replace 'any' type with Schedule type in AvailabilityTab.tsx
- Replace 'any' type with 'string' for SF Symbol icons in EventTypeListItem.ios.tsx
- Remove console.log error handling in SvgImage.tsx
- Extract duplicated formatDuration function to use shared utility from formatters.ts
- Extract repeated inline styles to sectionDividerStyle constant in AvailabilityTab.tsx

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Dhairyashil <dhairyashil10101010@gmail.com>
2025-12-23 00:26:16 +00:00
Dhairyashil ShindeandPeer Richelsen 6650fdb9ef refactor(companion): extract AvailabilityDetailScreen with AppPressable migration (#26106)
* refactor(companion): extract AvailabilityDetailScreen with AppPressable migration

Apply Phases 2, 3, 4 to availability-detail.tsx:
- Phase 2: Extract screen logic (~995 lines) to components/screens/AvailabilityDetailScreen.tsx
- Route file is now a minimal ~35 line wrapper with Stack.Screen config
- Phase 3: ScrollView+map pattern kept (7 days is a fixed small list, FlatList not needed)
- Phase 4: Migrate all TouchableOpacity to AppPressable (63 instances)

Follows the same pattern as EventTypeDetailScreen extraction.

* fix(companion): add GlassView header with Save button to AvailabilityDetailScreen (#26107)

* refactor(companion): extract BookingDetailScreen with AppPressable migration (#26108)

* refactor(companion): extract AvailabilityListScreen with AppPressable migration (#26109)

* fix(companion): add GlassView header with Save button to AvailabilityDetailScreen

* refactor(companion): extract BookingDetailScreen with AppPressable migration

* refactor(companion): extract AvailabilityListScreen with AppPressable migration

* refactor(companion): Standardized error handling with dev-only debug logs (#26110)

* address cubics comments

* address cubics comments

* addressed typecheck erros

* removed localhost from wxt.config for chrome extension

* declare FullScreenModal  FullScreenModalProps

---------

Co-authored-by: Peer Richelsen <peer@cal.com>

* add loggers for dev env

---------

Co-authored-by: Peer Richelsen <peer@cal.com>

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
2025-12-22 22:47:55 +00:00
Dhairyashil Shinde 9909ac53fc fix: use app name instead of app slug in email (#25285)
* fix: google calender name in email

* fix: google calender name in email

* cleaner code

* test: add test for human-readable app name in appsStatus
2025-12-23 03:41:08 +05:30
Anik Dhabal Babu f598a8ebdd ci: skip merge & report publishing when all E2E tests pass (#23552)
* fix

* Refactor PR workflow to use pull_request_target

Updated the GitHub Actions workflow

* update

* Update PR workflow trigger and permissions

Changed event trigger from 'pull_request_target' to 'pull_request' and removed branch restrictions.

* test

* revert
2025-12-22 19:31:43 +00:00
23920fe9ce fix: Org/team admin or owner should be marked as ‘no show’ in a member’s booking (#26009)
* fix: update tempOrgRediret on updating s
lug

* Update packages/trpc/server/routers/viewer/organizations/adminUpdate.handler.ts

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

* refactor

* fix

* fix

* revert

* fix handle no show

* revert

* Remove metadata from organization query selection

Removed metadata selection from organization query.

* use repo

* update

* Update packages/features/bookings/lib/checkIfUserIsAuthorizedToManageBooking.ts

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

* update

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
2025-12-22 15:42:20 -03:00
Anik Dhabal Babu 33eee8dfc3 fix: Email reminder issue with seated event (#26136)
* fix emailworkflow reminder attendee

* update
2025-12-22 18:26:40 +00:00
Amit Sharma f5b9b6b0f9 feat: store ads clickid when creating an account (#25763)
* feat: store ads clickid when creating an account

* fix: type check

* fix: google campaignId not being added to stripe

* remove tracking in stripe app
2025-12-22 23:29:51 +05:30
b664857809 feat: set HubSpot meeting owner from cal's organizer (#25998)
* feat: add HubSpot owner(organizer in cal)

* refactor: improve logging and token handling in HubSpot integration

* Update packages/app-store/hubspot/lib/CrmService.ts

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

* feat: enhance error handling for missing scopes

* refactor

* remove unused

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2025-12-22 22:52:04 +05:30
Anik Dhabal Babu c746fdcdfe ix disable cancelling persistence and rescheduling toggle (#26131) 2025-12-22 15:31:16 +00:00
76b53eb3d4 chore: remove webpack config and convert next.config to TypeScript (#26126)
* chore: remove webpack experimental flags from apps/web

- Remove webpackMemoryOptimizations and webpackBuildWorker from experimental config
- Remove unused DefinePlugin for process.env.BUILD_ID (not used anywhere in codebase)
- Remove unused buildId parameter from webpack function signature

These webpack-specific experimental flags were showing up during dev mode
even though Turbopack is used for development. The flags only apply to
production webpack builds and their presence in the config was confusing.

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

* chore: remove webpack function for full Turbopack build

- Remove entire webpack function (IgnorePlugin, PrismaPlugin, resolve.fallback, sideEffects)
- Remove unused PrismaPlugin import
- Turbopack handles all bundling without webpack configuration

Benchmark shows no performance regression:
- Main (with webpack): 67s compile
- Full webpack removal: 69s compile

Both fail at same pre-existing TypeScript error on main branch.

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

* chore: convert next.config.js to TypeScript

- Convert CommonJS require() to ES6 imports
- Add proper TypeScript types for config and plugins
- Remove /* eslint-disable */ comment
- Keep all existing functionality intact

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

* conditional load axiom

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-22 12:23:00 -03:00
Amit Sharma 090247a058 fix: event type not installed apps list empty (#26128) 2025-12-22 13:37:52 +00:00
Hariom BalharaandDevin AI e23354b48b fix: ESC key handler only works once in embed modal (#25615)
* fix: ESC key handler only works once in embed modal

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

* Fix bug found by cubic and add tests

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-22 10:06:09 -03:00
Syed Ali Shahbaz 0b7f51410a chore: Add webhook version documentation link to webapp (#26121)
* init

* further improvement
2025-12-22 10:22:41 +00:00
Anik Dhabal Babu 31da9eaa8f fix: e2e flakes on analytics apps tests (#26119)
* test

* test

* fix flakes
2025-12-22 15:30:01 +05:30
192d927232 fix: align page headers by removing py-2 padding from ShellMain (#26084)
* fix: align insights header with other pages by using ShellMainAppDir

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

* fix: remove py-2 padding from ShellMain to align with ShellMainAppDir

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

* address cubics comment - keep sticky bg, only remove padding

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Dhairyashil <dhairyashil10101010@gmail.com>
2025-12-22 09:31:56 +00:00
Rajiv Sahal f9ff3248e5 feat: endpoint to update location for existing booking (#25367)
* feat: init endpoint to update booking location

* chore: update controller

* chore: DTOs for update location endpoint

* chore: add update booking method

* fix: refactor

* feat: service update booking location

* chore: update platform libraries

* chore: update exports

* chore: e2e tests for updating booking location endpoint

* chore: update e2e tests for updating booking location endpoint

* chore: update bookings module

* fixup

* chore: update configs

* fixup: revert changes

* chore: remove unused handlers

* fix: update inputs

* refactor: booking location service

* fixup

* revert: unnecessary changes

* chore: cleanup

* fix: e2e tests

* chore: implement cubic feedback

* chore: implement feedback

* fixup

* chore: more PR feedback
2025-12-22 09:25:00 +00:00
mintlify[bot] 6ebd401953 Update docs/developing/guides/automation/webhooks.mdx (#26118)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2025-12-22 09:20:07 +00:00
mintlify[bot]andSyed Ali Shahbaz d69ba02851 Add versioned webhook payload reference with interactive selection (#26008)
* Update docs/developing/guides/automation/webhooks.mdx

Co-Authored-By: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>

* Update docs/developing/guides/automation/webhooks.mdx

Co-Authored-By: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>

* Update docs/developing/guides/automation/webhooks.mdx

Co-Authored-By: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>

* Update docs/developing/guides/automation/webhooks.mdx

Co-Authored-By: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>

* Update docs/developing/guides/automation/webhooks.mdx

Co-Authored-By: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>

* Update docs/developing/guides/automation/webhooks.mdx

Co-Authored-By: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>

* Documentation edits made through Mintlify web editor

* Documentation edits made through Mintlify web editor

---------

Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
2025-12-22 06:19:02 +00:00
Keith WilliamsandDevin AI 851cca25aa perf: add z.ZodType annotations to high-impact schemas (#26114)
* perf: add z.ZodType annotations to high-impact schemas

Add explicit type annotations to reduce TypeScript type inference overhead
and .d.ts file bloat in the tRPC package.

Changes:
- packages/prisma/zod-utils.ts: Add EventTypeLocation type and annotate eventTypeLocations schema
- packages/features/eventtypes/lib/schemas.ts: Add TEventTypeDuplicateInput and TCreateEventTypeInput types with annotations
- packages/app-store/routing-forms/zod.ts: Add FieldOption, TNonRouterField, TRouterField, TField, TFields types with annotations for zodField and zodFields

These schemas feed into multiple tRPC routers and were identified as high-impact
targets for reducing type checking time and declaration file sizes.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: replace z.infer with explicit types and define EventTypeLocation locally

Address PR feedback:
- Replace z.infer<typeof calVideoSettingsSchema> with explicit CalVideoSettings type
- Define EventTypeLocation type locally instead of importing from prisma
- Add z.ZodType annotation to calVideoSettingsSchema

Note: Still importing Zod schemas from @calcom/prisma/zod-utils for validation.
Awaiting guidance on whether to move those to @calcom/lib.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-21 23:15:04 -03:00
ee6989a575 refactor: split raqbUtils into server and client modules to fix RAQB import in server context (#26112)
* refactor: add server and client raqbUtils modules

Add two new modules to split RAQB utilities:
- raqbUtils.server.ts: Server-safe utilities without RAQB runtime
- raqbUtils.client.ts: Client-only utilities requiring RAQB runtime

This enables server-side code to import RAQB utilities without
pulling in the react-awesome-query-builder runtime library.

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

* refactor: update imports to use server/client raqbUtils modules and fix lint warnings

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

* fix: fix type errors in getLuckyUser.ts and remove dead code in RouteBuilder.tsx

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-22 02:00:19 +00:00
Keith WilliamsandDevin AI 545eed9b0d fix: always fetch fresh PR labels to avoid stale event payload data (#26113)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-22 01:06:05 +00:00
Volnei MunhozandDevin AI 40883386f0 fix: address peer dependency issues across the monorepo (#26105)
MOVE:
- Move @next/third-parties from root to apps/web (only used there)
- Remove date-fns-tz from root, add date-fns and date-fns-tz to root deps

REMOVE:
- Remove sonner from packages/lib (not used there, already in apps/web)
- Remove next-seo from packages/ui (not used there, already in apps/web)
- Remove next-auth from apps/api/v2 (only used for User type in tests)

ADD peerDependencies:
- packages/ui: add react-dom
- packages/features: add react, react-dom, react-is, date-fns-tz, stripe, zod
- packages/platform/atoms: add react-dom
- packages/trpc: add next, react, react-dom

ADD dependencies:
- apps/web: add @next/third-parties, i18next, react-i18next
- apps/api/v2: add axios (required by @nestjs/axios)

FIX:
- apps/api/v2: replace next-auth User type with @calcom/prisma/client User

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-21 23:20:49 +00:00
0673d667f8 feat: upgrade api/v1 and example apps to Next.js 16.1.0 (#26104)
* feat: upgrade api/v1 and example apps to Next.js 16.1.0

- Update apps/api/v1 to Next.js 16.1.0
- Rename middleware.ts to proxy.ts per Next.js 16 migration
- Add turbopack config to next.config.js
- Update packages/platform/examples/base to Next.js 16.1.0
- Add eslint-disable to config files (following apps/web pattern)

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

* chore: update yarn.lock for Next.js 16.1.0 upgrade

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-21 18:19:18 -03:00
a2e2b7ff97 chore: upgrade yarn from 3.4.1 to 4.12.0 (#26102)
* chore: upgrade yarn from 3.4.1 to 4.12.0

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

* fix: update engines.yarn to match upgraded version

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-21 17:20:56 -03:00
Keith WilliamsandDevin AI 127b7d5532 ci: consolidate changes and check-label jobs into prepare (#26101)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-21 19:51:37 +00:00
a9a3389039 feat: upgrade apps/web to Next.js 16.1.0 (#26093)
* feat: upgrade apps/web to Next.js 16.1.0

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: enable Turbopack for Next.js 16.1 production builds

- Add turbopack: {} config to next.config.js to enable Turbopack for builds
- Fix re-exported config errors in webhook API routes by defining config locally
  - alby/webhook.ts
  - btcpayserver/webhook.ts
  - paypal/webhook.ts
  - stripepayment/webhook.ts

Turbopack requires route segment configs to be statically analyzable,
so they cannot be re-exported from other modules.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: update revalidateTag calls for Next.js 16.1 API changes

Next.js 16.1 changed the revalidateTag signature to require a second
argument (profile). Using 'max' profile as recommended by Next.js docs
for stale-while-revalidate semantics.

Updated files:
- app/(booking-page-wrapper)/team/[slug]/[type]/actions.ts
- app/(use-page-wrapper)/(main-nav)/teams/actions.ts
- app/(use-page-wrapper)/settings/(settings-layout)/developer/api-keys/actions.ts
- app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/actions.ts
- app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/actions.ts
- app/cache/travelSchedule.ts

Also cleaned up next.config.js turbopack config formatting.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: add profile parameter to revalidateTag in membership.ts for Next.js 16.1

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix routing form

* refactor: rename middleware.ts to proxy.ts per Next.js 16 migration

Next.js 16 has deprecated the 'middleware' file convention and renamed it to 'proxy'.
This change follows the official migration guide:
https://nextjs.org/docs/messages/middleware-to-proxy

Changes:
- Renamed apps/web/middleware.ts to apps/web/proxy.ts
- Renamed the middleware function to proxy
- Updated CODEOWNERS reference
- Updated comment reference in logout page

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-12-21 16:31:48 -03:00
Eesh MidhaandDhairyashil Shinde 4b4565b19d fix: constrain AddActionDialog overflow and footer behavior (#26087)
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2025-12-21 17:08:37 +00:00
Keith WilliamsandDevin AI 2da6f5b3b1 fix: remove labeled trigger to prevent workflow reruns on non-E2E labels (#26099)
When any label is added to a PR, the workflow was re-running and causing issues
because the E2E check would fail for non-ready-for-e2e labels.

This change removes 'labeled' from the trigger types entirely. Users can now:
1. Add the ready-for-e2e label to a PR
2. Click 'Re-run all jobs' in GitHub Actions UI to trigger E2E tests

This is simpler and avoids the complexity of trying to maintain previous check
status when non-E2E labels are added.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-21 15:54:35 +00:00
Volnei Munhoz d61a613911 fix: use idiomatic dorny/paths-filter exclusion pattern (#26097) 2025-12-21 14:49:43 +00:00
Keith WilliamsandDevin AI 09f2aa113b fix: resolve lint warnings in web app (#26017)
* fix: resolve lint warnings in web app

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: remove unused isReadPermission variable

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: remove unused loading state entirely

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-21 14:38:27 +00:00
f7bb2683cb ci: skip Playwright install on cache hit (#26060)
* update

* Add cache-db action to e2e workflow

* fix: remove cache-db from prepare job (requires Postgres service)

The prepare job doesn't have a Postgres service container, so cache-db
was failing with 'connection refused' when trying to run psql.

cache-db should only run in the e2e shards which have the Postgres service.

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

* update

* Update e2e.yml

* Add newline at end of e2e.yml file

* fix

* Fix indentation in e2e.yml for retention-days

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-21 10:58:41 -03:00
83ee986bbd fix: lock package versions and organize devDependencies (#26095)
* fix: lock package versions to exact versions from yarn.lock

Replace version ranges (^, ~) with exact resolved versions from yarn.lock
to ensure consistent dependency resolution across all environments.

This change affects 26 package.json files with 89 version updates including:
- TypeScript: ^5.9.0-beta -> 5.9.2
- Zod: ^3.22.4 -> 3.25.76
- React: ^18 -> 18.2.0
- Various Radix UI, Vite, PostCSS, and other dependencies

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

* fix: preserve npm alias format for @radix-ui packages

The previous commit incorrectly converted npm aliases like
'npm:@radix-ui/react-dialog@^1.0.4' to just '1.0.4', which broke
yarn install as it tried to find non-existent packages.

This fix restores the npm alias format while keeping the pinned versions:
- @radix-ui/react-dialog-atoms: npm:@radix-ui/react-dialog@1.0.4
- @radix-ui/react-tooltip-atoms: npm:@radix-ui/react-tooltip@1.0.6

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

* refactor: move dev dependencies to devDependencies section

Move 26 dependencies that are clearly development-only to the
devDependencies section across 10 packages:

- Testing: @types/jest, jest, ts-jest, @golevelup/ts-jest
- Build tools: typescript, ts-node, concurrently, dotenv-cli
- Linting: eslint-*, eslint-config-*, eslint-plugin-*
- Types: @types/express, @types/turndown, @types/uuid

This improves dependency organization and ensures production builds
don't include unnecessary development dependencies.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-12-21 10:18:07 -03:00
Dhairyashil Shinde c8af45537a fix: three dots icon missing for ios availability (#26094) 2025-12-21 12:54:13 +00:00
19ac0139c1 fix: Remove all links legacyBehavior (#25585)
* Remove all links legacyBehavior

* fix: resolve type errors in Button and Dropdown when using Link without legacyBehavior

- Button.tsx: Only pass ref to button element, not to Link (Link manages its own anchor)
- Dropdown.tsx: Strip ref from props when using Link to avoid type incompatibility

This fixes the type errors that were causing API V1 and V2 builds to fail.

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

* fix: only pass disabled and type props to button element, not to Link

Link component doesn't accept disabled or type props, so these should only be passed when rendering a button element.

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

* fix: strip ref from passThroughProps when rendering Link

The passThroughProps spread was including a ref property that's incompatible with Link's expected ref type. This strips the ref when rendering a Link component.

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

* fix: use type assertion for React.createElement props to handle union types

The Button component uses a union type for props that can be either Link or button props. TypeScript can't narrow the union type properly when using React.createElement with a dynamic element type, so we use a type assertion to cast the props to the correct type.

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

* fix: render Link and button separately to avoid type conflicts

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

* fix: preserve data-testid when rendering Button as Link

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

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-21 12:40:27 +00:00
Dhairyashil Shinde 067fa7e137 feat(companion): Add Google Calendar mark no-show functionality (#26085) 2025-12-21 13:30:31 +01:00
Beto aa127b2c2b refactor: (companion) (iOS) create an ios screen for specific native logic (#26081)
* create an ios screen for specific native logic

* feat: update bookings layout to conditionally show header on iOS

* feat: add useActiveBookingFilter hook for managing booking filter state

- Introduced a new hook, useActiveBookingFilter, to manage the active booking filter state and provide related utilities.
- Added filter options for "upcoming", "unconfirmed", "past", and "cancelled" bookings.
- Updated index.ts to export the new hook and its types for UI state management.

* refactor: streamline booking filter management and remove unused code

- Integrated the useActiveBookingFilter hook to manage booking filter state, simplifying the filter logic.
- Removed redundant state variables and functions related to booking filters.
- Cleaned up the code by eliminating unused imports and commented-out code sections.
- Updated the booking fetching logic to utilize the new filter parameters directly.

* refactor: move meeting URL extraction and info retrieval to utility module

- Extracted meeting URL extraction and meeting info retrieval logic into a new utility module, meetings-utils.ts.
- Updated index.ios.tsx and index.tsx to utilize the new getMeetingInfo function from the utility module, simplifying the component code.
- Removed redundant helper functions from the component files to enhance maintainability.

* refactor: extract empty state content logic into utility module

- Moved the getEmptyStateContent function from the Bookings component files to a new utility module, bookings-utils.ts, to enhance code reusability and maintainability.
- Updated index.ios.tsx and index.tsx to utilize the new utility function, simplifying the component code and improving clarity.
- Removed redundant empty state logic from the component files.

* refactor: enhance booking filtering and utility functions

- Updated the Bookings component to utilize new utility functions for filtering bookings by event type and search queries, improving code clarity and maintainability.
- Moved formatting functions (formatTime, formatDate, formatMonthYear, getMonthYearKey) and grouping logic (groupBookingsByMonth) to the bookings-utils module, centralizing related functionality.
- Removed redundant code from the Bookings component files, streamlining the overall structure.

* refactor: implement booking actions hook for improved management

- Introduced a new `useBookingActions` hook to centralize booking-related actions such as rescheduling, confirming, and rejecting bookings, enhancing code organization and reusability.
- Updated the `Bookings` component to utilize the new hook, simplifying the component logic and reducing redundancy.
- Removed outdated state management and functions related to booking actions, streamlining the overall code structure.

* refactor: consolidate booking components and enhance structure

- Refactored the Bookings component to utilize a new BookingListScreen component, improving code organization and readability.
- Introduced BookingListItem for individual booking rendering, streamlining the rendering logic and enhancing maintainability.
- Removed redundant state management and functions from the Bookings component, simplifying the overall structure.
- Updated modal handling for booking actions, rescheduling, and rejection to improve user experience and code clarity.

* feat: add BookingListItem component for enhanced booking display

- Introduced a new BookingListItem component for rendering individual booking details, improving modularity and reusability.
- Implemented context menu actions for booking management, including rescheduling, editing location, and adding guests.
- Updated type definitions for BookingListItemProps to include additional action handlers for better functionality.
- Refactored existing BookingListItem component to utilize the new props structure, enhancing clarity and maintainability.

* refactor: simplify layout and styling in BookingListItem and BookingListScreen components

- Updated the BookingListItem component to streamline the layout by reducing padding and adjusting the gap between elements.
- Modified the BookingListScreen component to remove unnecessary horizontal padding in the content container style, enhancing the overall layout consistency.
2025-12-21 11:38:42 +00:00
51bce6763f refactor: split tRPC build into server and react phases (#26082)
* refactor: import AppRouter from generated types instead of server source

This change improves tRPC build performance by having the client-side code
import AppRouter from pre-generated type declarations instead of traversing
the entire server router tree.

Changes:
- Create type bridge file at packages/trpc/types/app-router.ts
- Update packages/trpc/react/trpc.ts to import from the bridge
- Update .gitignore to only ignore types/server (generated files)
- Update eslint.config.mjs to only ignore types/server (generated files)

The type bridge provides:
1. Faster typechecking - avoids parsing 458 server files
2. Stable import location that's easy to lint against
3. Single place to adjust if generated path changes

Build order is already enforced in turbo.json (type-check depends on @calcom/trpc#build).

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: move bridge file to react/ to avoid TS5055 error

Move the AppRouter type bridge file from types/app-router.ts to react/app-router.ts
to avoid the TS5055 'Cannot write file because it would overwrite input file' error.

The issue was that placing the bridge file in types/ caused TypeScript to treat
the generated .d.ts files as input files during the tRPC build, then fail when
trying to emit to the same location.

By placing the bridge in react/ (which is excluded from the tRPC server build),
the bridge file is only used by client code and doesn't interfere with the
server type generation.

Changes:
- Move bridge file from types/app-router.ts to react/app-router.ts
- Update import in react/trpc.ts to use ./app-router
- Revert .gitignore to ignore all of types/ (generated files)
- Revert eslint.config.mjs to ignore all of types/**

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* refactor: split tRPC build into server and react phases

- Create tsconfig.server.json for server-only type generation
- Create tsconfig.react.json for react/client type generation
- Update build script to run server build first, then react build
- Remove || true so build properly fails on errors
- This allows react code to import from generated server types

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* refactor: split @calcom/trpc exports to separate server and react entrypoints

- Remove react exports from @calcom/trpc root (index.ts)
- Update 89 files to import from @calcom/trpc/react instead of @calcom/trpc
- This fixes the boundary leak where server builds were pulling in react code
- Server build no longer compiles react/app-router.ts, fixing the chicken-and-egg
  issue where react code needed generated server types that didn't exist yet

This improves TypeScript build performance by preventing the server type
generation from traversing the entire react/client type graph.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: import WorkflowType from lib/types instead of React component

This fixes a boundary leak where the server build was pulling in React
components through the WorkflowRepository import chain. By importing
WorkflowListType from lib/types instead of WorkflowListPage.tsx, the
server build no longer traverses React component files.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: extract server-safe types to prevent boundary leaks in tRPC build

- Extract ChildrenEventType to lib/childrenEventType.ts (server-safe)
- Extract Slots type to calendars/lib/slots.ts (server-safe)
- Create types.server.ts files for eventtypes and bookings
- Update server code to import from server-safe type files
- Update DatePicker.tsx to use extracted Slots type
- Update app-store utils to use BookerEventForAppData type

This prevents the server build from pulling in React files through
transitive imports from @calcom/features barrel exports.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: update Segment.test.tsx mock path to @calcom/trpc/react

The test was mocking @calcom/trpc but importing from @calcom/trpc/react.
After the entrypoint separation, the mock path needs to match the import path.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: temporarily restore || true to unblock PR merge

The pre-existing Prisma type errors (~345 errors) will be addressed in a follow-up PR.
This allows the two-phase build architecture changes to be merged first.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* Run trpc build as part of API v2 build

* Removed the bridge file

* refactor: extract event type schemas to server-safe file

- Create packages/features/eventtypes/lib/schemas.ts with createEventTypeInput and EventTypeDuplicateInput
- Update types.ts to re-export schemas from the new server-safe location
- Update tRPC schema files to import from schemas.ts instead of types.server.ts
- Delete types.server.ts (was duplicating ~200 lines unnecessarily)

This keeps the server build graph clean while avoiding code duplication.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* Removed the optionality of the tRPC builds

* Removed the extra command for API v2

* refactor: rename calendars/lib/slots.ts to types.ts

Per Keith's feedback, renamed the file to types.ts since it contains type definitions.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* Added back tRPC build:server for API v2

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2025-12-20 23:43:04 -03:00
Keith WilliamsandDevin AI 0e3696f20c chore: dedupe yarn.lock to remove duplicate package versions (#26068)
* chore: dedupe yarn.lock to remove duplicate package versions

- Consolidated 762 duplicate package versions
- Reduced yarn.lock from 51,211 lines (1.7MB) to 44,471 lines (1.5MB)
- Removed 7,071 lines (~200KB) of redundant dependency entries
- Major packages deduplicated include: resolve, semver, type-fest, commander, glob, lru-cache, dotenv, @babel/* packages, typescript, tslib, postcss, and many more

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: resolve type errors in managed event types and react-select components

- Add Zod-compatible versions of allManagedEventTypeProps and unlockedManagedEventTypeProps
  that only include scalar fields (excludes Prisma relation fields)
- Update handleChildrenEventTypes.ts and queries.ts to use the new Zod-compatible props
- Fix react-select CSS type errors in Select.tsx files by using Object.assign
  instead of spread operator to avoid TypeScript type inference issues
- Fix lint warning in updateNewTeamMemberEventTypes by using if statement

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: derive eventType parameter type from actual query result

Use Awaited<ReturnType<typeof getEventTypesToAddNewMembers>>[number] to ensure
type safety at call sites, avoiding Prisma type namespace mismatches.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: disable turbo cache for @calcom/trpc#build to prevent stale type errors

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: correct field names in API v1 validation schemas

- Remove timeZone from booking schema (field doesn't exist on Booking model)
- Remove bookingId from destination-calendar schema (field doesn't exist, only booking relation)
- Change avatar to avatarUrl in user schema (correct field name)

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: use Object.assign for react-select styles to fix TypeScript spread type errors

The spread operator causes TypeScript to compute an incompatible type with
CSSObjectWithLabel due to the accentColor property. Using Object.assign
preserves the correct type inference.

Fixed files:
- FormEdit.tsx
- DestinationCalendarSelector.tsx (features and platform)
- TimezoneSelect.tsx
- ApiKeyDialogForm.tsx
- Select.tsx (features/form)
- WebhookForm.tsx

Also fixed pre-existing lint warnings:
- Constant truthiness in label assignment
- Unused variant parameter

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: remove extra fields from allManagedEventTypePropsForZod to match original behavior

The Zod-compatible version should only include scalar fields that were in the
original allManagedEventTypeProps. Removed instantMeetingScheduleId, profileId,
rrSegmentQueryValue, and assignRRMembersUsingSegment which were incorrectly
added and caused test failures by including extra fields in Prisma update payloads.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: remove access to fields not in Zod schema

- Remove unnecessary destructuring of profileId and instantMeetingScheduleId
  from managedEventTypeValues in handleChildrenEventTypes.ts (these fields
  are already sourced from other variables)
- Set rrSegmentQueryValue to undefined directly in queries.ts instead of
  accessing it from managedEventTypeValues (not applicable for managed children)

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: add name property to mock credentials and revert turbo cache change

- Add name property to MockCredential type and factory function in
  InstallAppButtonChild.test.tsx to match expected credentials type
- Revert turbo cache disable for @calcom/trpc#build (per review comment)

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: revert API v1 validation changes and restore eslint comment

- Revert API v1 validation changes (booking.ts, destination-calendar.ts,
  user.ts) since API v1 is deprecated and these could be breaking changes
- Restore eslint-disable comment for @typescript-eslint/no-empty-function
  in Select.tsx that was accidentally removed

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: add inputs to @calcom/trpc#build to properly invalidate cache

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: properly fix API v1 validation schemas for stricter zod 3.25.76

The yarn dedupe upgraded zod from 3.22.4 to 3.25.76, which has stricter
.pick() typing that now properly rejects picking non-existent fields.

Changes:
- user.ts: Pick avatarUrl (actual Prisma field) and extend with avatar
  for API v1 backward compatibility
- booking.ts: Remove timeZone from pick (not a field on Booking model,
  only exists in nested attendees/user objects)
- destination-calendar.ts: Remove bookingId from pick (not a field on
  DestinationCalendar model)

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: remove turbo.json inputs for @calcom/trpc#build to use cached artifacts

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: use robust selector for react-select option in routing forms E2E test

The previous selector used .nth(1) which assumed the email text appeared
exactly twice in the DOM in a specific order. This broke when react-select
was upgraded from 5.7.2 to 5.8.0 via yarn dedupe.

The new approach waits for the react-select listbox to appear and clicks
the option within it, which is more robust against DOM structure changes.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-20 23:30:03 -03:00
github-actions[bot] f846126a2d chore: version packages (#25833)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-20 22:05:28 +00:00
Keith WilliamsandDevin AI 825f55c0a1 fix: extract shared types to non-React modules to fix circular dependencies (#26083)
- Move InvalidAppCredentialBannerProps to packages/features/users/types/invalidAppCredentials.ts
- Add WorkflowListType to packages/features/ee/workflows/lib/types.ts
- Update server file imports to use new type locations
- Update React component imports to re-export from new locations

This fixes circular dependencies where server files were importing from React
component modules that import from @calcom/trpc, creating:
server -> component -> @calcom/trpc -> react -> server (circular)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-21 01:43:49 +05:30
Pedro Castro 9ae2381755 fix(auth): validate IdP authority before SAML account linking (#26005)
* fix(auth): validate IdP authority before SAML account linking

Adds verification that SAML IdP is authoritative for the email domain
before allowing account conversion

* fix(auth): deny by default on missing SAML tenant + optimize membership query

 - Block account conversion when tenant is missing (deny by default)
 - Replace JOIN + ILIKE with two indexed lookups for O(1) performance

* refactor(auth): apply data minimization to security logs
2025-12-20 17:45:18 +00:00
Anik Dhabal Babu a37805439b fix: fix credential type mismatch in AppPage component (#25802)
* test

* update

* Refactor create-event-type.input.ts for slug handling

Removed unused import and transformation for slug.

* Remove slugify transformation from slug field

Removed slugify transformation from slug property.

* Remove slugify transformation from slug field

Removed slugify transformation from slug property.
2025-12-20 17:11:35 +00:00
253c33df33 perf: (googlecalendar): batch freebusy calls by delegation credential (#24332)
* perf(googlecalendar): batch freebusy calls by delegation credential

- Group selectedCalendars by delegationCredentialId before making API calls
- Make one batched freebusy query per delegation credential group
- Reduces total API calls while respecting credential boundaries
- Maintains existing caching behavior per group
- Updated both getAvailability and getAvailabilityWithTimeZones methods
- Added groupCalendarsByDelegationCredential helper method
- Handles edge case when no calendars provided but fallbackToPrimary is true
- Fixed linting issue: replaced hasOwnProperty with 'in' operator

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

* test(googlecalendar): add test for delegation credential batching

- Verify calendars are grouped by delegationCredentialId
- Ensure exactly 3 API calls made for 3 delegation credential groups
- Confirm all busy times from different groups are properly returned
- Fix type-safety issues by replacing 'as any' with proper type constraints
- Fix ESLint warnings: unused variables and any types in mock functions

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

* fix(googlecalendar): ensure fallback logic works with empty calendar groups

- Handle empty calendar groups by ensuring at least one iteration
- Add test for chunking groups larger than 50 calendars
- Verify all delegation credential batching logic works correctly

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

* fix: fix remaining type errors from merge conflict resolution

- Changed getCacheOrFetchAvailability to getFreeBusyData in getAvailabilityWithTimeZones
- Removed orphaned merge conflict marker in test file

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

* fix: reset test file to original PR version and update method name

- Reset CalendarService.test.ts to original PR version (0e9eb9e97a)
- Updated getCacheOrFetchAvailability to getFreeBusyData to match main branch

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

* fix: reset test file to main branch version

The original PR's test file had tests for caching features that have been
removed from main. Reset to main's version to fix type errors.

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

* test(googlecalendar): add tests for delegation credential batching logic

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

* Revamp devin's implementation

* Remove comsoles.log

* fix(tests): update delegation credential batching tests to match reimplementation

- Remove tests for private methods that no longer exist (groupCalendarsByDelegationCredential, chunkArray)
- Update getAvailability test to verify calendar fetching without expecting multiple API calls per delegation credential
- Keep existing tests for fallback to primary calendar and non-google calendar handling

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

* Remove dead code

* improve documentation

* docs: add README explaining Google Calendar availability batching feature

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

* docs: translate README to English

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

* docs: move README to calendar-batch package with comprehensive documentation

- Remove README from googlecalendar lib (wrong location)
- Add comprehensive README to packages/features/calendar-batch/
- Document CalendarBatchService and CalendarBatchWrapper
- Explain how getCalendar() integrates with batching
- Include architecture, data model, and performance considerations

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

* fix: address cubic-dev-ai review comments

- Remove misleading comment from getCalendar.ts cache block
- Fix typo 'optmization' -> 'optimization' in comment
- Add comprehensive tests for CalendarBatchWrapper batching behavior
  - Test separate calls for calendars without delegationCredentialId
  - Test batching calendars with same delegationCredentialId
  - Test chunking into groups of 50 for API limits
  - Test mixed calendars handling
  - Test fallbackToPrimary with empty array
  - Test result flattening from batched calls
  - Test pass-through methods delegation

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

* fix: add shouldServeCache param to CalendarBatchWrapper and verify batching call count

- Fix CalendarBatchWrapper.getAvailability signature to match Calendar interface (add shouldServeCache param)
- Update CalendarBatchWrapper tests to pass shouldServeCache parameter
- Add integration test in CalendarService.test.ts that verifies CalendarBatchWrapper makes separate API calls for different delegationCredentialIds (call count assertion)
- This fixes the getCalendarsEvents test failures caused by signature mismatch

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

* Improve CalendarBatchImplementation

* test: add shouldServeCache forwarding and order-independent batching verification tests

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

* test: make CalendarBatchWrapper tests order-independent

Refactored tests to avoid relying on Promise.all execution order:
- 'should make separate calls for calendars without delegationCredentialId'
  now uses set comparison instead of toHaveBeenNthCalledWith
- 'should batch calendars with the same delegationCredentialId together'
  now finds calls by delegation credential instead of call order

This addresses Sean's review comment about potential flakiness due to
Promise.all not guaranteeing execution order of parallel promises.

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

* Remove hardcoded ID

* test: add comprehensive tests for resolveCalendarServeStrategy

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* feat: use Promise.allSettled for partial failure handling in CalendarBatchWrapper

- Changed Promise.all to Promise.allSettled in getAvailability and getAvailabilityWithTimeZones
- Returns partial results when some batches fail instead of failing entirely
- Logs warnings for failed batches with error details
- Added tests for partial failure scenarios

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* test: add comprehensive tests for getAvailabilityWithTimeZones

- Added batching behavior tests (separate calls, batching by delegationCredentialId, chunking)
- Added partial failure handling tests (partial results, all fail, no throw)
- Added edge case test for underlying calendar not implementing the method
- Total: 24 tests now covering both getAvailability and getAvailabilityWithTimeZones

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: add required serviceAccountKey fields to delegatedTo mock objects

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: add missing client_id and private_key to serviceAccountKey mock

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: keith@cal.com <keithwillcode@gmail.com>
2025-12-20 13:45:44 -03:00
ed0c9392a9 fix: E2E flakes after splitting the tests (#26059)
* fix: split confirm-emails e2e test to authenticate as booking owner

The test was flaky because it authenticated as an admin user (authEmail)
but created bookings for different users (emailsEnabledSetup.user and
emailsDisabledSetup.user). When confirming/declining bookings, the
ApiAuthGuard + BookingUidGuard rejected requests with 401 because the
authenticated user wasn't the booking owner.

The fix splits the test into two separate describe blocks, each with its
own app instance that authenticates as the actual booking owner. This
ensures the authenticated user always matches the booking owner.

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

* fix: add unauthenticated app for attendee reschedule test

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

* fix: use real API key authentication instead of withApiAuth mock

- Replace withApiAuth mock with real API key authentication using ApiKeysRepositoryFixture
- This avoids Passport strategy registration conflicts between test suites
- For attendee reschedule test, use unauthenticated request (no auth header) since endpoint uses OptionalApiAuthGuard
- Remove separate unauthenticatedApp instance as it's no longer needed

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

* fix flakes

* update

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-20 13:04:21 -03:00
Anik Dhabal Babu 0098b076a1 fix: cache build error on CI (#25958)
* fix(slots): hide out-of-office slots across timezones

* update

* fix build error
2025-12-20 15:24:56 +00:00
sean-brydon 95a4567f35 perf: use compound key (#25970) 2025-12-20 14:57:55 +00:00
spandevandDhairyashil Shinde d8ddb466b0 fix: slots in email embed sorted (#25676)
* fix: slots in email embed sorted

* fix: removed mutating of original array

* fix: incorrect access of sortedTimes

---------

Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2025-12-20 13:12:50 +00:00
Benny Joo 6e5b812286 fix: add missing Korean translation (#26075) 2025-12-20 15:02:03 +05:30
Alex van Andelandcubic-dev-ai[bot] 8568cab283 fix: Handle error when no default calendar exists for a o365 account (#26074)
* fix: Handle error when no default calendar exists for a o365 account

* Update packages/app-store/office365calendar/api/callback.ts

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

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-12-20 06:33:46 +00:00
694 changed files with 30066 additions and 29461 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"@calcom/atoms": minor
---
feat: Booker accepts rrHostSubsetIds to specify round robin hosts
+4 -1
View File
@@ -509,4 +509,7 @@ SEED_PLATFORM_OAUTH_CLIENT_SECRET=
ENABLE_ASYNC_TASKER="false" # set to "true" to enable
TRIGGER_SECRET_KEY=
TRIGGER_API_URL=https://api.trigger.dev
TRIGGER_DEV_PROJECT_REF=
TRIGGER_DEV_PROJECT_REF=
GOOGLE_ADS_ENABLED=1 # To enable Google Ads tracking (gclid)
LINKEDIN_ADS_ENABLED=1 # To enable LinkedIn Ads tracking (li_fat_id)
+2 -2
View File
@@ -7,7 +7,7 @@
/apps/web/lib/core/**/* @calcom/Foundation
/apps/web/lib/booking.ts @calcom/Foundation
/apps/web/lib/handleOrgRedirect.ts @calcom/Foundation
/apps/web/middleware.ts @calcom/Foundation
/apps/web/proxy.ts @calcom/Foundation
/deploy/**/* @calcom/Foundation
/packages/app-store/applecalendar/**/* @calcom/Foundation
/packages/app-store/caldavcalendar/**/* @calcom/Foundation
@@ -28,7 +28,7 @@
/packages/features/bookings/lib/getLuckyUser.ts @calcom/Foundation
/packages/features/schedules/lib/slots.ts @calcom/Foundation
/packages/platform/atoms/**/*WebWrapper.tsx @calcom/Consumer
/packages/platform/**/* @calcom/Platform @calcom/Foundation
/packages/platform/**/* @calcom/Consumer @calcom/Foundation
/packages/prisma/**/* @calcom/Foundation
/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @calcom/Foundation
/packages/trpc/server/routers/viewer/bookings/get.handler.ts @calcom/Foundation
@@ -17,5 +17,6 @@ runs:
${{ github.workspace }}/node_modules/playwright
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
- name: Yarn playwright install
if: steps.playwright-cache.outputs.cache-hit != 'true'
shell: bash
run: yarn playwright install --with-deps
+212
View File
@@ -0,0 +1,212 @@
name: E2E Report
on:
workflow_run:
workflows: ["PR Update"]
types:
- completed
workflow_dispatch:
inputs:
pr_number:
description: 'The PR number to generate report for'
required: true
type: string
jobs:
get-pr-info:
name: Get PR Info
runs-on: ubuntu-latest
# For workflow_run: only run if the triggering workflow failed
# For workflow_dispatch: always run (inputs are provided manually)
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'failure' }}
permissions:
actions: read
pull-requests: read
outputs:
pr-number: ${{ steps.get-info.outputs.pr-number }}
head-branch: ${{ steps.get-info.outputs.head-branch }}
has-blob-reports: ${{ steps.get-info.outputs.has-blob-reports }}
source-run-id: ${{ steps.get-info.outputs.source-run-id }}
source-run-number: ${{ steps.get-info.outputs.source-run-number }}
source-run-attempt: ${{ steps.get-info.outputs.source-run-attempt }}
source-head-sha: ${{ steps.get-info.outputs.source-head-sha }}
steps:
- name: Get PR and run info
id: get-info
uses: actions/github-script@v7
with:
script: |
const isDispatch = context.eventName === 'workflow_dispatch';
if (isDispatch) {
// For workflow_dispatch: look up everything from PR number
const prNumber = context.payload.inputs.pr_number;
console.log('Looking up PR:', prNumber);
// Get PR details
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: parseInt(prNumber)
});
const headSha = pr.head.sha;
const headBranch = pr.head.ref;
console.log('PR head SHA:', headSha);
console.log('PR head branch:', headBranch);
// Find the "PR Update" workflow
const { data: workflows } = await github.rest.actions.listRepoWorkflows({
owner: context.repo.owner,
repo: context.repo.repo
});
const prUpdateWorkflow = workflows.workflows.find(w => w.name === 'PR Update');
if (!prUpdateWorkflow) {
core.setFailed('Could not find "PR Update" workflow');
return;
}
console.log('Found PR Update workflow ID:', prUpdateWorkflow.id);
// Find the most recent completed run for this branch with blob-report artifacts
// We search by branch instead of SHA to handle cases where:
// 1. The PR was updated after E2E tests ran (artifacts exist for older SHA)
// 2. The current run for the SHA is still queued/in-progress
const { data: runs } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: prUpdateWorkflow.id,
branch: headBranch,
status: 'completed',
per_page: 20
});
console.log('Found', runs.workflow_runs.length, 'completed runs for branch', headBranch);
// Find a run with blob-report artifacts
let selectedRun = null;
let hasBlobReports = false;
for (const run of runs.workflow_runs) {
console.log('Checking run', run.id, 'conclusion:', run.conclusion);
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: run.id
});
const blobReports = artifacts.artifacts.filter(a => a.name.startsWith('blob-report-'));
if (blobReports.length > 0) {
console.log('Found run with blob reports:', run.id, 'artifacts:', blobReports.map(a => a.name));
selectedRun = run;
hasBlobReports = true;
break;
}
}
if (!selectedRun) {
console.log('No runs found with blob-report artifacts');
core.setOutput('pr-number', prNumber);
core.setOutput('head-branch', headBranch);
core.setOutput('source-head-sha', headSha);
core.setOutput('has-blob-reports', 'false');
core.setOutput('source-run-id', '');
core.setOutput('source-run-number', '');
core.setOutput('source-run-attempt', '');
return;
}
core.setOutput('pr-number', prNumber);
core.setOutput('head-branch', headBranch);
core.setOutput('source-head-sha', headSha);
core.setOutput('source-run-id', selectedRun.id.toString());
core.setOutput('source-run-number', selectedRun.run_number.toString());
core.setOutput('source-run-attempt', selectedRun.run_attempt.toString());
core.setOutput('has-blob-reports', 'true');
return;
}
// For workflow_run: extract from payload
const workflowRun = context.payload.workflow_run;
const headSha = workflowRun.head_sha;
const headBranch = workflowRun.head_branch;
console.log('Head SHA:', headSha);
console.log('Head Branch:', headBranch);
// Set source run info
core.setOutput('source-run-id', workflowRun.id.toString());
core.setOutput('source-run-number', workflowRun.run_number.toString());
core.setOutput('source-run-attempt', workflowRun.run_attempt.toString());
core.setOutput('source-head-sha', headSha);
// Check for blob report artifacts
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: workflowRun.id
});
const blobReports = artifacts.artifacts.filter(a => a.name.startsWith('blob-report-'));
console.log('Found blob reports:', blobReports.map(a => a.name));
core.setOutput('has-blob-reports', blobReports.length > 0 ? 'true' : 'false');
// Try to get PR number from workflow_run payload first
if (workflowRun.pull_requests && workflowRun.pull_requests.length > 0) {
const prNumber = workflowRun.pull_requests[0].number;
console.log('PR number from payload:', prNumber);
core.setOutput('pr-number', prNumber.toString());
core.setOutput('head-branch', headBranch);
return;
}
// Fall back to API lookup
try {
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: headSha
});
if (prs.length === 0) {
console.log('No PRs found for commit');
core.setOutput('pr-number', '');
core.setOutput('head-branch', headBranch);
return;
}
// Find open PR or use first one
const openPr = prs.find(pr => pr.state === 'open') || prs[0];
console.log('PR number from API:', openPr.number);
core.setOutput('pr-number', openPr.number.toString());
core.setOutput('head-branch', openPr.head.ref);
} catch (e) {
console.log('Error fetching PR:', e);
core.setOutput('pr-number', '');
core.setOutput('head-branch', headBranch);
}
merge-reports:
name: Merge reports
needs: [get-pr-info]
if: ${{ needs.get-pr-info.outputs.has-blob-reports == 'true' && needs.get-pr-info.outputs.pr-number != '' }}
uses: ./.github/workflows/merge-reports.yml
with:
source_run_id: ${{ needs.get-pr-info.outputs.source-run-id }}
source_run_number: ${{ needs.get-pr-info.outputs.source-run-number }}
source_run_attempt: ${{ needs.get-pr-info.outputs.source-run-attempt }}
source_head_sha: ${{ needs.get-pr-info.outputs.source-head-sha }}
# No secrets: inherit - merge-reports only needs the default GITHUB_TOKEN for artifact download
publish-report:
name: Publish HTML report
needs: [get-pr-info, merge-reports]
if: ${{ needs.get-pr-info.outputs.pr-number != '' }}
uses: ./.github/workflows/publish-report.yml
with:
head_branch: ${{ needs.get-pr-info.outputs.head-branch }}
source_run_id: ${{ needs.get-pr-info.outputs.source-run-id }}
source_run_number: ${{ needs.get-pr-info.outputs.source-run-number }}
source_run_attempt: ${{ needs.get-pr-info.outputs.source-run-attempt }}
pr_number: ${{ needs.get-pr-info.outputs.pr-number }}
secrets: inherit
+22 -2
View File
@@ -1,17 +1,37 @@
# https://playwright.dev/docs/test-sharding#merging-reports-from-multiple-shards
on:
workflow_call:
inputs:
source_run_id:
description: 'The run ID to download artifacts from (for workflow_run triggered calls)'
required: true
type: string
source_run_number:
description: 'The run number for artifact naming'
required: true
type: string
source_run_attempt:
description: 'The run attempt for artifact naming'
required: true
type: string
source_head_sha:
description: 'The head SHA to checkout for Playwright version consistency'
required: true
type: string
jobs:
merge-reports:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
ref: ${{ inputs.source_head_sha }}
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- name: Download blob reports from GitHub Actions Artifacts
uses: actions/download-artifact@v4
with:
run-id: ${{ inputs.source_run_id }}
github-token: ${{ github.token }}
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
@@ -20,6 +40,6 @@ jobs:
- name: Upload HTML report
uses: actions/upload-artifact@v4
with:
name: html-report--attempt-${{ github.run_number }}-${{ github.run_attempt }}
name: html-report--attempt-${{ inputs.source_run_number }}-${{ inputs.source_run_attempt }}
path: playwright-report
retention-days: 14
+76 -88
View File
@@ -2,7 +2,7 @@ name: PR Update
on:
pull_request_target:
types: [opened, synchronize, reopened, labeled]
types: [opened, synchronize, reopened]
branches:
- main
- gh-actions-test-branch
@@ -17,8 +17,8 @@ concurrency:
cancel-in-progress: true
jobs:
changes:
name: Detect changes
prepare:
name: Prepare
runs-on: buildjet-2vcpu-ubuntu-2204
permissions:
pull-requests: read
@@ -26,43 +26,44 @@ jobs:
has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }}
has_companion: ${{ steps.filter.outputs.has_companion }}
commit-sha: ${{ steps.get_sha.outputs.commit-sha }}
run-e2e: ${{ steps.check-if-pr-has-label.outputs.run-e2e == 'true' }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
- uses: dorny/paths-filter@v3
id: filter
with:
predicate-quantifier: "every"
filters: |
has-files-requiring-all-checks:
- "!(companion/**|**.md|**.mdx|.github/CODEOWNERS|docs/**|help/**|apps/web/public/static/locales/**/common.json|i18n.lock)"
- '**'
- '!companion/**'
- '!**/*.md'
- '!**/*.mdx'
- '!.github/CODEOWNERS'
- '!docs/**'
- '!help/**'
- '!apps/web/public/static/locales/**/common.json'
- '!i18n.lock'
has_companion:
- "companion/**"
- name: Get Latest Commit SHA
id: get_sha
run: |
echo "commit-sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
check-label:
needs: [changes]
runs-on: buildjet-2vcpu-ubuntu-2204
name: Check for E2E label
permissions:
pull-requests: read
outputs:
run-e2e: ${{ steps.check-if-pr-has-label.outputs.run-e2e == 'true' && (github.event.action != 'labeled' || github.event.label.name == 'ready-for-e2e') }}
steps:
- name: Check if PR exists with ready-for-e2e label for this SHA
id: check-if-pr-has-label
uses: actions/github-script@v7
with:
script: |
let labels = [];
let prNumber = null;
if (context.payload.pull_request) {
labels = context.payload.pull_request.labels;
prNumber = context.payload.pull_request.number;
} else {
try {
const sha = '${{ needs.changes.outputs.commit-sha }}';
const sha = '${{ steps.get_sha.outputs.commit-sha }}';
console.log('sha', sha);
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
@@ -76,132 +77,149 @@ jobs:
return;
}
const pr = prs[0];
console.log(`PR number: ${pr.number}`);
console.log(`PR title: ${pr.title}`);
console.log(`PR state: ${pr.state}`);
console.log(`PR URL: ${pr.html_url}`);
labels = pr.labels;
prNumber = prs[0].number;
}
catch (e) {
core.setOutput('run-e2e', false);
console.log(e);
return;
}
}
// Always fetch fresh PR data to get current labels
// This avoids stale label data from event payloads
try {
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
console.log(`PR number: ${pr.number}`);
console.log(`PR title: ${pr.title}`);
console.log(`PR state: ${pr.state}`);
console.log(`PR URL: ${pr.html_url}`);
labels = pr.labels;
}
catch (e) {
core.setOutput('run-e2e', false);
console.log(e);
return;
}
const labelFound = labels.map(l => l.name).includes('ready-for-e2e');
console.log('Found the label?', labelFound);
core.setOutput('run-e2e', labelFound);
deps:
name: Install dependencies
needs: [changes, check-label]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
needs: [prepare]
if: ${{ needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/yarn-install.yml
type-check:
name: Type check
needs: [changes, check-label, deps]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
needs: [prepare, deps]
if: ${{ needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/check-types.yml
secrets: inherit
lint:
name: Linters
needs: [changes, check-label, deps]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
needs: [prepare, deps]
if: ${{ needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/lint.yml
secrets: inherit
unit-test:
name: Tests
needs: [changes, check-label, deps]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
needs: [prepare, deps]
if: ${{ needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/unit-tests.yml
secrets: inherit
build-api-v1:
name: Production builds
needs: [changes, check-label, deps]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
needs: [prepare, deps]
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/api-v1-production-build.yml
secrets: inherit
build-api-v2:
name: Production builds
needs: [changes, check-label, deps]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
needs: [prepare, deps]
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/api-v2-production-build.yml
secrets: inherit
build-atoms:
name: Production builds
needs: [changes, check-label, deps]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
needs: [prepare, deps]
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/atoms-production-build.yml
secrets: inherit
build-docs:
name: Production builds
needs: [changes, check-label, deps]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
needs: [prepare, deps]
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/docs-build.yml
secrets: inherit
build-companion:
name: Companion builds
needs: [changes, check-label]
if: needs.changes.outputs.has_companion == 'true'
needs: [prepare]
if: needs.prepare.outputs.has_companion == 'true'
uses: ./.github/workflows/companion-build.yml
secrets: inherit
build:
name: Production builds
needs: [changes, check-label, deps]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
needs: [prepare, deps]
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/production-build-without-database.yml
secrets: inherit
integration-test:
name: Tests
needs: [changes, check-label, build, build-api-v1, build-api-v2]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
needs: [prepare, build, build-api-v1, build-api-v2]
if: ${{ needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/integration-tests.yml
secrets: inherit
e2e:
name: Tests
needs: [changes, check-label, build, build-api-v1, build-api-v2]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
needs: [prepare, build, build-api-v1, build-api-v2]
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e.yml
secrets: inherit
e2e-api-v2:
name: Tests
needs: [changes, check-label, build, build-api-v1, build-api-v2]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
needs: [prepare, build, build-api-v1, build-api-v2]
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e-api-v2.yml
secrets: inherit
e2e-app-store:
name: Tests
needs: [changes, check-label, build, build-api-v1, build-api-v2]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
needs: [prepare, build, build-api-v1, build-api-v2]
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e-app-store.yml
secrets: inherit
e2e-embed:
name: Tests
needs: [changes, check-label, build, build-api-v1, build-api-v2]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
needs: [prepare, build, build-api-v1, build-api-v2]
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e-embed.yml
secrets: inherit
e2e-embed-react:
name: Tests
needs: [changes, check-label, build, build-api-v1, build-api-v2]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
needs: [prepare, build, build-api-v1, build-api-v2]
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e-embed-react.yml
secrets: inherit
@@ -211,44 +229,14 @@ jobs:
uses: ./.github/workflows/nextjs-bundle-analysis.yml
secrets: inherit
merge-reports:
name: Merge reports
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
needs: [changes, check-label, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
uses: ./.github/workflows/merge-reports.yml
secrets: inherit
publish-report:
name: Publish HTML report
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
permissions:
contents: write
issues: write
pull-requests: write
needs: [changes, check-label, merge-reports]
uses: ./.github/workflows/publish-report.yml
secrets: inherit
cleanup-report:
name: Cleanup HTML report
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && (github.event.pull_request.merged == true || github.event.pull_request.state == 'closed') }}
permissions:
contents: write
issues: write
pull-requests: write
needs: [changes, check-label, publish-report]
uses: ./.github/workflows/cleanup-report.yml
secrets: inherit
required:
needs:
[
changes,
prepare,
lint,
type-check,
unit-test,
integration-test,
check-label,
build,
build-api-v1,
build-api-v2,
@@ -268,7 +256,7 @@ jobs:
run: exit 1
if: |
(
needs.changes.outputs.has-files-requiring-all-checks == 'true' &&
needs.prepare.outputs.has-files-requiring-all-checks == 'true' &&
(
needs.lint.result != 'success' ||
needs.type-check.result != 'success' ||
@@ -287,6 +275,6 @@ jobs:
)
) ||
(
needs.changes.outputs.has_companion == 'true' &&
needs.prepare.outputs.has_companion == 'true' &&
needs.build-companion.result != 'success'
)
+30 -9
View File
@@ -1,5 +1,26 @@
on:
workflow_call:
inputs:
head_branch:
description: 'The head branch name for report path'
required: true
type: string
source_run_id:
description: 'The source run ID for report path'
required: true
type: string
source_run_number:
description: 'The source run number for artifact naming and comment'
required: true
type: string
source_run_attempt:
description: 'The source run attempt for artifact naming and comment'
required: true
type: string
pr_number:
description: 'The PR number to post comment on'
required: true
type: string
permissions:
contents: write
issues: write
@@ -10,8 +31,8 @@ jobs:
continue-on-error: true
env:
# Unique URL path for each workflow run attempt
HTML_REPORT_URL_PATH: reports/${{ github.head_ref }}/${{ github.run_id }}/${{ github.run_attempt }}
BRANCH_REPORTS_DIR: reports/${{ github.head_ref }}
HTML_REPORT_URL_PATH: reports/${{ inputs.head_branch }}/${{ inputs.source_run_id }}/${{ inputs.source_run_attempt }}
BRANCH_REPORTS_DIR: reports/${{ inputs.head_branch }}
steps:
- name: Generate GitHub App token
id: generate-token
@@ -45,11 +66,11 @@ jobs:
run: |
rm -rf $BRANCH_REPORTS_DIR
git add .
git commit -m "workflow: remove all reports for branch ${{ github.head_ref }}"
git commit -m "workflow: remove all reports for branch ${{ inputs.head_branch }}"
- name: Download zipped HTML report
uses: actions/download-artifact@v4
with:
name: html-report--attempt-${{ github.run_number }}-${{ github.run_attempt }}
name: html-report--attempt-${{ inputs.source_run_number }}-${{ inputs.source_run_attempt }}
path: ${{ env.HTML_REPORT_URL_PATH }}
- name: Push HTML Report
timeout-minutes: 3
@@ -57,7 +78,7 @@ jobs:
# this is necessary when this job running at least twice at the same time (e.g. through two pushes at the same time)
run: |
git add .
git commit -m "workflow: add HTML report for run-id ${{ github.run_id }} (attempt: ${{ github.run_attempt }})"
git commit -m "workflow: add HTML report for run-id ${{ inputs.source_run_id }} (attempt: ${{ inputs.source_run_attempt }})"
while true; do
git pull --rebase
@@ -82,18 +103,18 @@ jobs:
uses: peter-evans/find-comment@v2
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
issue-number: ${{ inputs.pr_number }}
comment-author: "github-actions[bot]"
body-includes: <!-- GENERATED-E2E-RESULTS -->
- name: Create comment
if: steps.fc.outputs.comment-id == ''
uses: peter-evans/create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
issue-number: ${{ inputs.pr_number }}
body: |
<!-- GENERATED-E2E-RESULTS -->
## E2E results are ready!
- [Workflow #${{ github.run_number }}.${{ github.run_attempt }} latest results](${{ steps.url.outputs.link }})
- [Workflow #${{ inputs.source_run_number }}.${{ inputs.source_run_attempt }} latest results](${{ steps.url.outputs.link }})
- name: Update comment
if: steps.fc.outputs.comment-id != ''
uses: peter-evans/create-or-update-comment@v3
@@ -103,4 +124,4 @@ jobs:
body: |
<!-- GENERATED-E2E-RESULTS -->
## E2E results are ready!
- [Workflow #${{ github.run_number }}.${{ github.run_attempt }} latest results](${{ steps.url.outputs.link }})
- [Workflow #${{ inputs.source_run_number }}.${{ inputs.source_run_attempt }} latest results](${{ steps.url.outputs.link }})
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-873
View File
File diff suppressed because one or more lines are too long
+942
View File
File diff suppressed because one or more lines are too long
+5 -9
View File
@@ -1,11 +1,7 @@
compressionLevel: mixed
enableGlobalCache: false
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
spec: "@yarnpkg/plugin-version"
yarnPath: .yarn/releases/yarn-3.4.1.cjs
yarnPath: .yarn/releases/yarn-4.12.0.cjs
+49 -1
View File
@@ -255,7 +255,55 @@ import { TRPCError } from "@trpc/server";
### packages/features Import Restrictions
Files in `packages/features/**` should NOT import from `trpc`. This keeps the features package decoupled from the tRPC layer, making the code more reusable and testable. Use `ErrorWithCode` for error handling in these files, and let the tRPC middleware handle the conversion.
Files in `packages/features/**` should NOT import from `@calcom/trpc`. This keeps the features package decoupled from the tRPC layer, making the code more reusable and testable. Use `ErrorWithCode` for error handling in these files, and let the tRPC middleware handle the conversion.
**Architecture: packages/features vs apps/web/modules**
The `packages/features` package should contain only framework-agnostic code:
- Repositories (data access layer)
- Services (business logic)
- Core utilities and helpers
- Types and interfaces
Web-specific code, particularly anything that uses tRPC, should live in `apps/web/modules/...`. This includes:
- React hooks that use tRPC queries/mutations
- tRPC-specific utilities
- Web-only UI components that depend on tRPC
**Example:**
If you have a feature called `feature-opt-in`:
```
packages/features/feature-opt-in/
├── repository/
│ └── FeatureOptInRepository.ts # Data access - OK here
├── service/
│ └── FeatureOptInService.ts # Business logic - OK here
└── types.ts # Types - OK here
apps/web/modules/feature-opt-in/
└── hooks/
└── useFeatureOptIn.ts # tRPC hook - MUST be here, not in packages/features
```
```typescript
// ❌ Bad - tRPC hook in packages/features
// packages/features/feature-opt-in/hooks/useFeatureOptIn.ts
import { trpc } from "@calcom/trpc/react";
export function useFeatureOptIn() {
return trpc.viewer.featureOptIn.useQuery();
}
// ✅ Good - tRPC hook in apps/web/modules
// apps/web/modules/feature-opt-in/hooks/useFeatureOptIn.ts
import { trpc } from "@calcom/trpc/react";
export function useFeatureOptIn() {
return trpc.viewer.featureOptIn.useQuery();
}
```
This separation ensures that `packages/features` remains portable and can be used by other apps (like `apps/api/v2`) without pulling in web-specific dependencies like tRPC React hooks.
## Basic Performance Guidelines
+1 -1
View File
@@ -128,7 +128,7 @@ export const schemaBookingReadPublic = Booking.extend({
title: true,
startTime: true,
endTime: true,
timeZone: true,
// Note: timeZone is not a field on Booking model - it's in nested attendees/user objects
attendees: true,
user: true,
eventType: true,
@@ -2,11 +2,11 @@ import { z } from "zod";
import { DestinationCalendarSchema } from "@calcom/prisma/zod/modelSchema/DestinationCalendarSchema";
// Note: bookingId is not a field on DestinationCalendar model in Prisma schema
export const schemaDestinationCalendarBaseBodyParams = DestinationCalendarSchema.pick({
integration: true,
externalId: true,
eventTypeId: true,
bookingId: true,
userId: true,
}).partial();
@@ -38,11 +38,11 @@ export const schemaDestinationCalendarEditBodyParams = schemaDestinationCalendar
schemaDestinationCalendarEditParams
);
// Note: bookingId is not a field on DestinationCalendar model in Prisma schema
export const schemaDestinationCalendarReadPublic = DestinationCalendarSchema.pick({
id: true,
integration: true,
externalId: true,
eventTypeId: true,
bookingId: true,
userId: true,
});
+5 -1
View File
@@ -151,6 +151,7 @@ export const schemaUserCreateBodyParams = schemaUserBaseBodyParams
.strict();
// @note: These are the values that are always returned when reading a user
// Note: We pick avatarUrl from the Prisma schema and extend with avatar for API v1 backward compatibility
export const schemaUserReadPublic = UserSchema.pick({
id: true,
username: true,
@@ -158,7 +159,7 @@ export const schemaUserReadPublic = UserSchema.pick({
email: true,
emailVerified: true,
bio: true,
avatar: true,
avatarUrl: true,
timeZone: true,
weekStart: true,
endTime: true,
@@ -176,6 +177,9 @@ export const schemaUserReadPublic = UserSchema.pick({
verified: true,
invitedTo: true,
role: true,
}).extend({
// API v1 backward compatibility: expose avatarUrl as avatar
avatar: UserSchema.shape.avatarUrl,
});
export const schemaUsersReadPublic = z.array(schemaUserReadPublic);
+3 -1
View File
@@ -1,3 +1,4 @@
/* eslint-disable */
const { withAxiom } = require("next-axiom");
const { withSentryConfig } = require("@sentry/nextjs");
const { PrismaPlugin } = require("@prisma/nextjs-monorepo-workaround-plugin");
@@ -6,6 +7,7 @@ const plugins = [withAxiom];
/** @type {import("next").NextConfig} */
const nextConfig = {
turbopack: {},
transpilePackages: [
"@calcom/app-store",
"@calcom/dayjs",
@@ -94,7 +96,7 @@ const nextConfig = {
},
};
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
plugins.push((nextConfig) =>
withSentryConfig(nextConfig, {
autoInstrumentServerFunctions: true,
+4 -4
View File
@@ -21,7 +21,8 @@
"@calcom/eslint-config": "workspace:*",
"@calcom/tsconfig": "workspace:*",
"@calcom/types": "workspace:*",
"node-mocks-http": "^1.11.0"
"node-mocks-http": "1.16.2",
"typescript": "5.9.2"
},
"dependencies": {
"@calcom/app-store": "workspace:*",
@@ -35,14 +36,13 @@
"@sentry/nextjs": "^9.15.0",
"bcryptjs": "^2.4.3",
"memory-cache": "^0.2.0",
"next": "15.4.10",
"next": "16.1.0",
"next-api-middleware": "^1.0.1",
"next-axiom": "^0.17.0",
"next-swagger-doc": "^0.3.6",
"next-validations": "^0.2.0",
"typescript": "^5.9.0-beta",
"tzdata": "^1.0.30",
"uuid": "^8.3.2",
"zod": "^3.22.4"
"zod": "3.25.76"
}
}
@@ -1,7 +1,7 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
export function middleware(_request: NextRequest) {
function proxy(_request: NextRequest) {
// Matcher guarantees the last segment is one of the blocked segments, so we can
// immediately return a 403 without further path checks.
return NextResponse.json({ message: "Forbidden" }, { status: 403 });
@@ -17,3 +17,5 @@ export const config = {
"/:path*/_auth-middleware",
],
};
export default proxy;
+19 -19
View File
@@ -16,7 +16,7 @@
"_dev:build:watch:enums": "yarn workspace @calcom/platform-enums build:watch",
"_dev:build:watch:utils": "yarn workspace @calcom/platform-utils build:watch",
"_dev:build:watch:types": "yarn workspace @calcom/platform-types build:watch",
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build && yarn workspace @calcom/platform-libraries build",
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build && yarn workspace @calcom/platform-libraries build && yarn workspace @calcom/trpc build:server",
"dev": "yarn dev:build && ts-node scripts/docker-start.ts && yarn copy-swagger-module && yarn start --watch",
"dev:no-docker": "yarn dev:build && yarn copy-swagger-module && yarn start --watch",
"start:debug": "nest start --debug --watch",
@@ -43,7 +43,6 @@
"@calcom/platform-types": "workspace:*",
"@calcom/platform-utils": "workspace:*",
"@calcom/prisma": "workspace:*",
"@golevelup/ts-jest": "^0.4.0",
"@microsoft/microsoft-graph-types-beta": "^0.42.0-preview",
"@nest-lab/throttler-storage-redis": "1.0.0",
"@nestjs/axios": "^4.0.0",
@@ -60,59 +59,60 @@
"@sentry/node": "^9.15.0",
"@sentry/profiling-node": "^9.15.0",
"@snyk/protect": "latest",
"axios": "^1.9.0",
"body-parser": "^1.20.2",
"bull": "^4.12.4",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"concurrently": "^9.1.2",
"cookie-parser": "^1.4.6",
"dotenv": "^16.3.1",
"dotenv": "16.6.1",
"fs-extra": "^11.2.0",
"googleapis": "^84.0.0",
"helmet": "^7.1.0",
"ioredis": "^5.3.2",
"jsforce": "^1.11.0",
"lodash": "^4.17.21",
"luxon": "^3.4.4",
"lodash": "4.17.21",
"luxon": "3.4.4",
"nest-winston": "^1.9.4",
"next-auth": "^4.22.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"qs-stringify": "^1.2.1",
"querystring": "^0.2.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"rxjs": "7.8.2",
"stripe": "^15.3.0",
"uuid": "^8.3.2",
"winston": "^3.13.0",
"winston-transport": "^4.7.0",
"zod": "^3.22.4"
"winston-transport": "4.9.0",
"zod": "3.25.76"
},
"devDependencies": {
"@calcom/eslint-config": "workspace:*",
"@golevelup/ts-jest": "^0.4.0",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/schematics": "10.1.1",
"@nestjs/testing": "^10.0.0",
"@types/cookie-parser": "^1.4.6",
"@types/express": "^4.17.21",
"@types/express": "4.17.21",
"@types/fs-extra": "^11.0.4",
"@types/jest": "^29.5.10",
"@types/luxon": "^3.3.7",
"@types/jest": "29.5.12",
"@types/luxon": "3.4.2",
"@types/passport-jwt": "^3.0.13",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6",
"@typescript-eslint/parser": "^6",
"concurrently": "^9.1.2",
"jest": "^29.7.0",
"jest-date-mock": "^1.0.10",
"node-mocks-http": "^1.16.2",
"prettier": "^2.8.6",
"source-map-support": "^0.5.21",
"source-map-support": "0.5.21",
"supertest": "^6.3.3",
"ts-jest": "^29.1.1",
"ts-jest": "29.1.4",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.0",
"typescript": "^5.9.0-beta"
"ts-node": "10.9.2",
"tsconfig-paths": "4.2.0",
"typescript": "5.9.2"
},
"prisma": {
"schema": "../../../packages/prisma/schema.prisma"
@@ -1,9 +1,11 @@
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 { 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";
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
import { CalVideoOutputService } from "@/ee/bookings/2024-08-13/services/cal-video.output.service";
@@ -100,8 +102,13 @@ import { Module } from "@nestjs/common";
CalVideoService,
CalVideoOutputService,
BookingPbacGuard,
BookingLocationService_2024_08_13,
],
controllers: [
BookingsController_2024_08_13,
BookingGuestsController_2024_08_13,
BookingLocationController_2024_08_13,
],
controllers: [BookingsController_2024_08_13, BookingGuestsController_2024_08_13],
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
})
export class BookingsModule_2024_08_13 {}
@@ -0,0 +1,57 @@
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
import { UpdateBookingLocationOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/update-location.output";
import { BookingLocationService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-location.service";
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
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";
import { Controller, Patch, Logger, Body, UseGuards, Param, HttpCode, HttpStatus } from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags, ApiHeader } from "@nestjs/swagger";
import { BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import { UpdateBookingLocationInput_2024_08_13 } from "@calcom/platform-types";
@Controller({
path: "/v2/bookings/:bookingUid/location",
version: VERSION_2024_08_13_VALUE,
})
@UseGuards(PermissionsGuard)
@DocsTags("Bookings")
@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 BookingLocationController_2024_08_13 {
private readonly logger = new Logger("BookingLocationController_2024_08_13");
constructor(private readonly bookingLocationService: BookingLocationService_2024_08_13) {}
@Patch("/")
@HttpCode(HttpStatus.OK)
@Permissions([BOOKING_WRITE])
@UseGuards(ApiAuthGuard, BookingUidGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary: "Update booking location for an existing booking",
description: `**Current Limitation:** Updating a booking location will update the location in Cal.com, but the corresponding Calendar event will not be updated automatically. The old location will persist in the Calendar event. This is a known limitation that will be addressed in a future update.
<Note>The cal-api-version header is required for this endpoint. Without it, the request will fail with a 404 error.</Note>`,
})
async updateBookingLocation(
@Param("bookingUid") bookingUid: string,
@Body() body: UpdateBookingLocationInput_2024_08_13,
@GetUser() user: ApiAuthGuardUser
): Promise<UpdateBookingLocationOutput_2024_08_13> {
const booking = await this.bookingLocationService.updateBookingLocation(bookingUid, body, user);
return {
status: SUCCESS_STATUS,
data: booking,
};
}
}
@@ -16,9 +16,9 @@ import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.rep
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 { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
@@ -73,6 +73,7 @@ type EmailSetup = {
eventTypeId: number;
createdBookingUid: string;
rescheduledBookingUid: string;
accessToken: string;
};
describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
@@ -85,6 +86,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let tokensRepositoryFixture: TokensRepositoryFixture;
let emailsEnabledSetup: EmailSetup;
let emailsDisabledSetup: EmailSetup;
@@ -94,12 +96,9 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
const userEmailsDisabled = `confirm-emails-2024-08-13-user-${randomString()}@api.com`;
beforeAll(async () => {
const moduleRef = await withApiAuth(
authEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
})
)
const moduleRef = await Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
})
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
@@ -112,6 +111,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
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: `confirm-emails-2024-08-13-organization-${randomString()}`,
@@ -152,6 +152,8 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
},
});
const tokens = await tokensRepositoryFixture.createTokens(user.id, oAuthClientEmailsEnabled.id);
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: `confirm-emails-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
@@ -174,6 +176,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
eventTypeId: event.id,
createdBookingUid: "",
rescheduledBookingUid: "",
accessToken: tokens.accessToken,
};
}
@@ -188,6 +191,8 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
},
},
});
const tokens = await tokensRepositoryFixture.createTokens(user.id, oAuthClientEmailsDisabled.id);
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: `confirm-emails-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
@@ -209,6 +214,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
eventTypeId: event.id,
createdBookingUid: "",
rescheduledBookingUid: "",
accessToken: tokens.accessToken,
};
}
@@ -277,6 +283,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.createdBookingUid}/confirm`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${emailsDisabledSetup.accessToken}`)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
@@ -331,6 +338,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.createdBookingUid}/decline`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${emailsDisabledSetup.accessToken}`)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
@@ -391,6 +399,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/confirm`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${emailsEnabledSetup.accessToken}`)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
@@ -508,6 +517,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/decline`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${emailsEnabledSetup.accessToken}`)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
@@ -0,0 +1,492 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { UpdateBookingLocationOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/update-location.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";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as 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 {
CAL_API_VERSION_HEADER,
ERROR_STATUS,
SUCCESS_STATUS,
VERSION_2024_08_13,
} from "@calcom/platform-constants";
import { UpdateBookingLocationInput_2024_08_13 } from "@calcom/platform-types";
import type { BookingOutput_2024_08_13 } from "@calcom/platform-types";
import { Booking } from "@calcom/prisma/client";
import type { Team } from "@calcom/prisma/client";
type TestUser = {
id: number;
accessToken: string;
refreshToken: string;
email: string;
};
type TestSetup = {
organizer: TestUser;
unrelatedUser: TestUser;
eventTypeId: number;
};
describe("Bookings Endpoints 2024-08-13 update booking location", () => {
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 () => {
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: `update-booking-location-organization-${randomString()}`,
});
await setupTestData();
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
describe("PATCH /v2/bookings/:bookingUid/location", () => {
it("should return 401 when updating location without authentication", async () => {
const { bookingUid } = await createBooking();
await request(app.getHttpServer())
.patch(`/v2/bookings/${bookingUid}/location`)
.send({ location: "https://unauthenticated.example.com" })
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(401);
});
describe("can update booking with different locations", () => {
const address = "123 Main St";
const link = "https://cal.com/join/123456";
const phone = "+37121999999";
let booking: Booking;
let bookingId: number;
let bookingUid: string;
let eventTypeWithAllLocationsId: number;
beforeAll(async () => {
const createdEventType = await eventTypesRepositoryFixture.create(
{
title: "book using any location",
slug: `book-using-any-location-${randomString()}`,
length: 15,
locations: [
{ type: "integrations:daily" },
{ type: "inPerson", address: "123 Main St", displayLocationPublicly: true },
{ type: "link", link: "https://cal.com/join/123456", displayLocationPublicly: true },
{ type: "userPhone", hostPhoneNumber: "+37121999999", displayLocationPublicly: true },
{ type: "attendeeInPerson" },
{ type: "phone" },
{ type: "somewhereElse" },
],
},
testSetup.organizer.id
);
eventTypeWithAllLocationsId = createdEventType.id;
booking = await bookingsRepositoryFixture.create({
uid: `booking-uid-${randomString(10)}`,
title: "booking title",
startTime: "2048-08-14T09:00:00.000Z",
endTime: "2048-08-14T10:00:00.000Z",
eventType: {
connect: {
id: eventTypeWithAllLocationsId,
},
},
status: "ACCEPTED",
metadata: {},
responses: "null",
user: {
connect: {
id: testSetup.organizer.id,
},
},
});
bookingUid = booking.uid;
bookingId = booking.id;
});
it("can update location to type address", async () => {
const updatedBookingBody: UpdateBookingLocationInput_2024_08_13 = {
location: {
type: "address",
address: address,
},
};
const updatedBookingResponse = await request(app.getHttpServer())
.patch(`/v2/bookings/${bookingUid}/location`)
.send(updatedBookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
.expect(200);
const updatedBookingResponseBody: UpdateBookingLocationOutput_2024_08_13 =
updatedBookingResponse.body;
expect(updatedBookingResponseBody.status).toEqual(SUCCESS_STATUS);
if (!responseDataIsBooking(updatedBookingResponseBody.data)) {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
const updatedBooking = updatedBookingResponseBody.data as BookingOutput_2024_08_13;
expect(updatedBooking).toHaveProperty("id");
expect(updatedBooking.location).toEqual(address);
});
it("can update location to type link", async () => {
const updatedBookingBody: UpdateBookingLocationInput_2024_08_13 = {
location: {
type: "link",
link: link,
},
};
const updatedBookingResponse = await request(app.getHttpServer())
.patch(`/v2/bookings/${bookingUid}/location`)
.send(updatedBookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
.expect(200);
const updatedBookingResponseBody: UpdateBookingLocationOutput_2024_08_13 =
updatedBookingResponse.body;
expect(updatedBookingResponseBody.status).toEqual(SUCCESS_STATUS);
if (!responseDataIsBooking(updatedBookingResponseBody.data)) {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
const updatedBooking = updatedBookingResponseBody.data as BookingOutput_2024_08_13;
expect(updatedBooking).toHaveProperty("id");
expect(updatedBooking.location).toEqual(link);
});
it("can update location to type phone", async () => {
const updatedBookingBody: UpdateBookingLocationInput_2024_08_13 = {
location: {
type: "phone",
phone: phone,
},
};
const updatedBookingResponse = await request(app.getHttpServer())
.patch(`/v2/bookings/${bookingUid}/location`)
.send(updatedBookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
.expect(200);
const updatedBookingResponseBody: UpdateBookingLocationOutput_2024_08_13 =
updatedBookingResponse.body;
expect(updatedBookingResponseBody.status).toEqual(SUCCESS_STATUS);
if (!responseDataIsBooking(updatedBookingResponseBody.data)) {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
const updatedBooking = updatedBookingResponseBody.data as BookingOutput_2024_08_13;
expect(updatedBooking).toHaveProperty("id");
expect(updatedBooking.location).toEqual(phone);
});
it("can update location to type attendeeAddress", async () => {
const attendeeAddress = "123 Example St, City, Valhalla";
const updatedBookingBody: UpdateBookingLocationInput_2024_08_13 = {
location: {
type: "attendeeAddress",
address: attendeeAddress,
},
};
const updatedBookingResponse = await request(app.getHttpServer())
.patch(`/v2/bookings/${bookingUid}/location`)
.send(updatedBookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
.expect(200);
const updatedBookingResponseBody: UpdateBookingLocationOutput_2024_08_13 =
updatedBookingResponse.body;
expect(updatedBookingResponseBody.status).toEqual(SUCCESS_STATUS);
if (!responseDataIsBooking(updatedBookingResponseBody.data)) {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
const updatedBooking = updatedBookingResponseBody.data as BookingOutput_2024_08_13;
expect(updatedBooking).toHaveProperty("id");
expect(updatedBooking.location).toEqual(attendeeAddress);
});
it("can update location to type attendeePhone", async () => {
const attendeePhone = "+37120993151";
const updatedBookingBody: UpdateBookingLocationInput_2024_08_13 = {
location: {
type: "attendeePhone",
phone: attendeePhone,
},
};
const updatedBookingResponse = await request(app.getHttpServer())
.patch(`/v2/bookings/${bookingUid}/location`)
.send(updatedBookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
.expect(200);
const updatedBookingResponseBody: UpdateBookingLocationOutput_2024_08_13 =
updatedBookingResponse.body;
expect(updatedBookingResponseBody.status).toEqual(SUCCESS_STATUS);
if (!responseDataIsBooking(updatedBookingResponseBody.data)) {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
const updatedBooking = updatedBookingResponseBody.data as BookingOutput_2024_08_13;
expect(updatedBooking).toHaveProperty("id");
expect(updatedBooking.location).toEqual(attendeePhone);
});
it("can update location to type attendeeDefined", async () => {
const attendeeDefinedLocation = "namek 100";
const updatedBookingBody: UpdateBookingLocationInput_2024_08_13 = {
location: {
type: "attendeeDefined",
location: attendeeDefinedLocation,
},
};
const updatedBookingResponse = await request(app.getHttpServer())
.patch(`/v2/bookings/${bookingUid}/location`)
.send(updatedBookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
.expect(200);
const updatedBookingResponseBody: UpdateBookingLocationOutput_2024_08_13 =
updatedBookingResponse.body;
expect(updatedBookingResponseBody.status).toEqual(SUCCESS_STATUS);
if (!responseDataIsBooking(updatedBookingResponseBody.data)) {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
const updatedBooking = updatedBookingResponseBody.data as BookingOutput_2024_08_13;
expect(updatedBooking).toHaveProperty("id");
expect(updatedBooking.location).toEqual(attendeeDefinedLocation);
});
afterAll(async () => {
if (responseDataIsBooking(booking)) {
await bookingsRepositoryFixture.deleteById(bookingId);
} else {
throw new Error("Unexpected response data type");
}
});
});
it("should return booking unchanged when no location payload is provided", async () => {
const initialLocation = `https://initial-${randomString()}.example.com`;
const { bookingUid } = await createBooking(initialLocation);
const response = await request(app.getHttpServer())
.patch(`/v2/bookings/${bookingUid}/location`)
.send({})
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
.expect(200);
const responseBody: UpdateBookingLocationOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
if (!responseDataIsBooking(responseBody.data)) {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
expect(responseBody.data.location).toEqual(initialLocation);
});
it("should return 403 when unrelated user attempts to update booking location", async () => {
const { bookingUid } = await createBooking();
await request(app.getHttpServer())
.patch(`/v2/bookings/${bookingUid}/location`)
.send({ location: { type: "link", link: "https://non-organizer.example.com" } })
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.unrelatedUser.accessToken}`)
.expect(403);
});
it("should return 400 when invalid location payload is provided", async () => {
const { bookingUid } = await createBooking();
const response = await request(app.getHttpServer())
.patch(`/v2/bookings/${bookingUid}/location`)
.send({ location: { type: "invalid-location-type" } })
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
.expect(400);
expect(response.body.status).toEqual(ERROR_STATUS);
});
});
afterAll(async () => {
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(testSetup.organizer.email);
await userRepositoryFixture.deleteByEmail(testSetup.unrelatedUser.email);
await bookingsRepositoryFixture.deleteAllBookings(testSetup.organizer.id, testSetup.organizer.email);
await app.close();
});
async function setupTestData() {
const oAuthClient = await createOAuthClient(organization.id);
const organizerUser = await userRepositoryFixture.create({
email: `update-booking-location-organizer-${randomString()}@api.com`,
platformOAuthClients: {
connect: { id: oAuthClient.id },
},
});
const unrelatedUser = await userRepositoryFixture.create({
email: `update-booking-location-unrelated-${randomString()}@api.com`,
platformOAuthClients: {
connect: { id: oAuthClient.id },
},
});
const organizerTokens = await tokensRepositoryFixture.createTokens(organizerUser.id, oAuthClient.id);
const unrelatedTokens = await tokensRepositoryFixture.createTokens(unrelatedUser.id, oAuthClient.id);
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: `update-booking-location-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(organizerUser.id, userSchedule);
const eventType = await eventTypesRepositoryFixture.create(
{
title: `update-booking-location-event-type-${randomString()}`,
slug: `update-booking-location-event-type-${randomString()}`,
length: 45,
},
organizerUser.id
);
testSetup = {
organizer: {
id: organizerUser.id,
email: organizerUser.email,
accessToken: organizerTokens.accessToken,
refreshToken: organizerTokens.refreshToken,
},
unrelatedUser: {
id: unrelatedUser.id,
email: unrelatedUser.email,
accessToken: unrelatedTokens.accessToken,
refreshToken: unrelatedTokens.refreshToken,
},
eventTypeId: eventType.id,
};
}
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["http://localhost:4444"],
permissions: 32,
areEmailsEnabled: true,
};
const secret = "secret";
return oauthClientRepositoryFixture.create(organizationId, data, secret);
}
async function createBooking(location?: string) {
const bookingLocation = location ?? `https://initial-${randomString()}.example.com`;
const bookingUid = `booking-uid-${randomString(10)}`;
await bookingsRepositoryFixture.create({
uid: bookingUid,
title: "booking title",
startTime: new Date(Date.UTC(2035, 0, 9, 13, 0, 0)).toISOString(),
endTime: new Date(Date.UTC(2035, 0, 9, 13, 45, 0)).toISOString(),
eventType: {
connect: {
id: testSetup.eventTypeId,
},
},
status: "ACCEPTED",
metadata: {},
responses: "null",
location: bookingLocation,
user: {
connect: {
id: testSetup.organizer.id,
},
},
});
return {
bookingUid,
location: bookingLocation,
};
}
function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data !== null && "id" in data;
}
});
@@ -0,0 +1,3 @@
import { GetBookingOutput_2024_08_13 } from "@calcom/platform-types";
export class UpdateBookingLocationOutput_2024_08_13 extends GetBookingOutput_2024_08_13 {}
@@ -206,4 +206,14 @@ export class BookingsRepository_2024_08_13 {
},
});
}
async updateBooking(bookingUid: string, body: Prisma.BookingUpdateInput) {
return this.dbWrite.prisma.booking.update({
where: {
uid: bookingUid,
},
data: body,
select: { uid: true },
});
}
}
@@ -0,0 +1,118 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
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 { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable, Logger, NotFoundException, ForbiddenException } from "@nestjs/common";
import type {
UpdateBookingLocationInput_2024_08_13,
BookingInputLocation_2024_08_13,
UpdateBookingInputLocation_2024_08_13,
} from "@calcom/platform-types";
import { Booking } from "@calcom/prisma/client";
@Injectable()
export class BookingLocationService_2024_08_13 {
private readonly logger = new Logger("BookingLocationService_2024_08_13");
constructor(
private readonly bookingsRepository: BookingsRepository_2024_08_13,
private readonly bookingsService: BookingsService_2024_08_13,
private readonly usersRepository: UsersRepository,
private readonly inputService: InputBookingsService_2024_08_13,
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly eventTypeAccessService: EventTypeAccessService
) {}
async updateBookingLocation(
bookingUid: string,
input: UpdateBookingLocationInput_2024_08_13,
user: ApiAuthGuardUser
) {
const existingBooking = await this.bookingsRepository.getByUidWithEventType(bookingUid);
if (!existingBooking) {
throw new NotFoundException(`Booking with uid=${bookingUid} not found`);
}
if (existingBooking.eventTypeId && existingBooking.eventType) {
const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(
existingBooking.eventTypeId
);
if (eventType) {
const isAllowed = await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(user, eventType);
if (!isAllowed) {
throw new ForbiddenException(
"User is not authorized to update this booking location. User must be the event type owner, host, team admin or owner, or org admin or owner."
);
}
}
}
const { location } = input;
if (location) {
return await this.updateLocation(existingBooking, location, user);
}
return this.bookingsService.getBooking(existingBooking.uid, user);
}
private async updateLocation(
existingBooking: Booking,
inputLocation: UpdateBookingInputLocation_2024_08_13,
user: ApiAuthGuardUser
) {
const bookingUid = existingBooking.uid;
const bookingLocation = this.getLocationValue(inputLocation) ?? existingBooking.location;
if (!existingBooking.userId) {
throw new NotFoundException(`No user found for booking with uid=${bookingUid}`);
}
if (!existingBooking.eventTypeId) {
throw new NotFoundException(`No event type found for booking with uid=${bookingUid}`);
}
const existingBookingHost = await this.usersRepository.findById(existingBooking.userId);
if (!existingBookingHost) {
throw new NotFoundException(`No user found for booking with uid=${bookingUid}`);
}
const bookingFieldsLocation = this.inputService.transformLocation(
inputLocation as BookingInputLocation_2024_08_13
);
const responses = (existingBooking.responses || {}) as Record<string, unknown>;
const { location: _existingLocation, ...rest } = responses;
const updatedBookingResponses = { ...rest, location: bookingFieldsLocation };
const updatedBooking = await this.bookingsRepository.updateBooking(bookingUid, {
location: bookingLocation,
responses: updatedBookingResponses,
});
return this.bookingsService.getBooking(updatedBooking.uid, user);
}
private getLocationValue(loc: UpdateBookingInputLocation_2024_08_13): string | undefined {
if (loc.type === "address") return loc.address;
if (loc.type === "link") return loc.link;
if (loc.type === "phone") return loc.phone;
if (loc.type === "attendeeAddress") return loc.address;
if (loc.type === "attendeePhone") return loc.phone;
if (loc.type === "attendeeDefined") return loc.location;
this.logger.log(
`Booking location service getLocationValue - loc ${JSON.stringify(
loc
)} was passed but the type is not supported.`
);
return undefined;
}
}
@@ -167,7 +167,7 @@ describe("Organizations Users Endpoints", () => {
await userRepositoryFixture.create({
email: nonMemberEmail,
username: "non-member",
username: `non-member-${randomString()}`,
});
const orgMembers = await Promise.all(
@@ -379,6 +379,108 @@ describe("Organizations Users Endpoints", () => {
createdUser = userData;
});
it("creates a new org user with username and avatarUrl", async () => {
const shortRandom = randomString().substring(0, 8);
const testUsername = `user${shortRandom}`;
const testEmail = `org-user-${shortRandom}@api.com`;
const githubAvatarUrl = "https://avatars.githubusercontent.com/u/583231?v=4";
const newOrgUserWithUsernameAndAvatar: CreateUserInput = {
email: testEmail,
username: testUsername,
avatarUrl: githubAvatarUrl,
bio,
metadata,
timeZone: "America/Sao_Paulo",
timeFormat: 24,
locale: "pt",
};
const emailSpy = jest
.spyOn(EmailService.prototype, "sendSignupToOrganizationEmail")
.mockImplementation(() => Promise.resolve());
const { body } = await request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/users`)
.send(newOrgUserWithUsernameAndAvatar)
.set("Content-Type", "application/json")
.set("Accept", "application/json");
const userData = body.data;
// Verify response status
expect(body.status).toBe(SUCCESS_STATUS);
// Verify all user fields are correctly set
expect(userData.email).toBe(testEmail);
expect(userData.username).toBe(testUsername);
expect(userData.avatarUrl).toBe(githubAvatarUrl);
expect(userData.bio).toBe(bio);
expect(userData.metadata).toEqual(metadata);
expect(userData.timeZone).toBe("America/Sao_Paulo");
expect(userData.timeFormat).toBe(24);
expect(userData.locale).toBe("pt");
// Verify email was sent with correct parameters (using email, not username)
expect(emailSpy).toHaveBeenCalledWith({
usernameOrEmail: testEmail,
orgName: org.name,
orgId: org.id,
inviterName: userEmail,
locale: "pt",
});
// Clean up the created user
await userRepositoryFixture.deleteByEmail(testEmail);
});
it("creates a new org user with avatarUrl as base64 image", async () => {
const shortRandom = randomString().substring(0, 8);
const testEmail = `org-b64-${shortRandom}@api.com`;
const base64Avatar =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
const newOrgUserWithBase64Avatar: CreateUserInput = {
email: testEmail,
avatarUrl: base64Avatar,
bio,
metadata,
};
const emailSpy = jest
.spyOn(EmailService.prototype, "sendSignupToOrganizationEmail")
.mockImplementation(() => Promise.resolve());
const { body } = await request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/users`)
.send(newOrgUserWithBase64Avatar)
.set("Content-Type", "application/json")
.set("Accept", "application/json");
const userData = body.data;
// Verify response status
expect(body.status).toBe(SUCCESS_STATUS);
// Verify base64 avatar is accepted and stored
expect(userData.email).toBe(testEmail);
expect(userData.avatarUrl).toBe(base64Avatar);
expect(userData.bio).toBe(bio);
expect(userData.metadata).toEqual(metadata);
// Verify email was sent
expect(emailSpy).toHaveBeenCalledWith({
usernameOrEmail: testEmail,
orgName: org.name,
orgId: org.id,
inviterName: userEmail,
locale: null,
});
// Clean up the created user
await userRepositoryFixture.deleteByEmail(testEmail);
});
it("should delete an org user", async () => {
const { body } = await request(app.getHttpServer())
.delete(`/v2/organizations/${org.id}/users/${createdUser.id}`)
@@ -68,13 +68,11 @@ export class OrganizationsUsersService {
await this.checkForUsernameConflicts(org.id, userCreateBody.username);
}
const usernameOrEmail = userCreateBody.username ? userCreateBody.username : userCreateBody.email;
// Create new org user
const createdUserCall = await createNewUsersConnectToOrgIfExists({
invitations: [
{
usernameOrEmail: usernameOrEmail,
usernameOrEmail: userCreateBody.email,
role: userCreateBody.organizationRole,
},
],
@@ -84,7 +82,7 @@ export class OrganizationsUsersService {
parentId: null,
autoAcceptEmailDomain: "not-required-for-this-endpoint",
orgConnectInfoByUsernameOrEmail: {
[usernameOrEmail]: {
[userCreateBody.email]: {
orgId: org.id,
autoAccept: userCreateBody.autoAccept,
},
@@ -106,7 +104,7 @@ export class OrganizationsUsersService {
// Need to send email to new user to create password
await this.emailService.sendSignupToOrganizationEmail({
usernameOrEmail,
usernameOrEmail: userCreateBody.email,
orgName: org.name,
orgId: org.id,
locale: user?.locale,
@@ -307,6 +307,57 @@ describe("Teams Memberships Endpoints", () => {
});
});
it("should assign team wide events when membership transitions from accepted=false to accepted=true via PATCH", async () => {
const pendingUserEmail = `pending-user-${randomString()}@api.com`;
const pendingUser = await userRepositoryFixture.create({
email: pendingUserEmail,
username: pendingUserEmail,
bio,
metadata,
});
const createPendingMembershipBody: CreateTeamMembershipInput = {
userId: pendingUser.id,
accepted: false,
role: "MEMBER",
};
const createResponse = await request(app.getHttpServer())
.post(`/v2/teams/${team.id}/memberships`)
.send(createPendingMembershipBody)
.expect(201);
const pendingMembership: TeamMembershipOutput = createResponse.body.data;
expect(pendingMembership.accepted).toEqual(false);
const teamEventTypesBeforePatch = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id);
const collectiveEventBeforePatch = teamEventTypesBeforePatch?.find(
(eventType) => eventType.slug === teamEventType.slug
);
const userHostBeforePatch = collectiveEventBeforePatch?.hosts.find((host) => host.userId === pendingUser.id);
expect(userHostBeforePatch).toBeFalsy();
const updateToAcceptedBody: UpdateTeamMembershipInput = {
accepted: true,
};
const patchResponse = await request(app.getHttpServer())
.patch(`/v2/teams/${team.id}/memberships/${pendingMembership.id}`)
.send(updateToAcceptedBody)
.expect(200);
const acceptedMembership: TeamMembershipOutput = patchResponse.body.data;
expect(acceptedMembership.accepted).toEqual(true);
await userHasCorrectEventTypes(pendingUser.id);
await request(app.getHttpServer())
.delete(`/v2/teams/${team.id}/memberships/${pendingMembership.id}`)
.expect(200);
await userRepositoryFixture.deleteByEmail(pendingUserEmail);
});
it("should delete the membership of the org's team we created via api", async () => {
return request(app.getHttpServer())
.delete(`/v2/teams/${team.id}/memberships/${membershipCreatedViaApi.id}`)
@@ -118,8 +118,6 @@ export class TeamsMembershipsController {
@Param("membershipId", ParseIntPipe) membershipId: number,
@Body() body: UpdateTeamMembershipInput
): Promise<UpdateTeamMembershipOutput> {
const membership = await this.teamsMembershipsService.updateTeamMembership(teamId, membershipId, body);
const currentMembership = await this.teamsMembershipsService.getTeamMembership(teamId, membershipId);
const updatedMembership = await this.teamsMembershipsService.updateTeamMembership(
@@ -127,6 +125,7 @@ export class TeamsMembershipsController {
membershipId,
body
);
if (!currentMembership.accepted && updatedMembership.accepted) {
try {
await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId);
@@ -134,9 +133,10 @@ export class TeamsMembershipsController {
this.logger.error("Could not update new team member eventTypes", err);
}
}
return {
status: SUCCESS_STATUS,
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
data: plainToClass(TeamMembershipOutput, updatedMembership, { strategy: "excludeAll" }),
};
}
@@ -10,8 +10,9 @@ import { TeamsModule } from "@/modules/teams/teams/teams.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { User } from "next-auth";
import Stripe from "stripe";
import { User } from "@calcom/prisma/client";
import * as request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
@@ -0,0 +1,158 @@
import { IsOptional, Validate, validateSync } from "class-validator";
import { AvatarValidator } from "./avatarValidator";
// Mock DTO for testing the validator
class MockAvatarDto {
@IsOptional()
@Validate(AvatarValidator)
avatar?: string;
}
// Factory function to create DTOs with avatar
const createAvatarDto = (avatar?: string): MockAvatarDto => {
const dto = new MockAvatarDto();
dto.avatar = avatar;
return dto;
};
describe("AvatarValidator", () => {
describe("when value is undefined", () => {
it("accepts undefined values", () => {
const dto = createAvatarDto(undefined);
const errors = validateSync(dto);
expect(errors).toHaveLength(0);
});
});
describe("when value is a URL", () => {
it("rejects HTTP URLs (HTTPS only for security)", () => {
const dto = createAvatarDto("http://example.com/avatar.jpg");
const errors = validateSync(dto);
expect(errors).toHaveLength(1);
expect(errors[0].constraints).toHaveProperty("avatarValidator");
});
it("accepts valid HTTPS URLs", () => {
const dto = createAvatarDto("https://avatars.githubusercontent.com/u/583231?v=4");
const errors = validateSync(dto);
expect(errors).toHaveLength(0);
});
it("accepts URLs with query parameters", () => {
const dto = createAvatarDto("https://example.com/avatar.jpg?size=256&format=png");
const errors = validateSync(dto);
expect(errors).toHaveLength(0);
});
it("accepts URLs with special characters in path", () => {
const dto = createAvatarDto("https://cdn.example.com/users/john-doe/avatar_2025.jpg");
const errors = validateSync(dto);
expect(errors).toHaveLength(0);
});
it("rejects URLs without protocol", () => {
const dto = createAvatarDto("example.com/avatar.jpg");
const errors = validateSync(dto);
expect(errors).toHaveLength(1);
expect(errors[0].constraints).toHaveProperty("avatarValidator");
});
it("rejects FTP protocol for security", () => {
const dto = createAvatarDto("ftp://example.com/avatar.jpg");
const errors = validateSync(dto);
expect(errors).toHaveLength(1);
expect(errors[0].constraints).toHaveProperty("avatarValidator");
});
it("rejects file protocol for security", () => {
const dto = createAvatarDto("file:///path/to/avatar.jpg");
const errors = validateSync(dto);
expect(errors).toHaveLength(1);
expect(errors[0].constraints).toHaveProperty("avatarValidator");
});
it("rejects javascript protocol to prevent XSS", () => {
const dto = createAvatarDto("javascript:alert('XSS')");
const errors = validateSync(dto);
expect(errors).toHaveLength(1);
expect(errors[0].constraints).toHaveProperty("avatarValidator");
});
});
describe("when value is base64 encoded image", () => {
it("accepts valid PNG base64", () => {
const dto = createAvatarDto(
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
);
const errors = validateSync(dto);
expect(errors).toHaveLength(0);
});
it("accepts valid JPEG base64", () => {
const dto = createAvatarDto("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAA==");
const errors = validateSync(dto);
expect(errors).toHaveLength(0);
});
it("accepts valid GIF base64", () => {
const dto = createAvatarDto(
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
);
const errors = validateSync(dto);
expect(errors).toHaveLength(0);
});
it("accepts valid WEBP base64", () => {
const dto = createAvatarDto(
"data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA="
);
const errors = validateSync(dto);
expect(errors).toHaveLength(0);
});
it("rejects base64 with non-image MIME type", () => {
const dto = createAvatarDto("data:text/plain;base64,SGVsbG8gV29ybGQ=");
const errors = validateSync(dto);
expect(errors).toHaveLength(1);
expect(errors[0].constraints).toHaveProperty("avatarValidator");
});
it("rejects malformed base64 data URL", () => {
const dto = createAvatarDto("data:image/png;base64,INVALID_BASE64_@#$%");
const errors = validateSync(dto);
expect(errors).toHaveLength(1);
expect(errors[0].constraints).toHaveProperty("avatarValidator");
});
it("rejects base64 with application/json MIME type", () => {
const dto = createAvatarDto("data:application/json;base64,eyJmb28iOiJiYXIifQ==");
const errors = validateSync(dto);
expect(errors).toHaveLength(1);
expect(errors[0].constraints).toHaveProperty("avatarValidator");
});
});
describe("when value is invalid", () => {
it("rejects plain text strings", () => {
const dto = createAvatarDto("not a url or base64");
const errors = validateSync(dto);
expect(errors).toHaveLength(1);
expect(errors[0].constraints).toHaveProperty("avatarValidator");
});
it("rejects empty strings (use null to reset avatar)", () => {
const dto = createAvatarDto("");
const errors = validateSync(dto);
expect(errors).toHaveLength(1);
expect(errors[0].constraints).toHaveProperty("avatarValidator");
});
it("rejects whitespace-only strings", () => {
const dto = createAvatarDto(" ");
const errors = validateSync(dto);
expect(errors).toHaveLength(1);
expect(errors[0].constraints).toHaveProperty("avatarValidator");
});
});
});
@@ -1,11 +1,30 @@
import { ValidatorConstraint } from "class-validator";
import type { ValidatorConstraintInterface } from "class-validator";
const BASE64_IMAGE_REGEX =
/^data:image\/[^;]+;base64,(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
@ValidatorConstraint({ name: "avatarValidator", async: false })
export class AvatarValidator implements ValidatorConstraintInterface {
validate(avatarString: string) {
// Checks if avatar string is a valid base 64 image
const regex = /^data:image\/[^;]+;base64,(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
return regex.test(avatarString);
validate(avatarString: string): boolean {
if (!avatarString?.trim()) return false;
return this.isValidUrl(avatarString) || this.isValidBase64Image(avatarString);
}
private isValidUrl(value: string): boolean {
try {
const url = new URL(value);
return url.protocol === "https:";
} catch {
return false;
}
}
private isValidBase64Image(value: string): boolean {
return BASE64_IMAGE_REGEX.test(value);
}
defaultMessage(): string {
return "avatarUrl must be a valid HTTPS URL (HTTP not allowed) or base64 encoded image";
}
}
@@ -10,7 +10,7 @@ export async function revalidateTeamDataCache({
teamSlug: string;
orgSlug: string | null;
}) {
revalidateTag(`team:${orgSlug ? `${orgSlug}:` : ""}${teamSlug}`);
revalidateTag(`team:${orgSlug ? `${orgSlug}:` : ""}${teamSlug}`, "max");
}
// Coupled with `getCachedTeamEventType` in `queries.ts`
@@ -23,5 +23,5 @@ export async function revalidateTeamEventTypeCache({
meetingSlug: string;
orgSlug: string | null;
}) {
revalidateTag(`event-type:${orgSlug ? `${orgSlug}:` : ""}${teamSlug}:${meetingSlug}`);
revalidateTag(`event-type:${orgSlug ? `${orgSlug}:` : ""}${teamSlug}:${meetingSlug}`, "max");
}
@@ -1,7 +1,7 @@
import { ShellMainAppDirBackButton } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDirBackButton";
import classNames from "classnames";
import type { LayoutProps } from "@calcom/features/shell/Shell";
import type { LayoutProps } from "~/shell/Shell";
// Copied from `ShellMain` but with a different `ShellMainAppDirBackButton` import
// for client/server component separation
@@ -2,9 +2,10 @@
import { useRouter } from "next/navigation";
import type { LayoutProps } from "@calcom/features/shell/Shell";
import { Button } from "@calcom/ui/components/button";
import type { LayoutProps } from "~/shell/Shell";
export const ShellMainAppDirBackButton = ({ backPath }: { backPath: LayoutProps["backPath"] }) => {
const router = useRouter();
return (
@@ -8,10 +8,10 @@ import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
import { getScheduleListItemData } from "@calcom/lib/schedules/transformers/getScheduleListItemData";
import { MembershipRole } from "@calcom/prisma/enums";
import { availabilityRouter } from "@calcom/trpc/server/routers/viewer/availability/_router";
import { AvailabilitySliderTable } from "@calcom/web/modules/timezone-buddy/components/AvailabilitySliderTable";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -64,8 +64,8 @@ const Page = async ({ searchParams: _searchParams }: PageProps) => {
const organizationRepository = getOrganizationRepository();
const isOrgPrivate = organizationId
? await organizationRepository.checkIfPrivate({
orgId: organizationId,
})
orgId: organizationId,
})
: false;
const permissionService = new PermissionCheckService();
@@ -80,9 +80,7 @@ const Page = async ({ searchParams: _searchParams }: PageProps) => {
<ShellMainAppDir
heading={t("availability")}
subtitle={t("configure_availability")}
CTA={
<AvailabilityCTA canViewTeamAvailability={canViewTeamAvailability} />
}>
CTA={<AvailabilityCTA canViewTeamAvailability={canViewTeamAvailability} />}>
{searchParams?.type === "team" && canViewTeamAvailability ? (
<AvailabilitySliderTable isOrg={!!organizationId} />
) : (
@@ -3,10 +3,11 @@ import { cookies, headers } from "next/headers";
import React from "react";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import Shell from "@calcom/features/shell/Shell";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import Shell from "~/shell/Shell";
const Layout = async ({ children }: { children: React.ReactNode }) => {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
if (session?.user?.id) SentrySetUser({ id: session.user.id });
@@ -3,5 +3,5 @@
import { revalidateTag } from "next/cache";
export async function revalidateTeamsList() {
revalidateTag("viewer.teams.list");
revalidateTag("viewer.teams.list", "max");
}
@@ -2,7 +2,6 @@ import type { SearchParams } from "app/_types";
import type { Session } from "next-auth";
import { unstable_cache } from "next/cache";
import { TeamsListing } from "@calcom/features/ee/teams/components/TeamsListing";
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
import { TeamService } from "@calcom/features/ee/teams/services/teamService";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
@@ -10,6 +9,8 @@ import { ErrorWithCode } from "@calcom/lib/errors";
import prisma from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { TeamsListing } from "~/ee/teams/components/TeamsListing";
import { TeamsCTA } from "./CTA";
const getCachedTeams = unstable_cache(
@@ -3,9 +3,10 @@
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
import { TeamsCTA } from "app/(use-page-wrapper)/(main-nav)/teams/CTA";
import SkeletonLoaderTeamList from "@calcom/features/ee/teams/components/SkeletonloaderTeamList";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import SkeletonLoaderTeamList from "~/ee/teams/components/SkeletonloaderTeamList";
export const TeamsListSkeleton = () => {
const { t } = useLocale();
return (
@@ -162,14 +162,14 @@ function Field({
<SelectField
maxMenuHeight={200}
styles={{
singleValue: (baseStyles) => ({
...baseStyles,
fontSize: "14px",
}),
option: (baseStyles) => ({
...baseStyles,
fontSize: "14px",
}),
singleValue: (baseStyles) =>
Object.assign({}, baseStyles, {
fontSize: "14px",
}),
option: (baseStyles) =>
Object.assign({}, baseStyles, {
fontSize: "14px",
}),
}}
label="Type"
isDisabled={!!router}
@@ -8,9 +8,9 @@ import type { ImmutableTree, BuilderProps, Config } from "react-awesome-query-bu
import type { JsonTree } from "react-awesome-query-builder";
import type { UseFormReturn } from "react-hook-form";
import { Toaster } from "sonner";
import type { z } from "zod";
import { buildEmptyQueryValue, raqbQueryValueUtils } from "@calcom/app-store/_utils/raqb/raqbUtils";
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";
@@ -39,7 +39,6 @@ import type {
EditFormRoute,
AttributeRoutingConfig,
} from "@calcom/app-store/routing-forms/types/types";
import type { zodRoutes } from "@calcom/app-store/routing-forms/zod";
import { RouteActionType } from "@calcom/app-store/routing-forms/zod";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import type { EventTypesByViewer } from "@calcom/features/eventtypes/lib/getEventTypesByViewer";
@@ -83,32 +82,36 @@ function useEnsureEventTypeIdInRedirectUrlAction({
eventOptions: { label: string; value: string; eventTypeId: number }[];
setRoute: SetRoute;
}) {
const routeActionValue = isRouter(route) ? undefined : route.action.value;
const routeActionType = isRouter(route) ? undefined : route.action.type;
const routeActionEventTypeId = isRouter(route) ? undefined : route.action.eventTypeId;
useEffect(() => {
if (isRouter(route)) {
return;
}
if (
route.action.type !== RouteActionType.EventTypeRedirectUrl ||
routeActionType !== RouteActionType.EventTypeRedirectUrl ||
// Must not be set already. Could be zero as well for custom
route.action.eventTypeId !== undefined
routeActionEventTypeId !== undefined
) {
return;
}
const matchingOption = eventOptions.find((eventOption) => eventOption.value === route.action.value);
const matchingOption = eventOptions.find((eventOption) => eventOption.value === routeActionValue);
if (!matchingOption) {
return;
}
setRoute(route.id, {
action: { ...route.action, eventTypeId: matchingOption.eventTypeId },
});
}, [eventOptions, setRoute, route.id, (route as unknown as any).action?.value]);
}, [eventOptions, setRoute, route, routeActionValue, routeActionType, routeActionEventTypeId]);
}
const hasRules = (route: EditFormRoute) => {
if (isRouter(route)) return false;
route.queryValue.children1 && Object.keys(route.queryValue.children1).length;
return route.queryValue.children1 && Object.keys(route.queryValue.children1).length;
};
function getEmptyQueryValue() {
@@ -147,14 +150,14 @@ const buildEventsData = ({
label: string;
value: string;
eventTypeId: number;
eventTypeAppMetadata?: Record<string, any>;
eventTypeAppMetadata?: Record<string, unknown>;
isRRWeightsEnabled: boolean;
}[] = [];
const eventTypesMap = new Map<
number,
{
schedulingType: SchedulingType | null;
eventTypeAppMetadata?: Record<string, any>;
eventTypeAppMetadata?: Record<string, unknown>;
}
>();
eventTypesByGroup?.eventTypeGroups.forEach((group) => {
@@ -392,13 +395,11 @@ const Route = ({
? eventOptions[0].value.substring(0, eventOptions[0].value.lastIndexOf("/") + 1)
: "";
const [customEventTypeSlug, setCustomEventTypeSlug] = useState<string>("");
useEffect(() => {
const [customEventTypeSlug, setCustomEventTypeSlug] = useState<string>(() => {
const isCustom =
!isRouter(route) && !eventOptions.find((eventOption) => eventOption.value === route.action.value);
setCustomEventTypeSlug(isCustom && !isRouter(route) ? route.action.value.split("/").pop() ?? "" : "");
}, []);
return isCustom && !isRouter(route) ? route.action.value.split("/").pop() ?? "" : "";
});
useEnsureEventTypeIdInRedirectUrlAction({
route,
@@ -1147,67 +1148,6 @@ const Routes = ({
hookForm,
});
const { data: allForms } = trpc.viewer.appRoutingForms.forms.useQuery();
const notHaveAttributesQuery = ({ form }: { form: { routes: z.infer<typeof zodRoutes> } }) => {
return form.routes?.every((route) => {
if (isRouter(route)) {
return true;
}
return !route.attributesQueryValue;
});
};
const availableRouters =
allForms?.filtered
.filter(({ form: router }) => {
const routerValidInContext = areTheySiblingEntities({
entity1: {
teamId: router.teamId ?? null,
// group doesn't have userId. The query ensures that it belongs to the user only, if teamId isn't set. So, I am manually setting it to the form userId
userId: router.userId,
},
entity2: {
teamId: hookForm.getValues().teamId ?? null,
userId: hookForm.getValues().userId,
},
});
return router.id !== hookForm.getValues().id && routerValidInContext;
})
// We don't want to support picking forms that have attributes query. We can consider it later.
// This is mainly because the Router picker feature is pretty much not used and we don't want to complicate things
.filter(({ form }) => {
return notHaveAttributesQuery({ form: form });
})
.map(({ form: router }) => {
return {
value: router.id,
label: router.name,
name: router.name,
description: router.description,
isDisabled: false,
};
}) || [];
// const isConnectedForm = (id: string) => form.connectedForms.map((f) => f.id).includes(id);
// const routers: any[] = [];
/* Disable this feature for new forms till we get it fully working with Routing Form with Attributes. This isn't much used feature */
// const routers = availableRouters.map((r) => {
// // Reset disabled state
// r.isDisabled = false;
// // Can't select a form as router that is already a connected form. It avoids cyclic dependency
// if (isConnectedForm(r.value)) {
// r.isDisabled = true;
// }
// // A route that's already used, can't be reselected
// if (routes.find((route) => route.id === r.value)) {
// r.isDisabled = true;
// }
// return r;
// });
const createRoute = useCreateRoute({
routes,
setRoutes,
@@ -5,11 +5,12 @@ import { RoutingFormAuthGuard } from "./RoutingFormAuthGuard";
interface LayoutProps {
children: ReactNode;
params: { pages: string[] };
params: Promise<{ pages: string[] }>;
}
export default async function Layout({ children, params }: LayoutProps) {
const isPublic = params.pages?.[0] === "routing-link";
const { pages } = await params;
const isPublic = pages?.[0] === "routing-link";
if (isPublic) {
return <FormProvider>{children}</FormProvider>;
}
@@ -1,21 +1,16 @@
"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 posthog from "posthog-js";
import { isFallbackRoute } from "@calcom/app-store/routing-forms/lib/isFallbackRoute";
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
import { useHasPaidPlan } from "@calcom/features/billing/hooks/useHasPaidPlan";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import SkeletonLoaderTeamList from "@calcom/features/ee/teams/components/SkeletonloaderTeamList";
import { CreateButtonWithTeamsList } from "@calcom/features/ee/teams/components/createButton/CreateButtonWithTeamsList";
import { FilterResults } from "@calcom/features/filters/components/FilterResults";
import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
import { ShellMain } from "@calcom/features/shell/Shell";
import { UpgradeTip } from "@calcom/features/tips";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
@@ -39,6 +34,12 @@ import {
FormActionsProvider,
} from "@calcom/web/components/apps/routing-forms/FormActions";
import { useHasPaidPlan } from "~/billing/hooks/useHasPaidPlan";
import SkeletonLoaderTeamList from "~/ee/teams/components/SkeletonloaderTeamList";
import { CreateButtonWithTeamsList } from "~/ee/teams/components/createButton/CreateButtonWithTeamsList";
import { ShellMain } from "~/shell/Shell";
import { UpgradeTip } from "~/shell/UpgradeTip";
function NewFormButton({ setNewFormDialogState }: { setNewFormDialogState: SetNewFormDialogState }) {
const { t } = useLocale();
return (
@@ -3,10 +3,11 @@ import { redirect } from "next/navigation";
import type { ReactNode } from "react";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import Shell from "@calcom/features/shell/Shell";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import Shell from "~/shell/Shell";
import FormProvider from "../[...pages]/FormProvider";
export default async function Layout({ children }: { children: ReactNode }) {
@@ -47,6 +47,8 @@ const ServerPage = async ({ searchParams }: PageProps) => {
? "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");
}
return t("error_during_login") + (error ? ` Error code: ${error}` : "");
};
@@ -17,7 +17,7 @@ export const generateMetadata = async () => {
};
const Page = async ({ params, searchParams }: PageProps) => {
// cookie will be cleared in `/apps/web/middleware.ts`
// cookie will be cleared in `/apps/web/proxy.ts`
const h = await headers();
const context = buildLegacyCtx(h, await cookies(), await params, await searchParams);
@@ -1,8 +1,9 @@
"use client";
import Shell from "@calcom/features/shell/Shell";
import { SkeletonText, SkeletonContainer, SkeletonButton } from "@calcom/ui/components/skeleton";
import Shell from "~/shell/Shell";
export function AvailabilityScheduleSkeleton() {
return (
<Shell withoutMain={true}>
@@ -106,7 +107,7 @@ export function AvailabilityScheduleSkeleton() {
</div>
{/* Sidebar - only visible on desktop */}
<div className="min-w-40 col-span-3 hidden stack-y-2 md:block lg:col-span-1 xl:w-[25%]">
<div className="min-w-40 stack-y-2 col-span-3 hidden md:block lg:col-span-1 xl:w-[25%]">
<div className="xl:max-w-80 w-full pr-4 sm:ml-0 sm:mr-36 sm:p-0">
<div>
<SkeletonText className="mb-1 h-4 w-24 text-sm" /> {/* Timezone */}
@@ -1,9 +1,10 @@
"use client";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert } from "@calcom/ui/components/alert";
import Shell from "~/shell/Shell";
export default function Error() {
const { t } = useLocale();
return (
@@ -1,6 +1,5 @@
"use client";
import Shell from "@calcom/features/shell/Shell";
import {
SkeletonButton,
SkeletonContainer,
@@ -8,6 +7,8 @@ import {
SkeletonAvatar,
} from "@calcom/ui/components/skeleton";
import Shell from "~/shell/Shell";
export const EventTypeEditPageSkeleton = () => {
const backButtonSkeleton = (
<div className="bg-default sticky top-0 z-10 mb-0 flex items-center py-2 md:mb-9 md:mt-0">
@@ -33,7 +34,7 @@ export const EventTypeEditPageSkeleton = () => {
<div className="flex flex-col xl:flex-row xl:space-x-6">
<div className="hidden xl:block">
<div className="primary-navigation w-64 shrink-0">
<div className="flex flex-col stack-y-1">
<div className="stack-y-1 flex flex-col">
{/* Tab navigation items */}
{Array.from({ length: 8 }).map((_, index) => (
<div
@@ -58,14 +59,14 @@ export const EventTypeEditPageSkeleton = () => {
<SkeletonText className="h-8 w-full max-w-md" />
</div>
<div className="mb-6 stack-y-6">
<div className="stack-y-6 mb-6">
{/* Form fields */}
<div className="flex flex-col stack-y-2">
<div className="stack-y-2 flex flex-col">
<SkeletonText className="h-4 w-32" />
<SkeletonText className="h-10 w-full" />
</div>
<div className="flex flex-col stack-y-2">
<div className="stack-y-2 flex flex-col">
<SkeletonText className="h-4 w-40" />
<div className="flex items-center">
<SkeletonText className="h-10 w-full" />
@@ -73,12 +74,12 @@ export const EventTypeEditPageSkeleton = () => {
</div>
</div>
<div className="flex flex-col stack-y-2">
<div className="stack-y-2 flex flex-col">
<SkeletonText className="h-4 w-36" />
<SkeletonText className="h-24 w-full" />
</div>
<div className="flex flex-col stack-y-2">
<div className="stack-y-2 flex flex-col">
<SkeletonText className="h-4 w-28" />
<div className="flex items-center">
<SkeletonText className="h-10 w-full" />
@@ -86,7 +87,7 @@ export const EventTypeEditPageSkeleton = () => {
</div>
</div>
<div className="flex flex-col stack-y-2">
<div className="stack-y-2 flex flex-col">
<SkeletonText className="h-4 w-32" />
<SkeletonText className="h-10 w-full" />
</div>
@@ -2,13 +2,14 @@
import { useSession } from "next-auth/react";
import { UpgradeTip } from "@calcom/features/tips";
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 { UpgradeTip } from "~/shell/UpgradeTip";
export default function UpgradeTipWrapper({ children }: { children: React.ReactNode }) {
const { t } = useLocale();
const session = useSession();
@@ -1,7 +1,9 @@
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
import { getTranslate } from "app/_utils";
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
import Shell from "@calcom/features/shell/Shell";
import Shell from "~/shell/Shell";
import UpgradeTipWrapper from "./UpgradeTipWrapper";
@@ -9,14 +11,13 @@ export default async function InsightsLayout({ children }: { children: React.Rea
const t = await getTranslate();
return (
<div>
<Shell
withoutMain={false}
<Shell withoutMain={true}>
<ShellMainAppDir
heading={t("insights")}
subtitle={t("insights_subtitle")}
actions={<div className={`mb-2 flex items-center gap-2 ${CTA_CONTAINER_CLASS_NAME}`} />}>
actions={<div className={`flex items-center gap-2 ${CTA_CONTAINER_CLASS_NAME}`} />}>
<UpgradeTipWrapper>{children}</UpgradeTipWrapper>
</Shell>
</div>
</ShellMainAppDir>
</Shell>
);
}
@@ -29,7 +29,6 @@ export default async function PageWrapperLayout({ children }: { children: React.
key={script.id}
nonce={nonce}
id={script.id}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: script.script,
}}
@@ -1,4 +1,3 @@
import { createRouterCaller } from "app/_trpc/context";
import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
@@ -8,7 +8,6 @@ import { APP_NAME } from "@calcom/lib/constants";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { TeamInviteEmailView } from "~/onboarding/teams/invite/email/team-invite-email-view";
import { TeamInviteView } from "~/onboarding/teams/invite/team-invite-view";
export const generateMetadata = async () => {
return await _generateMetadata(
@@ -30,7 +30,6 @@ const fetchReferralsToken = async () => {
// The enabled referrals page implementation
export const DubReferralsPage = () => {
const [token, setToken] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const { t } = useLocale();
const { resolvedTheme } = useTheme();
@@ -42,13 +41,11 @@ export const DubReferralsPage = () => {
} catch (err) {
console.error("Error fetching referrals token:", err);
showToast(t("unexpected_error_try_again"), "error");
} finally {
setLoading(false);
}
};
getToken();
}, []);
}, [t]);
if (!IS_DUB_REFERRALS_ENABLED || !token) {
return null;
@@ -1,9 +1,10 @@
import { getTranslate } from "app/_utils";
import type { Metadata } from "next";
import Shell from "@calcom/features/shell/Shell";
import { IS_DUB_REFERRALS_ENABLED } from "@calcom/lib/constants";
import Shell from "~/shell/Shell";
import { DubReferralsPage } from "./DubReferralsPage";
export const metadata: Metadata = {
@@ -4,10 +4,11 @@ import { usePathname } from "next/navigation";
import type { ComponentProps } from "react";
import React from "react";
import type Shell from "@calcom/features/shell/Shell";
import type { UserPermissionRole } from "@calcom/prisma/enums";
import { ErrorBoundary } from "@calcom/ui/components/errorBoundary";
import type Shell from "~/shell/Shell";
export type AdminLayoutProps = {
children: React.ReactNode;
userRole: UserPermissionRole | "INACTIVE_ADMIN" | undefined;
@@ -18,7 +19,7 @@ export default function AdminLayoutAppDirClient({ children }: AdminLayoutProps)
const isAppsPage = pathname?.startsWith("/settings/admin/apps");
return (
<div className="divide-subtle bg-default mx-auto flex max-w-4xl flex-row divide-y">
<div className={isAppsPage ? "min-w-0" : "flex flex-1 *:flex-1"}>
<div className={isAppsPage ? "min-w-0" : "*:flex-1 flex flex-1"}>
<ErrorBoundary>{children}</ErrorBoundary>
</div>
</div>
@@ -3,7 +3,7 @@ import { _generateMetadata, getTranslate } from "app/_utils";
import { z } from "zod";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { OrgForm } from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgEditPage";
import { OrgForm } from "~/ee/organizations/admin/AdminOrgEditPage";
import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
@@ -2,7 +2,7 @@ import { _generateMetadata } from "app/_utils";
import { getTranslate } from "app/_utils";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import AdminOrgTable from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";
import AdminOrgTable from "~/ee/organizations/admin/AdminOrgPage";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export const generateMetadata = async () =>
@@ -1,7 +1,7 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import WorkspacePlatformsPage from "@calcom/features/ee/organizations/pages/settings/admin/WorkspacePlatformPage";
import WorkspacePlatformsPage from "~/ee/organizations/admin/WorkspacePlatformPage";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export const generateMetadata = async () =>
@@ -13,7 +13,6 @@ import { useOrgBranding } from "@calcom/features/ee/organizations/context/provid
import type { OrganizationBranding } from "@calcom/features/ee/organizations/context/provider";
import type { TeamFeatures } from "@calcom/features/flags/config";
import { useIsFeatureEnabledForTeam } from "@calcom/features/flags/hooks/useIsFeatureEnabledForTeam";
import Shell from "@calcom/features/shell/Shell";
import { HOSTED_CAL_FEATURES, IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
@@ -30,6 +29,8 @@ import type { VerticalTabItemProps } from "@calcom/ui/components/navigation";
import { VerticalTabItem } from "@calcom/ui/components/navigation";
import { Skeleton } from "@calcom/ui/components/skeleton";
import Shell from "~/shell/Shell";
const getTabs = (orgBranding: OrganizationBranding | null) => {
const tabs: VerticalTabItemProps[] = [
{
@@ -3,5 +3,5 @@
import { revalidateTag } from "next/cache";
export async function revalidateApiKeysList() {
revalidateTag("viewer.apiKeys.list");
revalidateTag("viewer.apiKeys.list", "max");
}
@@ -1,4 +1,4 @@
import { SkeletonLoader } from "@calcom/features/webhooks/pages/webhooks-skeleton";
import { SkeletonLoader } from "~/webhooks/views/webhooks-skeleton";
export default function Loading() {
return <SkeletonLoader />;
@@ -4,12 +4,13 @@ import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import WebhooksView from "@calcom/features/webhooks/pages/webhooks-view";
import { APP_NAME } from "@calcom/lib/constants";
import { webhookRouter } from "@calcom/trpc/server/routers/viewer/webhook/_router";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import WebhooksView from "~/webhooks/views/webhooks-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("webhooks"),
@@ -2,9 +2,10 @@ import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WebhookRepository } from "@calcom/features/webhooks/lib/repository/WebhookRepository";
import { EditWebhookView } from "@calcom/features/webhooks/pages/webhook-edit-view";
import { APP_NAME } from "@calcom/lib/constants";
import { EditWebhookView } from "~/webhooks/views/webhook-edit-view";
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
await _generateMetadata(
(t) => t("webhooks"),
@@ -1,4 +1,4 @@
import { SkeletonLoader } from "@calcom/features/webhooks/pages/webhook-new-skeleton";
import { SkeletonLoader } from "~/webhooks/views/webhook-new-skeleton";
export default function Loading() {
return <SkeletonLoader />;
@@ -1,11 +1,12 @@
import { createRouterCaller } from "app/_trpc/context";
import { _generateMetadata } from "app/_utils";
import { NewWebhookView } from "@calcom/features/webhooks/pages/webhook-new-view";
import { APP_NAME } from "@calcom/lib/constants";
import { appsRouter } from "@calcom/trpc/server/routers/viewer/apps/_router";
import { webhookRouter } from "@calcom/trpc/server/routers/viewer/webhook/_router";
import { NewWebhookView } from "~/webhooks/views/webhook-new-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("webhooks"),
@@ -1,7 +1,7 @@
import { _generateMetadata } from "app/_utils";
import OrgAttributesEditPage from "@calcom/ee/organizations/pages/settings/attributes/attributes-edit-view";
import { MembershipRole } from "@calcom/prisma/enums";
import OrgAttributesEditPage from "@calcom/web/modules/ee/organizations/attributes/attributes-edit-view";
import { validateUserHasOrgPerms } from "../../../../actions/validateUserHasOrgPerms";
@@ -1,6 +1,6 @@
import { _generateMetadata } from "app/_utils";
import OrgAttributesCreatePage from "@calcom/ee/organizations/pages/settings/attributes/attributes-create-view";
import OrgAttributesCreatePage from "@calcom/web/modules/ee/organizations/attributes/attributes-create-view";
import { MembershipRole } from "@calcom/prisma/enums";
import { validateUserHasOrgPerms } from "../../../actions/validateUserHasOrgPerms";
@@ -1,12 +1,13 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { redirect } from "next/navigation";
import OrgSettingsAttributesPage from "@calcom/ee/organizations/pages/settings/attributes/attributes-list-view";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
import OrgSettingsAttributesPage from "~/ee/organizations/attributes/attributes-list-view";
import { validateUserHasOrg } from "../../actions/validateUserHasOrg";
export const generateMetadata = async () =>
@@ -1,6 +1,6 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import DelegationCredentialList from "@calcom/features/ee/organizations/pages/settings/delegationCredential";
import DelegationCredentialList from "~/ee/organizations/delegationCredential";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
@@ -1,7 +1,7 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { redirect } from "next/navigation";
import GuestNotificationsView from "@calcom/features/ee/organizations/pages/settings/guest-notifications";
import GuestNotificationsView from "~/ee/organizations/guest-notifications";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
@@ -1,7 +1,7 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { redirect } from "next/navigation";
import PrivacyView from "@calcom/features/ee/organizations/pages/settings/privacy";
import PrivacyView from "~/ee/organizations/privacy";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
@@ -1,8 +1,9 @@
import { getTranslate, _generateMetadata } from "app/_utils";
import { AdminAPIView } from "@calcom/features/ee/organizations/pages/settings/admin-api";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { AdminAPIView } from "~/ee/organizations/admin-api";
import { validateUserHasOrg } from "../actions/validateUserHasOrg";
export const generateMetadata = async () =>
@@ -1,6 +1,6 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/general";
import LegacyPage from "~/ee/organizations/general";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
@@ -1,7 +1,7 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { redirect } from "next/navigation";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/profile";
import LegacyPage from "~/ee/organizations/profile";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
@@ -139,7 +139,6 @@ export function AdvancedPermissionGroup({
}
const isChecked = selectedPermissions.includes(permission);
const isReadPermission = action === CrudAction.Read;
const isAutoEnabled = isReadAutoEnabled(action);
return (
@@ -6,7 +6,7 @@ import { useState, useMemo, useEffect } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import type { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import type { Resource, PermissionString } from "@calcom/features/pbac/domain/types/permission-registry";
import {
CrudAction,
Scope,
@@ -160,7 +160,7 @@ export function RoleSheet({
teamId,
roleId: role.id,
name: values.name,
permissions: values.permissions as any,
permissions: values.permissions as PermissionString[],
color: values.color,
});
} else {
@@ -169,7 +169,7 @@ export function RoleSheet({
name: values.name,
description: values.description,
color: values.color,
permissions: values.permissions as any,
permissions: values.permissions as PermissionString[],
});
}
};
@@ -10,13 +10,13 @@ export async function revalidateTeamRoles(teamId: number) {
revalidatePath("/settings/teams/[id]/roles", "page");
// Invalidate cache tags that match the unstable_cache keys
revalidateTag("team-roles");
revalidateTag("resource-permissions");
revalidateTag("team-feature");
revalidateTag("team-roles", "max");
revalidateTag("resource-permissions", "max");
revalidateTag("team-feature", "max");
// Also invalidate team-specific cache tags for completeness
revalidateTag(`team-roles-${teamId}`);
revalidateTag(`resource-permissions-${teamId}`);
revalidateTag(`team-members-${teamId}`);
revalidateTag(`team-features-${teamId}`);
revalidateTag(`team-roles-${teamId}`, "max");
revalidateTag(`resource-permissions-${teamId}`, "max");
revalidateTag(`team-members-${teamId}`, "max");
revalidateTag(`team-features-${teamId}`, "max");
}
@@ -1,9 +1,10 @@
"use client";
import SkeletonLoaderTeamList from "@calcom/features/ee/teams/components/SkeletonloaderTeamList";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import SkeletonLoaderTeamList from "~/ee/teams/components/SkeletonloaderTeamList";
export function SkeletonLoader() {
const { t } = useLocale();
return (
@@ -1,8 +1,9 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import LegacyPage from "@calcom/features/ee/teams/pages/team-appearance-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import LegacyPage from "~/ee/teams/views/team-appearance-view";
import { validateUserHasOrg } from "../../../../actions/validateUserHasOrg";
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
@@ -2,7 +2,7 @@ import { _generateMetadata, getTranslate } from "app/_utils";
import LegacyPage, {
TeamMembersCTA,
} from "@calcom/features/ee/organizations/pages/settings/other-team-members-view";
} from "~/ee/organizations/other-team-members-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { validateUserHasOrg } from "../../../../actions/validateUserHasOrg";
@@ -1,6 +1,6 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/other-team-profile-view";
import LegacyPage from "~/ee/organizations/other-team-profile-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { validateUserHasOrg } from "../../../../actions/validateUserHasOrg";
@@ -1,8 +1,9 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import LegacyPage from "@calcom/features/ee/teams/pages/team-appearance-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import LegacyPage from "~/ee/teams/views/team-appearance-view";
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
await _generateMetadata(
(t) => t("booking_appearance"),
@@ -11,11 +11,10 @@ import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
import { prisma } from "@calcom/prisma";
import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router";
import { TeamMembersView } from "@calcom/web/modules/ee/teams/views/team-members-view";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { TeamMembersView } from "~/teams/team-members-view";
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
await _generateMetadata(
(t) => t("team_members"),
@@ -1,7 +1,7 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import LegacyPage from "@calcom/features/ee/teams/pages/team-profile-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import LegacyPage from "@calcom/web/modules/ee/teams/views/team-profile-view";
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
await _generateMetadata(
@@ -1,7 +1,7 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import TeamSettingsView from "@calcom/features/ee/teams/pages/team-settings-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import TeamSettingsView from "@calcom/web/modules/ee/teams/views/team-settings-view";
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
await _generateMetadata(
@@ -1,6 +1,6 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/organizations/pages/settings/appearance";
import Page from "~/ee/organizations/appearance";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -1,7 +1,8 @@
import { getTranslate } from "app/_utils";
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
import Shell from "@calcom/features/shell/Shell";
import Shell from "~/shell/Shell";
export default async function Layout({ children }: { children: React.ReactNode }) {
const t = await getTranslate();
@@ -1,4 +1,4 @@
import { UserListTableSkeleton } from "@calcom/features/users/components/UserTable/UserListTableSkeleton";
import { UserListTableSkeleton } from "@calcom/web/modules/users/components/UserTable/UserListTableSkeleton";
export default function Loading() {
return <UserListTableSkeleton />;
@@ -3,5 +3,5 @@
import { revalidateTag } from "next/cache";
export async function revalidateAttributesList() {
revalidateTag("viewer.attributes.list");
revalidateTag("viewer.attributes.list", "max");
}
@@ -1,4 +1,4 @@
import { UserListTableSkeleton } from "@calcom/features/users/components/UserTable/UserListTableSkeleton";
import { UserListTableSkeleton } from "@calcom/web/modules/users/components/UserTable/UserListTableSkeleton";
export default function Loading() {
return <UserListTableSkeleton />;
@@ -1,6 +1,6 @@
import { _generateMetadata } from "app/_utils";
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/about-view";
import LegacyPage, { LayoutWrapper } from "~/ee/organizations/new/about-view";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -1,6 +1,6 @@
import { _generateMetadata } from "app/_utils";
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/add-teams-view";
import LegacyPage, { LayoutWrapper } from "~/ee/organizations/new/add-teams-view";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -1,6 +1,6 @@
import { _generateMetadata } from "app/_utils";
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboarding-handover";
import LegacyPage, { LayoutWrapper } from "~/ee/organizations/new/onboarding-handover";
export const generateMetadata = async () =>
await _generateMetadata(

Some files were not shown because too many files have changed in this diff Show More