Compare commits

..
47 Commits
Author SHA1 Message Date
emrysal 68663231d8 chore: release v6.1.16
Post release / release (push) Canceled after 0s
Release Docker / Prepare Release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2026-02-12 23:27:31 +00:00
d3bbed01f6 feat: add signup watchlist review mode (#27912)
* feat: add signup watchlist review feature flag and handler logic

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

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

* test: add isAccountUnderReview tests to fetchSignup test suite

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

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

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

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

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

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

* fix: remove !token condition from watchlist review check

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

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

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

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

* refactor: move user lock to UserRepository.lockByEmail

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

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

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

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

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

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

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

---------

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

* Improves README file by pruning License Section and Aligning Properly

* Remove License section from README.md

---------

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

* matched style of api keys page

* organised the out of office page

* Clean up unused imports in OutOfOfficeEntriesList

Removed unused imports from OutOfOfficeEntriesList.tsx

* fixed import issues

* remove unused imports

---------

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

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

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

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

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

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

---------

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

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

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

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

* fix: update remaining broken import paths for moved hooks

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

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

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

---------

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

* clean up

* adjust comment

---------

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

* fix type errors

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

* chore: use correct method of DI

* feat: add onchagne

* add logic to HWM stat

* add webhook resolver methods to each statergy

* move seat tracking + webhooks over to own statergy

* Move to factory base approach

* move logic to correct class

* rename create -> createByTeamId

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

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

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

Co-Authored-By: unknown <>

* feat: active user billing

* add tests

* UI side for users on active billing

* use correct period of stripe sub

* feat: claude feedback

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

Co-Authored-By: unknown <>

* feat: feedback

* feat: only render when active users mode is set

* fix type error

* fix: constants + feature flags

* fix: default to null in tests

* chore: use seats in test

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* refactor: migrate Icon to component icons in UI package

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: unknown <>

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

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

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

* fix: resolve missing Icon references in settings and workflows

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

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

* fix: resolve icon component type errors

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

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

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

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

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

* Update package.json

* Update package.json

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

Co-Authored-By: unknown <>

---------

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

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

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

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

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

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

## Visual Demo (For contributors especially)

#### Image Demo (if applicable):

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

## Mandatory Tasks (DO NOT REMOVE)

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

## How should this be tested?

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

## Human Review Checklist

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

## Checklist

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

---

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

* When report is submitted write to the db

* Prevent duplicate reportings

* test: add migration and tests for WrongAssignmentReport table

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

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

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

Addresses Cubic AI review feedback on PR #27405

Co-Authored-By: unknown <>

* feat: add routingFormId to WrongAssignmentReport and fix Select clearing

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

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

* chore: add migration for routingFormId in WrongAssignmentReport

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

* feat: add wrong assignment reports dashboard under routing tab

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

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

* fix: move hooks before early return and fix indentation

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

* fix: address Udit's review comments

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

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

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

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

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

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

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

* fix: throw UNAUTHORIZED in hasWrongAssignmentReport and squash migrations

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

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

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

* Use data table for wrong reports

* Add option to view routing trace

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

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

* feat: add RoutingFormResponseSheet component for viewing form submissions

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

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

* feat: improve wrong assignment reports UX

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* refactor: reuse existing WrongAssignmentDialog from parent

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

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

* refactor: use repository instead of direct Prisma in getFormResponseDisplay

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

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

* refactor: replace direct Prisma calls with repository methods

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

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

* refactor: use PBAC services for wrong assignment report auth

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

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

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

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

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

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

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

This reverts commit 0bd60e9661858a59aab1573d14d57d81733b7991.

* Only update reviewed fields when not pending

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

* refactor: wrap handleStatusChange in useCallback to fix useMemo recalculation

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

* Fix routing sheet UI

* fix: use appropriate error message in getFormResponseDisplay handler

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

* refactor: extract WrongAssignmentReportService from tRPC handlers

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

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

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

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

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

* fix: localize hard-coded success message in WrongAssignmentReportService

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

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

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

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

* merge: resolve conflicts with main branch

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

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

* Add guard when accessing assignmentReasonSortedByCreatedAt

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

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

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

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

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

* refactor: remove unused findByTeamIdAndStatus and findByTeamIdAndStatuses methods

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

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

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

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

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

---------

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

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

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

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

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

## Mandatory Tasks (DO NOT REMOVE)

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

## How should this be tested?

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

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

## Human Review Checklist

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

---

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

---

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

* Update apps/web/proxy.ts

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

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

Generated by Paragon from proposal for PR #27883

* Update proxy.test.ts

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

Co-Authored-By: unknown <>

---------

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

* comment removed

* remove empty lines

---------

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

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

* refactor: make onOptInSuccess a required prop

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

---------

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

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

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

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

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

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

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

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

---------

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

* Fetch the assignment reason

* Update tests

* Label as first assignment reason

* Pass single booking object

* Remove unused repository method

* Only pass bookingUid to handler

* Rename booking `assignmentReason` to `assignmentReasonSortedByCreatedAt`

* fix: use renamed assignmentReasonSortedByCreatedAt property

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

* Remove take statement

* fix: address Udit's review comments

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

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

* test: update tests to use BookingAccessService mock

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

* fix: use latest assignment reason in BookingDetailsSheet for consistency

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

---------

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

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

Also includes straggler apps/web BookEventForm.tsx.

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

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

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

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

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

Format bookings and insights subdirectories in packages/features.

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

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

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

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

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

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

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

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

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

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

---------

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

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

* test: add unionAll and distinct to kysely test mocks

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

* test: add integration tests for getBookings query

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

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

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

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

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

* Apply suggestion from @keithwillcode

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

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

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

Generated by Paragon from proposal for PR #27841

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

Generated by Paragon from proposal for PR #27841

---------

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

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

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

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

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

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

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

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

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

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

* fix: only return fallbackAction when no users are found

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

* refactor: restructure fallback section to mirror main route structure

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

* fix: ensure backwards compatibility for existing routes with fallbackAttributesQueryValue

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

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

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

* fix: include fallbackAction in getRoutesToSave to persist changes

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

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

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

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

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

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

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

* fix: prioritize fallbackAction over fallbackAttributesQueryValue

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

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

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

* refactor: extract RouteActionSelector shared component for action selection UI

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

* fix: add i18n string for default custom page message

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: initialize fallbackAction for new routes in useCreateRoute hook

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

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

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

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

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

* fix: initialize fallbackAction when main action changes to EventTypeRedirectUrl

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

* fix: only show fallback section when main action has valid event type selected

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

* fix: check for valid fallbackAction event type before preserving existing value

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

* Give more structure to main and fallback routes

* Update comment

* Rename fallbackAttributesQueryBuilder to matchingMembersFallbackRoute

* Display custom event redirect input

* fix: address Cubic AI review feedback for routing form fallback

- Simplify getFallbackAction logic to use checkedFallback flag instead of
  manually parsing attributesQueryValue (confidence 9/10)
- Add i18n translation for 'And connect with specific team members' string
  using t('connect_with_specific_team_members') (confidence 9/10)

Co-Authored-By: unknown <>

* fix: update getFallbackAction to use checkedFallback and handle missing orgId case

- Use checkedFallback flag as primary indicator for triggering fallback action
- Also handle edge case where attribute routing was configured but couldn't run
  due to missing orgId (teamMemberIdsMatchingAttributeLogic is null)
- Update test to mock checkedFallback: true when expecting fallback action

Co-Authored-By: unknown <>

* Address UI comment

* fix: Use fallbackAttributesQueryValue when fallback action is event redirect

Only use fallbackAttributesQueryValue when:
1. fallbackAction is EventTypeRedirectUrl (attribute routing applies)
2. fallbackAction is not set (backwards compatibility)

Skip it for CustomPageMessage and ExternalRedirectUrl fallback actions
since attribute routing doesn't apply to those action types.

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

* fix: correct JSX syntax error in matchingMembersFallbackRoute

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

* refactor: extract getCustomSlug helper to deduplicate custom event type slug logic

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

* fix: only show fallback attribute filtering for teams under orgs

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

* fix: show fallback action for all team forms, gate attribute filtering on isOrganization

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2026-02-11 09:15:16 -05:00
saptarshi-bose f6fa24de2b adjusted zoho Token expiry time to miliseconds. (#27855)
The Date.now() method returns time in epoch miliseconds, adding 60 * 60 = 3.6seconds leads to a bug that results in expiring the access token in 3.6 seconds, although it been valid. It creates unnecessary api calls to the zoho servers before most the api calls.
2026-02-11 12:49:44 +00:00
Shrey-SutariyaandRomit b75000ea7b feat(form-builder): format field labels with spaces (#27858)
Co-authored-by: Romit <85230081+romitg2@users.noreply.github.com>
2026-02-11 11:37:45 +00:00
Benny Joo dc7b9d91ae chore: add agent rules to prevent circular dependencies between packages (#27860) 2026-02-11 12:00:57 +01:00
36713fc12e feat: add UTM parameter data to insights bookings CSV download (#27854)
* feat: add UTM parameter data to insights bookings CSV download

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

* test: add UTM data tests and update snapshots for csvDataTransformer

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

* fix: use Tracking relation for UTM data instead of flat fields on Booking

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-11 15:28:54 +05:30
Anik Dhabal Babu 285f6d54a3 fix: unable to add and remove org banner (#27848) 2026-02-11 06:58:20 +00:00
Peer Richelsen 1e0cf3d0f6 feat: caretta.so app (#27840)
* new caretta.so app

* added logo and screenshots

* nit
2026-02-11 06:45:34 +00:00
681d191537 fix: ensure metadata key is always present in BOOKING_CREATED webhook payload (#27300)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: ali@cal.com <ali@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2026-02-11 06:05:51 +00:00
Anik Dhabal BabuandDevin AI 269ed87db4 fix: use unique UIDs in seated slots E2E tests to prevent flaky uid collisions (#27847)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-11 00:55:38 -03:00
Madhav KumarandVolnei Munhoz 0191f54519 Populated Contributing Section with a welcoming summary (#27830)
Co-authored-by: Volnei Munhoz <volnei@cal.com>
2026-02-11 03:02:54 +00:00
Everton Resende dc3e681e01 fix: hide cancel/reschedule links in emails when disabled (#27637)
When disableCancelling or disableRescheduling is enabled on an event type,
the confirmation email still showed non-functional cancel/reschedule links.

The ManageLink email component already had correct conditional logic to
hide these links, but two flows constructed the CalendarEvent manually
without including the flags: the booking confirmation handler and the
payment booking flow. This caused the flags to be undefined, which
ManageLink interpreted as enabled.

Added disableCancelling and disableRescheduling to the Prisma select and
CalendarEvent object in both confirm.handler.ts and getBooking.ts.

Closes #22906
2026-02-10 18:57:54 -03:00
Romit 30bba3da15 fix (#27478) 2026-02-10 18:56:45 -03:00
Udit Takkar cfa0783ebc feat: add claude specs (#26852)
* feat: claude templates

* refactor: new file

* feat: symlink
2026-02-10 19:06:31 +00:00
81e4241f85 refactor: apply biome formatting to apps/web (#27692)
* refactor: apply biome formatting to apps/web (batch 1)

Formats apps/web non-modules directories:
- apps/web/app
- apps/web/lib
- apps/web/pages
- apps/web/styles
- apps/web/server
- apps/web/test
- Root-level .ts and .mjs files

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

* refactor: apply biome formatting to apps/web/modules (batch 2)

Formats smaller apps/web/modules directories:
- onboarding, data-table, users, apps, auth
- integration-attribute-sync, shell, availability
- feature-flags, team, webhooks, getting-started
- feature-opt-in, troubleshooter, schedules, notifications
- signup-view.tsx

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

* refactor: apply biome formatting to apps/web/modules (batch 3)

Formats medium apps/web/modules directories:
- bookings
- event-types
- insights
- embed

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

* refactor: apply biome formatting to apps/web/modules/ee (batch 4)

Formats apps/web/modules/ee directory.

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

* refactor: apply biome formatting to apps/web (batch 5)

Formats remaining apps/web directories:
- apps/web/modules/settings
- apps/web/modules/booking-audit
- apps/web/playwright

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

* refactor: apply biome formatting to apps/web/components (batch 6)

Formats remaining apps/web/components files.

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

* refactor: apply biome formatting to new apps/web files from main

Formats newly added/modified files from main merge:
- WorkflowStepContainer.tsx (resolved merge conflicts)
- Newly moved files (blocklist, form-builder, schedules, etc.)
- Other files modified in main

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

* refactor: apply biome formatting to new apps/web files from main

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 23:25:24 +05:30
f4abbb2de1 feat: refactor billing to strategy implemention (#27828)
* factory and statergie

* chore: use correct method of DI

* feat: add onchagne

* add logic to HWM stat

* add webhook resolver methods to each statergy

* move seat tracking + webhooks over to own statergy

* Move to factory base approach

* move logic to correct class

* rename create -> createByTeamId

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

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

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

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 13:11:36 -03:00
3908cda372 perf: optimize getBusyTimesForLimitChecks with batched parallel queries (#27399)
* perf: optimize getBusyTimesForLimitChecks with batched parallel queries and raw SQL

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

* test: add unit tests for getBusyTimesForLimitChecks batched query optimization

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

* refactor: use Prisma findMany instead of raw SQL for better testability

The batching (50 user chunks) and parallel execution (Promise.all) provide
the main performance optimization. Using findMany instead of $queryRaw
maintains the same performance characteristics while being compatible
with prismock for testing.

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

* fix: type error

* refactor: add integration test

* chore: remove log

* chore: remove log

* fix: avoid logging PII (userId) in busyTimes debug log

Replace JSON.stringify(busyTimes) with count to avoid exposing userId
in logs. Addresses Cubic AI review feedback (confidence 9/10).

Co-Authored-By: unknown <>

* chore: undo

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 15:56:30 +00:00
Rajiv Sahal db76980644 fix: API v2 @GetWebhook() decorator doesn't generate OpenAPI path (#27612)
* fix: openapi spec for GetWebhook decorator

* fix: openapi spec
2026-02-10 11:56:55 -03:00
sean-brydon d8e6ecc7a0 fix: add i18n + migration (#27836) 2026-02-10 14:44:19 +00:00
16eb96e27d fix: update booking page terms text to use APP_NAME instead of "our" (#27834)
* fix: update booking page terms text to use APP_NAME instead of 'our'

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

* remove changes for platform booker

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2026-02-10 14:41:00 +00:00
Pedro Castro 83c42cfb2b chore: resolve @modelcontextprotocol/sdk to 1.26.0 (#27640) 2026-02-10 22:28:21 +09:00
918 changed files with 19306 additions and 12540 deletions
+2
View File
@@ -209,11 +209,13 @@ NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE=
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
STRIPE_TEAM_MONTHLY_PRICE_ID=
STRIPE_TEAM_ANNUAL_PRICE_ID=
NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID=
ORG_MONTHLY_CREDITS=
STRIPE_TEAM_PRODUCT_ID=
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
STRIPE_ORG_MONTHLY_PRICE_ID=
STRIPE_ORG_ANNUAL_PRICE_ID=
STRIPE_ORG_PRODUCT_ID=
# Used to pass trial days for orgs during the Stripe checkout session
STRIPE_ORG_TRIAL_DAYS=
+8 -1
View File
@@ -7,4 +7,11 @@ GET /v2/bookings/{bookingUid} added 'subschema #3, subschema #6' to the 'data' r
POST /v2/bookings/{bookingUid}/cancel added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
POST /v2/bookings/{bookingUid}/confirm added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
POST /v2/bookings/{bookingUid}/decline added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
POST /v2/bookings/{bookingUid}/guests added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
POST /v2/bookings/{bookingUid}/guests added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
DELETE /v2/event-types/{eventTypeId}/webhooks/{webhookId} added the new path request parameter 'webhookId'
GET /v2/event-types/{eventTypeId}/webhooks/{webhookId} added the new path request parameter 'webhookId'
DELETE /v2/oauth-clients/{clientId}/webhooks/{webhookId} added the new path request parameter 'webhookId'
GET /v2/oauth-clients/{clientId}/webhooks/{webhookId} added the new path request parameter 'webhookId'
DELETE /v2/teams/{teamId}/event-types/{eventTypeId}/webhooks/{webhookId} added the new path request parameter 'webhookId'
GET /v2/teams/{teamId}/event-types/{eventTypeId}/webhooks/{webhookId} added the new path request parameter 'webhookId'
GET /v2/webhooks/{webhookId} added the new path request parameter 'webhookId'
+3
View File
@@ -108,6 +108,9 @@ packages/**/.yarn/ci-cache/
.claude/plans/
.claude/settings.local.json
# Spec drafts (rough work, not ready for review)
specs/**/_drafts/
# trigger.dev
.trigger
agents/devin-knowledge.json
+8
View File
@@ -226,6 +226,14 @@ import { ProfileRepository } from "@calcom/features/profile/repositories/Profile
- Fix type errors before test failures - they're often the root cause
- Run `yarn prisma generate` if you see missing enum/type errors
## Spec-Driven Development (Opt-In)
For complex features, you can use spec-driven development when explicitly requested.
**To enable:** Tell the AI "use spec-driven development" or "follow the spec workflow"
See [SPEC-WORKFLOW.md](SPEC-WORKFLOW.md) for the full workflow documentation.
## Extended Documentation
For detailed information, see the `agents/` directory:
+6 -9
View File
@@ -767,11 +767,14 @@ Cal.com, Inc. is a commercial open source company, which means some parts of thi
<img width="100%" src="https://repobeats.axiom.co/api/embed/6bfca2f20f39738048b6e70ca205efde46352c3d.svg" />
<!-- CONTRIBUTING -->
## Contributing
We ❤️ contributions! Whether it’s fixing a typo, improving documentation, or building new features, your help makes Cal.com better.
Please see our [contributing guide](/CONTRIBUTING.md).
- Check out our [Contributing Guide](./CONTRIBUTING.md) for detailed steps.
- Join the discussion on [GitHub Discussions](https://github.com/calcom/cal.com/discussions) or our community channels.
- Please follow our coding standards and commit message conventions to keep the project consistent.
Even small improvements matter — thank you for helping us grow!
### Good First Issues
@@ -984,12 +987,6 @@ An example of good readme is [atoms readme](https://github.com/calcom/cal.com/bl
1. Follow semantic versioning when using changesets.
2. Mark breaking changes using `❗️Breaking change`
<!-- LICENSE -->
## License
Distributed under the [AGPLv3 License](https://github.com/calcom/cal.com/blob/main/LICENSE). See `LICENSE` for more information.
<!-- ACKNOWLEDGEMENTS -->
## Acknowledgements
+105
View File
@@ -0,0 +1,105 @@
# Spec-Driven Development Workflow
This workflow is **opt-in**. Use it when explicitly requested by saying "use spec-driven development" or "follow the spec workflow".
## Design Documents
The `specs/` folder contains design documents for features in development.
Each spec follows this structure:
```
specs/{feature}/
├── CLAUDE.md # Feature-specific instructions (read this first)
├── design.md # The specification
├── implementation.md # Current status and what's done
├── decisions.md # Why decisions were made
├── prompts.md # Reusable prompts
├── future-work.md # What's deferred
└── docs/ # Documentation with screenshots
```
**Workflow:**
1. Read the spec's `CLAUDE.md` for specific instructions
2. Read `design.md` to understand what we're building
3. Check `implementation.md` for current status
4. Find the relevant code in the codebase
5. Implement in small pieces, update `implementation.md` after each
---
## When Implementing Features (Spec Mode)
1. **Check for design doc** in `specs/` — if it exists, follow it
2. **If no spec exists** — ask if you should create one first
3. **Look at existing patterns** — find similar code and follow conventions
4. **Update implementation.md** — mark what's done after each piece
5. **Update decisions.md** — when choosing between approaches
---
## Creating a New Spec
When user asks to build a new feature:
1. Copy the template: `cp -r specs/_templates specs/{feature-name}`
2. Explore the codebase to understand existing patterns
3. Write `design.md` with technical spec
4. Write `CLAUDE.md` with feature-specific instructions
5. Initialize `implementation.md` with "not-started" status
6. Ask user to review before implementing
---
## Updating Spec Files
### implementation.md — After completing each piece:
```markdown
## Status: in-progress
## Completed
- [x] Database schema
- [x] Migration file
## In Progress
- tRPC endpoints
## Next Steps
1. UI components
2. Tests
## Session Notes
### 2024-01-15
- Done: Added schema, created migration
- Next: Implement tRPC router
```
### decisions.md — When choosing between approaches:
```markdown
## ADR-001: Use Separate Table for Custom Locations
### Context
Need to store user-defined locations.
### Options
1. JSON field — simpler, but harder to query
2. Separate table — more flexible, better indexing
### Decision
Separate table for better querying.
### Consequences
- Need migration
- Need new tRPC router
```
---
## Don't (When Using Spec-Driven Development)
- Don't implement features without checking for a design doc first
- Don't skip updating implementation.md after completing work
- Don't make architectural decisions without recording them in decisions.md
@@ -0,0 +1,193 @@
---
title: Prevent Circular Dependencies Between Packages
impact: CRITICAL
impactDescription: Prevents build failures, type errors, ensures honest implementation, and maintains clean dependency graph
tags: architecture, dependencies, packages, imports, circular
---
## Prevent Circular Dependencies Between Packages
**Impact: CRITICAL**
Circular dependencies between packages cause build failures, type errors, and make the codebase unmaintainable. The dependency graph must be acyclic, with clear layers from low-level utilities up to high-level features.
The dependency hierarchy (from lowest to highest):
```
packages/lib (lowest - no feature dependencies)
↓
packages/app-store
↓
packages/features
↓
packages/trpc
↓
apps/web (highest - can depend on all packages)
```
**Benefits:**
- **Honest implementation**: The dependency graph accurately reflects what code depends on what, with no hidden or circular relationships that create false expectations
- **Predictable behavior**: Code behaves as the dependency structure suggests - no surprises from circular imports
- **Clear mental model**: Developers can reason about the system without tracking complex circular relationships
- **Build reliability**: No circular dependency errors during compilation or bundling
- **Easier testing**: Lower-level packages can be tested independently without pulling in the entire codebase
### `packages/lib`
**Rules:**
1. No files in `packages/lib` import from `@calcom/app-store` or `../app-store/**`
2. No files in `packages/lib` import from `@calcom/features` or `../features/**`
3. No files in `packages/lib` import from `@calcom/trpc` or `../trpc/**`
4. No files in `packages/lib` import from `@trpc/server`
5. **BONUS:** No repository files allowed in `packages/lib` (repositories belong in features or app-store)
**Incorrect:**
```typescript
// Bad - lib importing from features
import { getBooking } from "@calcom/features/bookings";
import { EventRepository } from "@calcom/features/events/repositories";
// Bad - lib importing from app-store
import { googleCalendarHandler } from "@calcom/app-store/googlecalendar";
// Bad - lib importing from trpc
import { router } from "@calcom/trpc/server/trpc";
```
**Correct:**
```typescript
// Good - lib only imports from other lib files or external packages
import { logger } from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
import type { User } from "@prisma/client";
```
### `packages/app-store`
**Rules:**
6. No files in `packages/app-store` import from `@calcom/features` or `../features/**`
7. No files in `packages/app-store` import from `@calcom/trpc` or `../trpc/**`
**Incorrect:**
```typescript
// Bad - app-store importing from features
import { BookingService } from "@calcom/features/bookings/services";
// Bad - app-store importing from trpc
import { publicProcedure } from "@calcom/trpc/server/trpc";
```
**Correct:**
```typescript
// Good - app-store imports from lib and prisma
import { logger } from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
import type { Credential } from "@prisma/client";
```
### `packages/features`
**Rules:**
8. No files in `packages/features` import from `@calcom/trpc` or `../trpc/**`
9. No files in `packages/features` import from `@calcom/web` or `@calcom/web/**`
10. No files in `packages/features` import from `../../apps/web/**`
**Incorrect:**
```typescript
// Bad - features importing from trpc
import { router } from "@calcom/trpc/server/trpc";
// Bad - features importing from web app
import { getServerSession } from "@calcom/web/lib/auth";
import { WebComponent } from "../../apps/web/components/WebComponent";
```
**Correct:**
```typescript
// Good - features import from lib, app-store, and prisma
import { logger } from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
import { googleCalendarHandler } from "@calcom/app-store/googlecalendar";
```
### `packages/trpc`
**Rules:**
11. No files in `packages/trpc` import from `../apps/web/**`
**Incorrect:**
```typescript
// Bad - trpc importing from web app
import { getServerSession } from "../../apps/web/lib/auth";
```
**Correct:**
```typescript
// Good - trpc imports from features, lib, and prisma
import { BookingService } from "@calcom/features/bookings/services";
import { logger } from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
```
### `packages/testing`
**Rules:**
12. No files in `packages/testing` import from `@calcom/web` or `@calcom/web/**`
13. No files in `packages/testing` import from `@calcom/features` or `@calcom/features/**`
14. No files in `packages/testing` import from `../../apps/web/**`
15. No files in `packages/testing` import from `../features/**`
**Incorrect:**
```typescript
// Bad - testing importing from web or features
import { WebComponent } from "@calcom/web/components";
import { BookingService } from "@calcom/features/bookings";
```
**Correct:**
```typescript
// Good - testing imports from lib and prisma
import { prisma } from "@calcom/prisma";
import { logger } from "@calcom/lib/logger";
```
### `packages/platform/atoms`
**Rules:**
16. No files in `packages/platform/atoms` import from `@calcom/trpc` or `@calcom/trpc/**`
17. No files in `packages/platform/atoms` import from `../../trpc` or `../../trpc/**`
18. No files in `packages/platform/atoms` import from `@calcom/web`
**Incorrect:**
```typescript
// Bad - platform/atoms importing from trpc or web
import { router } from "@calcom/trpc/server/trpc";
import { trpc } from "../../trpc";
import { WebComponent } from "@calcom/web";
```
**Correct:**
```typescript
// Good - platform/atoms imports from lib, features, and ui
import { logger } from "@calcom/lib/logger";
import { Button } from "@calcom/ui/components/button";
import type { Booking } from "@calcom/features/bookings/types";
```
## Enforcement
These rules should be enforced through:
- ESLint rules that detect forbidden import paths
- CI checks that fail on circular dependencies
- Code review guidelines that flag violations
+1 -1
View File
@@ -1 +1 @@
export const TRIGGER_VERSION = '20260210.1';
export const TRIGGER_VERSION = '20260212.3';
+1 -1
View File
@@ -27,7 +27,7 @@ CALENDSO_ENCRYPTION_KEY="Lmqy57DlHKPvwwrtkwjL/8QtxJWxC/OV"
# Service Account Encryption Key for encrypting/decrypting service account keys
CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY="LDBFYLEVICRZBYQZUZCSTZRJFZUKZMEI"
# url to redis instance running in Docker and used to cache data. Redis is setup in apps/api/v2/docker-compose.yaml
REDIS_URL="rediss://localhost:6379"
REDIS_URL="redis://localhost:6379"
# KEEP THIS EMPTY, DISABLE SENTRY CLIENT INSIDE OF LIBRARIES USED BY APIv2
NEXT_PUBLIC_SENTRY_DSN=
@@ -26,7 +26,7 @@ import {
Patch,
ParseIntPipe,
} from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -84,6 +84,7 @@ export class EventTypeWebhooksController {
@Get("/:webhookId")
@ApiOperation({ summary: "Get a webhook" })
@ApiParam({ name: "webhookId", type: String, required: true })
async getEventTypeWebhook(@GetWebhook() webhook: Webhook): Promise<EventTypeWebhookOutputResponseDto> {
return {
status: SUCCESS_STATUS,
@@ -116,6 +117,7 @@ export class EventTypeWebhooksController {
@Delete("/:webhookId")
@ApiOperation({ summary: "Delete a webhook" })
@ApiParam({ name: "webhookId", type: String, required: true })
async deleteEventTypeWebhook(@GetWebhook() webhook: Webhook): Promise<EventTypeWebhookOutputResponseDto> {
await this.webhooksService.deleteWebhook(webhook.id);
return {
@@ -3,7 +3,7 @@ import { MembershipRole } from "@calcom/platform-libraries";
import { SkipTakePagination } from "@calcom/platform-types";
import type { Webhook } from "@calcom/prisma/client";
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
@@ -94,6 +94,7 @@ export class OAuthClientWebhooksController {
summary: "Get a webhook",
description: `<Warning>These endpoints are deprecated and will be removed in the future.</Warning>`,
})
@ApiParam({ name: "webhookId", type: String, required: true })
@UseGuards(IsOAuthClientWebhookGuard)
async getOAuthClientWebhook(@GetWebhook() webhook: Webhook): Promise<OAuthClientWebhookOutputResponseDto> {
return {
@@ -135,6 +136,7 @@ export class OAuthClientWebhooksController {
summary: "Delete a webhook",
description: `<Warning>These endpoints are deprecated and will be removed in the future.</Warning>`,
})
@ApiParam({ name: "webhookId", type: String, required: true })
@UseGuards(IsOAuthClientWebhookGuard)
async deleteOAuthClientWebhook(
@GetWebhook() webhook: Webhook
@@ -549,7 +549,7 @@ describe("Slots 2024-04-15 Endpoints", () => {
});
bookingSeatsRepositoryFixture.create({
referenceUid: "100",
referenceUid: randomString(),
data: {},
booking: {
connect: {
@@ -632,7 +632,7 @@ describe("Slots 2024-04-15 Endpoints", () => {
});
bookingSeatsRepositoryFixture.create({
referenceUid: "100",
referenceUid: randomString(),
data: {},
booking: {
connect: {
@@ -866,7 +866,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
});
bookingSeatsRepositoryFixture.create({
referenceUid: "100",
referenceUid: randomString(),
data: {},
booking: {
connect: {
@@ -995,7 +995,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
});
bookingSeatsRepositoryFixture.create({
referenceUid: "100",
referenceUid: randomString(),
data: {},
booking: {
connect: {
@@ -13,7 +13,7 @@ import {
Query,
UseGuards,
} from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_HEADER } from "@/lib/docs/headers";
@@ -87,6 +87,7 @@ export class TeamsEventTypesWebhooksController {
@Get("/:webhookId")
@ApiOperation({ summary: "Get a webhook for a team event type" })
@ApiParam({ name: "webhookId", type: String, required: true })
@Roles("TEAM_MEMBER")
async getTeamEventTypeWebhook(@GetWebhook() webhook: Webhook): Promise<EventTypeWebhookOutputResponseDto> {
return {
@@ -122,6 +123,7 @@ export class TeamsEventTypesWebhooksController {
@Delete("/:webhookId")
@Roles("TEAM_ADMIN")
@ApiOperation({ summary: "Delete a webhook for a team event type" })
@ApiParam({ name: "webhookId", type: String, required: true })
async deleteTeamEventTypeWebhook(
@GetWebhook() webhook: Webhook
): Promise<EventTypeWebhookOutputResponseDto> {
@@ -16,7 +16,7 @@ import { WebhookOutputPipe } from "@/modules/webhooks/pipes/WebhookOutputPipe";
import { UserWebhooksService } from "@/modules/webhooks/services/user-webhooks.service";
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
import { Controller, Post, Body, UseGuards, Get, Param, Query, Delete, Patch } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -75,6 +75,7 @@ export class WebhooksController {
@Get("/:webhookId")
@ApiOperation({ summary: "Get a webhook" })
@ApiParam({ name: "webhookId", type: String, required: true })
@UseGuards(IsUserWebhookGuard)
async getWebhook(@GetWebhook() webhook: Webhook): Promise<UserWebhookOutputResponseDto> {
return {
+1 -1
View File
@@ -1 +1 @@
export const TRIGGER_VERSION = '20260210.1';
export const TRIGGER_VERSION = '20260212.3';
@@ -1,6 +1,6 @@
// Added as a separate route for now to ease the testing of the audit logs feature
// It partially matches the figma design - https://www.figma.com/design/wleA2SR6rn60EK7ORxAfMy/Cal.com-New-Features?node-id=5641-6732&p=f
// TOOD: Move it to the booking page side bar later
// TOOD: Move it to the booking page side bar later
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
import type { PageProps } from "app/_types";
import { _generateMetadata, getTranslate } from "app/_utils";
@@ -14,35 +14,34 @@ import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { BookingHistoryPage } from "@calcom/web/modules/booking-audit/components/BookingHistoryPage";
export const generateMetadata = async ({ params }: { params: Promise<{ uid: string }> }) =>
await _generateMetadata(
(t) => t("booking_history"),
(t) => t("booking_history_description"),
undefined,
undefined,
`/booking/${(await params).uid}/logs`
);
await _generateMetadata(
(t) => t("booking_history"),
(t) => t("booking_history_description"),
undefined,
undefined,
`/booking/${(await params).uid}/logs`
);
const Page = async ({ params }: PageProps) => {
const resolvedParams = await params;
const bookingUid = resolvedParams.uid;
const resolvedParams = await params;
const bookingUid = resolvedParams.uid;
if (!bookingUid || typeof bookingUid !== "string") {
redirect("/bookings/upcoming");
}
if (!bookingUid || typeof bookingUid !== "string") {
redirect("/bookings/upcoming");
}
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
if (!session?.user?.id) {
redirect("/auth/login");
}
if (!session?.user?.id) {
redirect("/auth/login");
}
return (
<ShellMainAppDir heading={t("booking_history")} subtitle={t("booking_history_description")}>
<BookingHistoryPage bookingUid={bookingUid} />
</ShellMainAppDir>
);
return (
<ShellMainAppDir heading={t("booking_history")} subtitle={t("booking_history_description")}>
<BookingHistoryPage bookingUid={bookingUid} />
</ShellMainAppDir>
);
};
export default Page;
@@ -5,11 +5,7 @@ import type { RouterOutputs } from "@calcom/trpc/react";
import { eventTypesRouter } from "@calcom/trpc/server/routers/viewer/eventTypes/_router";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { createRouterCaller, getTRPCContext } from "app/_trpc/context";
import type {
PageProps,
ReadonlyHeaders,
ReadonlyRequestCookies,
} from "app/_types";
import type { PageProps, ReadonlyHeaders, ReadonlyRequestCookies } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { unstable_cache } from "next/cache";
import { cookies, headers } from "next/headers";
@@ -26,26 +22,25 @@ const getCachedEventGroups: (
userIds?: number[] | undefined;
upIds?: string[] | undefined;
}
) => Promise<RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"]> =
unstable_cache(
async (
headers: ReadonlyHeaders,
cookies: ReadonlyRequestCookies,
filters?: {
teamIds?: number[] | undefined;
userIds?: number[] | undefined;
upIds?: string[] | undefined;
}
): Promise<RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"]> => {
const eventTypesCaller = await createRouterCaller(
eventTypesRouter,
await getTRPCContext(headers, cookies)
);
return await eventTypesCaller.getUserEventGroups({ filters });
},
["viewer.eventTypes.getUserEventGroups"],
{ revalidate: 3600 } // seconds
);
) => Promise<RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"]> = unstable_cache(
async (
headers: ReadonlyHeaders,
cookies: ReadonlyRequestCookies,
filters?: {
teamIds?: number[] | undefined;
userIds?: number[] | undefined;
upIds?: string[] | undefined;
}
): Promise<RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"]> => {
const eventTypesCaller = await createRouterCaller(
eventTypesRouter,
await getTRPCContext(headers, cookies)
);
return await eventTypesCaller.getUserEventGroups({ filters });
},
["viewer.eventTypes.getUserEventGroups"],
{ revalidate: 3600 } // seconds
);
const Page = async ({ searchParams }: PageProps): Promise<ReactElement> => {
const _searchParams = await searchParams;
@@ -71,23 +66,12 @@ const Page = async ({ searchParams }: PageProps): Promise<ReactElement> => {
}
const filters = getTeamsFiltersFromQuery(_searchParams);
const userEventGroupsData = await getCachedEventGroups(
_headers,
_cookies,
filters
);
const userEventGroupsData = await getCachedEventGroups(_headers, _cookies, filters);
return (
<EventTypesWrapper
userEventGroupsData={userEventGroupsData}
user={session.user}
/>
);
return <EventTypesWrapper userEventGroupsData={userEventGroupsData} user={session.user} />;
};
export const generateMetadata = async (): Promise<
ReturnType<typeof _generateMetadata>
> =>
export const generateMetadata = async (): Promise<ReturnType<typeof _generateMetadata>> =>
await _generateMetadata(
(t) => t("event_types_page_title"),
(t) => t("event_types_page_subtitle"),
@@ -15,7 +15,7 @@ export const TeamsCTA = () => {
size="sm"
type="button"
onClick={() => {
posthog.capture("add_team_button_clicked")
posthog.capture("add_team_button_clicked");
}}
href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}>
{t("new")}
@@ -38,12 +38,15 @@ const ServerPage = async () => {
const categoryQuery = appStore.map(({ categories }) => ({
categories: categories || [],
}));
const categories = categoryQuery.reduce((c, app) => {
for (const category of app.categories) {
c[category] = c[category] ? c[category] + 1 : 1;
}
return c;
}, {} as Record<string, number>);
const categories = categoryQuery.reduce(
(c, app) => {
for (const category of app.categories) {
c[category] = c[category] ? c[category] + 1 : 1;
}
return c;
},
{} as Record<string, number>
);
const props = {
categories: Object.entries(categories)
@@ -14,10 +14,10 @@ import {
SelectField,
TextField,
} from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { Tooltip } from "@calcom/ui/components/tooltip";
import type { getServerSidePropsForSingleFormView as getServerSideProps } from "@calcom/web/lib/apps/routing-forms/[...pages]/getServerSidePropsSingleForm";
import SingleForm from "@components/apps/routing-forms/SingleForm";
import { ChevronDownIcon, MenuIcon } from "@coss/ui/icons";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { UseFormReturn } from "react-hook-form";
@@ -93,8 +93,7 @@ function Field({
}
: null
}
deleteField={router ? null : deleteField}
>
deleteField={router ? null : deleteField}>
<FormCardBody>
<div className="mb-3 w-full">
<TextField
@@ -114,17 +113,12 @@ function Field({
hookForm.setValue(`${hookFieldNamespace}.label`, newLabel, {
shouldDirty: true,
});
const currentIdentifier = hookForm.getValues(
`${hookFieldNamespace}.identifier`
);
const currentIdentifier = hookForm.getValues(`${hookFieldNamespace}.identifier`);
// Only auto-update identifier if it was auto-generated from the previous label
// This preserves manual identifier changes
const isIdentifierGeneratedFromPreviousLabel =
currentIdentifier === getFieldIdentifier(previousLabel).toLowerCase();
if (
!currentIdentifier ||
isIdentifierGeneratedFromPreviousLabel
) {
if (!currentIdentifier || isIdentifierGeneratedFromPreviousLabel) {
hookForm.setValue(
`${hookFieldNamespace}.identifier`,
getFieldIdentifier(newLabel).toLowerCase(),
@@ -142,19 +136,11 @@ function Field({
name={`${hookFieldNamespace}.identifier`}
required
placeholder={t("identifies_name_field")}
value={
identifier ||
routerField?.identifier ||
label ||
routerField?.label ||
""
}
value={identifier || routerField?.identifier || label || routerField?.label || ""}
onChange={(e) => {
hookForm.setValue(
`${hookFieldNamespace}.identifier`,
e.target.value.toLowerCase(),
{ shouldDirty: true }
);
hookForm.setValue(`${hookFieldNamespace}.identifier`, e.target.value.toLowerCase(), {
shouldDirty: true,
});
}}
/>
</div>
@@ -164,9 +150,7 @@ function Field({
control={hookForm.control}
defaultValue={routerField?.type}
render={({ field: { value, onChange } }) => {
const defaultValue = FieldTypes.find(
(fieldType) => fieldType.value === value
);
const defaultValue = FieldTypes.find((fieldType) => fieldType.value === value);
if (disableTypeChange) {
return (
<div className="data-testid-field-type">
@@ -179,15 +163,9 @@ function Field({
className={classNames(
"h-8 w-full justify-between text-left text-sm",
!!router && "bg-subtle cursor-not-allowed"
)}
>
<span className="text-default">
{defaultValue?.label || "Select field type"}
</span>
<Icon
name="chevron-down"
className="text-default h-4 w-4"
/>
)}>
<span className="text-default">{defaultValue?.label || t("select_field_type")}</span>
<ChevronDownIcon className="text-default h-4 w-4" />
</Button>
</Tooltip>
</div>
@@ -304,9 +282,7 @@ const FormEdit = ({
<div className="w-full py-4 lg:py-8">
<div ref={animationRef} className="flex w-full flex-col rounded-md">
{hookFormFields.map((field, key) => {
const existingField = Boolean(
(form.fields || []).find((f) => f.id === field.id)
);
const existingField = Boolean((form.fields || []).find((f) => f.id === field.id));
const hasFormResponses = (form._count?.responses ?? 0) > 0;
return (
<Field
@@ -343,13 +319,7 @@ const FormEdit = ({
</div>
{hookFormFields.length ? (
<div className={classNames("flex")}>
<Button
data-testid="add-field"
type="button"
StartIcon="plus"
color="secondary"
onClick={addField}
>
<Button data-testid="add-field" type="button" StartIcon="plus" color="secondary" onClick={addField}>
Add question
</Button>
</div>
@@ -363,7 +333,7 @@ const FormEdit = ({
{/* Icon card - Top */}
<div className="bg-default border-subtle z-30 col-start-1 col-end-1 row-start-1 row-end-1 h-10 w-10 transform rounded-md border shadow-sm">
<div className="text-emphasis flex h-full items-center justify-center">
<Icon name="menu" className="text-emphasis h-4 w-4" />
<MenuIcon className="text-emphasis h-4 w-4" />
</div>
</div>
{/* Left fanned card */}
@@ -389,12 +359,7 @@ const FormEdit = ({
Fields are the form fields that the booker would see.
</p>
</div>
<Button
data-testid="add-field"
onClick={addField}
StartIcon="plus"
className="mt-6"
>
<Button data-testid="add-field" onClick={addField} StartIcon="plus" className="mt-6">
Add question
</Button>
</div>
@@ -414,9 +379,7 @@ export default function FormEditPage({
{...props}
appUrl={appUrl}
permissions={permissions}
Page={({ hookForm, form }) => (
<FormEdit appUrl={appUrl} hookForm={hookForm} form={form} />
)}
Page={({ hookForm, form }) => <FormEdit appUrl={appUrl} hookForm={hookForm} form={form} />}
/>
</>
);
@@ -1,10 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import type z from "zod";
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
import { WhenToWriteToRecord, SalesforceFieldType } from "@calcom/app-store/salesforce/lib/enums";
import { SalesforceFieldType, WhenToWriteToRecord } from "@calcom/app-store/salesforce/lib/enums";
import type { writeToRecordDataSchema as salesforceWriteToRecordDataSchema } from "@calcom/app-store/salesforce/zod";
import { routingFormIncompleteBookingDataSchema as salesforceRoutingFormIncompleteBookingDataSchema } from "@calcom/app-store/salesforce/zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -12,15 +9,13 @@ import { IncompleteBookingActionType } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Button } from "@calcom/ui/components/button";
import { Switch } from "@calcom/ui/components/form";
import { InputField } from "@calcom/ui/components/form";
import { Select } from "@calcom/ui/components/form";
import { Label } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { InputField, Label, Select, Switch } from "@calcom/ui/components/form";
import { showToast } from "@calcom/ui/components/toast";
import type { getServerSidePropsForSingleFormView as getServerSideProps } from "@calcom/web/lib/apps/routing-forms/[...pages]/getServerSidePropsSingleForm";
import SingleForm from "@components/apps/routing-forms/SingleForm";
import { GlobeIcon } from "@coss/ui/icons";
import { useEffect, useState } from "react";
import type z from "zod";
function Page({ form }: { form: RoutingFormWithResponseCount }) {
const { t } = useLocale();
@@ -88,8 +83,8 @@ function Page({ form }: { form: RoutingFormWithResponseCount }) {
setSelectedCredential(
credentialOptions
? credentialOptions.find((option) => option.value === salesforceAction?.credentialId) ??
selectedCredential
? (credentialOptions.find((option) => option.value === salesforceAction?.credentialId) ??
selectedCredential)
: selectedCredential
);
}
@@ -105,7 +100,7 @@ function Page({ form }: { form: RoutingFormWithResponseCount }) {
<div className="flex items-center justify-between">
<div className="flex items-center gap-0.5">
<div className="border-subtle rounded-lg border p-1">
<Icon name="globe" className="text-subtle h-4 w-4" />
<GlobeIcon className="text-subtle h-4 w-4" />
</div>
<div className="flex flex-col">
<span className="text-emphasis ml-2 text-sm font-medium">
@@ -128,17 +123,17 @@ function Page({ form }: { form: RoutingFormWithResponseCount }) {
<div className="bg-default mt-2 rounded-xl px-2 py-2">
<Label>Credential to use</Label>
<Select
size="sm"
options={credentialOptions}
value={selectedCredential}
onChange={(option) => {
if (!option) {
return;
}
setSelectedCredential(option);
}}
/>
</div>
size="sm"
options={credentialOptions}
value={selectedCredential}
onChange={(option) => {
if (!option) {
return;
}
setSelectedCredential(option);
}}
/>
</div>
</>
)}
@@ -1,43 +1,34 @@
"use client";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import Link from "next/link";
import React, { useCallback, useState, useEffect } from "react";
import { Query, Builder, Utils as QbUtils } from "react-awesome-query-builder";
import type { ImmutableTree, BuilderProps, Config } from "react-awesome-query-builder";
import type { JsonTree } from "react-awesome-query-builder";
import type { UseFormReturn } from "react-hook-form";
import { Toaster } from "sonner";
import { buildEmptyQueryValue } from "@calcom/app-store/_utils/raqb/raqbUtils.client";
import { raqbQueryValueUtils } from "@calcom/app-store/_utils/raqb/raqbUtils.server";
import { routingFormAppComponents } from "@calcom/app-store/routing-forms/appComponents";
import DynamicAppComponent from "@calcom/app-store/routing-forms/components/DynamicAppComponent";
import { EmptyState } from "@calcom/app-store/routing-forms/components/_components/EmptyState";
import { RoutingSkeleton } from "@calcom/app-store/routing-forms/components/_components/RoutingSkeleton";
import DynamicAppComponent from "@calcom/app-store/routing-forms/components/DynamicAppComponent";
import {
withRaqbSettingsAndWidgets,
ConfigFor,
withRaqbSettingsAndWidgets,
} from "@calcom/app-store/routing-forms/components/react-awesome-query-builder/config/uiConfig";
import { RoutingPages } from "@calcom/app-store/routing-forms/lib/RoutingPages";
import { createFallbackRoute } from "@calcom/app-store/routing-forms/lib/createFallbackRoute";
import getEventTypeAppMetadata from "@calcom/app-store/routing-forms/lib/getEventTypeAppMetadata";
import {
getQueryBuilderConfigForFormFields,
getQueryBuilderConfigForAttributes,
type FormFieldsQueryBuilderConfigWithRaqbFields,
type AttributesQueryBuilderConfigWithRaqbFields,
type FormFieldsQueryBuilderConfigWithRaqbFields,
getQueryBuilderConfigForAttributes,
getQueryBuilderConfigForFormFields,
isDynamicOperandField,
} from "@calcom/app-store/routing-forms/lib/getQueryBuilderConfig";
import isRouter from "@calcom/app-store/routing-forms/lib/isRouter";
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
import { RoutingPages } from "@calcom/app-store/routing-forms/lib/RoutingPages";
import type {
Attribute,
AttributeRoutingConfig,
EditFormRoute,
GlobalRoute,
LocalRoute,
RoutingFormWithResponseCount,
SerializableRoute,
Attribute,
EditFormRoute,
AttributeRoutingConfig,
} from "@calcom/app-store/routing-forms/types/types";
import { RouteActionType } from "@calcom/app-store/routing-forms/zod";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
@@ -52,15 +43,25 @@ import classNames from "@calcom/ui/classNames";
import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { FormCard } from "@calcom/ui/components/card";
import { SelectWithValidation as Select, TextArea } from "@calcom/ui/components/form";
import { TextField } from "@calcom/ui/components/form";
import { SelectField } from "@calcom/ui/components/form";
import { Switch } from "@calcom/ui/components/form";
import {
SelectWithValidation as Select,
SelectField,
Switch,
TextArea,
TextField,
} from "@calcom/ui/components/form";
import type { IconName } from "@calcom/ui/components/icon";
import { Icon } from "@calcom/ui/components/icon";
import type { getServerSidePropsForSingleFormView as getServerSideProps } from "@calcom/web/lib/apps/routing-forms/[...pages]/getServerSidePropsSingleForm";
import SingleForm from "@components/apps/routing-forms/SingleForm";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import Link from "next/link";
import type React from "react";
import { useCallback, useEffect, useState } from "react";
import type { BuilderProps, Config, ImmutableTree, JsonTree } from "react-awesome-query-builder";
import { Builder, Utils as QbUtils, Query } from "react-awesome-query-builder";
import type { UseFormReturn } from "react-hook-form";
import { Toaster } from "sonner";
type Form = inferSSRProps<typeof getServerSideProps>["form"];
@@ -342,6 +343,165 @@ const WeightedAttributesSelector = ({
) : null;
};
type RouteActionSelectorProps = {
action: LocalRoute["action"] | undefined;
onActionChange: (action: LocalRoute["action"]) => void;
onEventTypeChange?: (action: LocalRoute["action"]) => void;
eventTypeOptions: { label: string; value: string; eventTypeId: number }[];
selectedEventTypeOption:
| { label: string; value: string; eventTypeId: number }
| { label: string; value: "custom"; eventTypeId: 0 }
| undefined;
disabled?: boolean;
className?: string;
showCustomEventTypeInput?: boolean;
customEventTypeSlug?: string;
onCustomEventTypeSlugChange?: (slug: string) => void;
eventTypePrefix?: string;
fieldIdentifiers?: string[];
};
const RouteActionSelector = ({
action,
onActionChange,
onEventTypeChange,
eventTypeOptions,
selectedEventTypeOption,
disabled = false,
className = "",
showCustomEventTypeInput = false,
customEventTypeSlug = "",
onCustomEventTypeSlugChange,
eventTypePrefix = "",
fieldIdentifiers = [],
}: RouteActionSelectorProps) => {
const { t } = useLocale();
return (
<div className={classNames("flex w-full flex-col gap-2 text-sm lg:flex-row", className)}>
<div className="flex grow items-center gap-2">
<Select
size="sm"
isDisabled={disabled}
className="data-testid-select-routing-action block w-full grow"
required
value={RoutingPages.find((page) => page.value === action?.type)}
onChange={(item) => {
if (!item) {
return;
}
const newAction: LocalRoute["action"] = {
type: item.value,
value: "",
};
if (newAction.type === "customPageMessage") {
newAction.value = t("default_custom_page_message");
} else {
newAction.value = "";
}
onActionChange(newAction);
}}
options={RoutingPages}
/>
</div>
{action?.type ? (
action?.type === "customPageMessage" ? (
<TextArea
required
disabled={disabled}
name="customPageMessage"
className="border-default flex grow lg:w-fit"
style={{
minHeight: "38px",
}}
value={action.value}
onChange={(e) => {
onActionChange({ ...action, value: e.target.value });
}}
/>
) : action?.type === "externalRedirectUrl" ? (
<TextField
size="sm"
disabled={disabled}
name="externalRedirectUrl"
className="border-default flex grow text-sm"
containerClassName="grow"
type="url"
required
labelSrOnly
value={action.value}
onChange={(e) => {
onActionChange({ ...action, value: e.target.value });
}}
placeholder="https://example.com"
/>
) : (
<div className="grow">
<Select
size="sm"
required
className="data-testid-eventTypeRedirectUrl-select"
isDisabled={disabled}
options={eventTypeOptions}
onChange={(option) => {
if (!option) {
return;
}
const newAction = {
...action,
value: option.value,
eventTypeId: option.eventTypeId,
};
if (onEventTypeChange) {
onEventTypeChange(newAction);
} else {
onActionChange(newAction);
}
}}
value={selectedEventTypeOption}
/>
{showCustomEventTypeInput &&
eventTypeOptions.length !== 0 &&
action.value !== "" &&
(action.value === "custom" ||
!eventTypeOptions.find((eventOption) => eventOption.value === action.value) ||
customEventTypeSlug.length) ? (
<>
<TextField
disabled={disabled}
className="border-default flex w-full grow text-sm"
containerClassName="grow mt-2"
addOnLeading={eventTypePrefix}
required
value={customEventTypeSlug}
onChange={(e) => {
onCustomEventTypeSlugChange?.(e.target.value);
onActionChange({
...action,
value: `${eventTypePrefix}${e.target.value}`,
});
}}
placeholder="event-url"
/>
<div className="mt-2 ">
<p className="text-subtle text-xs">
{fieldIdentifiers.length
? t("field_identifiers_as_variables_with_example", {
variable: `{${fieldIdentifiers[0]}}`,
})
: t("field_identifiers_as_variables")}
</p>
</div>
</>
) : null}
</div>
)
) : null}
</div>
);
};
const Route = ({
form,
route,
@@ -395,10 +555,20 @@ const Route = ({
? eventOptions[0].value.substring(0, eventOptions[0].value.lastIndexOf("/") + 1)
: "";
const getCustomSlug = (actionValue: string | undefined) => {
if (!actionValue) return "";
const isCustom = !eventOptions.find((eventOption) => eventOption.value === actionValue);
return isCustom ? (actionValue.split("/").pop() ?? "") : "";
};
const [customEventTypeSlug, setCustomEventTypeSlug] = useState<string>(() => {
const isCustom =
!isRouter(route) && !eventOptions.find((eventOption) => eventOption.value === route.action.value);
return isCustom && !isRouter(route) ? route.action.value.split("/").pop() ?? "" : "";
if (isRouter(route)) return "";
return getCustomSlug(route.action.value);
});
const [customFallbackEventTypeSlug, setCustomFallbackEventTypeSlug] = useState<string>(() => {
if (isRouter(route)) return "";
return getCustomSlug(route.fallbackAction?.value);
});
useEnsureEventTypeIdInRedirectUrlAction({
@@ -558,7 +728,7 @@ const Route = ({
const attributesQueryBuilder =
// team member attributes are only available for organization teams
route.action?.type === RouteActionType.EventTypeRedirectUrl && isTeamForm && isOrganization ? (
<div className="mt-4">
<div>
{/* TODO: */}
{eventTypeRedirectUrlSelectedOption?.eventTypeAppMetadata &&
"salesforce" in eventTypeRedirectUrlSelectedOption.eventTypeAppMetadata ? (
@@ -573,13 +743,13 @@ const Route = ({
</div>
) : null}
<div className="bg-default border-subtle cal-query-builder-container mt-2 rounded-2xl border p-2">
<div className="cal-query-builder-container mt-4" data-testid="attributes-query-builder">
<div className="ml-2 flex items-center gap-0.5">
<div className="border-subtle rounded-lg border p-1">
<Icon name="user-check" className="text-subtle h-4 w-4" />
</div>
<span className="text-emphasis ml-2 text-sm font-medium">
And connect with specific team members
{t("connect_with_specific_team_members")}
</span>
</div>
{route.attributesQueryBuilderState && attributesQueryBuilderConfigWithRaqbSettingsAndWidgets && (
@@ -600,30 +770,70 @@ const Route = ({
</div>
) : null;
const fallbackAttributesQueryBuilder =
route.action?.type === RouteActionType.EventTypeRedirectUrl && isTeamForm ? (
const fallbackActionOptions = eventTypeRedirectUrlOptions;
const fallbackActionSelectedOption = route.fallbackAction?.eventTypeId
? eventTypeRedirectUrlOptions.find((option) => option.eventTypeId === route.fallbackAction?.eventTypeId)
: eventTypeRedirectUrlOptions.find((option) => option.value === route.fallbackAction?.value);
// Only show fallback section when main action has a valid event type selected
// This prevents showing an empty fallback action selector before user selects an event type
const hasValidEventTypeSelected =
route.action?.type === RouteActionType.EventTypeRedirectUrl &&
(route.action?.eventTypeId || route.action?.value);
const matchingMembersFallbackRoute =
hasValidEventTypeSelected && isTeamForm ? (
<div className="bg-default border-subtle cal-query-builder-container mt-2 rounded-2xl border p-2">
<div className="ml-2 flex items-center gap-0.5">
<div className="border-subtle rounded-lg border p-1">
<Icon name="blocks" className="text-subtle h-4 w-4" />
</div>
<span className="text-emphasis ml-2 text-sm font-medium">Fallback</span>
<span className="text-emphasis ml-2 text-sm font-medium">{t("fallback_action")}</span>
</div>
{route.fallbackAttributesQueryBuilderState &&
attributesQueryBuilderConfigWithRaqbSettingsAndWidgets && (
<Query
{...attributesQueryBuilderConfigWithRaqbSettingsAndWidgets}
value={route.fallbackAttributesQueryBuilderState.tree}
onChange={(immutableTree, attributesQueryBuilderConfig) => {
onChangeFallbackTeamMembersQuery(
route,
immutableTree,
attributesQueryBuilderConfig as unknown as AttributesQueryBuilderConfigWithRaqbFields
);
}}
renderBuilder={renderBuilder}
/>
)}
<div className="bg-cal-muted mt-2 rounded-xl p-2">
<RouteActionSelector
action={route.fallbackAction}
onActionChange={(newAction) => setRoute(route.id, { fallbackAction: newAction })}
eventTypeOptions={fallbackActionOptions}
selectedEventTypeOption={fallbackActionSelectedOption}
disabled={disabled}
showCustomEventTypeInput
customEventTypeSlug={customFallbackEventTypeSlug}
onCustomEventTypeSlugChange={setCustomFallbackEventTypeSlug}
eventTypePrefix={eventTypePrefix}
fieldIdentifiers={fieldIdentifiers}
/>
</div>
{isOrganization && (
<div className="mt-4">
{route.fallbackAction?.type === RouteActionType.EventTypeRedirectUrl &&
route.fallbackAttributesQueryBuilderState &&
attributesQueryBuilderConfigWithRaqbSettingsAndWidgets && (
<div>
<div className="ml-2 flex items-center gap-0.5">
<div className="border-subtle rounded-lg border p-1">
<Icon name="user-check" className="text-subtle h-4 w-4" />
</div>
<span className="text-emphasis ml-2 text-sm font-medium">
{t("connect_with_specific_team_members")}
</span>
</div>
<Query
{...attributesQueryBuilderConfigWithRaqbSettingsAndWidgets}
value={route.fallbackAttributesQueryBuilderState.tree}
onChange={(immutableTree, attributesQueryBuilderConfig) => {
onChangeFallbackTeamMembersQuery(
route,
immutableTree,
attributesQueryBuilderConfig as unknown as AttributesQueryBuilderConfigWithRaqbFields
);
}}
renderBuilder={renderBuilder}
/>
</div>
)}
</div>
)}
</div>
) : null;
@@ -659,283 +869,85 @@ const Route = ({
{formFieldsQueryBuilder}
<div>
{route.isFallback ? (
<div className="flex w-full flex-col gap-2 text-sm lg:flex-row">
<div className="flex grow items-center gap-2">
{/* <div className="flex grow-0 whitespace-nowrap">
<span>{t("send_booker_to")}</span>
</div> */}
<Select
size="sm"
isDisabled={disabled}
className="data-testid-select-routing-action block w-full grow"
required
value={RoutingPages.find((page) => page.value === route.action?.type)}
onChange={(item) => {
if (!item) {
return;
}
const action: LocalRoute["action"] = {
type: item.value,
value: "",
};
if (action.type === "customPageMessage") {
action.value = "We are not ready for you yet :(";
} else {
action.value = "";
}
setRoute(route.id, { action });
}}
options={RoutingPages}
/>
</div>
{route.action?.type ? (
route.action?.type === "customPageMessage" ? (
<TextArea
required
disabled={disabled}
name="customPageMessage"
className="border-default flex grow lg:w-fit"
style={{
minHeight: "38px",
}}
value={route.action.value}
onChange={(e) => {
setRoute(route.id, { action: { ...route.action, value: e.target.value } });
}}
/>
) : route.action?.type === "externalRedirectUrl" ? (
<TextField
disabled={disabled}
name="externalRedirectUrl"
className="border-default flex grow text-sm"
containerClassName="grow"
type="url"
required
labelSrOnly
value={route.action.value}
onChange={(e) => {
setRoute(route.id, { action: { ...route.action, value: e.target.value } });
}}
placeholder="https://example.com"
/>
) : (
<div className="grow">
<Select
size="sm"
required
className="data-testid-eventTypeRedirectUrl-select"
isDisabled={disabled}
options={eventTypeRedirectUrlOptions}
onChange={(option) => {
if (!option) {
return;
}
if (option.value !== "custom") {
setRoute(route.id, {
action: {
...route.action,
value: option.value,
eventTypeId: option.eventTypeId,
},
attributeRoutingConfig: {},
});
setCustomEventTypeSlug("");
} else {
setRoute(route.id, {
action: { ...route.action, value: "custom", eventTypeId: 0 },
attributeRoutingConfig: {},
});
setCustomEventTypeSlug("");
}
}}
value={eventTypeRedirectUrlSelectedOption}
/>
{eventOptions.length !== 0 &&
route.action.value !== "" &&
(!eventOptions.find((eventOption) => eventOption.value === route.action.value) ||
customEventTypeSlug.length) ? (
<>
<TextField
disabled={disabled}
className="border-default flex w-full grow text-sm"
containerClassName="grow mt-2"
addOnLeading={eventTypePrefix}
required
value={customEventTypeSlug}
onChange={(e) => {
setCustomEventTypeSlug(e.target.value);
setRoute(route.id, {
action: { ...route.action, value: `${eventTypePrefix}${e.target.value}` },
});
}}
placeholder="event-url"
/>
<div className="mt-2 ">
<p className="text-subtle text-xs">
{fieldIdentifiers.length
? t("field_identifiers_as_variables_with_example", {
variable: `{${fieldIdentifiers[0]}}`,
})
: t("field_identifiers_as_variables")}
</p>
</div>
</>
) : (
<></>
)}
</div>
)
) : null}
</div>
<RouteActionSelector
action={route.action}
onActionChange={(newAction) => setRoute(route.id, { action: newAction })}
onEventTypeChange={(newAction) => {
// Initialize fallbackAction with the same event type if not already set with a valid event type
// Check if fallbackAction has a valid event type (eventTypeId or non-empty value)
const hasValidFallbackEventType =
route.fallbackAction?.eventTypeId || route.fallbackAction?.value;
const fallbackAction = hasValidFallbackEventType
? route.fallbackAction
: newAction.type === RouteActionType.EventTypeRedirectUrl
? {
type: newAction.type,
value: newAction.value,
eventTypeId: newAction.eventTypeId,
}
: undefined;
setRoute(route.id, { action: newAction, attributeRoutingConfig: {}, fallbackAction });
setCustomEventTypeSlug("");
}}
eventTypeOptions={eventTypeRedirectUrlOptions}
selectedEventTypeOption={eventTypeRedirectUrlSelectedOption}
disabled={disabled}
showCustomEventTypeInput
customEventTypeSlug={customEventTypeSlug}
onCustomEventTypeSlugChange={setCustomEventTypeSlug}
eventTypePrefix={eventTypePrefix}
fieldIdentifiers={fieldIdentifiers}
/>
) : (
<div className="bg-default border-subtle my-3 rounded-xl border p-2">
<div className="mb-2 ml-2 flex items-center gap-0.5">
<div className="border-subtle rounded-lg border p-1">
<Icon name="arrow-right" className="text-subtle h-4 w-4" />
</div>
<span className="text-emphasis ml-2 text-sm font-medium">Send booker to</span>
<span className="text-emphasis ml-2 text-sm font-medium">{t("send_booker_to")}</span>
</div>
<div className="bg-cal-muted flex w-full flex-col gap-2 rounded-xl p-2 text-sm lg:flex-row">
<div className="flex grow items-center gap-2">
<Select
size="sm"
isDisabled={disabled}
className="data-testid-select-routing-action block w-full grow"
required
value={RoutingPages.find((page) => page.value === route.action?.type)}
onChange={(item) => {
if (!item) {
return;
}
const action: LocalRoute["action"] = {
type: item.value,
value: "",
};
if (action.type === "customPageMessage") {
action.value = "We are not ready for you yet :(";
} else {
action.value = "";
}
setRoute(route.id, { action });
}}
options={RoutingPages}
/>
</div>
{route.action?.type ? (
route.action?.type === "customPageMessage" ? (
<TextArea
required
disabled={disabled}
name="customPageMessage"
className="border-default flex grow lg:w-fit"
style={{
minHeight: "38px",
}}
value={route.action.value}
onChange={(e) => {
setRoute(route.id, { action: { ...route.action, value: e.target.value } });
}}
/>
) : route.action?.type === "externalRedirectUrl" ? (
<TextField
size="sm"
disabled={disabled}
name="externalRedirectUrl"
className="border-default flex grow text-sm"
containerClassName="grow"
type="url"
required
labelSrOnly
value={route.action.value}
onChange={(e) => {
setRoute(route.id, { action: { ...route.action, value: e.target.value } });
}}
placeholder="https://example.com"
/>
) : (
<div className="grow">
<Select
size="sm"
required
className="data-testid-eventTypeRedirectUrl-select"
isDisabled={disabled}
options={eventTypeRedirectUrlOptions}
onChange={(option) => {
if (!option) {
return;
<div className="bg-cal-muted rounded-xl p-2">
<RouteActionSelector
action={route.action}
onActionChange={(newAction) => setRoute(route.id, { action: newAction })}
onEventTypeChange={(newAction) => {
// Initialize fallbackAction with the same event type if not already set with a valid event type
// Check if fallbackAction has a valid event type (eventTypeId or non-empty value)
const hasValidFallbackEventType =
route.fallbackAction?.eventTypeId || route.fallbackAction?.value;
const fallbackAction = hasValidFallbackEventType
? route.fallbackAction
: newAction.type === RouteActionType.EventTypeRedirectUrl
? {
type: newAction.type,
value: newAction.value,
eventTypeId: newAction.eventTypeId,
}
if (option.value !== "custom") {
setRoute(route.id, {
action: {
...route.action,
value: option.value,
eventTypeId: option.eventTypeId,
},
attributeRoutingConfig: {},
});
setCustomEventTypeSlug("");
} else {
setRoute(route.id, {
action: { ...route.action, value: "custom", eventTypeId: 0 },
attributeRoutingConfig: {},
});
setCustomEventTypeSlug("");
}
}}
value={eventTypeRedirectUrlSelectedOption}
/>
{eventOptions.length !== 0 &&
route.action.value !== "" &&
(!eventOptions.find((eventOption) => eventOption.value === route.action.value) ||
customEventTypeSlug.length) ? (
<>
<TextField
disabled={disabled}
className="border-default flex w-full grow text-sm"
containerClassName="grow mt-2"
addOnLeading={eventTypePrefix}
required
value={customEventTypeSlug}
onChange={(e) => {
setCustomEventTypeSlug(e.target.value);
setRoute(route.id, {
action: { ...route.action, value: `${eventTypePrefix}${e.target.value}` },
});
}}
placeholder="event-url"
/>
<div className="mt-2 ">
<p className="text-subtle text-xs">
{fieldIdentifiers.length
? t("field_identifiers_as_variables_with_example", {
variable: `{${fieldIdentifiers[0]}}`,
})
: t("field_identifiers_as_variables")}
</p>
</div>
</>
) : (
<></>
)}
</div>
)
) : null}
: undefined;
setRoute(route.id, { action: newAction, attributeRoutingConfig: {}, fallbackAction });
setCustomEventTypeSlug("");
}}
eventTypeOptions={eventTypeRedirectUrlOptions}
selectedEventTypeOption={eventTypeRedirectUrlSelectedOption}
disabled={disabled}
showCustomEventTypeInput
customEventTypeSlug={customEventTypeSlug}
onCustomEventTypeSlugChange={setCustomEventTypeSlug}
eventTypePrefix={eventTypePrefix}
fieldIdentifiers={fieldIdentifiers}
/>
</div>
{attributesQueryBuilder}
</div>
)}
{attributesQueryBuilder}
<WeightedAttributesSelector
attributes={attributes}
route={route}
eventTypeRedirectUrlSelectedOption={eventTypeRedirectUrlSelectedOption}
setRoute={setRoute}
/>
{fallbackAttributesQueryBuilder ? <>{fallbackAttributesQueryBuilder}</> : null}
{matchingMembersFallbackRoute ? <>{matchingMembersFallbackRoute}</> : null}
</div>
</div>
</div>
@@ -952,7 +964,7 @@ const buildState = <
| {
queryValue: AttributesQueryValue;
config: AttributesQueryBuilderConfigWithRaqbFields;
}
},
>({
queryValue,
config,
@@ -1026,6 +1038,16 @@ function useRoutes({
if (!r.fallbackAttributesQueryValue) {
r.fallbackAttributesQueryValue = getEmptyQueryValue() as LocalRoute["fallbackAttributesQueryValue"];
}
// Initialize fallbackAction with the main action for backwards compatibility
// This ensures existing routes show the correct default (same event type as main route)
if (!r.fallbackAction && r.action?.type === RouteActionType.EventTypeRedirectUrl) {
r.fallbackAction = {
type: r.action.type,
value: r.action.value,
eventTypeId: r.action.eventTypeId,
};
}
});
return _routes;
};
@@ -1056,6 +1078,17 @@ function useRoutes({
if (isRouter(route)) {
return route;
}
// Initialize fallbackAction from action if not set and action is EventTypeRedirectUrl
// This handles the case where user creates a new route and changes action to an event type
const fallbackAction =
route.fallbackAction ??
(route.action?.type === RouteActionType.EventTypeRedirectUrl
? {
type: route.action.type,
value: route.action.value,
eventTypeId: route.action.eventTypeId,
}
: undefined);
return {
id: route.id,
name: route.name,
@@ -1066,6 +1099,7 @@ function useRoutes({
attributesQueryValue: route.attributesQueryValue,
fallbackAttributesQueryValue: route.fallbackAttributesQueryValue,
attributeIdForWeights: route.attributeIdForWeights,
fallbackAction,
};
});
}
@@ -1087,10 +1121,21 @@ const useCreateRoute = ({
}) => {
const createRoute = useCallback(() => {
const newEmptyRoute = getEmptyRoute();
// Initialize fallbackAction with the main action for new routes
// This ensures new routes have a valid fallbackAction when saved
const fallbackAction =
newEmptyRoute.action?.type === RouteActionType.EventTypeRedirectUrl
? {
type: newEmptyRoute.action.type,
value: newEmptyRoute.action.value,
eventTypeId: newEmptyRoute.action.eventTypeId,
}
: undefined;
const newRoutes = [
...routes,
{
...newEmptyRoute,
fallbackAction,
formFieldsQueryBuilderState: buildState({
queryValue: newEmptyRoute.queryValue,
config: formFieldsQueryBuilderConfig,
@@ -10,7 +10,7 @@ import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { routingFormsComponents } from "./app-routing.client-config";
const normalizePages = (pages: string[] | string | undefined) => {
const normalizedPages = Array.isArray(pages) ? pages : pages?.split("/") ?? [];
const normalizedPages = Array.isArray(pages) ? pages : (pages?.split("/") ?? []);
return normalizedPages[0];
};
@@ -1,13 +1,7 @@
"use client";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import posthog from "posthog-js";
import { useEffect, useState } from "react";
import { useFormContext } from "react-hook-form";
import { isFallbackRoute } from "@calcom/app-store/routing-forms/lib/isFallbackRoute";
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
import LicenseRequired from "~/ee/common/components/LicenseRequired";
import { FilterResults } from "~/filters/components/FilterResults";
import { TeamsFilter } from "~/filters/components/TeamsFilter";
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
@@ -21,22 +15,33 @@ import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { Icon } from "@calcom/ui/components/icon";
import { List, ListLinkItem } from "@calcom/ui/components/list";
import { Tooltip } from "@calcom/ui/components/tooltip";
import type {
SetNewFormDialogState,
NewFormDialogState,
SetNewFormDialogState,
} from "@calcom/web/components/apps/routing-forms/FormActions";
import {
FormAction,
FormActionsDropdown,
FormActionsProvider,
} from "@calcom/web/components/apps/routing-forms/FormActions";
import {
ChartBarIcon,
CircleCheckIcon,
DownloadIcon,
FileTextIcon,
MailIcon,
ShuffleIcon,
} from "@coss/ui/icons";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import posthog from "posthog-js";
import { useEffect, useState } from "react";
import { useFormContext } from "react-hook-form";
import { useHasPaidPlan } from "~/billing/hooks/useHasPaidPlan";
import SkeletonLoaderTeamList from "~/ee/teams/components/SkeletonloaderTeamList";
import LicenseRequired from "~/ee/common/components/LicenseRequired";
import { CreateButtonWithTeamsList } from "~/ee/teams/components/createButton/CreateButtonWithTeamsList";
import SkeletonLoaderTeamList from "~/ee/teams/components/SkeletonloaderTeamList";
import { ShellMain } from "~/shell/Shell";
import { UpgradeTip } from "~/shell/UpgradeTip";
@@ -92,32 +97,32 @@ export default function RoutingForms({ appUrl }: { appUrl: string }) {
const forms = queryRes.data?.filtered;
const features = [
{
icon: <Icon name="file-text" className="h-5 w-5 text-orange-500" />,
icon: <FileTextIcon className="h-5 w-5 text-orange-500" />,
title: t("create_your_first_form"),
description: t("create_your_first_form_description"),
},
{
icon: <Icon name="shuffle" className="h-5 w-5 text-lime-500" />,
icon: <ShuffleIcon className="h-5 w-5 text-lime-500" />,
title: t("create_your_first_route"),
description: t("route_to_the_right_person"),
},
{
icon: <Icon name="chart-bar" className="h-5 w-5 text-blue-500" />,
icon: <ChartBarIcon className="h-5 w-5 text-blue-500" />,
title: t("reporting"),
description: t("reporting_feature"),
},
{
icon: <Icon name="circle-check" className="h-5 w-5 text-teal-500" />,
icon: <CircleCheckIcon className="h-5 w-5 text-teal-500" />,
title: t("test_routing_form"),
description: t("test_preview_description"),
},
{
icon: <Icon name="mail" className="h-5 w-5 text-yellow-500" />,
icon: <MailIcon className="h-5 w-5 text-yellow-500" />,
title: t("routing_forms_send_email_owner"),
description: t("routing_forms_send_email_owner_description"),
},
{
icon: <Icon name="download" className="h-5 w-5 text-violet-500" />,
icon: <DownloadIcon className="h-5 w-5 text-violet-500" />,
title: t("download_responses"),
description: t("download_responses_description"),
},
@@ -5,7 +5,7 @@ import { z } from "zod";
import { IdentityProvider } from "@calcom/prisma/enums";
import { Button } from "@calcom/ui/components/button";
import { Icon } from "@calcom/ui/components/icon";
import { XIcon } from "@coss/ui/icons";
import AuthContainer from "@components/ui/AuthContainer";
@@ -42,10 +42,10 @@ const ServerPage = async ({ searchParams }: PageProps) => {
provider === IdentityProvider.GOOGLE
? "Google"
: provider === IdentityProvider.CAL
? "Email and Password"
: provider === IdentityProvider.SAML
? "SAML (like Okta)"
: "your original login method";
? "Email and Password"
: provider === IdentityProvider.SAML
? "SAML (like Okta)"
: "your original login method";
return t("account_managed_by_identity_provider_error", { provider: providerName });
} else if (error === "saml-idp-not-authoritative") {
return t("saml_idp_not_authoritative_error");
@@ -61,7 +61,7 @@ const ServerPage = async ({ searchParams }: PageProps) => {
<AuthContainer>
<div>
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
<Icon name="x" className="h-6 w-6 text-red-600" />
<XIcon className="h-6 w-6 text-red-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
@@ -3,14 +3,14 @@
import React, { Suspense } from "react";
import { ErrorBoundary } from "@calcom/ui/components/errorBoundary";
import { Icon } from "@calcom/ui/components/icon";
import { LoaderIcon } from "@coss/ui/icons";
export default function TroubleshooterLayout({ children }: { children: React.ReactNode }) {
return (
<>
<div className="flex flex-1 *:flex-1">
<ErrorBoundary>
<Suspense fallback={<Icon name="loader" />}>{children}</Suspense>
<Suspense fallback={<LoaderIcon />}>{children}</Suspense>
</ErrorBoundary>
</div>
</>
@@ -7,7 +7,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui/components/button";
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
import { Icon } from "@calcom/ui/components/icon";
import { RefreshCcwIcon, UserPlusIcon, UsersIcon } from "@coss/ui/icons";
import { UpgradeTip } from "~/shell/UpgradeTip";
@@ -22,17 +22,17 @@ export default function UpgradeTipWrapper({ children }: { children: React.ReactN
const features = [
{
icon: <Icon name="users" className="h-5 w-5 text-red-500" />,
icon: <UsersIcon className="h-5 w-5 text-red-500" />,
title: t("view_bookings_across"),
description: t("view_bookings_across_description"),
},
{
icon: <Icon name="refresh-ccw" className="h-5 w-5 text-blue-500" />,
icon: <RefreshCcwIcon className="h-5 w-5 text-blue-500" />,
title: t("identify_booking_trends"),
description: t("identify_booking_trends_description"),
},
{
icon: <Icon name="user-plus" className="h-5 w-5 text-green-500" />,
icon: <UserPlusIcon className="h-5 w-5 text-green-500" />,
title: t("spot_popular_event_types"),
description: t("spot_popular_event_types_description"),
},
@@ -0,0 +1,26 @@
import { _generateMetadata } from "app/_utils";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import prisma from "@calcom/prisma";
import InsightsWrongRoutingPage from "~/insights/views/insights-wrong-routing-view";
import { checkInsightsPagePermission } from "../checkInsightsPagePermission";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("wrong_routing"),
(t) => t("wrong_routing_description"),
undefined,
undefined,
"/insights/wrong-routing"
);
export default async function Page() {
const session = await checkInsightsPagePermission();
const userRepository = new UserRepository(prisma);
const user = await userRepository.getTimeZoneAndDefaultScheduleId({ userId: session?.user.id ?? -1 });
return <InsightsWrongRoutingPage timeZone={user?.timeZone ?? "UTC"} />;
}
@@ -27,7 +27,9 @@ const ServerPage = async () => {
// If user has any team membership (pending or accepted), redirect them directly to personal onboarding
// This handles the case where users sign up with an invite token (membership is auto-accepted)
const hasTeamMembership = await MembershipRepository.hasAnyTeamMembershipByUserId({ userId: session.user.id });
const hasTeamMembership = await MembershipRepository.hasAnyTeamMembershipByUserId({
userId: session.user.id,
});
if (hasTeamMembership) {
return redirect("/onboarding/personal/settings");
}
@@ -32,4 +32,3 @@ const ServerPage = async () => {
};
export default ServerPage;
@@ -32,4 +32,3 @@ const ServerPage = async () => {
};
export default ServerPage;
@@ -31,7 +31,9 @@ const ServerPage = async (props: { searchParams: Promise<{ fromTeamOnboarding?:
const searchParams = await props.searchParams;
const fromTeamOnboarding = searchParams?.fromTeamOnboarding === "true";
return <PersonalSettingsView userEmail={userEmail} userName={userName} fromTeamOnboarding={fromTeamOnboarding} />;
return (
<PersonalSettingsView userEmail={userEmail} userName={userName} fromTeamOnboarding={fromTeamOnboarding} />
);
};
export default ServerPage;
@@ -32,4 +32,3 @@ const ServerPage = async () => {
};
export default ServerPage;
@@ -42,10 +42,7 @@ const Page = async ({ params }: { params: Params }) => {
const org = await organizationRepository.adminFindById({ id: input.data.id });
const t = await getTranslate();
return (
<SettingsHeader
title={`${t("editing_org")}: ${org.name}`}
description={t("admin_orgs_edit_description")}
>
<SettingsHeader title={`${t("editing_org")}: ${org.name}`} description={t("admin_orgs_edit_description")}>
<LicenseRequired>
<OrgForm org={org} />
</LicenseRequired>
@@ -17,10 +17,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
return (
<SettingsHeader
title={t("organizations")}
description={t("orgs_page_description")}
>
<SettingsHeader title={t("organizations")} description={t("orgs_page_description")}>
<LicenseRequired>
<AdminOrgTable />
</LicenseRequired>
@@ -3,7 +3,8 @@
import { useReactTable, getCoreRowModel, createColumnHelper } from "@tanstack/react-table";
import { useMemo } from "react";
import { ColumnFilterType, DataTableProvider } from "@calcom/features/data-table";
import { ColumnFilterType } from "@calcom/features/data-table";
import { DataTableProvider } from "~/data-table/DataTableProvider";
import type { DateRangeFilterOptions } from "@calcom/features/data-table/lib/types";
import { DateRangeFilter } from "~/data-table/components/filters/DateRangeFilter";
@@ -16,10 +16,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
return (
<SettingsHeader
title={t("workspace_platforms")}
description={t("workspace_platforms_description")}
>
<SettingsHeader title={t("workspace_platforms")} description={t("workspace_platforms_description")}>
<LicenseRequired>
<WorkspacePlatformsPage />
</LicenseRequired>
@@ -31,6 +31,7 @@ import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { ErrorBoundary } from "@calcom/ui/components/errorBoundary";
import { Icon } from "@calcom/ui/components/icon";
import { ArrowLeftIcon, ChevronDownIcon, ChevronRightIcon } from "@coss/ui/icons";
import type { VerticalTabItemProps } from "@calcom/ui/components/navigation";
import { VerticalTabItem } from "@calcom/ui/components/navigation";
import { Skeleton } from "@calcom/ui/components/skeleton";
@@ -475,10 +476,7 @@ const BackButtonInSidebar = ({ name }: { name: string }) => {
href="/event-types"
className="hover:bg-subtle todesktop:mt-10 [&[aria-current='page']]:bg-emphasis [&[aria-current='page']]:text-emphasis group-hover:text-default text-emphasis group my-6 flex h-6 max-h-6 w-full flex-row items-center rounded-md px-3 py-2 text-sm font-medium leading-4 transition"
data-testid={`vertical-tab-${name}`}>
<Icon
name="arrow-left"
className="h-4 w-4 stroke-[2px] ltr:mr-[10px] rtl:ml-[10px] rtl:rotate-180 md:mt-0"
/>
<ArrowLeftIcon className="h-4 w-4 stroke-[2px] ltr:mr-[10px] rtl:ml-[10px] rtl:rotate-180 md:mt-0" />
<Skeleton title={name} as="p" className="min-h-4 max-w-36 truncate" loadingClassName="ms-3">
{name}
</Skeleton>
@@ -602,9 +600,9 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record<number, T
}`}>
<div className="me-3">
{teamMenuState[index].teamMenuOpen ? (
<Icon name="chevron-down" className="h-4 w-4" />
<ChevronDownIcon className="h-4 w-4" />
) : (
<Icon name="chevron-right" className="h-4 w-4" />
<ChevronRightIcon className="h-4 w-4" />
)}
</div>
{}
@@ -949,9 +947,9 @@ const SettingsSidebarContainer = ({
}`}>
<div className="me-3">
{otherTeamMenuState[index].teamMenuOpen ? (
<Icon name="chevron-down" className="h-4 w-4" />
<ChevronDownIcon className="h-4 w-4" />
) : (
<Icon name="chevron-right" className="h-4 w-4" />
<ChevronRightIcon className="h-4 w-4" />
)}
</div>
{}
@@ -1036,7 +1034,7 @@ const MobileSettingsContainer = (props: { onSideContainerOpen?: () => void }) =>
<button
className="hover:bg-emphasis flex items-center space-x-2 rounded-md px-3 py-1 rtl:space-x-reverse"
onClick={() => router.back()}>
<Icon name="arrow-left" className="text-default h-4 w-4" />
<ArrowLeftIcon className="text-default h-4 w-4" />
<p className="text-emphasis font-semibold">{t("settings")}</p>
</button>
</div>
@@ -1052,12 +1050,7 @@ type SettingsLayoutProps = {
permissions?: SettingsPermissions;
} & ComponentProps<typeof Shell>;
function SettingsLayoutAppDirClient({
children,
teamFeatures,
permissions,
...rest
}: SettingsLayoutProps) {
function SettingsLayoutAppDirClient({ children, teamFeatures, permissions, ...rest }: SettingsLayoutProps) {
const pathname = usePathname();
const state = useState(false);
const [sideContainerOpen, setSideContainerOpen] = state;
@@ -25,9 +25,7 @@ const Page = async () => {
redirect("/auth/login?callbackUrl=/settings/developer/oauth");
}
return (
<OAuthClientsView />
);
return <OAuthClientsView />;
};
export default Page;
@@ -11,8 +11,8 @@ import {
import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@calcom/ui/classNames";
import { Checkbox, Label } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { Tooltip } from "@calcom/ui/components/tooltip";
import { ChevronRightIcon, InfoIcon } from "@coss/ui/icons";
import { usePermissions } from "./usePermissions";
@@ -54,10 +54,10 @@ export function AdvancedPermissionGroup({
const allPermissions = isAllResources
? ["*.*"]
: resourceConfig
? Object.entries(resourceConfig)
.filter(([action]) => action !== INTERNAL_DATAACCESS_KEY)
.map(([action]) => `${resource}.${action}`)
: [];
? Object.entries(resourceConfig)
.filter(([action]) => action !== INTERNAL_DATAACCESS_KEY)
.map(([action]) => `${resource}.${action}`)
: [];
// Check if all permissions for this resource are selected
const isAllSelected = isAllResources
@@ -98,8 +98,7 @@ export function AdvancedPermissionGroup({
}
}}>
<div className="flex items-center gap-1.5" onClick={() => setIsExpanded(!isExpanded)}>
<Icon
name="chevron-right"
<ChevronRightIcon
className={classNames(
"h-4 w-4 transition-transform",
isExpanded && !isAllResources ? "rotate-90" : ""
@@ -173,7 +172,7 @@ export function AdvancedPermissionGroup({
</span>
{isAutoEnabled && (
<Tooltip content={t("read_permission_auto_enabled_tooltip")}>
<Icon name="info" className="text-muted-foreground h-3 w-3" />
<InfoIcon className="text-muted-foreground h-3 w-3" />
</Tooltip>
)}
</div>
@@ -7,8 +7,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { Dialog, DialogContent } from "@calcom/ui/components/dialog";
import { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
import { LockIcon, ShieldCheckIcon, ShieldIcon, UsersIcon } from "@coss/ui/icons";
import { FingerprintAnimation } from "./FingerprintAnimation";
@@ -59,7 +59,7 @@ export function PbacOptInModal({ open, onOpenChange, revalidateRolesPath }: Pbac
<div className="bg-subtle border-subtle w-full stack-y-px overflow-hidden rounded-xl border">
<div className="bg-default flex gap-3 p-3">
<div className="bg-subtle flex h-8 w-8 shrink-0 items-center justify-center rounded-md">
<Icon name="shield-check" className="text-subtle h-5 w-5" />
<ShieldCheckIcon className="text-subtle h-5 w-5" />
</div>
<div className="flex flex-col gap-1">
<p className="text-emphasis text-sm font-semibold">{t("pbac_opt_in_custom_roles_title")}</p>
@@ -69,7 +69,7 @@ export function PbacOptInModal({ open, onOpenChange, revalidateRolesPath }: Pbac
<div className="bg-default flex gap-3 p-3">
<div className="bg-subtle flex h-8 w-8 shrink-0 items-center justify-center rounded-md">
<Icon name="lock" className="text-subtle h-5 w-5" />
<LockIcon className="text-subtle h-5 w-5" />
</div>
<div className="flex flex-col gap-1">
<p className="text-emphasis text-sm font-semibold">
@@ -81,7 +81,7 @@ export function PbacOptInModal({ open, onOpenChange, revalidateRolesPath }: Pbac
<div className="bg-default flex gap-3 p-3">
<div className="bg-subtle flex h-8 w-8 shrink-0 items-center justify-center rounded-md">
<Icon name="users" className="text-subtle h-5 w-5" />
<UsersIcon className="text-subtle h-5 w-5" />
</div>
<div className="flex flex-col gap-1">
<p className="text-emphasis text-sm font-semibold">
@@ -93,7 +93,7 @@ export function PbacOptInModal({ open, onOpenChange, revalidateRolesPath }: Pbac
<div className="bg-default flex gap-3 p-3">
<div className="bg-subtle flex h-8 w-8 shrink-0 items-center justify-center rounded-md">
<Icon name="shield" className="text-subtle h-5 w-5" />
<ShieldIcon className="text-subtle h-5 w-5" />
</div>
<div className="flex flex-col gap-1">
<p className="text-emphasis text-sm font-semibold">
@@ -27,9 +27,9 @@ const Page = async () => {
const organizationRepository = getOrganizationRepository();
const otherTeams = organizationId
? await organizationRepository.findTeamsInOrgIamNotPartOf({
userId: session?.user.id,
parentId: organizationId,
})
userId: session?.user.id,
parentId: organizationId,
})
: [];
return (
@@ -1,8 +1,6 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import LegacyPage, {
TeamMembersCTA,
} from "~/ee/organizations/other-team-members-view";
import LegacyPage, { TeamMembersCTA } from "~/ee/organizations/other-team-members-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { validateUserHasOrg } from "../../../../actions/validateUserHasOrg";
@@ -23,9 +23,7 @@ const getData = withAppDirSsr<inferSSRProps<typeof getServerSideProps>>(getServe
const ServerPage = async ({ params, searchParams }: PageProps) => {
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
return (
<SettingsNewView />
);
return <SettingsNewView />;
};
export default ServerPage;
+1 -7
View File
@@ -8,13 +8,7 @@ interface GeoContextValue {
const GeoContext = createContext<GeoContextValue | undefined>(undefined);
export function GeoProvider({
country,
children,
}: {
country: string;
children: React.ReactNode;
}) {
export function GeoProvider({ country, children }: { country: string; children: React.ReactNode }) {
return <GeoContext.Provider value={{ country }}>{children}</GeoContext.Provider>;
}
+2 -2
View File
@@ -35,8 +35,8 @@ const url =
typeof window !== "undefined"
? "/api/trpc"
: process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/trpc`
: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`;
? `https://${process.env.VERCEL_URL}/api/trpc`
: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`;
export const trpcClient = trpc.createClient({
links: [
+2 -3
View File
@@ -9,9 +9,8 @@ import { ErrorWithCode } from "@calcom/lib/errors";
import { getHttpStatusCode } from "@calcom/lib/server/getServerErrorFromUnknown";
async function handler(req: NextRequest) {
const { code, client_id, client_secret, grant_type, redirect_uri, code_verifier } = await parseUrlFormData(
req
);
const { code, client_id, client_secret, grant_type, redirect_uri, code_verifier } =
await parseUrlFormData(req);
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
return NextResponse.json({ message: OAUTH_ERROR_REASONS["encryption_key_missing"] }, { status: 500 });
@@ -1,17 +1,15 @@
import type { Mock } from "vitest";
import { vi } from "vitest";
import type { MockResponse } from "@calcom/features/auth/signup/handlers/__tests__/mocks/next.mocks";
import {
prismaMock,
resetPrismaMock,
} from "@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks";
import {
createMockTeam,
createMockFoundToken,
} from "@calcom/features/auth/signup/handlers/__tests__/mocks/signup.factories";
import type { SignupBody } from "@calcom/features/auth/signup/handlers/__tests__/mocks/signup.factories";
import {
createMockFoundToken,
createMockTeam,
} from "@calcom/features/auth/signup/handlers/__tests__/mocks/signup.factories";
import type { Mock } from "vitest";
import { vi } from "vitest";
const mockFindTokenByToken: Mock = vi.fn();
const mockValidateAndGetCorrectedUsernameForTeam: Mock = vi.fn();
@@ -27,19 +25,27 @@ type InnerHandler = (body: Record<string, string>, status: UsernameStatus) => Pr
var mockCapturedHandler: InnerHandler | null;
vi.mock("next/server", async () => {
const { createNextServerMock } = await import("@calcom/features/auth/signup/handlers/__tests__/mocks/next.mocks");
const { createNextServerMock } = await import(
"@calcom/features/auth/signup/handlers/__tests__/mocks/next.mocks"
);
return createNextServerMock();
});
vi.mock("next/headers", async () => {
const { createNextHeadersMock } = await import("@calcom/features/auth/signup/handlers/__tests__/mocks/next.mocks");
const { createNextHeadersMock } = await import(
"@calcom/features/auth/signup/handlers/__tests__/mocks/next.mocks"
);
return createNextHeadersMock();
});
vi.mock("@calcom/prisma", async () => {
const { createPrismaMock } = await import("@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks");
const { createPrismaMock } = await import(
"@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks"
);
return createPrismaMock();
});
vi.mock("@calcom/prisma/client", async () => {
const { createPrismaMock } = await import("@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks");
const { createPrismaMock } = await import(
"@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks"
);
return createPrismaMock();
});
vi.mock("@calcom/lib/logger", () => ({
@@ -49,9 +55,13 @@ vi.mock("@calcom/lib/auth/hashPassword", () => ({ hashPassword: vi.fn().mockReso
vi.mock("@calcom/lib/constants", () => ({ WEBAPP_URL: "http://localhost:3000" }));
vi.mock("@calcom/lib/tracking", () => ({ getTrackingFromCookies: vi.fn().mockReturnValue({}) }));
vi.mock("@calcom/app-store/stripepayment/lib/utils", () => ({ getPremiumMonthlyPlanPriceId: vi.fn() }));
vi.mock("@calcom/features/auth/lib/getLocaleFromRequest", () => ({ getLocaleFromRequest: vi.fn().mockResolvedValue("en") }));
vi.mock("@calcom/features/auth/lib/getLocaleFromRequest", () => ({
getLocaleFromRequest: vi.fn().mockResolvedValue("en"),
}));
vi.mock("@calcom/features/auth/lib/verifyEmail", () => ({ sendEmailVerification: vi.fn() }));
vi.mock("@calcom/features/auth/signup/utils/createOrUpdateMemberships", () => ({ createOrUpdateMemberships: vi.fn() }));
vi.mock("@calcom/features/auth/signup/utils/createOrUpdateMemberships", () => ({
createOrUpdateMemberships: vi.fn(),
}));
vi.mock("@calcom/features/auth/signup/utils/prefillAvatar", () => ({ prefillAvatar: vi.fn() }));
vi.mock("@calcom/features/auth/signup/utils/validateUsername", () => ({
validateAndGetCorrectedUsernameAndEmail: vi.fn().mockResolvedValue({ isValid: true, username: "testuser" }),
@@ -65,12 +75,29 @@ vi.mock("@calcom/features/watchlist/lib/telemetry", () => ({ sentrySpan: {} }));
vi.mock("@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller", () => ({
checkIfEmailIsBlockedInWatchlistController: vi.fn().mockResolvedValue(false),
}));
vi.mock("@calcom/features/di/containers/FeatureRepository", () => ({
getFeatureRepository: vi.fn().mockReturnValue({
checkIfFeatureIsEnabledGlobally: vi.fn().mockResolvedValue(false),
}),
}));
vi.mock("@calcom/features/watchlist/lib/repository/GlobalWatchlistRepository", () => {
return {
GlobalWatchlistRepository: class {
findBlockedEmail = vi.fn().mockResolvedValue(null);
createEntry = vi.fn().mockResolvedValue({});
},
};
});
vi.mock("@calcom/features/watchlist/lib/utils/normalization", () => ({
normalizeEmail: vi.fn((e: string) => e.toLowerCase()),
}));
vi.mock("@calcom/web/lib/buildLegacyCtx", () => ({ buildLegacyRequest: vi.fn() }));
vi.mock("@calcom/features/auth/signup/utils/organization", () => ({ joinAnyChildTeamOnOrgInvite: vi.fn() }));
vi.mock("@calcom/features/auth/signup/utils/token", () => ({
findTokenByToken: (...args: unknown[]) => mockFindTokenByToken(...args),
throwIfTokenExpired: vi.fn(),
validateAndGetCorrectedUsernameForTeam: (...args: unknown[]) => mockValidateAndGetCorrectedUsernameForTeam(...args),
validateAndGetCorrectedUsernameForTeam: (...args: unknown[]) =>
mockValidateAndGetCorrectedUsernameForTeam(...args),
}));
// Capture inner handler from usernameHandler wrapper
@@ -1,14 +1,23 @@
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
import process from "node:process";
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
import { getLocaleFromRequest } from "@calcom/features/auth/lib/getLocaleFromRequest";
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
import { SIGNUP_ERROR_CODES } from "@calcom/features/auth/signup/constants";
import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships";
import { joinAnyChildTeamOnOrgInvite } from "@calcom/features/auth/signup/utils/organization";
import { prefillAvatar } from "@calcom/features/auth/signup/utils/prefillAvatar";
import {
findTokenByToken,
throwIfTokenExpired,
validateAndGetCorrectedUsernameForTeam,
} from "@calcom/features/auth/signup/utils/token";
import { validateAndGetCorrectedUsernameAndEmail } from "@calcom/features/auth/signup/utils/validateUsername";
import { getBillingProviderService } from "@calcom/features/ee/billing/di/containers/Billing";
import { getFeatureRepository } from "@calcom/features/di/containers/FeatureRepository";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { GlobalWatchlistRepository } from "@calcom/features/watchlist/lib/repository/GlobalWatchlistRepository";
import { sentrySpan } from "@calcom/features/watchlist/lib/telemetry";
import { normalizeEmail } from "@calcom/features/watchlist/lib/utils/normalization";
import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
import { hashPassword } from "@calcom/lib/auth/hashPassword";
import { WEBAPP_URL } from "@calcom/lib/constants";
@@ -19,19 +28,17 @@ import type { CustomNextApiHandler } from "@calcom/lib/server/username";
import { usernameHandler } from "@calcom/lib/server/username";
import { getTrackingFromCookies } from "@calcom/lib/tracking";
import prisma from "@calcom/prisma";
import { CreationSource } from "@calcom/prisma/enums";
import { IdentityProvider } from "@calcom/prisma/enums";
import {
CreationSource,
IdentityProvider,
WatchlistAction,
WatchlistSource,
WatchlistType,
} from "@calcom/prisma/enums";
import { signupSchema } from "@calcom/prisma/zod-utils";
import { buildLegacyRequest } from "@calcom/web/lib/buildLegacyCtx";
import { joinAnyChildTeamOnOrgInvite } from "@calcom/features/auth/signup/utils/organization";
import { SIGNUP_ERROR_CODES } from "@calcom/features/auth/signup/constants";
import {
findTokenByToken,
throwIfTokenExpired,
validateAndGetCorrectedUsernameForTeam,
} from "@calcom/features/auth/signup/utils/token";
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
const log = logger.getSubLogger({ prefix: ["signupCalcomHandler"] });
@@ -291,6 +298,34 @@ const handler: CustomNextApiHandler = async (body, usernameStatus, query) => {
}
}
const featureRepository = getFeatureRepository();
const signupWatchlistReviewEnabled =
await featureRepository.checkIfFeatureIsEnabledGlobally("signup-watchlist-review");
if (signupWatchlistReviewEnabled && !token) {
const globalWatchlistRepo = new GlobalWatchlistRepository(prisma);
const normalizedEmail = normalizeEmail(email);
const existing = await globalWatchlistRepo.findBlockedEmail(normalizedEmail);
if (!existing) {
await globalWatchlistRepo.createEntry({
type: WatchlistType.EMAIL,
value: normalizedEmail,
action: WatchlistAction.BLOCK,
source: WatchlistSource.SIGNUP,
description: "Auto-added during signup review",
});
}
const userRepository = new UserRepository(prisma);
await userRepository.lockByEmail({ email });
return NextResponse.json(
{ message: "Created user", stripeCustomerId: customer.stripeCustomerId, accountUnderReview: true },
{ status: 201 }
);
}
if (checkoutSessionId) {
console.log("Created user but missing payment", checkoutSessionId);
return NextResponse.json(
@@ -15,15 +15,21 @@ const mockFindTokenByToken: Mock = vi.fn();
const mockValidateAndGetCorrectedUsernameForTeam: Mock = vi.fn();
vi.mock("next/server", async () => {
const { createNextServerMock } = await import("@calcom/features/auth/signup/handlers/__tests__/mocks/next.mocks");
const { createNextServerMock } = await import(
"@calcom/features/auth/signup/handlers/__tests__/mocks/next.mocks"
);
return createNextServerMock();
});
vi.mock("@calcom/prisma", async () => {
const { createPrismaMock } = await import("@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks");
const { createPrismaMock } = await import(
"@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks"
);
return createPrismaMock();
});
vi.mock("@calcom/prisma/client", async () => {
const { createPrismaMock } = await import("@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks");
const { createPrismaMock } = await import(
"@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks"
);
return createPrismaMock();
});
vi.mock("@calcom/lib/logger", () => ({
@@ -32,9 +38,13 @@ vi.mock("@calcom/lib/logger", () => ({
vi.mock("@calcom/lib/auth/hashPassword", () => ({ hashPassword: vi.fn().mockResolvedValue("hashed") }));
vi.mock("@calcom/lib/slugify", () => ({ default: vi.fn((s: string) => s.toLowerCase()) }));
vi.mock("@calcom/lib/constants", () => ({ IS_PREMIUM_USERNAME_ENABLED: false }));
vi.mock("@calcom/lib/server/username", () => ({ isUsernameReservedDueToMigration: vi.fn().mockResolvedValue(false) }));
vi.mock("@calcom/lib/server/username", () => ({
isUsernameReservedDueToMigration: vi.fn().mockResolvedValue(false),
}));
vi.mock("@calcom/features/auth/lib/verifyEmail", () => ({ sendEmailVerification: vi.fn() }));
vi.mock("@calcom/features/auth/signup/utils/createOrUpdateMemberships", () => ({ createOrUpdateMemberships: vi.fn() }));
vi.mock("@calcom/features/auth/signup/utils/createOrUpdateMemberships", () => ({
createOrUpdateMemberships: vi.fn(),
}));
vi.mock("@calcom/features/auth/signup/utils/validateUsername", () => ({
validateAndGetCorrectedUsernameAndEmail: vi.fn().mockResolvedValue({ isValid: true, username: "testuser" }),
}));
@@ -43,7 +53,8 @@ vi.mock("@calcom/features/auth/signup/utils/prefillAvatar", () => ({ prefillAvat
vi.mock("@calcom/features/auth/signup/utils/token", () => ({
findTokenByToken: (...args: unknown[]) => mockFindTokenByToken(...args),
throwIfTokenExpired: vi.fn(),
validateAndGetCorrectedUsernameForTeam: (...args: unknown[]) => mockValidateAndGetCorrectedUsernameForTeam(...args),
validateAndGetCorrectedUsernameForTeam: (...args: unknown[]) =>
mockValidateAndGetCorrectedUsernameForTeam(...args),
}));
// Import after mocks
@@ -100,7 +100,7 @@ export default async function handler(body: Record<string, string>) {
}
}
const organizationId = team.isOrganization ? team.id : team.parent?.id ?? null;
const organizationId = team.isOrganization ? team.id : (team.parent?.id ?? null);
const existingUserByUsername = await prisma.user.findFirst({
where: {
@@ -200,10 +200,7 @@ export default async function handler(body: Record<string, string>) {
if (isPrismaError(error) && error.code === "P2002") {
const target = String(error.meta?.target ?? "");
if (target.includes("email") || target.includes("username")) {
return NextResponse.json(
{ message: SIGNUP_ERROR_CODES.USER_ALREADY_EXISTS },
{ status: 409 }
);
return NextResponse.json({ message: SIGNUP_ERROR_CODES.USER_ALREADY_EXISTS }, { status: 409 });
}
}
throw error;
@@ -91,8 +91,7 @@ async function deleteHandler(req: NextRequest) {
const user = await authMiddleware();
const searchParams = Object.fromEntries(req.nextUrl.searchParams.entries());
const { integration, externalId, eventTypeId } =
selectedCalendarSelectSchema.parse(searchParams);
const { integration, externalId, eventTypeId } = selectedCalendarSelectSchema.parse(searchParams);
await SelectedCalendarRepository.delete({
where: {
+14 -14
View File
@@ -71,20 +71,20 @@ export async function handleCreateCredentials() {
`Creating credentials for ${toProcessMembers.length} members out of ${delegatedMembers.length} delegated members in organization ${organization.id}`
);
const credentialCreationPromises = toProcessMembers.map(async (member) => {
try {
const credentialData = buildCredentialCreateData({
type: "google_calendar",
key: {
// The in-memory credential that we create for Delegation Credential has access_token as "NOOP_UNUSED_DELEGATION_TOKEN. This is to mark it is in DB, in case we need to identify that.
access_token: "NOOP_UNUSED_DELEGATION_TOKEN_DB",
},
userId: member.userId,
appId: "google-calendar",
delegationCredentialId: delegationCredential.id,
});
await CredentialRepository.create(credentialData);
log.info(`Created credential for member ${member.userId}`);
const credentialCreationPromises = toProcessMembers.map(async (member) => {
try {
const credentialData = buildCredentialCreateData({
type: "google_calendar",
key: {
// The in-memory credential that we create for Delegation Credential has access_token as "NOOP_UNUSED_DELEGATION_TOKEN. This is to mark it is in DB, in case we need to identify that.
access_token: "NOOP_UNUSED_DELEGATION_TOKEN_DB",
},
userId: member.userId,
appId: "google-calendar",
delegationCredentialId: delegationCredential.id,
});
await CredentialRepository.create(credentialData);
log.info(`Created credential for member ${member.userId}`);
} catch (error) {
log.error(`Error creating credential for member ${member.userId}:`, safeStringify(error));
throw error;
@@ -44,7 +44,7 @@ async function postHandler(request: NextRequest) {
const teamBillingFactory = getTeamBillingServiceFactory();
const teamsBilling = teamBillingFactory.initMany(teams);
const teamBillingPromises = teamsBilling.map((teamBilling) => teamBilling.updateQuantity());
const teamBillingPromises = teamsBilling.map((teamBilling) => teamBilling.updateQuantity("sync"));
await Promise.allSettled(teamBillingPromises);
pageNumber++;
@@ -12,12 +12,16 @@ const getPrimaryCalendarMock = vi.fn();
vi.mock("@calcom/app-store/googlecalendar/lib/CalendarService", () => {
return {
__esModule: true,
createGoogleCalendarServiceWithGoogleType: vi.fn().mockImplementation(function() { return {
getPrimaryCalendar: getPrimaryCalendarMock,
}; }),
default: vi.fn().mockImplementation(function() { return {
getPrimaryCalendar: getPrimaryCalendarMock,
}; }),
createGoogleCalendarServiceWithGoogleType: vi.fn().mockImplementation(function () {
return {
getPrimaryCalendar: getPrimaryCalendarMock,
};
}),
default: vi.fn().mockImplementation(function () {
return {
getPrimaryCalendar: getPrimaryCalendarMock,
};
}),
};
});
@@ -267,10 +267,13 @@ export async function handleCreateSelectedCalendars() {
}
// Groups delegationUserCredentials by delegationCredentialId
const groupedDelegationUserCredentials = allDelegationUserCredentials.reduce((acc, curr) => {
acc[curr.delegationCredentialId] = [...(acc[curr.delegationCredentialId] || []), curr];
return acc;
}, {} as Record<string, typeof allDelegationUserCredentials>);
const groupedDelegationUserCredentials = allDelegationUserCredentials.reduce(
(acc, curr) => {
acc[curr.delegationCredentialId] = [...(acc[curr.delegationCredentialId] || []), curr];
return acc;
},
{} as Record<string, typeof allDelegationUserCredentials>
);
let totalSuccess = 0;
let totalFailures = 0;
@@ -279,9 +282,8 @@ export async function handleCreateSelectedCalendars() {
groupedDelegationUserCredentials
)) {
log.info(`Processing delegation user credentials for delegationCredentialId: ${delegationCredentialId}`);
const delegationUserCredentialsToProcess = await getDelegationUserCredentialsToProcess(
delegationUserCredentials
);
const delegationUserCredentialsToProcess =
await getDelegationUserCredentialsToProcess(delegationUserCredentials);
log.info(
`Found ${delegationUserCredentialsToProcess.length} delegationUserCredentials to process for delegationCredentialId: ${delegationCredentialId}`
);
@@ -47,9 +47,7 @@ describe("defaultResponderForAppDir", () => {
});
it("should respond with status code 409 for NoAvailableUsersFound", async () => {
const f = vi
.fn()
.mockRejectedValue(new Error(ErrorCode.NoAvailableUsersFound));
const f = vi.fn().mockRejectedValue(new Error(ErrorCode.NoAvailableUsersFound));
const req = { method: "GET", url: "/api/test" } as unknown as NextRequest;
const params = Promise.resolve<Params>({});
@@ -65,9 +63,7 @@ describe("defaultResponderForAppDir", () => {
});
it("should respond with a 429 status code for rate limit errors", async () => {
const f = vi
.fn()
.mockRejectedValue(new TRPCError({ code: "TOO_MANY_REQUESTS" }));
const f = vi.fn().mockRejectedValue(new TRPCError({ code: "TOO_MANY_REQUESTS" }));
const req = { method: "POST", url: "/api/test" } as unknown as NextRequest;
const params = Promise.resolve<Params>({});
+1 -1
View File
@@ -4,4 +4,4 @@ import getIP from "@calcom/lib/getIP";
export async function GET(req: Request) {
const requestorIp = getIP(req);
return NextResponse.json({ ip: requestorIp });
}
}
@@ -6,14 +6,16 @@ import { getOGImageVersion } from "@calcom/lib/OgImages";
import { GET } from "../route";
vi.mock("next/og", () => ({
ImageResponse: vi.fn().mockImplementation(function() { return {
body: new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([1, 2, 3, 4]));
controller.close();
},
}),
}; }),
ImageResponse: vi.fn().mockImplementation(function () {
return {
body: new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([1, 2, 3, 4]));
controller.close();
},
}),
};
}),
}));
vi.mock("@calcom/lib/OgImages", async (importOriginal) => {
+5 -7
View File
@@ -77,13 +77,11 @@ async function handler(req: NextRequest) {
const etag = await getOGImageVersion("meeting");
const img = new ImageResponse(
(
<Meeting
title={title}
profile={{ name: meetingProfileName, image: meetingImage }}
users={names.map((name, index) => ({ name, username: usernames[index] }))}
/>
),
<Meeting
title={title}
profile={{ name: meetingProfileName, image: meetingImage }}
users={names.map((name, index) => ({ name, username: usernames[index] }))}
/>,
ogConfig
);
@@ -76,10 +76,12 @@ const mockSendCreditBalanceLimitReachedEmails = vi.fn();
const mockSendCreditBalanceLowWarningEmails = vi.fn();
vi.mock("@calcom/features/ee/billing/credit-service", () => ({
CreditService: vi.fn().mockImplementation(function() { return {
hasAvailableCredits: mockHasAvailableCredits,
chargeCredits: mockChargeCredits,
}; }),
CreditService: vi.fn().mockImplementation(function () {
return {
hasAvailableCredits: mockHasAvailableCredits,
chargeCredits: mockChargeCredits,
};
}),
}));
vi.mock("@calcom/emails/email-manager", () => ({
@@ -92,15 +94,19 @@ const mockFindByPhoneNumber = vi.fn();
const mockFindByProviderAgentId = vi.fn();
vi.mock("@calcom/features/calAIPhone/repositories/PrismaPhoneNumberRepository", () => ({
PrismaPhoneNumberRepository: vi.fn().mockImplementation(function() { return {
findByPhoneNumber: mockFindByPhoneNumber,
}; }),
PrismaPhoneNumberRepository: vi.fn().mockImplementation(function () {
return {
findByPhoneNumber: mockFindByPhoneNumber,
};
}),
}));
vi.mock("@calcom/features/calAIPhone/repositories/PrismaAgentRepository", () => ({
PrismaAgentRepository: vi.fn().mockImplementation(function() { return {
findByProviderAgentId: mockFindByProviderAgentId,
}; }),
PrismaAgentRepository: vi.fn().mockImplementation(function () {
return {
findByProviderAgentId: mockFindByProviderAgentId,
};
}),
}));
vi.mock("next/server", () => ({
+1 -4
View File
@@ -12,10 +12,7 @@ import { notFound } from "next/navigation";
*/
const Page = (): JSX.Element => {
// Gate this page to E2E test mode or dev only
if (
process.env.NEXT_PUBLIC_IS_E2E !== "1" &&
process.env.NODE_ENV !== "development"
) {
if (process.env.NEXT_PUBLIC_IS_E2E !== "1" && process.env.NODE_ENV !== "development") {
notFound();
}
+6 -4
View File
@@ -41,8 +41,9 @@ function getPageInfo(pathname: string, host: string) {
return {
username: currentOrgDomain ?? "",
pageType: PageType.ORG,
url: `${WEBSITE_URL}/signup?callbackUrl=settings/organizations/new%3Fslug%3D${currentOrgDomain?.replace("/", "") ?? ""
}`,
url: `${WEBSITE_URL}/signup?callbackUrl=settings/organizations/new%3Fslug%3D${
currentOrgDomain?.replace("/", "") ?? ""
}`,
};
}
}
@@ -162,8 +163,9 @@ export function NotFound({ host }: { host: string }) {
<span className="focus:outline-none">
<span className="absolute inset-0" aria-hidden="true" />
{t("register")}{" "}
<strong className="text-green-500">{`${pageType === PageType.TEAM ? `${new URL(WEBSITE_URL).host}/team/` : ""
}${username}${pageType === PageType.ORG ? `.${subdomainSuffix()}` : ""}`}</strong>
<strong className="text-green-500">{`${
pageType === PageType.TEAM ? `${new URL(WEBSITE_URL).host}/team/` : ""
}${username}${pageType === PageType.ORG ? `.${subdomainSuffix()}` : ""}`}</strong>
</span>
</span>
</h3>
+2 -2
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon } from "@calcom/ui/components/icon";
import { XIcon } from "@coss/ui/icons";
export default function AddToHomescreen() {
const { t } = useLocale();
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
type="button"
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
<span className="sr-only">{t("dismiss")}</span>
<Icon name="x" className="text-inverted h-6 w-6 dark:text-white" aria-hidden="true" />
<XIcon className="text-inverted h-6 w-6 dark:text-white" />
</button>
</div>
</div>
+7 -7
View File
@@ -3,7 +3,7 @@
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 { ChartBarIcon, CreditCardIcon, GlobeIcon, LockIcon, PaintbrushIcon, UsersIcon } from "@coss/ui/icons";
import Shell from "~/shell/Shell";
import { UpgradeTip } from "~/shell/UpgradeTip";
@@ -13,32 +13,32 @@ export default function EnterprisePage() {
const features = [
{
icon: <Icon name="globe" className="h-5 w-5 text-red-500" />,
icon: <GlobeIcon className="h-5 w-5 text-red-500" />,
title: t("branded_subdomain"),
description: t("branded_subdomain_description"),
},
{
icon: <Icon name="chart-bar" className="h-5 w-5 text-blue-500" />,
icon: <ChartBarIcon className="h-5 w-5 text-blue-500" />,
title: t("org_insights"),
description: t("org_insights_description"),
},
{
icon: <Icon name="paintbrush" className="h-5 w-5 text-pink-500" />,
icon: <PaintbrushIcon className="h-5 w-5 text-pink-500" />,
title: t("extensive_whitelabeling"),
description: t("extensive_whitelabeling_description"),
},
{
icon: <Icon name="users" className="h-5 w-5 text-orange-500" />,
icon: <UsersIcon className="h-5 w-5 text-orange-500" />,
title: t("unlimited_teams"),
description: t("unlimited_teams_description"),
},
{
icon: <Icon name="credit-card" className="h-5 w-5 text-green-500" />,
icon: <CreditCardIcon className="h-5 w-5 text-green-500" />,
title: t("unified_billing"),
description: t("advanced_managed_events_description"),
},
{
icon: <Icon name="lock" className="h-5 w-5 text-purple-500" />,
icon: <LockIcon className="h-5 w-5 text-purple-500" />,
title: t("advanced_managed_events"),
description: t("advanced_managed_events_description"),
},
+2 -2
View File
@@ -1,9 +1,9 @@
/**
* PAGES ROUTER ONLY - Used exclusively by Next.js Pages Router
*
*
* Currently only serves the /router endpoint (routing forms redirect page).
* DO NOT add new features here - this file will be deprecated once we remove apps/web/pages.
*
*
* For App Router, use PageWrapperAppDir.tsx instead.
*/
@@ -13,7 +13,6 @@ export type PageWrapperProps = Readonly<{
}>;
function PageWrapper(props: PageWrapperProps) {
// On client side don't let nonce creep into DOM
// It also avoids hydration warning that says that Client has the nonce value but server has "" because browser removes nonce attributes before DOM is built
// See https://github.com/kentcdodds/nonce-hydration-issues
+19 -9
View File
@@ -19,7 +19,15 @@ import type { App as AppType } from "@calcom/types/App";
import classNames from "@calcom/ui/classNames";
import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { Icon } from "@calcom/ui/components/icon";
import {
BookOpenIcon,
CircleAlertIcon,
ExternalLinkIcon,
FileIcon,
FlagIcon,
MailIcon,
ShieldIcon,
} from "@coss/ui/icons";
import { SkeletonButton, SkeletonText } from "@calcom/ui/components/skeleton";
import { showToast } from "@calcom/ui/components/toast";
@@ -184,7 +192,9 @@ export const AppPage = ({
enabled: !!dependencies,
});
const disableInstall = dependencyData.data ? dependencyData.data.some((dependency) => !dependency.installed) : false;
const disableInstall = dependencyData.data
? dependencyData.data.some((dependency) => !dependency.installed)
: false;
// const disableInstall = requiresGCal && !gCalInstalled.data;
@@ -383,7 +393,7 @@ export const AppPage = ({
<div className="items-start space-x-2.5">
<div className="text-info flex items-start">
<div>
<Icon name="circle-alert" className="mr-2 mt-1 font-semibold" />
<CircleAlertIcon className="mr-2 mt-1 font-semibold" />
</div>
<div>
<span className="font-semibold">{t("msteams_calendar_warning_body")}</span>
@@ -443,7 +453,7 @@ export const AppPage = ({
rel="noreferrer"
className="text-emphasis text-sm font-normal no-underline hover:underline"
href={docs}>
<Icon name="book-open" className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
<BookOpenIcon className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
{t("documentation")}
</a>
</li>
@@ -455,7 +465,7 @@ export const AppPage = ({
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={website}>
<Icon name="external-link" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
<ExternalLinkIcon className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{website.replace("https://", "")}
</a>
</li>
@@ -467,7 +477,7 @@ export const AppPage = ({
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={`mailto:${email}`}>
<Icon name="mail" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
<MailIcon className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{email}
</a>
@@ -480,7 +490,7 @@ export const AppPage = ({
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={tos}>
<Icon name="file" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
<FileIcon className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{t("terms_of_service")}
</a>
</li>
@@ -492,7 +502,7 @@ export const AppPage = ({
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={privacy}>
<Icon name="shield" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
<ShieldIcon className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{t("privacy_policy")}
</a>
</li>
@@ -503,7 +513,7 @@ export const AppPage = ({
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
</span>
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
<Icon name="flag" className="inline h-3 w-3" /> {t("report_app")}
<FlagIcon className="inline h-3 w-3" /> {t("report_app")}
</a>
</div>
</div>
@@ -169,7 +169,11 @@ export function CalendarListContainer({
})}
description={t(`no_category_apps_description_calendar`)}
buttonRaw={
<Button EndIcon="external-link" color="secondary" data-testid="connect-calendar-apps" href="/apps/categories/calendar">
<Button
EndIcon="external-link"
color="secondary"
data-testid="connect-calendar-apps"
href="/apps/categories/calendar">
{t("connect_your_first_calendar")}
</Button>
}
@@ -23,24 +23,14 @@ export const InstallAppButtonChild = ({
// Also, cal.ai(the only paid app at the moment) doesn't support team install either
if (paid) {
return (
<Button
data-testid="install-app-button"
{...props}
disabled={isDisabled}
color="primary"
size="base">
<Button data-testid="install-app-button" {...props} disabled={isDisabled} color="primary" size="base">
{paid.trial ? t("start_paid_trial") : t("subscribe")}
</Button>
);
}
return (
<Button
data-testid="install-app-button"
{...props}
disabled={isDisabled}
color="primary"
size="base">
<Button data-testid="install-app-button" {...props} disabled={isDisabled} color="primary" size="base">
{multiInstall ? t("install_another") : t("install_app")}
</Button>
);
+2 -2
View File
@@ -12,8 +12,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
import { InfoIcon } from "@coss/ui/icons";
export default function AlbySetup(props: IAlbySetupProps) {
const params = useCompatSearchParams();
@@ -165,7 +165,7 @@ function AlbySetupPage(props: IAlbySetupProps) {
{/* TODO: remove when invoices are generated using user identifier */}
<div className="mt-4 rounded bg-blue-50 p-3 text-sm text-blue-700 dark:bg-blue-950 dark:text-blue-300">
<Icon name="info" className="mb-0.5 inline-flex h-4 w-4" /> Your Alby lightning address will be
<InfoIcon className="mb-0.5 inline-flex h-4 w-4" /> Your Alby lightning address will be
used to generate invoices. If you update your lightning address, please disconnect and setup the
Alby app again.
</div>
@@ -1,150 +1,150 @@
"use client";
import { useEffect, useState } from "react";
import z from "zod";
import { useBookingSuccessRedirect } from "@calcom/features/bookings/lib/bookingSuccessRedirect";
import type { PaymentPageProps } from "@calcom/features/ee/payments/pages/payment";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useCopy } from "@calcom/lib/hooks/useCopy";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { Spinner } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
interface IPaymentComponentProps {
payment: {
// Will be parsed on render
data: unknown;
};
paymentPageProps: PaymentPageProps;
}
// Create zod schema for data
const PaymentBTCPayDataSchema = z.object({
invoice: z.object({ checkoutLink: z.string() }).required(),
});
export const BtcpayPaymentComponent = (props: IPaymentComponentProps) => {
const { payment } = props;
const { data } = payment;
const [iframeLoaded, setIframeLoaded] = useState(false);
const { copyToClipboard, isCopied } = useCopy();
const wrongUrl = (
<>
<p className="mt-3 text-center">Couldn&apos;t obtain payment URL</p>
</>
);
const parsedData = PaymentBTCPayDataSchema.safeParse(data);
if (!parsedData.success || !parsedData.data?.invoice?.checkoutLink) return wrongUrl;
const checkoutUrl = parsedData.data.invoice.checkoutLink;
const handleOpenInNewTab = () => {
window.open(checkoutUrl, "_blank", "noopener,noreferrer");
};
return (
<div className="mb-4 mt-8 flex h-full w-full flex-col items-center justify-center gap-4">
<PaymentChecker {...props.paymentPageProps} />
{!iframeLoaded && (
<div className="flex items-center justify-center">
<Spinner className="mr-2 h-5 w-5" />
<p>Loading payment page...</p>
</div>
)}
<div className={`w-full ${iframeLoaded ? "block" : "hidden"}`}>
<iframe
src={checkoutUrl}
title="BTCPay Payment"
className="h-[1000px] w-full rounded-md border-0"
onLoad={() => setIframeLoaded(true)}
/>
</div>
<div className="flex flex-row flex-wrap items-center justify-center gap-4">
<Button
size="sm"
color="secondary"
onClick={() => copyToClipboard(checkoutUrl)}
className="text-subtle rounded-md"
StartIcon={isCopied ? "clipboard-check" : "clipboard"}>
Copy Payment Link
</Button>
<Button onClick={handleOpenInNewTab}>Open in New Tab</Button>
</div>
</div>
);
};
type PaymentCheckerProps = PaymentPageProps;
function PaymentChecker(props: PaymentCheckerProps) {
// TODO: move booking success code to a common lib function
// TODO: subscribe rather than polling
const searchParams = useCompatSearchParams();
const bookingSuccessRedirect = useBookingSuccessRedirect();
const utils = trpc.useUtils();
const { t } = useLocale();
useEffect(() => {
if (searchParams === null) {
return;
}
// use closure to ensure non-nullability
const sp = searchParams;
const interval = setInterval(() => {
(async () => {
try {
if (props.booking.status === "ACCEPTED") {
return;
}
const { booking: bookingResult } = await utils.viewer.bookings.find.fetch({
bookingUid: props.booking.uid,
});
if (bookingResult?.paid) {
showToast("Payment successful", "success");
const params: {
uid: string;
email: string | null;
location: string;
} = {
uid: props.booking.uid,
email: sp.get("email"),
location: t("web_conferencing_details_to_follow"),
};
bookingSuccessRedirect({
successRedirectUrl: props.eventType.successRedirectUrl,
query: params,
booking: props.booking,
forwardParamsSuccessRedirect: props.eventType.forwardParamsSuccessRedirect,
});
}
} catch (e) {}
})();
}, 2000);
return () => clearInterval(interval);
}, [
bookingSuccessRedirect,
props.booking,
props.booking.id,
props.booking.status,
props.eventType.id,
props.eventType.successRedirectUrl,
props.eventType.forwardParamsSuccessRedirect,
props.payment.success,
searchParams,
t,
utils.viewer.bookings,
]);
return null;
}
"use client";
import { useEffect, useState } from "react";
import z from "zod";
import { useBookingSuccessRedirect } from "@calcom/features/bookings/lib/bookingSuccessRedirect";
import type { PaymentPageProps } from "@calcom/features/ee/payments/pages/payment";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useCopy } from "@calcom/lib/hooks/useCopy";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { Spinner } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
interface IPaymentComponentProps {
payment: {
// Will be parsed on render
data: unknown;
};
paymentPageProps: PaymentPageProps;
}
// Create zod schema for data
const PaymentBTCPayDataSchema = z.object({
invoice: z.object({ checkoutLink: z.string() }).required(),
});
export const BtcpayPaymentComponent = (props: IPaymentComponentProps) => {
const { payment } = props;
const { data } = payment;
const [iframeLoaded, setIframeLoaded] = useState(false);
const { copyToClipboard, isCopied } = useCopy();
const wrongUrl = (
<>
<p className="mt-3 text-center">Couldn&apos;t obtain payment URL</p>
</>
);
const parsedData = PaymentBTCPayDataSchema.safeParse(data);
if (!parsedData.success || !parsedData.data?.invoice?.checkoutLink) return wrongUrl;
const checkoutUrl = parsedData.data.invoice.checkoutLink;
const handleOpenInNewTab = () => {
window.open(checkoutUrl, "_blank", "noopener,noreferrer");
};
return (
<div className="mb-4 mt-8 flex h-full w-full flex-col items-center justify-center gap-4">
<PaymentChecker {...props.paymentPageProps} />
{!iframeLoaded && (
<div className="flex items-center justify-center">
<Spinner className="mr-2 h-5 w-5" />
<p>Loading payment page...</p>
</div>
)}
<div className={`w-full ${iframeLoaded ? "block" : "hidden"}`}>
<iframe
src={checkoutUrl}
title="BTCPay Payment"
className="h-[1000px] w-full rounded-md border-0"
onLoad={() => setIframeLoaded(true)}
/>
</div>
<div className="flex flex-row flex-wrap items-center justify-center gap-4">
<Button
size="sm"
color="secondary"
onClick={() => copyToClipboard(checkoutUrl)}
className="text-subtle rounded-md"
StartIcon={isCopied ? "clipboard-check" : "clipboard"}>
Copy Payment Link
</Button>
<Button onClick={handleOpenInNewTab}>Open in New Tab</Button>
</div>
</div>
);
};
type PaymentCheckerProps = PaymentPageProps;
function PaymentChecker(props: PaymentCheckerProps) {
// TODO: move booking success code to a common lib function
// TODO: subscribe rather than polling
const searchParams = useCompatSearchParams();
const bookingSuccessRedirect = useBookingSuccessRedirect();
const utils = trpc.useUtils();
const { t } = useLocale();
useEffect(() => {
if (searchParams === null) {
return;
}
// use closure to ensure non-nullability
const sp = searchParams;
const interval = setInterval(() => {
(async () => {
try {
if (props.booking.status === "ACCEPTED") {
return;
}
const { booking: bookingResult } = await utils.viewer.bookings.find.fetch({
bookingUid: props.booking.uid,
});
if (bookingResult?.paid) {
showToast("Payment successful", "success");
const params: {
uid: string;
email: string | null;
location: string;
} = {
uid: props.booking.uid,
email: sp.get("email"),
location: t("web_conferencing_details_to_follow"),
};
bookingSuccessRedirect({
successRedirectUrl: props.eventType.successRedirectUrl,
query: params,
booking: props.booking,
forwardParamsSuccessRedirect: props.eventType.forwardParamsSuccessRedirect,
});
}
} catch (e) {}
})();
}, 2000);
return () => clearInterval(interval);
}, [
bookingSuccessRedirect,
props.booking,
props.booking.id,
props.booking.status,
props.eventType.id,
props.eventType.successRedirectUrl,
props.eventType.forwardParamsSuccessRedirect,
props.payment.success,
searchParams,
t,
utils.viewer.bookings,
]);
return null;
}
@@ -16,8 +16,8 @@ import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
import { LoaderIcon } from "@coss/ui/icons";
export default function BTCPaySetup(props: IBTCPaySetupProps) {
const params = useCompatSearchParams();
@@ -324,7 +324,7 @@ function BTCPaySetupPage(props: IBTCPaySetupProps) {
disabled={loading || validating || !updatable}>
{loading ? (
<>
<Icon name="loader" className="mr-2 animate-spin" />
<LoaderIcon className="mr-2 animate-spin" />
{t("connecting")}
</>
) : (
+2 -2
View File
@@ -16,7 +16,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { Switch } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { InfoIcon } from "@coss/ui/icons";
import { showToast } from "@calcom/ui/components/toast";
export default function HitPaySetup(props: IHitPaySetupProps) {
@@ -205,7 +205,7 @@ function HitPaySetupPage(props: IHitPaySetupProps) {
{showContent ? (
<div className="flex w-full w-full max-w-[43em] flex-col items-center justify-center stack-y-4 p-4 lg:stack-y-5">
<div className="rounded bg-blue-50 p-3 text-sm text-blue-700 dark:bg-blue-950 dark:text-blue-300">
<Icon name="info" className="mb-0.5 inline-flex h-4 w-4" /> Create or connect to an existing
<InfoIcon className="mb-0.5 inline-flex h-4 w-4" /> Create or connect to an existing
HitPay account to receive payments for your paid bookings.
</div>
@@ -8,7 +8,7 @@ import { Alert } from "@calcom/ui/components/alert";
import { Button } from "@calcom/ui/components/button";
import { Form } from "@calcom/ui/components/form";
import { TextField } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { PlusIcon, TrashIcon } from "@coss/ui/icons";
export default function ICSFeedSetup() {
const { t } = useLocale();
@@ -78,7 +78,7 @@ export default function ICSFeedSetup() {
type="button"
className="mb-2 h-min text-sm"
onClick={() => setUrls((urls) => urls.filter((_, ii) => i !== ii))}>
<Icon name="trash" size={16} />
<TrashIcon size={16} />
</button>
) : null}
</div>
@@ -91,7 +91,7 @@ export default function ICSFeedSetup() {
onClick={() => {
setUrls((urls) => urls.concat(""));
}}>
{t("add")} <Icon className="inline" name="plus" size={16} />
{t("add")} <PlusIcon className="inline" size={16} />
</button>
{errorMessage && (
@@ -36,13 +36,15 @@ const EventTypeCard: FC<EventTypeCardProps> = ({
userName,
}) => {
const parsedMetaData = EventTypeMetaDataSchema.safeParse(metadata);
const multipleDuration = parsedMetaData.success && parsedMetaData.data?.multipleDuration
? parsedMetaData.data.multipleDuration
: [];
const multipleDuration =
parsedMetaData.success && parsedMetaData.data?.multipleDuration
? parsedMetaData.data.multipleDuration
: [];
const durations = multipleDuration.length > 0
? [length, ...multipleDuration.filter((duration) => duration !== length)].sort()
: [length];
const durations =
multipleDuration.length > 0
? [length, ...multipleDuration.filter((duration) => duration !== length)].sort()
: [length];
return (
<div
data-testid={`select-event-type-${id}`}
@@ -66,7 +68,6 @@ const EventTypeCard: FC<EventTypeCardProps> = ({
// biome-ignore lint/security/noDangerouslySetInnerHtml: Content is sanitized via markdownToSafeHTML
<div
className="text-subtle line-clamp-4 wrap-break-word text-sm sm:max-w-[650px] [&>*:not(:first-child)]:hidden [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{
__html: markdownToSafeHTML(description),
}}
@@ -120,7 +121,9 @@ const EventTypeGroup: FC<EventTypeGroupProps> = ({ groupIndex, userName, ...prop
/>
))
) : (
<div className="text-subtle bg-cal-muted w-full p-2 text-center text-sm">Team has no Events</div>
<div className="text-subtle bg-cal-muted w-full p-2 text-center text-sm">
Team has no Events
</div>
)}
</ul>
</ScrollableArea>
+2 -2
View File
@@ -8,9 +8,9 @@ import ServerTrans from "@calcom/lib/components/ServerTrans";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
import { Tooltip } from "@calcom/ui/components/tooltip";
import { ClipboardIcon } from "@coss/ui/icons";
const MAKE = "make";
@@ -164,7 +164,7 @@ const CopyApiKey = ({ apiKey }: { apiKey: string }) => {
}}
type="button"
className="mt-4 text-base sm:mt-0 sm:rounded-l-none">
<Icon name="clipboard" className="h-4 w-4 ltr:mr-2 rtl:ml-2" />
<ClipboardIcon className="h-4 w-4 ltr:mr-2 rtl:ml-2" />
{t("copy")}
</Button>
</Tooltip>
+7 -5
View File
@@ -7,7 +7,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { TextField } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { CircleAlertIcon } from "@coss/ui/icons";
import { showToast } from "@calcom/ui/components/toast";
export default function PayPalSetup() {
@@ -91,7 +91,7 @@ export default function PayPalSetup() {
</p>
<p className="text-lgf text-default mt-5 inline-flex font-bold">
<Icon name="circle-alert" className="mr-2 mt-1 h-4 w-4" /> Important requirements:
<CircleAlertIcon className="mr-2 mt-1 h-4 w-4" /> Important requirements:
</p>
<ul className="text-default ml-1 mt-2 list-disc pl-2">
<li>Paypal Business account</li>
@@ -102,7 +102,8 @@ export default function PayPalSetup() {
<a
className="text-orange-600 underline"
target="_blank"
href="https://developer.paypal.com/api/rest/#link-getclientidandclientsecret" rel="noreferrer">
href="https://developer.paypal.com/api/rest/#link-getclientidandclientsecret"
rel="noreferrer">
Link to Paypal developer API REST Setup Guide:
https://developer.paypal.com/api/rest/#link-getclientidandclientsecret
</a>
@@ -121,7 +122,8 @@ export default function PayPalSetup() {
<a
target="_blank"
href="https://developer.paypal.com/dashboard/applications/live"
className="text-orange-600 underline" rel="noreferrer">
className="text-orange-600 underline"
rel="noreferrer">
{t("here")}
</a>
.
@@ -143,7 +145,7 @@ export default function PayPalSetup() {
<li>You should be all setup after this.</li>
</ol>
<p className="text-default mt-5 inline-flex font-bold">
<Icon name="circle-alert" className="mr-2 mt-1 h-4 w-4" />
<CircleAlertIcon className="mr-2 mt-1 h-4 w-4" />
Reminder:
</p>
<p className="text-default mt-2">
@@ -8,7 +8,10 @@ import { Dialog } from "@calcom/features/components/controlled-dialog";
import { dataTableQueryParamsSerializer } from "@calcom/features/data-table/lib/serializers";
import { ColumnFilterType } from "@calcom/features/data-table/lib/types";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { RoutingFormEmbedButton, RoutingFormEmbedDialog } from "@calcom/web/modules/embed/components/RoutingFormEmbed";
import {
RoutingFormEmbedButton,
RoutingFormEmbedDialog,
} from "@calcom/web/modules/embed/components/RoutingFormEmbed";
import { EmbedDialogProvider } from "@calcom/features/embed/lib/hooks/useEmbedDialogCtx";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -9,7 +9,7 @@ import type { RoutingFormWithResponseCount } from "@calcom/routing-forms/types/t
import { Button } from "@calcom/ui/components/button";
import { DropdownMenuSeparator } from "@calcom/ui/components/dropdown";
import { ToggleGroup } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { MenuIcon, PencilIcon, WaypointsIcon } from "@coss/ui/icons";
import { Tooltip } from "@calcom/ui/components/tooltip";
import { FormAction, FormActionsDropdown } from "./FormActions";
@@ -298,7 +298,7 @@ export function Header({
color="minimal"
onClick={() => setIsEditing(true)}
CustomStartIcon={
<Icon name="pencil" className="text-subtle group-hover:text-default h-3 w-3" />
<PencilIcon className="text-subtle group-hover:text-default h-3 w-3" />
}>
<span className="sr-only">Edit</span>
</Button>
@@ -319,13 +319,13 @@ export function Header({
{
value: "form-edit",
label: <span className="sr-only sm:not-sr-only">{t("form")}</span>,
iconLeft: <Icon name="menu" className="h-3 w-3" />,
iconLeft: <MenuIcon className="h-3 w-3" />,
dataTestId: "toggle-group-item-form-edit",
},
{
value: "route-builder",
label: <span className="sr-only sm:not-sr-only">{t("routing")}</span>,
iconLeft: <Icon name="waypoints" className="h-3 w-3" />,
iconLeft: <WaypointsIcon className="h-3 w-3" />,
},
]}
/>
@@ -355,12 +355,12 @@ export function Header({
{
value: "form-edit",
label: <span className="sr-only sm:not-sr-only">{t("form")}</span>,
iconLeft: <Icon name="menu" className="h-3 w-3" />,
iconLeft: <MenuIcon className="h-3 w-3" />,
},
{
value: "route-builder",
label: <span className="sr-only sm:not-sr-only">{t("routing")}</span>,
iconLeft: <Icon name="waypoints" className="h-3 w-3" />,
iconLeft: <WaypointsIcon className="h-3 w-3" />,
},
]}
/>
@@ -9,7 +9,7 @@ import logger from "@calcom/lib/logger";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/components/dialog";
import { Icon } from "@calcom/ui/components/icon";
import { ClockIcon } from "@coss/ui/icons";
import { showToast } from "@calcom/ui/components/toast";
interface IConfirmDialogWipe {
@@ -83,7 +83,7 @@ export const ConfirmDialog = (props: IConfirmDialogWipe) => {
<DialogContent onOpenAutoFocus={(e) => e.preventDefault()}>
<div className="flex flex-row space-x-3">
<div className="flex h-10 w-10 shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<Icon name="clock" className="m-auto h-5 w-5" />
<ClockIcon className="m-auto h-5 w-5" />
</div>
<div className="pt-1">
<DialogHeader title="Wipe My Calendar" />
+18 -22
View File
@@ -552,19 +552,14 @@ function BookingListItem(booking: BookingItemProps) {
userTimeZone={userTimeZone}
isRescheduled={isRescheduled}
onAssignmentReasonClick={
booking.assignmentReason.length > 0 ? () => setIsOpenRoutingTraceSheet(true) : undefined
booking.assignmentReasonSortedByCreatedAt.length > 0 ? () => setIsOpenRoutingTraceSheet(true) : undefined
}
/>
{isBookingFromRoutingForm && (
<WrongAssignmentDialog
isOpenDialog={isOpenWrongAssignmentDialog}
setIsOpenDialog={setIsOpenWrongAssignmentDialog}
bookingUid={booking.uid}
routingReason={booking.assignmentReason[0]?.reasonString ?? null}
guestEmail={booking.attendees[0]?.email ?? ""}
hostEmail={booking.user?.email ?? ""}
hostName={booking.user?.name ?? null}
teamId={booking.eventType?.team?.id ?? null}
booking={booking}
/>
)}
</div>
@@ -606,7 +601,7 @@ const BookingItemBadges = ({
</Badge>
</Tooltip>
)}
{isRejected && !isRescheduled && booking.assignmentReason.length === 0 && (
{isRejected && !isRescheduled && booking.assignmentReasonSortedByCreatedAt.length === 0 && (
<Badge variant="gray" className="ltr:mr-2 rtl:ml-2">
{t("rejected")}
</Badge>
@@ -616,9 +611,9 @@ const BookingItemBadges = ({
{booking.eventType.team.name}
</Badge>
)}
{booking?.assignmentReason.length > 0 && (
{booking?.assignmentReasonSortedByCreatedAt.length > 0 && (
<AssignmentReasonTooltip
assignmentReason={booking.assignmentReason[0]}
assignmentReason={booking.assignmentReasonSortedByCreatedAt[booking.assignmentReasonSortedByCreatedAt.length - 1]}
onClick={onAssignmentReasonClick}
/>
)}
@@ -804,20 +799,19 @@ const Attendee = (
const { copyToClipboard, isCopied } = useCopy();
const noShowMutation = trpc.viewer.loggedInViewerRouter.markNoShow.useMutation({
onSuccess: async (data) => {
showToast(data.message, "success");
await utils.viewer.bookings.invalidate();
},
onError: (err) => {
showToast(err.message, "error");
},
});
onSuccess: async (data) => {
showToast(data.message, "success");
await utils.viewer.bookings.invalidate();
},
onError: (err) => {
showToast(err.message, "error");
},
});
const displayName = user?.name || name || user?.email || email;
const isTeamMemberOrHost =
email === organizerEmail ||
eventTypeHosts?.some((host) => host.user?.email === email);
email === organizerEmail || eventTypeHosts?.some((host) => host.user?.email === email);
const shouldHideEmail = hideOrganizerEmail && isTeamMemberOrHost;
return (
@@ -858,7 +852,7 @@ const Attendee = (
onClick={(e) => {
e.preventDefault();
const isEmailCopied = isSmsCalEmail(email);
copyToClipboard(isEmailCopied ? email : phoneNumber ?? "");
copyToClipboard(isEmailCopied ? email : (phoneNumber ?? ""));
setOpenDropdown(false);
showToast(isEmailCopied ? t("email_copied") : t("phone_number_copied"), "success");
}}>
@@ -1102,7 +1096,9 @@ const DisplayAttendees = ({
return (
<div className="text-emphasis text-sm" onClick={(e) => e.stopPropagation()}>
{user && <FirstAttendee user={user} currentEmail={currentEmail} hideOrganizerEmail={hideOrganizerEmail} />}
{user && (
<FirstAttendee user={user} currentEmail={currentEmail} hideOrganizerEmail={hideOrganizerEmail} />
)}
{attendees.length > 1 ? <span>,&nbsp;</span> : <span>&nbsp;{t("and")}&nbsp;</span>}
<Attendee
{...attendees[0]}
@@ -10,8 +10,8 @@ import type { RecurringEvent } from "@calcom/types/Calendar";
import classNames from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
import { Label, Select, TextArea, CheckboxField } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
import { InfoIcon, XIcon } from "@coss/ui/icons";
interface InternalNotePresetsSelectProps {
internalNotePresets: { id: number; name: string }[];
@@ -182,7 +182,7 @@ export default function CancelBooking(props: Props) {
{error && !props.showErrorAsToast && (
<div className="mt-8">
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
<Icon name="x" className="h-6 w-6 text-red-600" />
<XIcon className="h-6 w-6 text-red-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
@@ -230,7 +230,7 @@ export default function CancelBooking(props: Props) {
/>
{isCancellationUserHost ? (
<div className="-mt-2 mb-4 flex items-center gap-2">
<Icon name="info" className="text-subtle h-4 w-4" />
<InfoIcon className="text-subtle h-4 w-4" />
<p className="text-subtle text-sm leading-none">
{t("notify_attendee_cancellation_reason_warning")}
</p>
@@ -0,0 +1,72 @@
"use client";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetBody } from "@calcom/ui/components/sheet";
interface RoutingFormResponseSheetProps {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
formResponseId: number;
}
export function RoutingFormResponseSheet({
isOpen,
setIsOpen,
formResponseId,
}: RoutingFormResponseSheetProps) {
const { t } = useLocale();
const { data, isLoading } = trpc.viewer.appRoutingForms.getFormResponseDisplay.useQuery(
{ formResponseId },
{
enabled: isOpen && !!formResponseId,
staleTime: 10 * 60 * 1000,
}
);
return (
<Sheet open={isOpen} onOpenChange={setIsOpen}>
<SheetContent>
<SheetHeader>
<SheetTitle>{t("form_submission")}</SheetTitle>
</SheetHeader>
<SheetBody>
{isLoading && (
<div className="flex flex-col gap-4 py-2">
{[1, 2, 3].map((i) => (
<div key={i} className="flex flex-col gap-1.5">
<div className="bg-muted h-3 w-24 animate-pulse rounded" />
<div className="bg-muted h-5 w-full animate-pulse rounded" />
</div>
))}
</div>
)}
{!isLoading && (!data?.responses || data.responses.length === 0) && (
<p className="text-subtle text-sm">{t("no_responses_found")}</p>
)}
{!isLoading && data?.responses && data.responses.length > 0 && (
<div className="flex flex-col gap-4">
{data.form?.name && (
<div className="border-subtle border-b pb-3">
<p className="text-emphasis text-sm font-medium">{data.form.name}</p>
{data.form.description && (
<p className="text-subtle text-xs">{data.form.description}</p>
)}
</div>
)}
{data.responses.map((item) => (
<div key={item.fieldId} className="flex flex-col gap-1">
<label className="text-subtle text-xs font-medium uppercase tracking-wide">
{item.label}
</label>
<p className="text-emphasis text-sm">{item.displayValue}</p>
</div>
))}
</div>
)}
</SheetBody>
</SheetContent>
</Sheet>
);
}
@@ -5,16 +5,34 @@ import { DomainIcon } from "@calcom/features/routing-trace/components/DomainIcon
import { getDomainLabel } from "@calcom/features/routing-trace/presenters/getDomainLabel";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetBody } from "@calcom/ui/components/sheet";
import { Button } from "@calcom/ui/components/button";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetBody,
} from "@calcom/ui/components/sheet";
interface RoutingTraceSheetProps {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
bookingUid: string;
/** Callback to open the wrong assignment report dialog. If not provided, report button is hidden */
onReport?: () => void;
hasExistingReport?: boolean;
}
export function RoutingTraceSheet({ isOpen, setIsOpen, bookingUid }: RoutingTraceSheetProps) {
export function RoutingTraceSheet({
isOpen,
setIsOpen,
bookingUid,
onReport,
hasExistingReport,
}: RoutingTraceSheetProps) {
const { t } = useLocale();
const showReportButton = !!onReport;
const { data, isLoading } = trpc.viewer.bookings.getRoutingTrace.useQuery(
{ bookingUid },
@@ -27,8 +45,24 @@ export function RoutingTraceSheet({ isOpen, setIsOpen, bookingUid }: RoutingTrac
return (
<Sheet open={isOpen} onOpenChange={setIsOpen}>
<SheetContent>
<SheetHeader>
<SheetTitle>{t("routing_trace")}</SheetTitle>
<SheetHeader showCloseButton={!showReportButton} className="w-full">
{showReportButton ? (
<div className="flex w-full items-center justify-between">
<SheetTitle>{t("routing_trace")}</SheetTitle>
<div className="flex items-center gap-2 pl-2">
<Button
color="secondary"
size="sm"
StartIcon="flag"
disabled={hasExistingReport}
onClick={onReport}>
{t("report")}
</Button>
</div>
</div>
) : (
<SheetTitle>{t("routing_trace")}</SheetTitle>
)}
</SheetHeader>
<SheetBody>
{isLoading && (
@@ -1,14 +1,9 @@
import { useState } from "react";
import type { z } from "zod";
import { MeetingSessionDetailsDialog } from "@calcom/web/modules/ee/video/components/MeetingSessionDetailsDialog";
import ViewRecordingsDialog from "~/ee/video/components/ViewRecordingsDialog";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import classNames from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
import { Dialog, DialogContent, DialogFooter, DialogClose } from "@calcom/ui/components/dialog";
import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui/components/dialog";
import {
Dropdown,
DropdownItem,
@@ -19,10 +14,10 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@calcom/ui/components/dropdown";
import { Tooltip } from "@calcom/ui/components/tooltip";
import type { ActionType } from "@calcom/ui/components/table";
import { showToast } from "@calcom/ui/components/toast";
import { Tooltip } from "@calcom/ui/components/tooltip";
import { MeetingSessionDetailsDialog } from "@calcom/web/modules/ee/video/components/MeetingSessionDetailsDialog";
import { AddGuestsDialog } from "@components/dialog/AddGuestsDialog";
import { CancelBookingDialog } from "@components/dialog/CancelBookingDialog";
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
@@ -33,20 +28,22 @@ import { ReportBookingDialog } from "@components/dialog/ReportBookingDialog";
import { RerouteDialog } from "@components/dialog/RerouteDialog";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
import { WrongAssignmentDialog } from "@components/dialog/WrongAssignmentDialog";
import { RoutingTraceSheet } from "../RoutingTraceSheet";
import { useState } from "react";
import type { z } from "zod";
import ViewRecordingsDialog from "~/ee/video/components/ViewRecordingsDialog";
import { useBookingConfirmation } from "../hooks/useBookingConfirmation";
import { RoutingTraceSheet } from "../RoutingTraceSheet";
import type { BookingItemProps } from "../types";
import { useBookingActionsStoreContext } from "./BookingActionsStoreProvider";
import {
type BookingActionContext,
getAfterEventActions,
getCancelEventAction,
getEditEventActions,
getAfterEventActions,
getPendingActions,
getReportAction,
shouldShowEditActions,
shouldShowPendingActions,
getPendingActions,
type BookingActionContext,
} from "./bookingActions";
interface BookingActionsDropdownProps {
@@ -278,8 +275,8 @@ export function BookingActionsDropdown({
recurringEventId: booking.recurringEventId,
})
: action.id === "reject"
? () => handleReject()
: undefined,
? () => handleReject()
: undefined,
})) as ActionType[];
const shouldShowEdit = shouldShowEditActions(actionContext);
@@ -291,14 +288,14 @@ export function BookingActionsDropdown({
action.id === "reschedule_request"
? () => setIsOpenRescheduleDialog(true)
: action.id === "reroute"
? () => setRerouteDialogIsOpen(true)
: action.id === "change_location"
? () => setIsOpenLocationDialog(true)
: action.id === "add_members"
? () => setIsOpenAddGuestsDialog(true)
: action.id === "reassign"
? () => setIsOpenReassignDialog(true)
: undefined,
? () => setRerouteDialogIsOpen(true)
: action.id === "change_location"
? () => setIsOpenLocationDialog(true)
: action.id === "add_members"
? () => setIsOpenAddGuestsDialog(true)
: action.id === "reassign"
? () => setIsOpenReassignDialog(true)
: undefined,
})) as ActionType[];
const baseAfterEventActions = getAfterEventActions(actionContext);
@@ -308,22 +305,22 @@ export function BookingActionsDropdown({
action.id === "view_recordings"
? () => setViewRecordingsDialogIsOpen(true)
: action.id === "meeting_session_details"
? () => setMeetingSessionDetailsDialogIsOpen(true)
: action.id === "charge_card"
? () => setChargeCardDialogIsOpen(true)
: action.id === "no_show"
? () => {
if (attendeeList.length === 1) {
const attendee = attendeeList[0];
noShowMutation.mutate({
bookingUid: booking.uid,
attendees: [{ email: attendee.email, noShow: !attendee.noShow }],
});
return;
}
setIsNoShowDialogOpen(true);
}
: undefined,
? () => setMeetingSessionDetailsDialogIsOpen(true)
: action.id === "charge_card"
? () => setChargeCardDialogIsOpen(true)
: action.id === "no_show"
? () => {
if (attendeeList.length === 1) {
const attendee = attendeeList[0];
noShowMutation.mutate({
bookingUid: booking.uid,
attendees: [{ email: attendee.email, noShow: !attendee.noShow }],
});
return;
}
setIsNoShowDialogOpen(true);
}
: undefined,
disabled:
action.disabled ||
(action.id === "no_show" && !(isBookingInPast || isOngoing)) ||
@@ -455,23 +452,20 @@ export function BookingActionsDropdown({
status={getBookingStatus()}
/>
{isBookingFromRoutingForm && (
<WrongAssignmentDialog
isOpenDialog={isOpenWrongAssignmentDialog}
setIsOpenDialog={setIsOpenWrongAssignmentDialog}
bookingUid={booking.uid}
routingReason={booking.assignmentReason[0]?.reasonString ?? null}
guestEmail={booking.attendees[0]?.email ?? ""}
hostEmail={booking.user?.email ?? ""}
hostName={booking.user?.name ?? null}
teamId={booking.eventType?.team?.id ?? null}
/>
)}
{booking.assignmentReason.length > 0 && (
<RoutingTraceSheet
isOpen={isOpenRoutingTraceSheet}
setIsOpen={setIsOpenRoutingTraceSheet}
bookingUid={booking.uid}
/>
<>
<WrongAssignmentDialog
isOpenDialog={isOpenWrongAssignmentDialog}
setIsOpenDialog={setIsOpenWrongAssignmentDialog}
booking={booking}
/>
<RoutingTraceSheet
isOpen={isOpenRoutingTraceSheet}
setIsOpen={setIsOpenRoutingTraceSheet}
bookingUid={booking.uid}
onReport={() => setIsOpenWrongAssignmentDialog(true)}
hasExistingReport={!!booking.report}
/>
</>
)}
{booking.paid && booking.payment[0] && (
<ChargeCardDialog
@@ -84,7 +84,7 @@ function createMockContext(overrides: Partial<BookingActionContext> = {}): Booki
location: "integrations:daily",
payment: [],
seatsReferences: [],
assignmentReason: [],
assignmentReasonSortedByCreatedAt: [],
metadata: null,
routedFromRoutingFormReponse: null,
listingStatus: "upcoming",
@@ -237,7 +237,15 @@ export function shouldShowIndividualReportButton(context: BookingActionContext):
}
export function isActionDisabled(actionId: string, context: BookingActionContext): boolean {
const { booking, isBookingInPast, isDisabledRescheduling, isDisabledCancelling, isAttendee, isCancelled, isRejected } = context;
const {
booking,
isBookingInPast,
isDisabledRescheduling,
isDisabledCancelling,
isAttendee,
isCancelled,
isRejected,
} = context;
switch (actionId) {
case "reschedule":
@@ -8,8 +8,8 @@ import { trpc } from "@calcom/trpc/react";
import { MultiEmail } from "@calcom/ui/components/address";
import { Button } from "@calcom/ui/components/button";
import { DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/components/dialog";
import { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
import { TriangleAlertIcon, UserPlusIcon } from "@coss/ui/icons";
interface IAddGuestsDialog {
isOpenDialog: boolean;
@@ -59,7 +59,7 @@ export const AddGuestsDialog = (props: IAddGuestsDialog) => {
<DialogContent enableOverflow>
<div className="flex flex-row md:space-x-3">
<div className="bg-subtle hidden h-10 w-10 shrink-0 justify-center rounded-full md:flex">
<Icon name="user-plus" className="m-auto h-6 w-6" />
<UserPlusIcon className="m-auto h-6 w-6" />
</div>
<div className="w-full md:pt-1">
<DialogHeader title={t("additional_guests")} />
@@ -75,7 +75,7 @@ export const AddGuestsDialog = (props: IAddGuestsDialog) => {
{isInvalidEmail && (
<div className="my-4 flex text-sm text-red-700">
<div className="shrink-0">
<Icon name="triangle-alert" className="h-5 w-5" />
<TriangleAlertIcon className="h-5 w-5" />
</div>
<div className="ml-3">
<p className="font-medium">{t("emails_must_be_unique_valid")}</p>
@@ -6,8 +6,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { DialogContent, DialogFooter, DialogHeader, DialogClose } from "@calcom/ui/components/dialog";
import { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
import { CreditCardIcon, TriangleAlertIcon } from "@coss/ui/icons";
interface IRescheduleDialog {
isOpenDialog: boolean;
@@ -45,7 +45,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
<DialogContent>
<div className="flex flex-row space-x-3">
<div className=" bg-subtle flex h-10 w-10 shrink-0 justify-center rounded-full">
<Icon name="credit-card" className="m-auto h-6 w-6" />
<CreditCardIcon className="m-auto h-6 w-6" />
</div>
<div className="pt-1">
<DialogHeader title={t("charge_card")} />
@@ -53,7 +53,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
{chargeError && (
<div className="mt-4 flex text-red-500">
<Icon name="triangle-alert" className="mr-2 h-5 w-5 " aria-hidden="true" />
<TriangleAlertIcon className="mr-2 h-5 w-5" />
<p className="text-sm">{chargeError}</p>
</div>
)}
@@ -23,7 +23,7 @@ import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/components/dialog";
import { Form, Input } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { MapPinIcon } from "@coss/ui/icons";
import { QueryCell } from "../../lib/QueryCell";
@@ -258,7 +258,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
}}>
<div className="flex flex-row md:space-x-3">
<div className="bg-subtle hidden h-10 w-10 shrink-0 justify-center rounded-full md:flex">
<Icon name="map-pin" className="m-auto h-6 w-6" />
<MapPinIcon className="m-auto h-6 w-6" />
</div>
<div className="w-full md:pt-1">
<DialogHeader title={t("edit_location")} />
+40 -38
View File
@@ -20,9 +20,9 @@ import {
ConfirmationDialogContent,
} from "@calcom/ui/components/dialog";
import { TextAreaField, Form, Label, Input } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { RadioAreaGroup as RadioArea } from "@calcom/ui/components/radio";
import { showToast } from "@calcom/ui/components/toast";
import { CheckIcon, LoaderIcon } from "@coss/ui/icons";
enum ReassignType {
AUTO = "auto",
@@ -252,7 +252,9 @@ export const ReassignDialog = ({
<strong className="mb-1 block">
{isManagedEvent ? t("auto_reassign") : t("round_robin")}
</strong>
<p>{isManagedEvent ? t("auto_reassign_description") : t("round_robin_reassign_description")}</p>
<p>
{isManagedEvent ? t("auto_reassign_description") : t("round_robin_reassign_description")}
</p>
</RadioArea.Item>
) : null}
<RadioArea.Item
@@ -285,7 +287,7 @@ export const ReassignDialog = ({
{isFetching && teamMemberOptions.length === 0 ? (
<div className="flex h-full items-center justify-center">
<div className="flex flex-col items-center gap-2">
<Icon name="loader" className="text-subtle h-5 w-5 animate-spin" />
<LoaderIcon className="text-subtle h-5 w-5 animate-spin" />
<p className="text-subtle text-sm">{t("loading")}</p>
</div>
</div>
@@ -295,42 +297,42 @@ export const ReassignDialog = ({
</div>
) : (
teamMemberOptions.map((member) => (
<label
key={member.value}
tabIndex={watchedTeamMemberId === member.value ? -1 : 0}
role="radio"
aria-checked={watchedTeamMemberId === member.value}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
form.setValue("teamMemberId", member.value);
}
}}
className={classNames(
"hover:bg-subtle focus:bg-subtle focus:ring-emphasis cursor-pointer items-center justify-between gap-0.5 rounded-sm py-2 outline-none focus:ring-2",
watchedTeamMemberId === member.value && "bg-subtle"
)}>
<div className="flex flex-1 items-center space-x-3">
<input
type="radio"
className="hidden"
checked={watchedTeamMemberId === member.value}
onChange={() => form.setValue("teamMemberId", member.value)}
/>
<div
className={classNames(
"h-3 w-3 shrink-0 rounded-full",
member.status === "unavailable" ? "bg-red-500" : "bg-green-500"
<label
key={member.value}
tabIndex={watchedTeamMemberId === member.value ? -1 : 0}
role="radio"
aria-checked={watchedTeamMemberId === member.value}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
form.setValue("teamMemberId", member.value);
}
}}
className={classNames(
"hover:bg-subtle focus:bg-subtle focus:ring-emphasis cursor-pointer items-center justify-between gap-0.5 rounded-sm py-2 outline-none focus:ring-2",
watchedTeamMemberId === member.value && "bg-subtle"
)}>
<div className="flex flex-1 items-center space-x-3">
<input
type="radio"
className="hidden"
checked={watchedTeamMemberId === member.value}
onChange={() => form.setValue("teamMemberId", member.value)}
/>
<div
className={classNames(
"h-3 w-3 shrink-0 rounded-full",
member.status === "unavailable" ? "bg-red-500" : "bg-green-500"
)}
/>
<span className="text-emphasis w-full text-sm">{member.label}</span>
{watchedTeamMemberId === member.value && (
<div className="place-self-end pr-2">
<CheckIcon className="text-emphasis h-4 w-4" />
</div>
)}
/>
<span className="text-emphasis w-full text-sm">{member.label}</span>
{watchedTeamMemberId === member.value && (
<div className="place-self-end pr-2">
<Icon name="check" className="text-emphasis h-4 w-4" />
</div>
)}
</div>
</label>
</div>
</label>
))
)}
{teamMemberOptions.length > 0 && (
+1 -1
View File
@@ -903,7 +903,7 @@ export const RerouteDialog = ({ isOpenDialog, setIsOpenDialog, booking }: Rerout
return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent preventCloseOnOutsideClick enableOverflow>
<DialogContent preventCloseOnOutsideClick enableOverflow>
<DialogHeader title={t("reroute_booking")} subtitle={t("reroute_booking_description")} />
<RerouteDialogContentAndFooter
booking={teamEventTypeBooking}
@@ -7,8 +7,8 @@ import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/components/dialog";
import { TextArea } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
import { ClockIcon } from "@coss/ui/icons";
interface IRescheduleDialog {
isOpenDialog: boolean;
@@ -39,7 +39,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
<DialogContent enableOverflow data-testid="reschedule-dialog">
<div className="flex flex-row md:space-x-3">
<div className="bg-subtle hidden h-10 w-10 shrink-0 justify-center rounded-full md:flex">
<Icon name="clock" className="m-auto h-6 w-6" />
<ClockIcon className="m-auto h-6 w-6" />
</div>
<div className="w-full md:pt-1">
<DialogHeader title={t("send_reschedule_request")} />
@@ -1,8 +1,10 @@
import { Dialog } from "@calcom/features/components/controlled-dialog";
import { useCopy } from "@calcom/lib/hooks/useCopy";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { AssignmentReasonEnum } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/components/alert";
import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/components/dialog";
import { Label, Select, TextArea } from "@calcom/ui/components/form";
@@ -12,15 +14,32 @@ import type { Dispatch, SetStateAction } from "react";
import type { Control, ControllerRenderProps } from "react-hook-form";
import { Controller, useForm } from "react-hook-form";
import assignmentReasonBadgeTitleMap from "@lib/booking/assignmentReasonBadgeTitleMap";
interface BookingData {
uid: string;
eventType?: {
team?: {
id: number;
} | null;
} | null;
user?: {
email: string;
name: string | null;
} | null;
assignmentReasonSortedByCreatedAt: Array<{
reasonString: string | null;
reasonEnum: AssignmentReasonEnum | null;
}>;
attendees: Array<{
email: string;
}>;
}
interface IWrongAssignmentDialog {
isOpenDialog: boolean;
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
bookingUid: string;
routingReason: string | null;
guestEmail: string;
hostEmail: string;
hostName: string | null;
teamId: number | null;
booking: BookingData;
}
interface FormValues {
@@ -36,6 +55,7 @@ interface TeamMemberOption {
interface RoutingInfoSectionProps {
routingReason: string | null;
routingReasonEnum: AssignmentReasonEnum | null;
noRoutingReasonText: string;
routingReasonLabel: string;
guestEmail: string;
@@ -48,8 +68,10 @@ interface RoutingInfoSectionProps {
}
function RoutingInfoSection(props: RoutingInfoSectionProps): JSX.Element {
const { t } = useLocale();
const {
routingReason,
routingReasonEnum,
noRoutingReasonText,
routingReasonLabel,
guestEmail,
@@ -79,9 +101,14 @@ function RoutingInfoSection(props: RoutingInfoSectionProps): JSX.Element {
<div className="-mt-2 mb-4 space-y-3">
<div>
<Label className="text-emphasis mb-1 block text-sm font-medium">{routingReasonLabel}</Label>
<p className="text-default bg-muted rounded-md px-3 py-2 text-sm">
{routingReason || noRoutingReasonText}
</p>
<div className="text-default bg-muted flex items-center gap-2 rounded-md px-3 py-2 text-sm">
{routingReasonEnum && (
<Badge variant="gray" className="shrink-0">
{t(assignmentReasonBadgeTitleMap(routingReasonEnum))}
</Badge>
)}
<span className="flex-1">{routingReason || noRoutingReasonText}</span>
</div>
</div>
<div>
@@ -212,16 +239,15 @@ export function WrongAssignmentDialog(props: IWrongAssignmentDialog): JSX.Elemen
const { t } = useLocale();
const utils = trpc.useUtils();
const { copyToClipboard, isCopied } = useCopy();
const {
isOpenDialog,
setIsOpenDialog,
bookingUid,
routingReason,
guestEmail,
hostEmail,
hostName,
teamId,
} = props;
const { isOpenDialog, setIsOpenDialog, booking } = props;
const bookingUid = booking.uid;
const teamId = booking.eventType?.team?.id ?? null;
const routingReason = booking.assignmentReasonSortedByCreatedAt[0]?.reasonString ?? null;
const routingReasonEnum = booking.assignmentReasonSortedByCreatedAt[0]?.reasonEnum ?? null;
const guestEmail = booking.attendees[0]?.email ?? "";
const hostEmail = booking.user?.email ?? "";
const hostName = booking.user?.name ?? null;
const {
control,
@@ -234,10 +260,8 @@ export function WrongAssignmentDialog(props: IWrongAssignmentDialog): JSX.Elemen
},
});
const { data: existingReport, isPending: isCheckingReport } = trpc.viewer.bookings.hasWrongAssignmentReport.useQuery(
{ bookingUid },
{ enabled: isOpenDialog }
);
const { data: existingReport, isPending: isCheckingReport } =
trpc.viewer.bookings.hasWrongAssignmentReport.useQuery({ bookingUid }, { enabled: isOpenDialog });
const alreadyReported = existingReport?.hasReport ?? false;
const teamIdForQuery = teamId ?? 0;
@@ -288,8 +312,9 @@ export function WrongAssignmentDialog(props: IWrongAssignmentDialog): JSX.Elemen
<RoutingInfoSection
routingReason={routingReason}
routingReasonEnum={routingReasonEnum}
noRoutingReasonText={t("no_routing_reason")}
routingReasonLabel={t("routing_reason")}
routingReasonLabel={t("first_assignment_reason")}
guestEmail={guestEmail}
whoBookedItLabel={t("who_booked_it")}
hostEmail={hostEmail}
@@ -329,7 +354,11 @@ export function WrongAssignmentDialog(props: IWrongAssignmentDialog): JSX.Elemen
<Button type="button" color="secondary" onClick={handleCloseClick} disabled={isPending}>
{t("close")}
</Button>
<Button type="submit" color="primary" disabled={isPending || alreadyReported || isCheckingReport} loading={isPending || isCheckingReport}>
<Button
type="submit"
color="primary"
disabled={isPending || alreadyReported || isCheckingReport}
loading={isPending || isCheckingReport}>
{t("submit")}
</Button>
</DialogFooter>
@@ -10,8 +10,8 @@ import { trpc } from "@calcom/trpc/react";
import type { App } from "@calcom/types/App";
import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
import { ArrowRightIcon, CircleAlertIcon } from "@coss/ui/icons";
interface IAppConnectionItem {
title: string;
@@ -79,7 +79,7 @@ const AppConnectionItem = (props: IAppConnectionItem) => {
<div className="items-start space-x-2.5">
<div className="flex items-start">
<div>
<Icon name="circle-alert" className="mr-2 mt-1 font-semibold" />
<CircleAlertIcon className="mr-2 mt-1 font-semibold" />
</div>
<div>
<span className="text-xs font-semibold">
@@ -99,7 +99,7 @@ const AppConnectionItem = (props: IAppConnectionItem) => {
<span className="mr-1">
{t("connect_app", { dependencyName: dependency.name })}
</span>
<Icon name="arrow-right" className="inline-block h-3 w-3" />
<ArrowRightIcon className="inline-block h-3 w-3" />
</Link>
</>
</div>
@@ -61,7 +61,7 @@ const EventReservationSchema: FC<EventReservationSchemaInterface> = ({
? ({ "@type": "Person", name: organizer.name, email: organizer.email } as Person)
: undefined,
attendee: attendees?.map(
(person) => ({ "@type": "Person", name: person.name, email: person.email } as Person)
(person) => ({ "@type": "Person", name: person.name, email: person.email }) as Person
),
location: location || undefined,
description: description || undefined,
@@ -1,10 +1,10 @@
import { Icon } from "@calcom/ui/components/icon";
import { ShieldIcon } from "@coss/ui/icons";
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
return (
<div className="mb-4 sm:flex sm:items-start">
<div className="bg-brand/5 text-brandcontrast dark:bg-darkmodebrand/5 dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
<Icon name="shield" className="text-inverted h-6 w-6" />
<ShieldIcon className="text-inverted h-6 w-6" />
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<h3 className="font-cal text-emphasis text-lg font-medium leading-6" id="modal-title">
@@ -5,8 +5,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants/permissions";
import { Button } from "@calcom/ui/components/button";
import { Label, TextField } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { Tooltip } from "@calcom/ui/components/tooltip";
import { InfoIcon } from "@coss/ui/icons";
type OAuthClientFormProps = {
defaultValues?: Partial<FormValues>;
@@ -227,7 +227,7 @@ export const OAuthClientForm = ({
className="max-w-[400px] whitespace-normal"
content="If enabled and the managed user has calendar connected, an event in the calendar will be created. By default true. Disable it if you want to create events in the calendar manually.">
<div className="ml-1">
<Icon name="info" className="h-4 w-4 text-gray-500" aria-hidden="true" />
<InfoIcon className="h-4 w-4 text-gray-500" />
</div>
</Tooltip>
</div>
@@ -252,7 +252,7 @@ export const OAuthClientForm = ({
className="max-w-[400px] whitespace-normal"
content="If enabled, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Leave this disabled if you want to create a managed user and then manually create event types for the user.">
<div className="ml-1">
<Icon name="info" className="h-4 w-4 text-gray-500" aria-hidden="true" />
<InfoIcon className="h-4 w-4 text-gray-500" />
</div>
</Tooltip>
</div>

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