Compare commits

...
100 Commits
Author SHA1 Message Date
emrysal 2701b4de79 chore: release v5.9.6
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-27 11:05:14 +00:00
Pasquale Vitiello a9f83ac5e2 fix: minor styling issues (#25421)
* style: remove unwanted bg

* fix: adjust FAB positioning

* style: skeleton improvements

* fix: reset password button

* fix: remove left border from remove icon

* style: improve fields

* style: wrap base form styles into layer base

* style: remove email-specific focus styles from TextField component
2025-11-27 11:00:44 +00:00
sean-brydonandcubic-dev-ai[bot] ff1533d344 chore: UI Nits onboarding v3 (#25409)
* Fix UI nits

* fix bio + team name on invite orgs

* use bg-cal-muted

* Fix error from mass edit

* Update apps/web/public/static/locales/en/common.json

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

* fix org labels

* Fix i18n

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-11-27 08:18:43 +00:00
Benny JooandDevin AI d9cddd85ff fix: break circular dependency by passing creditCheckFn in messageDispatcher (#25343)
* fix: break circular dependency in messageDispatcher via dependency injection

Break the 4-file circular dependency chain:
credit-service → reminderScheduler → smsReminderManager → messageDispatcher → credit-service

Solution:
- Add optional creditCheckFn parameter to messageDispatcher functions
- Thread creditCheckFn through the call chain: scheduleWorkflowReminders → scheduleSMSReminder/scheduleWhatsappReminder → messageDispatcher
- When creditCheckFn is provided, use it; otherwise fall back to dynamic CreditService import for backward compatibility
- This breaks the workflows → billing import while preserving immediate fallback behavior

Changes:
- messageDispatcher: Accept optional creditCheckFn parameter, use it if provided
- smsReminderManager: Thread creditCheckFn through scheduleSMSReminder
- whatsappReminderManager: Thread creditCheckFn through scheduleWhatsappReminder
- reminderScheduler: Add creditCheckFn to ScheduleWorkflowRemindersArgs and pass through processWorkflowStep

All type checks, lint checks, and unit tests pass.

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

* feat: wire creditCheckFn from all callers to complete circular dependency fix

- Add creditCheckFn parameter to WorkflowService.scheduleFormWorkflows
- Wire creditCheckFn from all 10 entry points that call workflow scheduling:
  * formSubmissionUtils.ts (form submissions)
  * roundRobinManualReassignment.ts (round-robin reassignment)
  * triggerFormSubmittedNoEventWorkflow.ts (form workflow trigger)
  * handleBookingRequested.ts (booking requests)
  * RegularBookingService.ts (2 calls - payment initiated & new bookings)
  * handleSeats.ts (seated bookings)
  * handleConfirmation.ts (2 calls - confirmation & payment)
  * handleMarkNoShow.ts (no-show updates)
  * confirm.handler.ts (booking rejection)
- Update test expectations to use expect.objectContaining()
- Fix pre-existing lint warning in handleMarkNoShow.ts (any type)
- This completes the messageDispatcher circular dependency fix by ensuring
  creditCheckFn is actually passed through the call chain, breaking the
  4-file circular dependency at runtime

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

* fix: use generic type with type guard in logFailedResults to fix type check error

- Replace constrained type with generic type parameter
- Add proper type guard for rejected promises
- Fixes CI type check failure in handleMarkNoShow.ts:385
- Avoids 'any' type while accepting any fulfilled value shape

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

* wip

* wip

* wip

* revert

* revert

* feat: wire creditCheckFn from all remaining callers to eliminate fallbacks

- Wire creditCheckFn in packages/sms/sms-manager.ts (can safely import CreditService)
- Create makeHandler factory pattern for CRON endpoints (scheduleSMSReminders.ts, scheduleWhatsappReminders.ts)
- Wire creditCheckFn from apps/web CRON routes to factories
- Add warning log in messageDispatcher when fallback is used
- Complete creditCheckFn wiring from all direct callers (activateEventType.handler.ts, util.ts)

This eliminates all fallbacks to dynamic import except as a safety net for unforeseen call sites.
The circular dependency (workflows ↔ billing) remains acceptable as discussed with user (Option C).

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

* test: update formSubmissionUtils tests to expect creditCheckFn parameter

The scheduleFormWorkflows function now receives creditCheckFn as a parameter.
Updated test assertions to use expect.objectContaining() with creditCheckFn: expect.any(Function)
to account for the new dependency injection parameter.

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

* test: update sms-manager test to expect creditCheckFn parameter

The sendSmsOrFallbackEmail function now receives creditCheckFn as a parameter.
Updated test assertion to use expect.objectContaining() with creditCheckFn: expect.any(Function)
to account for the new dependency injection parameter. Also removed teamId: undefined
assertion as the key may be omitted entirely from the actual call.

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

* feat: make creditCheckFn required to fully break circular dependency

This commit completes the circular dependency fix by making creditCheckFn
required throughout the call chain, eliminating the dynamic import fallback
entirely.

Changes:
- Make creditCheckFn required in messageDispatcher functions (sendSmsOrFallbackEmail, scheduleSmsOrFallbackEmail)
- Remove dynamic import fallback and warning log from messageDispatcher
- Make creditCheckFn required in ScheduleTextReminderArgs (smsReminderManager)
- Make creditCheckFn required in processWorkflowStep and ScheduleWorkflowRemindersArgs (reminderScheduler)
- Add creditCheckFn to SendCancelledRemindersArgs and wire from handleCancelBooking

The circular dependency is now fully broken - no more dynamic imports of
CreditService from within the workflows package. All callers must explicitly
provide creditCheckFn via dependency injection.

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

* fix: make creditCheckFn required in WorkflowService.scheduleFormWorkflows

This commit fixes the CI type check error by making creditCheckFn required
in WorkflowService.scheduleFormWorkflows. Previously, creditCheckFn was
optional in scheduleFormWorkflows but required in scheduleWorkflowReminders,
causing a type mismatch.

Changes:
- Make creditCheckFn required in scheduleFormWorkflows signature
- Update WorkflowService.test.ts to pass mock creditCheckFn in all test cases
- Add responseId and routedEventTypeId to test calls for completeness

All callers of scheduleFormWorkflows already pass creditCheckFn, so this
change is safe and completes the circular dependency fix.

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

* remove

* fix

* refactor

* refactor

* refactor

* wip

* fix

* fix

* rm

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-27 16:31:26 +09:00
Alex van Andel 1ab4b9dfab chore: Fix circular dependency in tanstack-table.d.ts (#25411)
* chore: Fix circular dependency in tanstack-table.d.ts

* fix: Re-export TextFilterOperator

* Unable to export non-type values from @calcom/types

* Refactor data-table types in a way that ensures type-safety as before

* Add FilterPopover and further fixups

* Fix further type errors missed earlier

* More hidden type errors

* Type error in useFilterValue
2025-11-26 14:05:55 -03:00
Anik Dhabal Babu f3d0475840 fix: role isn't updating properly (#25415) 2025-11-26 16:13:59 +00:00
Lauris Skraucis 061283bab9 fix: api v2 cancellation reason and by not being returned (#25413)
* fix: api v2 cancellation reason and by not being returned

* fix: e2e
2025-11-26 17:09:18 +01:00
4e0798577a feat: OAuth PKCE (#25313)
* add public client

* implement PKCE

* pass codeChallenge and codeChallengeMethod to handler

* fixes for secure oauth flow

* fix type error

* clean up refresh token endpoint

* only support S256

* fix type error

* remove comment

* add tests

* fix type errors in route.test.ts

* add missing support for refresh token

* add e2e test for public client refresh tokens

* allow pkce for confidential clients

* fix type error

* fix e2e

* fix option pkce for confidential clients

* e2e test improvements

* fix test

* remove only

* add delay

* fix e2e tests

* remove only

* don't skip pkce if codeChallenge is set

* add service functions for token endpoint

* use service function in refreshToken endpoint

* use repository

* remove return types

* e2e test fixes

* fix e2e test

* remove .only in e2e test

* remove pause

* fix error responses in token endpoints

* adjust tests to new error responses

* fix error responses

* e2e improvements

* redirect on error

* adjust tests

* Update apps/web/modules/auth/oauth2/authorize-view.tsx

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

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-11-26 17:02:42 +01:00
Anik Dhabal Babu c95b0833c1 fix: oauth screen go back button not functional (#25401)
* fix: oauth screen back

* update
2025-11-26 15:18:53 +00:00
sean-brydon 2a7c6590a3 feat: add permission for editUsers + implement UI (#25402)
## What does this PR do?
This PR implements attributes PBAC - router checks + UI

## Visual Demo (For contributors especially)

A visual demonstration is strongly recommended, for both the original and new change **(video / image - any one)**.

#### Video Demo (if applicable):

- Show screen recordings of the issue or feature.
- Demonstrate how to reproduce the issue, the behavior before and after the change.

#### Image Demo (if applicable):

- Add side-by-side screenshots of the original and updated change.
- Highlight any significant change(s).

## 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?
Enable PBAC on an org 
Create a custom role -> advanced -> organizations -> "editUser" 
Assign it to a user
impersonate user
test they have access to all things attributes
remove permissions
check they dont have permissions. 

## Checklist

<!-- Remove bullet points below that don't apply to you -->

- 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










<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds a new PBAC “editUsers” permission and read gating for Attributes, updating the UI and backend so users can view and edit attributes only when allowed.

- **New Features**
  - Added CustomAction.EditUsers to the permission registry for organization-scoped attribute editing.
  - Settings computes canViewAttributes and shows the Attributes tab only when allowed.
  - Members page fetches Attributes permissions and exposes canViewAttributes and canEditAttributesForUser to the UI.
  - User Edit Sheet hides attributes without read permission and shows attribute editing and the bulk “Mass Assign Attributes” action only with editUsers; other user edits depend on changeMemberRole.
  - Attributes TRPC router gates create/edit/delete/toggle via PBAC and requires organization.attributes.editUsers for assign/bulk-assign; added a helper to create PBAC-aware procedures.

- **Migration**
  - Run the new Prisma migration to seed the admin role with editUsers.
  - If using custom roles, grant Edit Users under Attributes as needed.

<sup>Written for commit 856aa2e8e1521fc22cd71cb0fa6d720036efe8a2. Summary will update automatically on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
2025-11-26 13:58:29 +00:00
Anik Dhabal Babu 7d2a06ddb6 fix: hide userresponses (#25370) 2025-11-26 13:27:13 +00:00
Lauris Skraucis d34ad2b0f6 fix: v2 api rescheduledByEmail not returned (#25103)
* fix: output

* fix: creation

* test: e2e rescheduling

* refactor: remove explicit return type

* fix: e2e

* fix: e2e

* fix: e2e

* fix: e2e tests

* fix: e2e tests

* fix: e2e

* logs: add debug logs for e2e

* debug e2e

* Revert "debug e2e"

This reverts commit 3d92d2bfbc351021ab06492b7279ed95843bf36a.

* Revert "logs: add debug logs for e2e"

This reverts commit f3b321fd96853bc2cf408c7f3667ad38d0931435.

* try to fix e2e

* extract e2e in separate file

* fix: e2e
2025-11-26 14:58:45 +02:00
Anik Dhabal Babu e06249d2df fix: wrong credentials (#25394) 2025-11-26 11:23:58 +00:00
sean-brydon a09dfe37a1 fix: redirect when creating orgs onboarding v3 (#25390)
* Fix redirect

* fix redirect
2025-11-26 09:54:16 +00:00
71e7f16b29 fix: prevent calendar credentials from leaking into video adapter calls (#25200)
* fix: prevent calendar credentials from leaking into video adapter calls

Split getCredentialAndWarnIfNotFound into two category-specific functions:
- getVideoCredentialAndWarnIfNotFound: only searches video credentials
- getCalendarCredentialAndWarnIfNotFound: only searches calendar credentials

This prevents the bug where calendar credentials (like google_calendar) were
being passed to getVideoAdapters() during booking deletion, causing
'Couldn't get adapter for googlecalendar' errors.

The root cause was the fallback logic that searched both videoCredentials
and calendarCredentials when a credential wasn't found by ID. Now each
function only searches within its own credential category and validates
the returned credential has the expected type suffix (_video/_conferencing
or _calendar).

Also fixed pre-existing ESLint warnings in EventManager.ts:
- Prefixed unused delegatedCredentialLast with underscore
- Replaced 'any' types with 'unknown' for better type safety
- Fixed unused variable warning in updateAllCalendarEvents error handler

Fixes the issue where deleteVideoEventForBookingReference could receive
a calendar credential when the video credential was missing, leading to
errors in getVideoAdapters.

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

* refactor: remove shared helper and implement logic directly in each function

- Removed getCredentialInternal shared helper
- Implemented logic directly in getVideoCredentialAndWarnIfNotFound
- Implemented logic directly in getCalendarCredentialAndWarnIfNotFound
- Changed fallback to explicitly use this.videoCredentials and this.calendarCredentials

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

* remove duplicate

* revert

* revert

* revert

* refactor

* add tests

* address

* clean up

* simplify

* simplify

* rename

* rename

* rename

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-11-26 00:04:21 +00:00
d6546c3107 feat: upgrade tailwind v4 (#24598)
* chore: refactor config files to prevent migration tool errors

* refactor: upgrade with the tailwind migration tool

* chore: restore pre-commit command + mc

* refactor(wip): update dependencies and migrate to Tailwind CSS v4 (mainly web)

* chore: resolve Tailwind v4 migration conflicts from merging main

* chore: remove unused Tailwind packages from config and update dependencies for v4 migration

* chore: uncomment Tailwind CSS utility classes in globals.css

* fix: resolve token conflicts between calcom and coss ui

* fix: textarea scrollbar

* Fix CUI-16

* fix: added @tailwindcss/forms plugin and cleaned up CSS classes in various components to remove unnecessary dark mode styles

* fix: selects and inputs of different sizes

* fix: remove unnecessary leading-20 class from modal titles in various components

* fix: update Checkbox component styles to remove unnecessary border on checked state

* fix: clean up styles in RequiresConfirmationController, Checkbox, and Radio components to enhance consistency

* fix: update button and filter component styles to remove unnecessary rounded classes for consistency

* fix: calendar

* fix: update KBarSearch

* fix: refine styles in Empty and Checkbox components for improved consistency

* Fix focus state email input

* fix: update button hover and active states to use 'not-disabled' instead of 'enabled'

* fix: line-height issues

* fix: update class name for muted background in BookingListItem component

* fix: sidebar spacing

* chore: update class names to use new Tailwind CSS color utilities

* fix embed

* chore: upgrade Tailwind CSS to version 4.1.16 and update related dependencies

* Map css variables and add a playground test for heavy css customization

* suggestion for coss-ui

* refactor: update CSS variable usage and clean up styles

- Replace instances of `--cal-brand-color` with `--cal-brand` in embed-related HTML files.
- Remove the now-unnecessary `addAppCssVars` function from the embed core.
- Import theme tokens in the embed core styles for better consistency.
- Clean up whitespace and formatting in CSS files for improved readability.
- Add a comment in `tokens.css` regarding its usage in both embed and webapp contexts.

* Handle within tokens.css instead of fixing coss-ui

* Remove initial, not needed. Also, remove tailwind.config.js as tailwidn scans the html automaically

* fix: examples app breaking

* fix: modal not resizing correctly

* feat: upgrade atoms to tailwind v4

* fix: atoms build breaking

* fix: atoms build breaking

* chore: upgrate examples/base to tailwind 4

* chore: update globals.css

* fix: add missing scheduler css variables

* fix: PlatformAdditionalCalendarSelector

* chore: update global styles

* chore: update tailwindcss and postcss dependencies to stable versions

* chore: remove unneeded class

* fix: dialog and toast animation

* fix: replace flex-shrink-0 with shrink-0 for consistent styling in various components

* fix: dialog modal for Apple connect

* add margin in SaveFilterSegmentButton

* Fix radix button nested states

* add cursor pointer to buttons but keep dsabled state

* Fix commandK selectors and adds cursor pointer

* Fix teams filter

* fix - round checkboxes

* fix filter checkbox

* fix select indicator's margin

* command group font size

* style: fix badge and tooltip radius

* chore: remove unneeded files

* Delete PR_REVIEW_MANAGED_EVENT_REASSIGNMENT.md

* remove ui-playground leftover

* fix: add missing react phone input styles in atoms

* Delete managed-event-reassignment-flow-and-architecture.mermaid

* fix: inter font not loading

* Add theme to skeleton container so that it can support dark mode

* fix: create custom stack-y-* utilities post tw4 upgrade

* fix: typo

* fix: atoms stack class + remove unused css file

* fix default radius valiue

* fix space-y in embed

* fix skeleton background

* Hardcode radius values to match production

* fix border in embed

* add missing externalThemeClass

* feat: create a custom stack-y-* utility

* fix: add stack utility to atom global css

* fix: Skeleton loader class modalbox

* Add stack-y utility in embed

* fix: add missing stack utilities in atoms globals.css

* update yarn.lock

* add popover portla

* update

---------

Co-authored-by: Sean Brydon <sean@cal.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Ryukemeister <sahalrajiv6900@gmail.com>
Co-authored-by: cal.com <morgan@cal.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Anik Dhabal Babu <adhabal2002@gmail.com>
2025-11-25 17:32:28 -03:00
625c260e40 fix: OOO days not correctly blocked (#25259)
* fix: OOO check in buildSlotsWithDateRanges

* added testcase

---------

Co-authored-by: chauhan_s <somaychauhan98@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-25 20:14:05 +00:00
Amit Sharma 81224f324a feat: google ads conversion tracking (#25198)
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
* feat: google ads conversion tracking

* gaClientid

* store gclid in stripe metadata

* tracking only in the us

* track google campaign id as well

* rename gclid -> google ads

* fix: build

* fix

* refactor

* fix: type check

* fix: type check

* fix: type check

* fix: type check

* fix: store it in cookie

* refactor

* fix

* cleanup

* linked ads tracking

* refactor checkout session tracking
2025-11-25 12:58:10 +00:00
emrysal 700d8e7f30 chore: release v5.9.5 2025-11-25 12:55:45 +00:00
Anik Dhabal Babu 301376e33b fix: set new icalUid when host reassign (#25365)
* fix seated event and workflow issue

* update

* revert

* Update smsReminderNumber assignment in handleSeats
2025-11-25 11:46:35 +00:00
Anik Dhabal Babu 57476ea392 chore: team update handler refactor (#25332)
* chore: team update handler refactor

* add tests

* fix
2025-11-25 11:27:26 +00:00
Anik Dhabal Babu 9c2d7f99c2 fix: google meet link is disappear (#25368) 2025-11-25 16:25:12 +05:30
sean-brydon ef3281f06f Remove square enforcing in resizeBase64Image (#25389) 2025-11-25 16:15:57 +05:30
sean-brydon d03f45dd0f fix: Onboarding v3 - create team then invite. (#25364)
## What does this PR do?
This PR changes the order of creating the team -> paying -> invite users.

The old approach was create team -> invite -> pay. But we are matching the current implementation of how billing works with usage based billing

## How should this be tested?

Enable onboarding-v3 flag on your instance
Ensure you have stripe enabled + stripe:listen running
login as onboarding@example.com, onboarding
Go through the team setup flow
Pay
Invite
Ensure uers were added to team

Create a new account
disable billing
Test again 

## Checklist

<!-- Remove bullet points below that don't apply to you -->

- 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
2025-11-25 09:14:59 +00:00
2b2bf36703 fix: Grab booking organizer credentials when team admins request reschedule (#24645)
* Change arg name from `bookingUId` to `bookingUid`

* Lint fix

* Use `BookingRepository` to find booking to reschedule

* Move early return further up if no booking is found

* Use `PermissionCheckService` if request rescheduling a team booking

* Remove redundent check

* Remove redundent eventType query

* Using `BookingRepository` to update the booking to rescheduled

* Update type in `getUsersCredentialsIncludeServiceAccountKey` to only
require params that are required

* Get booking organizer credentials

* Type fixes

* test: Add tests for team admin request reschedule with organizer credentials

- Add test for team admin requesting reschedule with proper permissions
- Add test verifying organizer's credentials are used (not requester's)
- Add test for team member without permissions (should fail)

These tests cover the fix in PR #24645 which ensures that when a team admin
requests a reschedule, the booking organizer's credentials are used to delete
calendar events instead of the requester's credentials.

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

* fix: Address code review comments for request reschedule

- Change user: true to user: { select: { id, email } } to only fetch required fields
- Change eventType include to select with explicit fields including teamId
- Remove sensitive information (user object, cancellationReason) from debug log
- All integration tests passing locally

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

* Type fix

* Remove businesss logic references from repository methods.

* Move business logic to handler

* Type fix

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-25 07:27:07 +00:00
Benny Joo 272d97c0a3 fix: prevent 500 errors in round-robin scheduling from OOO calibration for single host (#25369)
* wip

* add unit test
2025-11-25 09:13:04 +02:00
emrysal f2b2b6e491 chore: release v5.9.4
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-25 01:39:25 +00:00
15ef547e41 fix: enable block calendar slots by default for required confirmations (#25239)
* fix: enable block calendar slots by default for required confirmations

* remove unnecessary wasenabled

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-11-25 00:23:40 +05:30
Eunjae Lee 251d29f65b fix: improve overlapping events with dynamic offsets and widths (#25310)
## What does this PR do?

Improves the visual presentation of overlapping calendar events in the weekly view with two key enhancements:

- **Dynamic startHour per scenario**: Each playground scenario now displays the calendar starting at an appropriate hour based
on its earliest event time, rather than always starting at 6am
- **Full width for non-overlapping events**: Single events and non-overlapping events now display at 100% width (previously
80%) for maximum visibility

## Key Changes

### Overlapping Event Layout Algorithm

Replaces the previous uniform-width, fixed-offset layout with an intelligent spread algorithm:

**Previous behavior:**
- All overlapping events: 80% width with 8% offset steps
- Events clustered on the left side

**New behavior:**
- **2 overlapping events**: 80% and 50% widths
- **3 overlapping events**: 55%, ~42%, and 33% widths
- **4+ overlapping events**: Progressive narrowing from 40% down to minimum 25%
- **Spread algorithm**: Events distribute across the full width with the last event aligned to the right edge
- **Right edge distribution**: `ri = Rmin + (Rmax - Rmin) × i/(n-1)` for even spacing

### Visual Improvements

- Single/non-overlapping events: **100% width** (was 80%)
- Overlapping events: **Variable widths** based on cascade position (leftmost events wider, rightmost narrower)
- Last overlapping event: **Aligned to right border** for maximum scatter
- Minimum width: **25%** maintained for readability

**Devin session:** https://app.devin.ai/sessions/168d2227f5304c49ae4d34d17da5b025  
**Requested by:** eunjae@cal.com (@eunjae-lee)

## Visual Demo


https://github.com/user-attachments/assets/693546fa-448d-470a-b041-c08f4697c177



## Mandatory Tasks (DO NOT REMOVE)

- [x] I have self-reviewed the code
- [x] I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. **N/A** - playground-only changes
- [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. Navigate to `/settings/admin/playground/weekly-calendar`
2. Verify each scenario:
   - **Non-Overlapping Events**: Both events should be 100% width, no offset
   - **Touching Events**: Both events should be 100% width, no offset
   - **Two Overlapping Events**: First event 80% width, second 50% width aligned to right
   - **Three Overlapping Events**: Progressive narrowing with spread across full width
   - **Four Overlapping Events**: Four events spread across full width
3. Verify startHour values:
   - Most scenarios should start at 9am (events start at 10am)
   - Dense day scenario should start at 8am (events start at 9am)
   - Mixed statuses scenario should start at 1pm (events start at 2pm)
4. Test with real calendar data to ensure overlapping events look visually distinct

**Environment variables:** Standard Cal.com development setup  
**Test data:** Use playground scenarios or create overlapping events in your calendar

## Human Review Checklist

**⚠️ CRITICAL ITEMS:**

1. **Visual verification in playground** (MOST IMPORTANT):
   - Open `/settings/admin/playground/weekly-calendar`
   - Verify non-overlapping events are 100% width (not 80%)
   - Verify overlapping events spread properly across full width
   - Verify last overlapping event aligns to right edge
   - Verify each scenario starts at appropriate hour

2. **Algorithm correctness**:
   - Single events: 100% width (was 80%)
   - Two overlapping: 80%, 50% widths with last aligned to right
   - Three overlapping: 55%, ~42%, 33% widths spread across full width
   - Right-edge distribution: `ri = Rmin + (Rmax - Rmin) * i/(n-1)`

3. **Edge cases**:
   - Test with 10+ overlapping events to ensure no overflow
   - Verify minimum width (25%) is respected
   - Verify backward compatibility: custom `baseWidthPercent`/`offsetStepPercent` should use legacy behavior

4. **Type safety**:
   - `startHour` parameter now properly typed as `Hours` (union of 0-23)
   - All scenarios use valid `Hours` values

**Known limitations:**
- Local visual testing was not completed due to environment issues
- Easing curve parameters (curveExponent: 1.3) were chosen based on examples but may need visual tuning
- No E2E tests for visual appearance (only unit tests for layout calculations)

## Checklist

- [x] I have read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- [x] My code follows the style guidelines of this project
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have checked if my changes generate no new warnings
2025-11-24 12:01:05 +00:00
Benny JooandDevin AI 8b652613dd fix: break circular dependency between reminderScheduler and credit-service (#25312)
* fix: break circular dependency between reminderScheduler and credit-service

- Created WorkflowReminderRepository to handle workflow reminder queries
- Refactored cancelScheduledMessagesAndScheduleEmails to accept userIdsWithoutCredits parameter
- Moved credit-checking logic from workflows to CreditService
- Changed dynamic import to static import in CreditService
- Updated tests to pass userIdsWithoutCredits parameter
- Removed unused imports (prisma, WorkflowMethods)

This creates a one-way dependency (billing -> workflows) and follows the repository pattern by removing direct Prisma usage from reminderScheduler.

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

* fix: resolve type check errors in circular dependency fix

- Use MembershipRepository.listAcceptedTeamMemberIds instead of findAllAcceptedPublishedTeamMemberships
- Re-add prisma import to reminderScheduler.ts for UserRepository
- Update WorkflowReminderRepository to use WorkflowActions enum instead of string

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

* refactor

* refactor

* wip

* wip

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-24 10:00:57 +02:00
8e1a0999d2 fix: added pattern checking for urls while creating a team (#24777)
* fix: added pattern checking for urls while creating a team

* update: used slugify and replace logic

* update

* Fix formatting of rules in CreateANewTeamForm

---------

Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-11-22 15:49:28 +00:00
e29aa2097e feat: Ensure teams with conflicting slugs owned by the user are migrated(handled in backend, frontend already had this restriction) (#25291)
* feat: add backend validation for conflicting team slugs during org onboarding

- Added findOwnedTeamsByUserId method to TeamRepository
- Created buildTeamsAndInvites method in BaseOnboardingService that automatically:
  - Detects teams with same slug as organization
  - Marks conflicting teams for migration (isBeingMigrated: true)
  - Filters empty team names and invite emails
- Updated BillingEnabledOrgOnboardingService to use new method
- Updated SelfHostedOnboardingService to use new method
- Added comprehensive tests for slug conflict scenarios

This ensures backend validation even if frontend is bypassed, preventing
slug conflicts during organization creation. All inheriting classes
automatically get this validation without code changes.

* refactor: use TeamRepository in listOwnedTeamsHandler

Refactored listOwnedTeamsHandler to use TeamRepository.findOwnedTeamsByUserId
instead of direct Prisma queries. This:
- Reduces code duplication
- Ensures consistency across the codebase
- Follows repository pattern
- Makes the handler more maintainable

* fix: update tests to use renamed buildTeamsAndInvites method

- Renamed testFilterTeamsAndInvites to testBuildTeamsAndInvites
- Made test wrapper method async to match the async buildTeamsAndInvites
- Added orgSlug parameter to all test calls
- Updated all 9 test cases to use await with the new method signature
- Fixed lint warnings by using proper types instead of 'any'
- Imported OnboardingIntentResult and User types
- Used Pick<User> for mockUser type
- Removed all 'as any' type casts

Fixes test failures where filterTeamsAndInvites was renamed to buildTeamsAndInvites in the base service.

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

* fix: mock TeamRepository in tests to prevent database calls

Added vi.mock for TeamRepository to avoid database calls in unit tests.
The buildTeamsAndInvites method now calls ensureConflictingSlugTeamIsMigrated
which uses TeamRepository.findOwnedTeamsByUserId(). Mocking this prevents
Prisma errors in CI while keeping the tests focused on filtering logic.

The mock returns an empty array so no teams are found for migration,
allowing the tests to verify the filtering behavior without database access.

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

* refactor: improve readability of ensureConflictingSlugTeamIsMigrated

Refactored the conditional logic in ensureConflictingSlugTeamIsMigrated
for better readability while preserving exact behavior. Changed from
manual array manipulation to using .map() for updating team migration
status. This is a cosmetic change with no functional differences.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-22 09:07:28 +00:00
emrysal 1578dee622 chore: release v5.9.3
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-21 19:59:15 +00:00
Joe Au-Yeung e5ebf7feb4 fix: signup (#25334)
* Handle Stripe logic in `paymentCallback`

* Remove endpoint

* Do not send email verification email if premium username

* Remove logic from verify-view

* Callback send verification email

* Add `create` method to `VerificationToken` repository

* Create `VerificationTokenService`

* Early return if payment failed

* Refactor token generation

* Add tests

* Type fixes

* Type fixes
2025-11-21 19:51:19 +00:00
Rajiv Sahal 06348982a3 fix: make sure only admins and owners are authorized for team (#25333) 2025-11-21 18:44:51 +00:00
Syed Ali ShahbazandKeith Williams a7d87fc5be fix: Proxy Mintlify traffic through a Next.js API route (#25320)
* hotfix

* type fix and test fix

* update env example

* improvements

* more fix

* tada

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-11-21 18:43:09 +00:00
sean-brydonandUdit Takkar 0d77fc8474 Fix filter on invites (#25330)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-21 18:34:05 +00:00
Rajiv Sahal 0af77be127 chore: improve membership check (#25326)
* chore: improve membership check

* fix: update change role validation

* chore: add tests

* chore: update error message
2025-11-21 14:34:15 -03:00
Eunjae Lee afbfa11ab5 chore: update seed.ts (#25316)
* chore: update seed.ts

* Revert "chore: update seed.ts"

This reverts commit 2bd0c5a6dc57142632f8fbd267aa6b5eba6d1cea.

* use env vars instead
2025-11-21 16:37:57 +00:00
a7aabc8b4f fix: toggleEnabled handler (#25325)
* fix toggle enabled handler

* add tests

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-11-21 13:15:04 -03:00
sean-brydon c941192979 fix: access service class for editLocationHandler (#25315)
* fix access service class

* Move out of trpc
2025-11-21 13:13:06 -03:00
sean-brydon 44311f51ba refactor handler (#25323) 2025-11-21 15:51:23 +00:00
Morgan 0af6354954 fix: meeting ended page server component dto (#25318)
* fix: meeting ended page server component dto

* fix: repository function fetching too much
2025-11-21 15:29:53 +00:00
Joe Au-Yeung dd7c553cc3 fix: Remove hosts - verify event type belongs to event type (#25321)
* Add check that event type belongs to team

* Add `findAcceptedMembershipsByUserIdsInTeam` to `MembershipRepository`

* Validate that passed `userIds` belong to a team

* Add tests

* Typo fix
2025-11-21 10:26:22 -05:00
5cb1d4aad0 fix: Improve add users to org (#25314)
* Improve add users to org

* Improve add users to org

* test: Add comprehensive tests for handleUserEvents cross-tenant hijack prevention

- Add tests for cross-tenant hijack prevention security fix
- Test that users belonging to different organizations are blocked
- Test that users with null organizationId are blocked (prevents legacy user hijacking)
- Test that users belonging to correct organization succeed
- Test new user creation flow
- Test user activation/deactivation flows
- Test custom attribute syncing
- All 8 tests passing locally with proper type safety (no 'as any' usage)

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-21 12:18:41 -03:00
Eunjae Lee 701490a493 docs: update the app store doc regarding seed (#25317) 2025-11-21 14:45:29 +00:00
Morgan 6499243a42 fix: enable api v2 sms (#25311) 2025-11-21 13:40:27 +00:00
sean-brydonandDevin AI 3c8215add3 feat: filter out platform organizations from admin organization list (#24926)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-21 12:03:06 +00:00
mintlify[bot]andVolnei Munhoz 2bd7549f20 Add Cal.com API V1 to V2 Migration Guide (#25196)
* Update docs/api-reference/v2/migration-guide.mdx

* Update docs/api-reference/v2/v1-v2-differences.mdx

* Update docs/api-reference/v2/v1-v2-differences.mdx

* Update docs/api-reference/v2/migration-guide.mdx

---------

Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
Co-authored-by: Volnei Munhoz <volnei@cal.com>
2025-11-21 11:00:00 +00:00
mintlify[bot] 629371a5f1 Documentation edits made through Mintlify web editor (#25275)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2025-11-21 06:09:35 +00:00
dc3df122e2 feat: Add configurable trial days to org subscriptions + wizard warning (#25229)
* feat: Add trial days to organization subscriptions and workspace warning

- Add ORGANIZATION_TRIAL_DAYS environment variable for configurable trial periods
- Implement trial days in Stripe checkout session (only when env var is set)
- Add warning message to organization setup page about workspace structure
- Add translation string for organization trial workspace warning
- Add ORGANIZATION_TRIAL_DAYS to turbo.json env vars
- Fix pre-existing linting warnings in CreateANewOrganizationForm.tsx
- Add ESLint disable comments for turbo/no-undeclared-env-vars warnings

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

* Rename variable

* Add trial days to `purchaseTeamOrOrgSubscription`

* fix: Move organization trial warning to spot 3 below form card

- Add optional footer prop to WizardLayout component
- Use footer prop in create-new-view.tsx to render warning at spot 3
- Remove warning from inside CreateANewOrganizationForm component
- Warning now appears below the form card as requested

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

* feat: Add organization trial warning to all wizard steps

- Add warning footer to step 2 (about-view.tsx)
- Add warning footer to step 3 (add-teams-view.tsx)
- Add warning footer to step 4 (onboard-members-view.tsx)
- Add warning footer to step 5 (payment-status-view.tsx)
- Add warning footer to resume-view.tsx (step 1 resume page)
- Warning now appears on all steps of the organization creation wizard

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

* fix: Correct environment variable name to STRIPE_ORG_TRIAL_DAYS

- Changed ORGANIZATION_TRIAL_DAYS to STRIPE_ORG_TRIAL_DAYS in turbo.json
- This matches the actual usage in OrganizationPaymentService.ts and payments.ts
- Ensures the environment variable is properly recognized by the build system

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

* refactor: Create shared OrganizationWizardLayout component

- Create OrganizationWizardLayout component that wraps WizardLayout
- Centralizes the trial warning footer logic in one place
- Update all 6 wizard pages to use the shared component:
  - create-new-view.tsx
  - about-view.tsx
  - add-teams-view.tsx
  - onboard-members-view.tsx
  - payment-status-view.tsx
  - resume-view.tsx
- Remove duplicate useLocale/Alert imports and footer props from pages
- Simplifies maintenance by having warning logic in a single location

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

* Fix lint error

* Add `ORG_TRIAL_DAYS` as constant

* Add comment

* Ensure no negative number is passed

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-20 15:02:01 -03:00
176fb777fc fix: One flaky E2E tests (#24815)
* fix: exclude booking responses from calendar/CRM integrations for seated events

For seated events (events with seatsPerTimeSlot), booking responses are now excluded from being sent to calendar integrations (Google Calendar, etc.), CRM integrations (HubSpot, Salesforce, etc.), and other integrations.

This applies to both creating new seated bookings and rescheduling seated bookings.

The calendar event and CRM records are still created, but without the custom booking field responses included in the description/notes.

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

* test

* Refactor Office 365 Calendar OAuth redirect URI

Removed debug logging for redirect URI and client ID.

* test

* check

* Update Prisma connection logic for integration tests

Refactor database connection handling for integration tests.

* update

* Refactor conditionals and simplify role handling

* Add test comment in index.ts

* Remove integration test flag from index.ts

Remove integration test flag from database connection logic.

* update

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-20 22:10:02 +05:30
Alex van Andel ae7fd0cae2 refactor: Remove all code related to the old cache system (#25284)
* chore: Remove all code related to the old cache system

* Removed some redundant tests, some type fixes

* Further type fixes

* More type fixes re. tests

* Next iteration, couple of fixes remaining

* Remove cache from CredentialActionsDropdown

* Fix tests by mocking credential, instead of db queries

* Remove Cache DI wiring from v2

* Make sure apiv2 build passes

* Remove another cache cron

* Remove old tokens for calendar-cache v1
2025-11-20 18:02:18 +02:00
Piyanshu Saini d1184f65e2 fix(platform): show loading spinner only for selected plan, not all plans (#25290) (#25292)
## Problem
When users clicked on a plan card in the Cal.com Platform pricing UI, the loading
spinner would appear on ALL plan cards simultaneously instead of just the selected one.
This gave a confusing UX impression that all plans were being processed at the same time.

## Solution
Track the currently loading plan using a `loadingPlan` state variable that stores
which plan the user clicked on, rather than relying solely on the pending state from
the mutation hook.

## Changes
- Added `loadingPlan` state to track which specific plan is loading
- Update `loadingPlan` when a plan subscription is initiated
- Reset `loadingPlan` on error to ensure UI returns to normal state
- Modified `isLoading` prop condition to check if the current plan matches
  the `loadingPlan` before showing the spinner

This ensures that only the plan card the user clicked on displays the loading indicator,
while other plans remain in their normal state.
2025-11-20 15:25:12 +00:00
MorganandDevin AI 342e5baca2 feat: add hashedLink to BOOKING_REQUESTED webhook payload (#25274)
- Add hashedLink field to CalendarEvent type definition
- Add withHashedLink method to CalendarEventBuilder
- Pass hashedLink from booking request to CalendarEvent in RegularBookingService
- hashedLink will now be included in webhook payloads when booking via private API links

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-20 08:24:21 -03:00
chauhan_sandMorgan fbfa27735b feat: add validation for null values in bookingFieldsResponses (#25272)
* feat: add validation for null values in bookingFieldsResponses

- Add test case to verify 400 error when bookingFieldsResponses contains null values
- Create ValidateBookingFieldsResponses decorator to reject null values in booking field responses
- Apply validator to CreateBookingInput_2024_08_13.bookingFieldsResponses property
- Ensure all booking field response values are non-null strings

* feat: transform null values to empty strings in bookingFieldsResponses

- Remove ValidateBookingFieldsResponses validator that rejected null values
- Add Transform decorator to convert null values to empty strings in bookingFieldsResponses
- Update test to verify null values are transformed to empty strings instead of returning 400 error

* remove extra spaces

* test: add rescheduleReason null value test case

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-11-20 10:45:22 +00:00
emrysal 37c26793f5 chore: release v5.9.2
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-20 10:33:06 +00:00
sean-brydon 956f81fab3 chore: tidy up onboarding with new animations and illustrations (#25124)
<!-- This is an auto-generated description by cubic. -->
## Summary by cubic
Revamps onboarding with subtle route transitions, new org/teams illustrations, and clearer invite flows with real-time previews. Adds “Skip for now” on invites and guardrails to avoid incomplete team creation.

- **New Features**
  - Added email invite pages for organizations and teams with shared EmailInviteForm and RoleSelector.
  - Built a live invite browser preview that renders a 3×3 grid and updates as you type; works for orgs and teams.
  - Applied route-keyed framer-motion transitions to layout, cards, and browser views for smoother page changes.
  - Continuation prompt now detects saved org or team and routes to the right next step; localized copy.
  - Added loading state to plan selection to prevent double navigation.
  - Refreshed Organizations welcome modal with a new animated ring illustration and better scrolling.
  - Added “Skip for now” to org and team invite-by-email steps to proceed without invites.
  - Calendar browser preview now focuses on a time window around “now” for a more realistic demo.
  - Added an optional floating footer to keep actions visible on long, scrollable lists.

- **Refactors and Fixes**
  - Unified invite browser and replaced the org preview on the org email step.
  - Split and simplified legacy invite views; team and org routes are cleaner and easier to extend.
  - Streamlined the team invite step by removing a redundant action; non-admins are redirected to the email invite page.
  - Prevented creating teams with empty details by redirecting back to team details.
  - Minor UI cleanup: tighter borders, padding, and mobile max-heights across onboarding screens.
  - Hide team select on org invites when no teams exist.

<sup>Written for commit ae7f5277ab998560ae6e2f3f9144852a7b4959a7. Summary will update automatically on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
2025-11-20 10:20:38 +00:00
1dcd54aab7 feat: add avatarUrl and bio fields to /me endpoint response (#25224)
* feat: add avatarUrl to /v2/me endpoint response

- Add avatarUrl field to userSchemaResponse schema in packages/platform/types/me.ts
- Update e2e tests to verify avatarUrl is returned in GET and PATCH /v2/me responses
- Field is nullable to match User model in Prisma schema
- Fix pre-existing lint warnings by removing 'as any' type assertions in test file

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

* feat: add avatarUrl to MeOutput DTO for OpenAPI docs

- Add avatarUrl field to MeOutput class in apps/api/v2/src/ee/me/outputs/me.output.ts
- Field is nullable to match the Zod schema and Prisma model
- This ensures OpenAPI documentation will include avatarUrl when regenerated

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

* feat: add bio field to /v2/me endpoint response

- Add bio field to userSchemaResponse Zod schema in packages/platform/types/me.ts
- Add bio field to MeOutput NestJS DTO in apps/api/v2/src/ee/me/outputs/me.output.ts
- Update e2e tests to verify bio is returned in both GET and PATCH responses
- Field is nullable to match the User model in Prisma schema

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: chauhan_s <somaychauhan98@gmail.com>
2025-11-19 23:24:31 +00:00
68d3d9d964 docs: add Stripe Connect atom documentation (#24791)
* docs: add Stripe Connect atom documentation

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

* docs: restructure Stripe Connect docs with Steps component

- Add Steps component explaining two ways to connect Stripe
- Step 1: Direct Stripe Connect atom usage
- Step 2: Event Type Settings payment tab usage
- Include code examples for both methods
- Add image placeholders for visual demonstrations
- Move advanced usage examples under separate section

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

* chore: add link to stripe connect atom

* chore: update docs

* chore: implement feedback

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-19 23:12:05 +00:00
717a26f223 fix: prevent bulk update of locked locations in child managed event types (#24978)
* fix: prevent bulk update of locked locations in child managed event types

- Filter out child managed event types with locked locations in getBulkUserEventTypes
- Add validation in bulkUpdateEventsToDefaultLocation to prevent updating locked fields
- Implements defense in depth with validation at multiple layers

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

* Abstract filtering logic

* test: add comprehensive tests for bulk location update filtering

- Add unit tests for filterEventTypesWhereLocationUpdateIsAllowed
- Add unit tests for bulkUpdateEventsToDefaultLocation
- Add integration tests for getBulkUserEventTypes
- Fix bug: change unlockedFields?.locations check from !== undefined to === true
  This ensures that locations: false is properly treated as locked, addressing
  the security issue identified in PR review comments

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

* fix: filter locked managed event types on app installation page

- Add parentId to eventTypeSelect in getEventTypes function
- Apply filterEventTypesWhereLocationUpdateIsAllowed to both team and user event types
- Only filter when isConferencing is true to avoid affecting other app types
- Fixes issue where locked managed event types were showing in the event type selection list on /apps/installation/event-types page

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

* fix(embed-react): remove obsolete availabilityLoaded event listener

The availabilityLoaded event does not exist in the EventDataMap type system
in embed-core. This code was causing 5 TypeScript errors in CI:
- Type 'availabilityLoaded' does not satisfy constraint 'keyof EventDataMap'
- 'data' is of type 'unknown' (2 occurrences)
- Type 'availabilityLoaded' is not assignable to action union (2 occurrences)

Since this is an example file and the event is not defined in the type system,
removing this obsolete code resolves the type errors.

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

* fix: correct Prisma type for metadata in test helper function

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

* fix: use flexible PrismaLike type for better test compatibility

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

* fix: properly type mock Prisma objects in test files

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

* fix: properly mock Prisma methods in test file

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

* Filter out metadata

* Undo change in embed file

* Address feedback

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-19 15:46:05 -05:00
Alex van Andel 6818806c5e chore: No more clientside markdown when importing Checkbox (#25278) 2025-11-19 15:40:37 -03:00
Eunjae Lee dd7f108f08 fix: put booking details and calendar behind feature flag (#25175)
* Revert "fix: revert bookings redesign (#25172)"

This reverts commit 1f102bf3b4.

* add bookings-v3 feature flag

* put things behind a feature flag

* remove no longer needed test

* revert e2e tests

* put back description

* revert AvatarGroup

* apply feedback

* remove "view" booking action

* remove Alert (When the bookings query errors, this branch now renders only the alert and skips the data-table filter/segment controls. Those controls moved into BookingsList, so in error states users can no longer clear or tweak filters to recover from the failure, effectively trapping them behind the alert.)

* address feedback

* revert useMediaQuery
2025-11-19 16:27:13 +01:00
7e4d9e26c6 refactor: implement DI in team billing service and team billing data repository factory (#24803)
* Move TeamBillingRepositories

* WIP refactor team internal billing service

* Remove duplicate billing repository files

* Remove logic check in repository for billing is enabled

* Rename repository to `TeamBillingData`

* Use repository factory in main service

* Fix new import paths

* Rename  to

* Ensure `IS_TEAM_BILLING_ENABLED` is of type boolean

* Rename classes to TeamBillingService and TeamBillingServiceFactory

* Implement DI in `BookingServiceFactory`

* `TeamBillingService` use repository in `getOrgIfNeeded`

* DI `isTeamBillingEnabled` to `TeamBillingServiceFactory`

* Rename files for consistency

* Return stub BillingRepository if billing is not enabled

* Move Stripe billing service to service folder

* Rename file

* `StripeBillingService.getSubscriptionStatus` return `SubscriptionStatus`

* Type fices in StripeBillingService

* Type fix in `stubTeamBillingService`

* DI the `BillingProviderService` into the `TeamBillingService`

* Implement DI in `skipTeamTrials.handler`

* Implement DI for team billing in `inviteMember.handler`

* `skipTeamTrials.handler` use `team.isOrganization`

* Implement DI for billing in `hasActiveTeamPlan.handler`

* Type fixes

* Implement DI in `bulkDeleteUsers.handler`

* Implement `BillingProviderServiceFactory` in `updateProfile.handler`

* Implment `BillingProviderServiceFactory` in `buyCredits.handler`

* Fix import in `stripeCustomer.handler`

* Add a constructor to `teamBillingServiceFactory`

* Add DI to `PrismaTeamBillingRepository`

* Add DI to `StripeBillingService`

* Implement singleton in `BillingProviderServiceFactory`

* Add DI folder and contents to billing folder

* Use `getTeamBillingServiceFactory` in `inviteMember.handler`

* Add `saveTeamBilling` method to `ITeamBillingService`

* Implement DI in new team route

* Implement DI in `teamService`

* Implement DI in `OrganizationPaymentService`

* Implement DI in `credit-service`

* In `StripeBillingService` remove `static` from status methods

* Implemnt DI in `_invoice.paid.org`

* Refactor `hasActiveTeamPlan` to use `getTeamBillingFactory`

* Refactor `skipTeamTrials` to use `getTeamBillingFactory`

* Refactor `skipTeamTrials` to use `getTeamBillingServiceFactory`

* `stripeCustomer.handler` to use `getBillingProviderService`

* Remove old factories

* Type fix

* Remove unused factory

* Refactor `updateProfile.handler` to use `getBillingProviderService`

* Change name to `TeamBillingDataRepositoryFactory`

* Type Prisma return in `prisma.module`

* Type fix

* Refactor `buyCredits.handler` to use `getBillingProviderService`

* Refactor `credit-service` to use billing DI containers

* Type fix

* Add `getTeamBillingDataRepository`

* Refactor `_invoice.paid.org` to use DI container

* Refactor `_customer.subscription.deleted.team-plan` to use DI container

* Refactor `calcomHandler` to use DI container

* Refactor `getCustomerAndCheckoutSession` to use DI container

* Refactor `verify-email` to use DI containers

* Refactor `api/create/route` to use DI container

* Refactor downgradeUsers to use DI container

* Type fix

* Clean up console.logs

* Add await to `this.billingRepository.create` in `saveTeamBilling`

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

* Fix type errors

* Address comments

* fix: update tests to work with new DI pattern

- Update teamBillingService.test.ts to properly inject DI dependencies
- Remove unused billingModule import and mock
- Fix import naming in teamService.integration-test.ts (remove unused rename)
- Fix import path for TeamBillingPublishResponseStatus

All tests now properly mock IBillingProviderService, ITeamBillingDataRepository,
and IBillingRepository instead of using the old BillingRepositoryFactory pattern.

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

* fix: add compatibility layer and env setup for unit tests

- Add STRIPE_PRIVATE_KEY dummy value to vitest.config.ts to prevent DI module errors
- Fix import paths in credit-service.test.ts (StripeBillingService, TeamBillingService)
- Create compatibility barrel at packages/features/ee/billing/teams/index.ts for test mocking

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

* fix: update unit tests to mock DI container properly

- Update teamService.test.ts to mock getTeamBillingServiceFactory() instead of TeamBilling.findAndInit
- Update teamService.alternative.test.ts to mock DI container
- Update credit-service.test.ts to mock getBillingProviderService() and use SubscriptionStatus enum values
- Update OrganizationPaymentService.test.ts to mock DI container instead of direct StripeBillingService import
- Remove all 'as any' type casting to comply with Cal.com coding standards
- Fix unused variable warnings by prefixing with underscore

All 53 tests now passing (16 + 1 + 30 + 6)

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

* fix: update remaining unit tests to use DI pattern

- Fix StripeBillingService.test.ts to inject mock Stripe client directly
- Fix teamBillingFactory.test.ts to mock getTeamBillingServiceFactory() from DI container
- Fix skipTeamTrials.test.ts to mock DI container and use SubscriptionStatus enum

All 11 previously failing tests now pass (5 + 5 + 1)

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

* Undo changes made to Prisma module

* fix: address test-related PR comments

- Fix OrganizationPaymentService.test.ts mock path from @calcom/ee to @calcom/features/ee
- Refactor teamBillingFactory.test.ts to test real factory logic instead of mocking container
- Remove duplicate teamBillingService.test..ts file with incorrect double-dot filename

All three test files now pass successfully with proper DI patterns.

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

* Address feedback

* fix: update teamService integration test to mock new DI factory pattern

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

* refactor: remove duplicate imports in credit-service.test.ts

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

* Remove unused index file

* `getBySubscriptionId` to return team or null

* Address feedback

* Merge fix

* Refactor file names

* fix: correct mockStripe variable name to stripeMock in StripeBillingService.test.ts

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

* refactor: update internal-team-billing.test.ts to use new DI structure with TeamBillingService

- Replace InternalTeamBilling with TeamBillingService
- Use constructor injection with mock dependencies instead of factory pattern
- Remove BillingRepositoryFactory mock and import
- Update all test cases to use mockBillingProviderService, mockTeamBillingDataRepository, and mockBillingRepository
- Simplify saveTeamBilling tests to focus on repository.create calls
- All 11 tests now pass with the new DI structure

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

* fix: update createWithPaymentIntent.handler.test.ts to mock DI container's getBillingProviderService

- OrganizationPaymentService now uses getBillingProviderService() from DI container
- Test was mocking @calcom/features/ee/payments/server/stripe directly, which no longer works
- Added mock for @calcom/features/ee/billing/di/containers/Billing module
- Mock returns fake billing provider that delegates to mockSharedStripe
- Preserves all existing test assertions and helpers
- Fixed lint error by prefixing unused lastCreatedSessionId with underscore
- All 11 tests now pass (1 skipped as expected)

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

---------

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>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
2025-11-19 10:21:22 -05:00
sean-brydonandDevin AI a7352cf587 chore: remove ui-playground app (#25266)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-19 14:48:47 +00:00
Peer RichelsenandEunjae Lee 5f8db0557c chore: improve floating schedule button (#23873)
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-11-19 14:16:32 +00:00
emrysal 88be45457f chore: release v5.9.1
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-19 14:04:07 +00:00
Alex van Andel 90a42c37fd chore: Remove next-collect package (#25146) 2025-11-19 13:54:26 +00:00
cf7430d531 fix: convert booking field label error to log.error to prevent booking failures (#25062)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-19 13:30:46 +00:00
Hariom BalharaandDevin AI 105a82f97d feat: add uuidv7 and @db.Uuid to AuditActor and BookingAudit models (#25269)
- Update AuditActor.id to use uuid(7) with @db.Uuid
- Add @db.Uuid to BookingAudit.bookingUid and actorId fields
- Create migration to convert existing TEXT columns to UUID type

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-19 13:17:02 +00:00
Yash fdd3d11111 Fix dynamic group meeting slots not loading (#25101) 2025-11-19 12:56:47 +00:00
emrysal e7ea1884b7 chore: release v5.9.1 2025-11-19 11:43:50 +00:00
Hariom Balhara c04402e7ee feat: [Booking Audit Stack - 1] Add Booking Audit System foundation (database schema and repositories) (#24838)
* feat: Implement Booking Audit System with database architecture and repository interfaces

- Added `ARCHITECTURE.md` detailing the design and structure of the Booking Audit System, including core tables `AuditActor` and `BookingAudit`.
- Created repository interfaces `IAuditActorRepository` and `IBookingAuditRepository` for managing audit actor and booking audit records.
- Implemented `PrismaAuditActorRepository` and `PrismaBookingAuditRepository` for database interactions.
- Defined enums for `BookingAuditType`, `BookingAuditAction`, and `AuditActorType` in the Prisma schema.
- Added migration scripts to create necessary database tables and enums for the audit system.

This commit establishes a robust framework for tracking booking-related actions, ensuring compliance and data integrity.

* feat(audit): add system actor migration
2025-11-19 08:21:34 -03:00
075d209b6f feat: remove OAuth clients link from settings navigation (#25206)
* feat: remove OAuth clients link from settings navigation

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

* fix: resolve infinite loop in sidebar close effect

Use functional state update to avoid reading sideContainerOpen in dependency array, which was causing infinite re-renders when navigating between settings pages.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-19 11:09:07 +00:00
chauhan_s 26e8582302 fix: use UTC parsing for recurring booking dates to prevent timezone conversion issues (#25235)
Replace dayjs.tz() with dayjs.utc() when parsing recurring booking date strings to ensure dates are interpreted as UTC timestamps rather than being converted to the specified timezone. This prevents incorrect date/time display when the dateStr is already in UTC format.
2025-11-19 11:03:14 +00:00
Anik Dhabal Babu 9f277c5179 fix: archive meeting during host-change reschedules (#25098) 2025-11-19 10:24:50 +00:00
21411b8f93 docs: update embed-events.mdx with all currently emitted embed events (#25099)
* docs: update embed-events.mdx with all currently emitted embed events

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

* docs: remove V2 suffix from event names in documentation

Per feedback, the documentation should not expose internal versioning details.
Event names are now presented as clean, non-versioned names:
- bookingSuccessful (instead of bookingSuccessfulV2)
- rescheduleBookingSuccessful (instead of rescheduleBookingSuccessfulV2)
- dryRunBookingSuccessful (instead of dryRunBookingSuccessfulV2)
- dryRunRescheduleBookingSuccessful (instead of dryRunRescheduleBookingSuccessfulV2)

Added note about using wildcard listener for future-proofing.

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

* docs: use V2 suffix for embed event names to match code (#25263)

* docs: update embed events to use V2 event names

- Document bookingSuccessfulV2 instead of deprecated bookingSuccessful
- Document rescheduleBookingSuccessfulV2 instead of deprecated rescheduleBookingSuccessful
- Keep dryRunBookingSuccessful and dryRunRescheduleBookingSuccessful without V2 suffix (only V2 versions exist)
- Add comprehensive event documentation with all public and internal events
- Organize events into Public Events and Internal Events sections

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

* fix: use V2 suffix for dry-run event names

- Change dryRunBookingSuccessful to dryRunBookingSuccessfulV2
- Change dryRunRescheduleBookingSuccessful to dryRunRescheduleBookingSuccessfulV2
- These match the actual event names emitted in the code (only V2 versions exist)

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

---------

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2025-11-19 08:54:12 +00:00
Anik Dhabal Babu 0c160de404 fix: addtinonal notes isn't visisble (#25163) 2025-11-19 08:26:40 +00:00
a0eca13c18 feat: add organization-level autofill disable setting (#23504)
* feat: add organization-level autofill disable setting

- Create DisableAutofillOnBookingPageSwitch component following existing patterns
- Add toggle to organization general page alongside other settings
- Update tRPC organizations update handler to support new field
- Add organization-level check to useShouldBeDisabledDueToPrefill hook
- Add translation keys for new autofill disable setting
- Include database migration for disableAutofillOnBookingPage field
- Maintain backward compatibility with individual field settings

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

* feat: complete autofill disable implementation

- Add disableAutofillOnBookingPage to orgSettings type definition
- Update Prisma schema with new organization setting field
- Clean up test file formatting

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

* fix: resolve tRPC mocking issues in tests and add missing disableAutofillOnBookingPage to organization repository

- Fix tRPC module mocking in useShouldBeDisabledDueToPrefill tests
- Add disableAutofillOnBookingPage to organization repository select and return statements
- All form builder tests now pass (24/24)
- Organization-level autofill disable tests working correctly

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

* test: simplify autofill disable test to single focused test

- Replace multiple tests with one test that verifies org setting blocks autocomplete
- Test includes searchParams with prefill data to verify blocking behavior
- Removes unnecessary test complexity as requested

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

* fix: add missing disableAutofillOnBookingPage to organizationSettings select statements

- Add disableAutofillOnBookingPage to both parent and main organizationSettings select statements in getTeamWithMembers
- Resolves TypeScript error in getServerSideProps.tsx where MinimumOrganizationSettings type requires this property
- Ensures organization settings type compatibility across the codebase

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

* Remove disableAutofillOnBookingPage setting

Removed 'disableAutofillOnBookingPage' setting from organization configuration.

* update

* Remove duplicate settings in common.json

Removed duplicate entries for automatic transcription and autofill settings.

* Fix syntax error in common.json

* update

* add tests

* Remove comments for autofill disabled check

Removed comments explaining scenarios for autofill check.

* addressed review

* fix

* change

* Add handling for disableAutofillOnBookingPage input

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-19 08:25:46 +00:00
Anik Dhabal Babu 5cda123a4c fix test flakes (#25240) 2025-11-19 09:57:10 +02:00
Udit Takkar 6169b92e98 fix: dialog click propagation (#25154) 2025-11-19 11:34:34 +05:30
cc1efe1298 fix: Conditional fetch cache (#24816)
* Fix cache to fetch only when it's available

* Non hierarchical feature check

* Fix tests: Add missing mock method and comprehensive CalendarCacheWrapper tests

- Add checkIfUserHasFeatureNonHierarchical to features.repository mock to fix failing GoogleCalendar tests
- Add comprehensive unit tests for CalendarCacheWrapper covering:
  - Calendars with sync only (cache-only path)
  - Calendars without sync only (original calendar path)
  - Mixed calendars (both cache and original)
  - Timezone handling with UTC defaults
  - Edge cases (empty arrays, undefined methods, null ids)
- Use proper types instead of 'as any' to satisfy lint rules

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

* Fix: Sanitize logging to avoid exposing PII

- Replace logging full selectedCalendars objects with only calendar IDs and count
- Prevents exposure of email fields and other sensitive information in logs
- Addresses AI code reviewer feedback

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

* Apply suggestion from @volnei

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-19 01:11:52 +00:00
Joe Au-YeungandDevin AI 7635d338c0 fix: increase dropdown z-index to appear above logo (#24057)
- Change dropdown z-index from z-10 to z-50 to ensure dropdowns appear above navigation elements
- TopNav uses z-40, so dropdowns need higher z-index to be properly visible
- Fixes issue where Cal.com logo was appearing above dropdown menus

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-18 20:36:03 +00:00
Udit Takkar 03ebabd6b4 fix: transcript endpoint (#25136) 2025-11-18 21:00:38 +02:00
Anik Dhabal Babu 0ab1bd0b6c fix: correct assignment reason badge mapping (#25255) 2025-11-18 17:16:04 +00:00
Anik Dhabal Babuandcubic-dev-ai[bot] 908e5e05f2 fix: routing-form org redirects when profile slug equals legacy slug (#25003)
* fix: routing-form org redirects when profile slug equals legacy slug

* update

* Update packages/app-store/routing-forms/getEventTypeRedirectUrl.ts

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

* fix type error

* addressed review

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-11-18 09:12:58 +00:00
Peer Richelsen 3490272815 feat: normalize routing form identifier field with URL-safe format (#25128)
## What does this PR do?

- Fixes #25116
- Fixes [CAL-6741](https://linear.app/calcom/issue/CAL-6741/make-routing-edit-question-form-less-intrusive-on-how-it-handles-the)

https://github.com/user-attachments/assets/d78c4c62-2af5-44d8-b25c-e938cc8ddf6f


Improves the routing form field identifier auto-fill behavior to generate URL-safe identifiers from labels. Previously, the identifier field simply duplicated the label text. Now it normalizes the input to be URL-friendly.

**Changes:**
- Converts labels to lowercase with hyphens (e.g., "What is your name?" → "what-is-your-name")
- Limits identifiers to the first 5 words (e.g., "What do you work on and how can we help?" → "what-do-you-work-on")
- Updates the Identifier label to clarify it's a URL parameter with an example
- Preserves manual identifier edits when the label changes

**Link to Devin run:** https://app.devin.ai/sessions/f569297990fe4436bab1fec89d8b71ac
**Requested by:** @PeerRich (peer@cal.com)

## Key Implementation Details

The `normalizeIdentifier` function:
- Strips special characters and replaces them with spaces
- Converts to lowercase
- Splits on whitespace and takes first 5 words
- Joins with hyphens

The auto-fill logic only updates the identifier if:
1. The identifier field is empty, OR
2. The identifier matches the normalized version of the previous label

This preserves manual edits while still providing helpful auto-fill for new fields.

## 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](https://cal.com/docs). If N/A, write N/A here and check the checkbox. **N/A** - This is a UI behavior change that doesn't require documentation updates.
- [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works. **Note:** No automated tests were added for this change. Manual testing is recommended.

## How should this be tested?

**Test Cases:**

1. **Basic normalization:**
   - Create a new routing form field
   - Enter label: "What is your name?"
   - Verify identifier auto-fills to: "what-is-your-name"

2. **5-word limit:**
   - Enter label: "What do you work on and how can we help?"
   - Verify identifier becomes: "what-do-you-work-on"

3. **Special characters:**
   - Enter label: "Email (required)!"
   - Verify identifier becomes: "email-required"

4. **Manual edit preservation:**
   - Enter label: "Test Field"
   - Manually change identifier to: "custom-id"
   - Change label to: "New Test Field"
   - Verify identifier stays: "custom-id" (not auto-updated)

5. **Updated label text:**
   - Verify the Identifier field label shows: "Identifier (URL Parameter, i.e.: &what-is-your-name=john-doe)"

**Environment:**
- No special environment variables needed
- Test in the routing forms builder at `/apps/routing-forms/[formId]`

## Important Review Points

⚠️ **No automated tests**: The normalization function and auto-fill behavior lack test coverage. Consider adding unit tests for edge cases.

🔍 **Auto-fill logic**: The identifier only auto-updates if it's empty or matches the previous normalized label. This preserves manual edits but may be surprising to users in some scenarios.

🌍 **Unicode handling**: Non-ASCII characters are replaced with spaces. This may not be ideal for international users with non-English labels.

## Checklist

- [x] I have read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- [x] My code follows the style guidelines of this project
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have checked if my changes generate no new warnings
2025-11-18 11:24:33 +05:30
Mauryan Kansara 02dab51543 fix: Prevent silent loss of changes in routing form when only one field exists (#25197) 2025-11-18 02:02:57 +00:00
Anik Dhabal Babu 358b873aa7 fix: unable to update username (#25221)
* unable to update username

* update

* Add import for useEffect and useState hooks
2025-11-18 01:55:12 +00:00
Anik Dhabal Babu 44aa26292d fix: flaky integrations tests (#25218)
* Revert "fix: resolve flaky integration tests (#25030)"

This reverts commit 4e5d4f67d5.

* update

* test

* Remove connection pool setup in Prisma index

Set the connection pool to undefined, removing conditional pooling logic.

* update
2025-11-18 00:04:24 +00:00
b8530e2d78 fix: delegation credential error webhooks + refactor repeated code (#25232)
* fix: delegation credential error webhooks

* refactor: extract repeated delegation credential error webhook logic into helper methods

- Added private triggerDelegationCredentialError method in Office365CalendarService class
- Added triggerDelegationCredentialError helper function in TeamsVideoApiAdapter
- Replaced all 4 instances in Office365CalendarService with helper method call
- Replaced all 4 instances in TeamsVideoApiAdapter with helper function call
- Keeps code DRY by eliminating repeated if statement and webhook trigger logic

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-18 00:00:42 +02:00
Dhairyashil ShindeandPallav 7f1b94ad19 style: hide icons from booking actions dialog in mobile view (#25147)
* style: hide icons from booking actions dialog in mobile view

* apply correct padding in mobile view

---------

Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-11-17 20:50:41 +00:00
Dhairyashil ShindeandMorgan 801ba50221 fix: empty event type array if no username (#25178)
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-11-18 01:39:03 +05:30
spandevandDhairyashil Shinde 74af23136c fix: empty screen added in routing position page when no options are present (#25170)
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2025-11-17 20:06:39 +00:00
Anik Dhabal Babu da9ee5c186 chore: Update test results repository to test-results-2 (#25199) 2025-11-17 23:06:03 +05:30
037cb8ee2a feat: add defaultPhoneCountry prop with ISO 3166-1 alpha-2 type safety (#25204)
* feat: add defaultPhoneCountry prop to BookerPlatformWrapper

- Add defaultPhoneCountry to BookerStore type and implementation
- Add defaultPhoneCountry prop to BookerPlatformWrapper types
- Pass defaultPhoneCountry through store initialization
- Update PhoneInput to use defaultPhoneCountry from store
- Support default phone country extension for phone inputs in booker form

* feat: add strict typing for defaultPhoneCountry with ISO 3166-1 alpha-2 codes

- Define CountryCode type using ISO 3166-1 alpha-2 country codes
- Update defaultPhoneCountry prop type in BookerPlatformWrapper to use CountryCode
- Update defaultPhoneCountry type in BookerStore to use CountryCode
- Ensures type safety by only allowing valid country codes like 'us', 'gb', 'ee', etc.
- Fix lint warnings by prefixing type-only constants with underscore

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

* refactor: export CountryCode from store to avoid duplication

- Export CountryCode type from packages/features/bookings/Booker/store.ts
- Import CountryCode in packages/platform/atoms/booker/types.ts from store
- Remove duplicate CountryCode definition from types.ts
- Maintains single source of truth for country code type definition

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

* fix: add type-safe casts for CountryCode in PhoneInput

- Import CountryCode type from store
- Add explicit type annotation to useState<CountryCode>
- Add safe type casts with isSupportedCountry validation
- Validate navigator.language country code before using it
- Fixes CI type error: string not assignable to CountryCode

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

* docs: add defaultPhoneCountry prop documentation and changeset

- Add defaultPhoneCountry prop to booker.mdx documentation
- Add changeset for minor version bump
- Document ISO 3166-1 alpha-2 country code support

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
2025-11-17 15:56:10 +00:00
sean-brydon f0e10129a1 test: Improve create teams handler tests (#25213)
I am refactoring create teams handler to a service in another PR - thought we should improve the test in the mean time.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Expand and restructure tests for createTeamsHandler to cover auth, org/slug validation, team creation and migration, redirects, credits, invites, and subscriptions. This improves reliability ahead of the service refactor.

- **Refactors**
  - Rewrote tests into scenario-based suites for Authorization, Organization Validation, Slug Validation, Team Creation/Migration, Redirects, and Edge Cases.
  - Added helpers (createTestProfile, flexible createScenario/createTestMembership) and organization/profile support to model slug collisions.
  - Assert TRPCError codes/messages and redirect records; cover platform orgs, unpublished/non-existent teams, and empty names.
  - Mocked Stripe subscription cancel and inviteMembersWithNoInviterPermissionCheck; verified credit transfer and owner memberships.

<sup>Written for commit 8c216c670faa85bf9a06a8895030f40808fdda0e. Summary will update automatically on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
2025-11-17 11:08:33 +00:00
Dhairyashil ShindeandPallav 7c5731e893 fix: german-translation-link-meeting (#25210)
Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-11-17 10:57:58 +00:00
spandevandPallav 1012f2f153 fix: made show all columns work correctly (#25106)
* fix: made show all columns work correctly

* test: added test for show all columns in ColumnVisibilityButton

---------

Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-11-15 22:01:01 +00:00
Emmanuel IsenahandDhairyashil Shinde d6f6be7d50 fix: correct square image validation logic in resizeBase64Image (#25130)
The validation was comparing image height to itself instead of comparing height to width, causing the square check to always pass even for non-square images.

Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2025-11-16 01:50:50 +05:30
1020 changed files with 34330 additions and 46230 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@calcom/atoms": minor
---
Add defaultPhoneCountry prop to BookerPlatformWrapper with ISO 3166-1 alpha-2 type safety
+8 -1
View File
@@ -214,6 +214,8 @@ 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_PRODUCT_ID=
# Used to pass trial days for orgs during the Stripe checkout session
STRIPE_ORG_TRIAL_DAYS=
STRIPE_WEBHOOK_SECRET=
STRIPE_WEBHOOK_SECRET_APPS=
@@ -423,7 +425,8 @@ VAPID_PRIVATE_KEY=
# Mintlify chat api
# Power AI chat in for docs
NEXT_PUBLIC_MINTLIFY_CHAT_API_KEY=
MINTLIFY_CHAT_API_KEY=
NEXT_PUBLIC_ENABLE_MINTLIFY_CHAT=
NEXT_PUBLIC_CHAT_API_URL=
NEXT_PUBLIC_DOCS_URL=
@@ -489,3 +492,7 @@ SALESFORCE_GRAPHQL_MAX_RETRIES=3
# Force Node.js to use UTC as process timezone
TZ=UTC
# test oauth client for `packages/platform/examples/base` (optional)
SEED_PLATFORM_OAUTH_CLIENT_ID=
SEED_PLATFORM_OAUTH_CLIENT_SECRET=
-1
View File
@@ -1,6 +1,5 @@
/**/package.json @calcom/Foundation
/apps/api/**/* @calcom/API
/apps/ui-playground/**/* @calcom/Consumer @calcom/Foundation
/apps/web/**/layout.tsx @calcom/Consumer @calcom/Foundation
/apps/web/lib/csp.ts @calcom/Foundation
/apps/web/lib/buildNonce.ts @calcom/Foundation
+1 -1
View File
@@ -26,7 +26,7 @@ runs:
with:
path: ${{ inputs.path }}
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}
- run: USE_POOL=true yarn db-seed
- run: yarn db-seed
if: steps.cache-db.outputs.cache-hit != 'true'
shell: bash
- name: Postgres Dump Backup
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
- name: Checkout GitHub Pages Branch
uses: actions/checkout@v4
with:
repository: calcom/test-results
repository: calcom/test-results-2
ref: gh-pages
token: ${{ secrets.GH_ACCESS_TOKEN }}
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
- name: Checkout GitHub Pages Branch
uses: actions/checkout@v2
with:
repository: calcom/test-results
repository: calcom/test-results-2
ref: gh-pages
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Set Git User
+1
View File
@@ -5,6 +5,7 @@ permissions:
contents: read
env:
NODE_OPTIONS: --max-old-space-size=4096
INTEGRATION_TESTS: "true"
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
- name: Checkout GitHub Pages Branch
uses: actions/checkout@v4
with:
repository: calcom/test-results
repository: calcom/test-results-2
ref: gh-pages
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Set Git User
@@ -67,7 +67,7 @@ jobs:
- name: Output Report URL as Workflow Annotation
id: url
run: |
FULL_HTML_REPORT_URL=https://calcom.github.io/test-results/$HTML_REPORT_URL_PATH
FULL_HTML_REPORT_URL=https://calcom.github.io/test-results-2/$HTML_REPORT_URL_PATH
echo "::notice title=📋 Published Playwright Test Report::$FULL_HTML_REPORT_URL"
echo "link=$FULL_HTML_REPORT_URL" >> $GITHUB_OUTPUT
- name: Find Comment
-1
View File
@@ -4,7 +4,6 @@
"yzhang.markdown-all-in-one", // nicer markdown support
"esbenp.prettier-vscode", // prettier plugin
"dbaeumer.vscode-eslint", // eslint plugin
"bradlc.vscode-tailwindcss", // hinting / autocompletion for tailwind
"ban.spellright", // Spell check for docs
"stripe.vscode-stripe", // stripe VSCode extension
"Prisma.prisma", // syntax|format|completion for prisma
+1 -1
View File
@@ -8,5 +8,5 @@
"typescript.preferences.importModuleSpecifier": "non-relative",
"spellright.language": ["en"],
"spellright.documentTypes": ["markdown", "typescript", "typescriptreact"],
"tailwindCSS.experimental.classRegex": [["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]]
"tailwindCSS.classFunctions": ["cva"]
}
View File
View File
View File
View File
+2
View File
@@ -0,0 +1,2 @@
undecided:
- calcom-monorepo
View File
View File
-15
View File
@@ -1,15 +0,0 @@
Possible issue:
- We won't be able to use already built calendar-cache when Delegation Credential is enabled because existing CalendarCache entries don't have userId set.
## Approach of always using SelectedCalendar.credentialId for CalendarCache even for DelegationCredentials
- Delete Credentials when DelegationCredential is disabled
TO Test
- New members beyond the batch size are processed
+4 -4
View File
@@ -16,15 +16,15 @@ export const isAdminGuard = async (req: NextApiRequest) => {
where: {
userId: userId,
accepted: true,
role: {
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
},
team: {
isOrganization: true,
organizationSettings: {
is: {
isAdminAPIEnabled: true,
},
isAdminAPIEnabled: true,
},
},
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
},
select: {
team: {
@@ -1,7 +1,7 @@
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, it, expect } from "vitest";
import { describe, it, expect, beforeAll } from "vitest";
import prisma from "@calcom/prisma";
@@ -11,6 +11,30 @@ type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
describe("PATCH /api/bookings", () => {
beforeAll(async () => {
const acmeOrg = await prisma.team.findFirst({
where: {
slug: "acme",
isOrganization: true,
},
});
if (acmeOrg) {
await prisma.organizationSettings.upsert({
where: {
organizationId: acmeOrg.id,
},
update: {
isAdminAPIEnabled: true,
},
create: {
organizationId: acmeOrg.id,
orgAutoAcceptEmail: "acme.com",
isAdminAPIEnabled: true,
},
});
}
});
it("Returns 403 when user has no permission to the booking", async () => {
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } });
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
@@ -16,14 +16,33 @@ const DefaultPagination = {
skip: 0,
};
describe("GET /api/bookings", () => {
let proUser: Awaited<ReturnType<typeof prisma.user.findFirstOrThrow>>;
let proUserBooking: Awaited<ReturnType<typeof prisma.booking.findFirstOrThrow>>;
describe("GET /api/bookings", async () => {
beforeAll(async () => {
proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
proUserBooking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
const acmeOrg = await prisma.team.findFirst({
where: {
slug: "acme",
isOrganization: true,
},
});
if (acmeOrg) {
await prisma.organizationSettings.upsert({
where: {
organizationId: acmeOrg.id,
},
update: {
isAdminAPIEnabled: true,
},
create: {
organizationId: acmeOrg.id,
orgAutoAcceptEmail: "acme.com",
isAdminAPIEnabled: true,
},
});
}
});
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
const proUserBooking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
it("Does not return bookings of other users when user has no permission", async () => {
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } });
@@ -1,7 +1,7 @@
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, it, expect } from "vitest";
import { describe, it, expect, beforeAll } from "vitest";
import prisma from "@calcom/prisma";
@@ -12,6 +12,53 @@ type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
describe("isAdmin guard", () => {
beforeAll(async () => {
const acmeOrg = await prisma.team.findFirst({
where: {
slug: "acme",
isOrganization: true,
},
});
if (acmeOrg) {
await prisma.organizationSettings.upsert({
where: {
organizationId: acmeOrg.id,
},
update: {
isAdminAPIEnabled: true,
},
create: {
organizationId: acmeOrg.id,
orgAutoAcceptEmail: "acme.com",
isAdminAPIEnabled: true,
},
});
}
const dunderOrg = await prisma.team.findFirst({
where: {
slug: "dunder-mifflin",
isOrganization: true,
},
});
if (dunderOrg) {
await prisma.organizationSettings.upsert({
where: {
organizationId: dunderOrg.id,
},
update: {
isAdminAPIEnabled: false,
},
create: {
organizationId: dunderOrg.id,
orgAutoAcceptEmail: "dunder-mifflin.com",
isAdminAPIEnabled: false,
},
});
}
});
it("Returns false when user does not exist in the system", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
@@ -1,4 +1,4 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, beforeAll } from "vitest";
import prisma from "@calcom/prisma";
@@ -8,6 +8,30 @@ import {
} from "../../../lib/utils/retrieveScopedAccessibleUsers";
describe("retrieveScopedAccessibleUsers tests", () => {
beforeAll(async () => {
const acmeOrg = await prisma.team.findFirst({
where: {
slug: "acme",
isOrganization: true,
},
});
if (acmeOrg) {
await prisma.organizationSettings.upsert({
where: {
organizationId: acmeOrg.id,
},
update: {
isAdminAPIEnabled: true,
},
create: {
organizationId: acmeOrg.id,
orgAutoAcceptEmail: "acme.com",
isAdminAPIEnabled: true,
},
});
}
});
describe("getAccessibleUsers", () => {
it("Does not return members when only admin user ID is supplied", async () => {
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
@@ -189,7 +189,7 @@ export class BookingsController_2024_08_13 {
2. uid of one of the recurring booking recurrences
3. uid of recurring booking which will return an array of all recurring booking recurrences (stored as recurringBookingUid on one of the individual recurrences).
If you are fetching a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
@@ -213,7 +213,7 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Get all the recordings for the booking",
description: `Fetches all the recordings for the booking \`:bookingUid\`
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -229,9 +229,9 @@ export class BookingsController_2024_08_13 {
@Get("/:bookingUid/transcripts")
@UseGuards(BookingUidGuard)
@ApiOperation({
summary: "Get all the transcripts download links for the booking",
summary: "Get Cal Video real time transcript download links for the booking",
description: `Fetches all the transcripts download links for the booking \`:bookingUid\`
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -279,7 +279,7 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Reschedule a booking",
description: `Reschedule a booking or seated booking
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -321,7 +321,7 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Cancel a booking",
description: `:bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.
\nCancelling normal bookings:
If the booking is not seated and not recurring, simply pass :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and optionally cancellationReason in the request body \`{"cancellationReason": "Will travel"}\`.
@@ -329,11 +329,11 @@ export class BookingsController_2024_08_13 {
It is possible to cancel specific seat within a booking as an attendee or all of the seats as the host.
\n1. As an attendee - provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and seatUid in the request body \`{"seatUid": "123-123-123"}\` . This will remove this particular attendance from the booking.
\n2. As the host or org admin of host - host can cancel booking for all attendees aka for every seat, this also applies to org admins. Provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and cancellationReason in the request body \`{"cancellationReason": "Will travel"}\` and \`Authorization: Bearer token\` request header where token is event type owner (host) credential. This will cancel the booking for all attendees.
\nCancelling recurring seated bookings:
For recurring seated bookings it is not possible to cancel all of them with 1 call
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.
If you are cancelling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
@@ -374,7 +374,7 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Mark a booking absence",
description: `The provided authorization header refers to the owner of the booking.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -399,7 +399,7 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Reassign a booking to auto-selected host",
description: `Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -423,7 +423,7 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Reassign a booking to a specific host",
description: `Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -454,7 +454,7 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Confirm a booking",
description: `The provided authorization header refers to the owner of the booking.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -478,8 +478,8 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Decline a booking",
description: `The provided authorization header refers to the owner of the booking.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
async declineBooking(
@@ -502,8 +502,8 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Get 'Add to Calendar' links for a booking",
description: `Retrieve calendar links for a booking that can be used to add the event to various calendar services. Returns links for Google Calendar, Microsoft Office, Microsoft Outlook, and a downloadable ICS file.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@HttpCode(HttpStatus.OK)
@@ -20,11 +20,7 @@ import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
CreateBookingInput_2024_08_13,
GetBookingOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import { CreateBookingInput_2024_08_13, GetBookingOutput_2024_08_13, GetSeatedBookingOutput_2024_08_13 } from "@calcom/platform-types";
import { BookingOutput_2024_08_13 } from "@calcom/platform-types";
import type { Booking, PlatformOAuthClient, Team, User, EventType } from "@calcom/prisma/client";
@@ -854,7 +850,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
describe("Booking Field Type Validation", () => {
const basePayload = {
start: "2025-06-19T11:00:00.000Z",
start: new Date(Date.UTC(2030, 5, 19, 11, 0, 0)).toISOString(),
attendee: {
name: "Charlie TypeTest",
email: "charlie.typetest@example.com",
@@ -1162,6 +1158,31 @@ describe("Bookings Endpoints 2024-08-13", () => {
`One or more invalid options for booking field '${fieldName}'. Allowed options are: blue, red.`
);
});
it("should transform null values to empty strings in bookingFieldsResponses", async () => {
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFields.id,
bookingFieldsResponses: {
"favorite-movie": "The Matrix",
rescheduleReason: null,
notes: null,
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(response.status).toBe(201);
expect(response.body.status).toEqual(SUCCESS_STATUS);
if (responseDataIsBooking(response.body.data)) {
const data: BookingOutput_2024_08_13 = response.body.data;
expect(data.bookingFieldsResponses.notes).toBe("");
expect(data.bookingFieldsResponses.rescheduleReason).toBe("");
expect(data.bookingFieldsResponses["favorite-movie"]).toBe("The Matrix");
}
});
});
afterAll(async () => {
@@ -1176,4 +1197,4 @@ describe("Bookings Endpoints 2024-08-13", () => {
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
}
});
});
@@ -0,0 +1,230 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import type {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
GetBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import type { EventType, User } from "@calcom/prisma/client";
describe("Reschedule bookings 2024-08-13", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let schedulesService: SchedulesService_2024_04_15;
let normalBookingUser: User;
let normalBookingEventType: EventType;
let normalBookingEventTypeId: number;
const normalBookingUserEmail = `user-bookings-normal-user-${randomString(5)}@api.com`;
const normalBookingEventTypeSlug = `user-bookings-normal-event-type-${randomString(5)}`;
let oldNormalBooking: BookingOutput_2024_08_13;
let newNormalBooking: BookingOutput_2024_08_13;
const RESCHEDULE_REASON = "Flying to venus that day";
const RESCHEDULED_BY = `user-venus-bookings-rescheduler-${randomString(5)}@api.com`;
beforeAll(async () => {
const moduleRef = await withApiAuth(
normalBookingUserEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
})
)
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
// initialize normal booking user, schedule and event type
normalBookingUser = await userRepositoryFixture.create({
email: normalBookingUserEmail,
username: normalBookingUserEmail,
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: `user-bookings-normal-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(normalBookingUser.id, userSchedule);
normalBookingEventType = await eventTypesRepositoryFixture.create(
{
title: `user-bookings-normal-event-type-${randomString()}`,
slug: normalBookingEventTypeSlug,
length: 60,
},
normalBookingUser.id
);
normalBookingEventTypeId = normalBookingEventType.id;
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
describe("reschedule normal booking", () => {
it("should create a booking to be rescheduled", async () => {
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 1, 9, 10, 0, 0)).toISOString(),
eventTypeId: normalBookingEventTypeId,
attendee: {
name: `user-venus-bookings-attendee-${randomString(10)}`,
email: `user-venus-bookings-attendee-${randomString(10)}@gmail.com`,
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/oj12u83128u9312ou3",
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
expect(bookingResponseBody.data).toBeDefined();
expect(responseDataIsBooking(bookingResponseBody.data)).toBe(true);
if (responseDataIsBooking(bookingResponseBody.data)) {
const created: BookingOutput_2024_08_13 = bookingResponseBody.data;
oldNormalBooking = created;
expect(oldNormalBooking).toBeDefined();
expect(oldNormalBooking.uid).toBeDefined();
} else {
throw new Error("should create a booking to be rescheduled - Invalid response data");
}
});
it("should reschedule", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 1, 9, 12, 0, 0)).toISOString(),
reschedulingReason: RESCHEDULE_REASON,
rescheduledBy: RESCHEDULED_BY,
};
const beforeCreate = new Date();
return request(app.getHttpServer())
.post(`/v2/bookings/${oldNormalBooking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const afterCreate = new Date();
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: BookingOutput_2024_08_13 = responseBody.data as BookingOutput_2024_08_13;
expect(data.reschedulingReason).toEqual(RESCHEDULE_REASON);
expect(data.rescheduledFromUid).toEqual(oldNormalBooking.uid);
expect(data.rescheduledByEmail).toEqual(RESCHEDULED_BY);
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2040, 1, 9, 13, 0, 0)).toISOString());
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(normalBookingUser.id);
expect(data.hosts[0].username).toEqual(normalBookingUser.username);
expect(data.hosts[0].email).toEqual(normalBookingUser.email);
expect(data.status).toEqual(oldNormalBooking.status);
expect(data.duration).toEqual(oldNormalBooking.duration);
expect(data.eventTypeId).toEqual(oldNormalBooking.eventTypeId);
expect(data.attendees[0]).toEqual(oldNormalBooking.attendees[0]);
expect(data.location).toEqual(oldNormalBooking.location);
expect(data.absentHost).toEqual(oldNormalBooking.absentHost);
expect(data.metadata).toEqual(oldNormalBooking.metadata);
newNormalBooking = data;
expect(newNormalBooking).toBeDefined();
const createdAtDate = new Date(data.createdAt);
expect(createdAtDate.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
expect(createdAtDate.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
});
});
it("should fetch old booking and verify rescheduledToUid and rescheduledByEmail and status cancelled", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${oldNormalBooking.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.rescheduledToUid).toEqual(newNormalBooking.uid);
expect(data.rescheduledByEmail).toEqual(RESCHEDULED_BY);
expect(data.status).toEqual("cancelled");
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
it("should fetch new booking and verify rescheduledFromUid, rescheduledByEmail and reschedulingReason", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${newNormalBooking.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.rescheduledFromUid).toEqual(oldNormalBooking.uid);
expect(data.rescheduledByEmail).toEqual(RESCHEDULED_BY);
expect(data.reschedulingReason).toEqual(RESCHEDULE_REASON);
expect(data.status).toEqual("accepted");
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(normalBookingUser.email);
});
function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data !== null && data && "id" in data;
}
});
@@ -79,7 +79,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
const eventTypeRequiresConfirmationSlug = `user-bookings-event-type-requires-confirmation-${randomString()}`;
let createdBooking: BookingOutput_2024_08_13;
let rescheduledBooking: BookingOutput_2024_08_13;
let createdRecurringBooking: RecurringBookingOutput_2024_08_13[];
let bookingInThePast: Booking;
@@ -1360,139 +1359,217 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("reschedule bookings", () => {
it("should reschedule normal booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
const beforeCreate = new Date();
return request(app.getHttpServer())
.post(`/v2/bookings/${createdBooking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const afterCreate = new Date();
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 8, 15, 0, 0)).toISOString());
expect(data.rescheduledFromUid).toEqual(createdBooking.uid);
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.hosts[0].username).toEqual(user.username);
expect(data.hosts[0].email).toEqual(user.email);
expect(data.status).toEqual(createdBooking.status);
expect(data.duration).toEqual(createdBooking.duration);
expect(data.eventTypeId).toEqual(createdBooking.eventTypeId);
expect(data.attendees[0]).toEqual(createdBooking.attendees[0]);
expect(data.location).toEqual(createdBooking.location);
expect(data.absentHost).toEqual(createdBooking.absentHost);
expect(data.metadata).toEqual(createdBooking.metadata);
// When a booking is rescheduled, a new booking is created and the old booking is cancelled.
// We want to make sure the createdAt date of the new booking is between the beforeCreate and afterCreate dates.
const createdAtDate = new Date(data.createdAt);
expect(createdAtDate.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
expect(createdAtDate.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
rescheduledBooking = data;
describe("rescheduling bookings", () => {
describe("cant't reschedule cancelled or rescheduled booking booking", () => {
it("should not be able to reschedule cancelled booking", async () => {
const cancelledBooking = await bookingsRepositoryFixture.create({
status: "CANCELLED",
user: {
connect: {
id: user.id,
},
},
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
title: "peer coding lets goo",
uid: `cancelled-booking-${randomString()}`,
eventType: {
connect: {
id: eventTypeId,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: "Oldie",
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie",
locale: "lv",
timeZone: "Europe/Rome",
},
},
});
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings/${cancelledBooking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
expect(response.body.error.message).toEqual(
`Can't reschedule booking with uid=${cancelledBooking.uid} because it has been cancelled. Please provide uid of a booking that is not cancelled.`
);
await bookingsRepositoryFixture.deleteById(cancelledBooking.id);
});
it("should not be able to reschedule rescheduled booking", async () => {
const rescheduledBooking = await bookingsRepositoryFixture.create({
status: "CANCELLED",
rescheduled: true,
user: {
connect: {
id: user.id,
},
},
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
title: "peer coding lets goo",
uid: `cancelled-booking-${randomString()}`,
eventType: {
connect: {
id: eventTypeId,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: "Oldie",
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie",
locale: "lv",
timeZone: "Europe/Rome",
},
},
});
const newBooking = await bookingsRepositoryFixture.create({
status: "ACCEPTED",
user: {
connect: {
id: user.id,
},
},
fromReschedule: rescheduledBooking.uid,
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
title: "peer coding lets goo",
uid: `new-booking-${randomString()}`,
eventType: {
connect: {
id: eventTypeId,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: "Oldie",
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie",
locale: "lv",
timeZone: "Europe/Rome",
},
},
});
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings/${rescheduledBooking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
expect(response.body.error.message).toEqual(
`Can't reschedule booking with uid=${rescheduledBooking.uid} because it has been cancelled and rescheduled already to booking with uid=${newBooking.uid}. You probably want to reschedule ${newBooking.uid} instead by passing it within the request URL.`
);
await bookingsRepositoryFixture.deleteById(rescheduledBooking.id);
await bookingsRepositoryFixture.deleteById(newBooking.id);
});
});
it("should set rescheduled booking status to cancelled", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${createdBooking.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.status).toEqual("cancelled");
expect(data.rescheduledToUid).toEqual(rescheduledBooking.uid);
});
});
describe("recurring booking", () => {
it("should reschedule recurrence of a recurring booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars again",
};
it("should reschedule recurrence of a recurring booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars again",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${createdRecurringBooking[0].uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data: RecurringBookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.hosts[0].username).toEqual(user.username);
expect(data.hosts[0].email).toEqual(user.email);
expect(data.status).toEqual(createdRecurringBooking[0].status);
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 9, 15, 0, 0)).toISOString());
expect(data.duration).toEqual(createdRecurringBooking[0].duration);
expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid);
expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId);
expect(data.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]);
expect(data.location).toEqual(createdRecurringBooking[0].location);
expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost);
expect(data.metadata).toEqual(createdRecurringBooking[0].metadata);
return request(app.getHttpServer())
.post(`/v2/bookings/${createdRecurringBooking[0].uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data: RecurringBookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.hosts[0].username).toEqual(user.username);
expect(data.hosts[0].email).toEqual(user.email);
expect(data.status).toEqual(createdRecurringBooking[0].status);
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 9, 15, 0, 0)).toISOString());
expect(data.duration).toEqual(createdRecurringBooking[0].duration);
expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid);
expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId);
expect(data.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]);
expect(data.location).toEqual(createdRecurringBooking[0].location);
expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost);
expect(data.metadata).toEqual(createdRecurringBooking[0].metadata);
const oldBooking = await bookingsRepositoryFixture.getByUid(createdRecurringBooking[0].uid);
expect(oldBooking).toBeDefined();
expect(oldBooking?.status).toEqual("CANCELLED");
});
});
const oldBooking = await bookingsRepositoryFixture.getByUid(createdRecurringBooking[0].uid);
expect(oldBooking).toBeDefined();
expect(oldBooking?.status).toEqual("CANCELLED");
});
});
it("should get recurring booking recurrences after rescheduling one", async () => {
const recurringBookingUid = createdRecurringBooking[0].recurringBookingUid;
return request(app.getHttpServer())
.get(`/v2/bookings/${recurringBookingUid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
it("should get recurring booking recurrences after rescheduling one", async () => {
const recurringBookingUid = createdRecurringBooking[0].recurringBookingUid;
return request(app.getHttpServer())
.get(`/v2/bookings/${recurringBookingUid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
if (responseDataIsRecurringBooking(responseBody.data)) {
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
expect(data.length).toEqual(4);
const cancelled = data.find((booking) => booking.status === "cancelled");
expect(cancelled).toBeDefined();
const rescheduledNew = data.find(
(booking) => booking.start === new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString()
);
expect(rescheduledNew).toBeDefined();
createdRecurringBooking = data;
} else {
throw new Error(
"Invalid response data - expected recurring booking but received non array response"
);
}
});
if (responseDataIsRecurringBooking(responseBody.data)) {
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
expect(data.length).toEqual(4);
const cancelled = data.find((booking) => booking.status === "cancelled");
expect(cancelled).toBeDefined();
const rescheduledNew = data.find(
(booking) => booking.start === new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString()
);
expect(rescheduledNew).toBeDefined();
createdRecurringBooking = data;
} else {
throw new Error(
"Invalid response data - expected recurring booking but received non array response"
);
}
});
});
});
});
@@ -2385,95 +2462,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("rescheduling booking with invalid booking responses", () => {
it("should reschedule", async () => {
const email = `user-bookings-attendee-${randomString(10)}@gmail.com`;
const username = `user-bookings-attendee-${randomString(10)}`;
const locale = "it";
const timeZone = "Europe/Rome";
const attendee = await userRepositoryFixture.create({
email,
username,
locale,
timeZone,
});
const booking = await bookingsRepositoryFixture.create({
uid: `booking-uid-${randomString(10)}`,
title: "booking title",
startTime: "2060-09-05T11:00:00.000Z",
endTime: "2060-09-05T12:00:00.000Z",
eventType: {
connect: {
id: eventTypeId,
},
},
status: "ACCEPTED",
metadata: {},
responses: "null",
user: {
connect: {
id: user.id,
},
},
attendees: {
create: {
email: email,
name: username,
timeZone,
locale,
},
},
});
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2060, 0, 9, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${booking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2060, 0, 9, 15, 0, 0)).toISOString());
expect(data.rescheduledFromUid).toEqual(booking.uid);
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.hosts[0].username).toEqual(user.username);
expect(data.hosts[0].email).toEqual(user.email);
expect(data.status).toEqual("accepted");
expect(data.eventTypeId).toEqual(booking.eventTypeId);
expect(data.attendees.length).toEqual(1);
expect(data.attendees[0]).toEqual({
name: attendee.username,
email: attendee.email,
timeZone: attendee.timeZone,
language: attendee.locale,
absent: false,
});
expect(data.bookingFieldsResponses).toEqual({
name: attendee.username,
email: attendee.email,
rescheduledReason: body.reschedulingReason,
guests: [],
});
await bookingsRepositoryFixture.deleteById(data.id);
await userRepositoryFixture.delete(attendee.id);
});
});
});
describe("create booking with attendee sms reminder", () => {
it("should not be able create a booking if attendee sms reminder is missing", async () => {
const body: CreateBookingInput_2024_08_13 = {
@@ -2559,146 +2547,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("cant't reschedule cancelled booking", () => {
it("should not be able to reschedule cancelled booking", async () => {
const cancelledBooking = await bookingsRepositoryFixture.create({
status: "CANCELLED",
user: {
connect: {
id: user.id,
},
},
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
title: "peer coding lets goo",
uid: `cancelled-booking-${randomString()}`,
eventType: {
connect: {
id: eventTypeId,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: "Oldie",
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie",
locale: "lv",
timeZone: "Europe/Rome",
},
},
});
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings/${cancelledBooking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
expect(response.body.error.message).toEqual(
`Can't reschedule booking with uid=${cancelledBooking.uid} because it has been cancelled. Please provide uid of a booking that is not cancelled.`
);
await bookingsRepositoryFixture.deleteById(cancelledBooking.id);
});
it("should not be able to reschedule rescheduled booking", async () => {
const rescheduledBooking = await bookingsRepositoryFixture.create({
status: "CANCELLED",
rescheduled: true,
user: {
connect: {
id: user.id,
},
},
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
title: "peer coding lets goo",
uid: `cancelled-booking-${randomString()}`,
eventType: {
connect: {
id: eventTypeId,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: "Oldie",
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie",
locale: "lv",
timeZone: "Europe/Rome",
},
},
});
const newBooking = await bookingsRepositoryFixture.create({
status: "ACCEPTED",
user: {
connect: {
id: user.id,
},
},
fromReschedule: rescheduledBooking.uid,
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
title: "peer coding lets goo",
uid: `new-booking-${randomString()}`,
eventType: {
connect: {
id: eventTypeId,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: "Oldie",
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie",
locale: "lv",
timeZone: "Europe/Rome",
},
},
});
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings/${rescheduledBooking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
expect(response.body.error.message).toEqual(
`Can't reschedule booking with uid=${rescheduledBooking.uid} because it has been cancelled and rescheduled already to booking with uid=${newBooking.uid}. You probably want to reschedule ${newBooking.uid} instead by passing it within the request URL.`
);
await bookingsRepositoryFixture.deleteById(rescheduledBooking.id);
await bookingsRepositoryFixture.deleteById(newBooking.id);
});
});
describe("cant't cancel already cancelled booking", () => {
it("should not be able to cancel alraedy cancelled booking", async () => {
const cancelledBooking = await bookingsRepositoryFixture.create({
@@ -2752,6 +2600,56 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("cancellation reason and cancelled by returned if null in database", () => {
it("should return cancellation reason and cancelled by if null in database", async () => {
const dbBooking = await bookingsRepositoryFixture.create({
uid: randomString(10),
title: "test",
startTime: new Date(Date.UTC(2040, 0, 13, 16, 0, 0)),
endTime: new Date(Date.UTC(2040, 0, 13, 17, 0, 0)),
eventType: {
connect: {
id: eventTypeId,
},
},
user: {
connect: {
id: user.id,
},
},
metadata: {},
responses: {
name: "tester",
email: "tester@example.com",
guests: [],
},
cancellationReason: null,
cancelledBy: null,
});
const response = await request(app.getHttpServer())
.get(`/v2/bookings/${dbBooking.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200);
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.cancellationReason).toBe("");
expect(data.cancelledByEmail).toBe("");
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
describe("calendar events", () => {
beforeEach(() => {
jest.restoreAllMocks();
@@ -544,12 +544,18 @@ export class InputBookingsService_2024_08_13 {
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
newRequest.body = {
...bodyTransformed,
rescheduledBy: request.body.rescheduledBy,
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
};
} else {
Object.assign(newRequest, { userId, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: false, creationSource: CreationSource.API_V2 };
newRequest.body = {
...bodyTransformed,
noEmail: false,
creationSource: CreationSource.API_V2,
rescheduledBy: request.body.rescheduledBy,
};
}
return newRequest as unknown as BookingRequest;
@@ -25,7 +25,6 @@ import type {
ReassignBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import type { Booking, BookingSeat } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
export const bookingResponsesSchema = z
.object({
@@ -105,14 +104,8 @@ export class OutputBookingsService_2024_08_13 {
);
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
const location = metadata?.videoCallUrl || databaseBooking.location;
const rescheduledToInfo = databaseBooking.rescheduled
? await this.getRescheduledToInfo(databaseBooking.uid)
: undefined;
const rescheduledToUid = rescheduledToInfo?.uid;
const rescheduledByEmail = databaseBooking.rescheduled
? rescheduledToInfo?.rescheduledBy
: databaseBooking.rescheduledBy;
const rescheduledToUid = await this.getRescheduledToUid(databaseBooking);
const rescheduledByEmail = await this.getRescheduledByEmail(databaseBooking);
const booking = {
id: databaseBooking.id,
@@ -121,9 +114,8 @@ export class OutputBookingsService_2024_08_13 {
description: databaseBooking.description,
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason:
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
cancelledByEmail: databaseBooking.status === "CANCELLED" ? databaseBooking.cancelledBy : undefined,
cancellationReason: databaseBooking.cancellationReason || "",
cancelledByEmail: databaseBooking.cancelledBy || "",
reschedulingReason: bookingResponses?.rescheduledReason,
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
start: databaseBooking.startTime,
@@ -168,6 +160,23 @@ export class OutputBookingsService_2024_08_13 {
};
}
private async getRescheduledToUid(databaseBooking: DatabaseBooking) {
if (!databaseBooking.rescheduled) return undefined;
const rescheduledTo = await this.bookingsRepository.getByFromReschedule(databaseBooking.uid);
return rescheduledTo?.uid;
}
private async getRescheduledByEmail(databaseBooking: DatabaseBooking) {
if (databaseBooking.rescheduled) {
return databaseBooking.rescheduledBy;
}
if (databaseBooking.fromReschedule) {
const previousBooking = await this.bookingsRepository.getByUid(databaseBooking.fromReschedule);
return previousBooking?.rescheduledBy ?? databaseBooking.rescheduledBy;
}
return databaseBooking.rescheduledBy;
}
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
if (databaseMetadata === null) return {};
@@ -227,10 +236,8 @@ export class OutputBookingsService_2024_08_13 {
description: databaseBooking.description,
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason:
databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancellationReason : undefined,
cancelledByEmail:
databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancelledBy : undefined,
cancellationReason: databaseBooking.cancellationReason || "",
cancelledByEmail: databaseBooking.cancelledBy || "",
reschedulingReason: bookingResponses?.rescheduledReason,
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
start: databaseBooking.startTime,
@@ -284,14 +291,8 @@ export class OutputBookingsService_2024_08_13 {
const duration = dateEnd.diff(dateStart, "minutes").minutes;
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
const location = metadata?.videoCallUrl || databaseBooking.location;
const rescheduledToInfo = databaseBooking.rescheduled
? await this.getRescheduledToInfo(databaseBooking.uid)
: undefined;
const rescheduledToUid = rescheduledToInfo?.uid;
const rescheduledByEmail = databaseBooking.rescheduled
? rescheduledToInfo?.rescheduledBy
: databaseBooking.rescheduledBy;
const rescheduledToUid = await this.getRescheduledToUid(databaseBooking);
const rescheduledByEmail = await this.getRescheduledByEmail(databaseBooking);
const booking = {
id: databaseBooking.id,
@@ -300,8 +301,7 @@ export class OutputBookingsService_2024_08_13 {
description: databaseBooking.description,
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason:
databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancellationReason : undefined,
cancellationReason: databaseBooking.cancellationReason || "",
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
start: databaseBooking.startTime,
end: databaseBooking.endTime,
@@ -428,8 +428,7 @@ export class OutputBookingsService_2024_08_13 {
description: databaseBooking.description,
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason:
databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancellationReason : undefined,
cancellationReason: databaseBooking.cancellationReason || "",
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
start: databaseBooking.startTime,
end: databaseBooking.endTime,
@@ -263,6 +263,10 @@ export class EventTypesService_2024_06_14 {
return [dynamicEventType];
}
if (authUser?.id) {
return await this.getUserEventTypes(authUser.id);
}
return [];
}
@@ -95,6 +95,8 @@ describe("Me Endpoints", () => {
expect(responseBody.data.id).toEqual(user.id);
expect(responseBody.data.email).toEqual(user.email);
expect(responseBody.data.name).toEqual(user.name);
expect(responseBody.data.avatarUrl).toEqual(user.avatarUrl);
expect(responseBody.data.bio).toEqual(user.bio);
expect(responseBody.data.timeFormat).toEqual(user.timeFormat);
expect(responseBody.data.defaultScheduleId).toEqual(user.defaultScheduleId);
expect(responseBody.data.weekStart).toEqual(user.weekStart);
@@ -117,6 +119,8 @@ describe("Me Endpoints", () => {
expect(responseBody.data.id).toEqual(user.id);
expect(responseBody.data.email).toEqual(user.email);
expect(responseBody.data.avatarUrl).toEqual(user.avatarUrl);
expect(responseBody.data.bio).toEqual(user.bio);
expect(responseBody.data.timeFormat).toEqual(user.timeFormat);
expect(responseBody.data.defaultScheduleId).toEqual(user.defaultScheduleId);
expect(responseBody.data.weekStart).toEqual(user.weekStart);
@@ -151,13 +155,13 @@ describe("Me Endpoints", () => {
});
it("should not update user associated with access token given invalid time format", async () => {
const bodyWithIncorrectTimeFormat: UpdateManagedUserInput = { timeFormat: 100 as any };
const bodyWithIncorrectTimeFormat = { timeFormat: 100 };
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeFormat).expect(400);
});
it("should not update user associated with access token given invalid week start", async () => {
const bodyWithIncorrectWeekStart: UpdateManagedUserInput = { weekStart: "waba luba dub dub" as any };
const bodyWithIncorrectWeekStart = { weekStart: "waba luba dub dub" };
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectWeekStart).expect(400);
});
@@ -27,6 +27,14 @@ export class MeOutput {
@DocsProperty({ type: String, nullable: true })
name!: string | null;
@IsString()
@DocsProperty({ type: String, nullable: true })
avatarUrl!: string | null;
@IsString()
@DocsProperty({ type: String, nullable: true })
bio!: string | null;
@IsInt()
@DocsProperty()
timeFormat!: number;
@@ -10,7 +10,6 @@ import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository"
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
import { AvailableSlotsService } from "@/lib/services/available-slots.service";
import { BusyTimesService } from "@/lib/services/busy-times.service";
import { CacheService } from "@/lib/services/cache.service";
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
import { FilterHostsService } from "@/lib/services/filter-hosts.service";
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
@@ -35,7 +34,6 @@ import { Module } from "@nestjs/common";
PrismaFeaturesRepository,
PrismaMembershipRepository,
CheckBookingLimitsService,
CacheService,
AvailableSlotsService,
UserAvailabilityService,
BusyTimesService,
@@ -4,7 +4,6 @@ import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.rep
import { PrismaHostRepository } from "@/lib/repositories/prisma-host.repository";
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
import { CacheService } from "@/lib/services/cache.service";
import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service";
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
import { HashedLinkService } from "@/lib/services/hashed-link.service";
@@ -22,7 +21,6 @@ import { Module } from "@nestjs/common";
PrismaHostRepository,
PrismaOOORepository,
PrismaUserRepository,
CacheService,
CheckBookingAndDurationLimitsService,
CheckBookingLimitsService,
HashedLinkService,
@@ -8,7 +8,6 @@ import { PrismaSelectedSlotRepository } from "@/lib/repositories/prisma-selected
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
import { BusyTimesService } from "@/lib/services/busy-times.service";
import { CacheService } from "@/lib/services/cache.service";
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
@@ -34,7 +33,6 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
featuresRepository: PrismaFeaturesRepository,
qualifiedHostsService: QualifiedHostsService,
checkBookingLimitsService: CheckBookingLimitsService,
cacheService: CacheService,
userAvailabilityService: UserAvailabilityService,
busyTimesService: BusyTimesService,
noSlotsNotificationService: NoSlotsNotificationService
@@ -50,7 +48,6 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
userRepo: userRepository,
redisClient: redisService,
checkBookingLimitsService,
cacheService,
userAvailabilityService,
busyTimesService,
qualifiedHostsService,
@@ -1,11 +0,0 @@
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
import { Injectable } from "@nestjs/common";
import { CacheService as BaseCacheService } from "@calcom/platform-libraries";
@Injectable()
export class CacheService extends BaseCacheService {
constructor(featuresRepository: PrismaFeaturesRepository) {
super({ featuresRepository });
}
}
@@ -1,6 +1,5 @@
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
import { CacheService } from "@/lib/services/cache.service";
import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service";
import { HashedLinkService } from "@/lib/services/hashed-link.service";
import { LuckyUserService } from "@/lib/services/lucky-user.service";
@@ -13,7 +12,6 @@ import type { PrismaClient } from "@calcom/prisma";
@Injectable()
export class RegularBookingService extends BaseRegularBookingService {
constructor(
cacheService: CacheService,
checkBookingAndDurationLimitsService: CheckBookingAndDurationLimitsService,
prismaWriteService: PrismaWriteService,
bookingRepository: PrismaBookingRepository,
@@ -22,7 +20,6 @@ export class RegularBookingService extends BaseRegularBookingService {
userRepository: PrismaUserRepository
) {
super({
cacheService,
checkBookingAndDurationLimitsService,
prismaClient: prismaWriteService.prisma as unknown as PrismaClient,
bookingRepository,
-44
View File
@@ -1,44 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# Source maps
*.source/*
-36
View File
@@ -1,36 +0,0 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
-9
View File
@@ -1,9 +0,0 @@
import { HomeLayout } from "fumadocs-ui/layouts/home";
import type { ReactNode } from "react";
import { baseOptions } from "../layout.config";
export default function Layout({ children }: { children: ReactNode }) {
// @ts-expect-error weird type check
return <HomeLayout {...baseOptions}>{children}</HomeLayout>;
}
-12
View File
@@ -1,12 +0,0 @@
export default function Page() {
return (
<div className="font-cal -mt-16 flex min-h-screen w-screen items-center justify-center border pt-16 text-center ">
<div>
<h1 className="text-7xl leading-none md:text-8xl">
Open UI for absolutely
<br /> everyone
</h1>
</div>
</div>
);
}
@@ -1,16 +0,0 @@
import { componentSource } from "@/app/source";
import { createSearchAPI } from "fumadocs-core/search/server";
const indexes = [componentSource].flatMap((src) =>
src.getPages().map((page) => ({
title: page.data.title,
description: page.data.description,
structuredData: page.data.structuredData,
id: page.url,
url: page.url,
}))
);
export const { GET } = createSearchAPI("advanced", {
indexes,
});
@@ -1,21 +0,0 @@
import type { PropsWithChildren } from "react";
type Props = {
name: string;
};
export const Icon: React.FC<PropsWithChildren<Props>> = (props) => {
return (
<div className="text-gray-12 flex flex-col items-center justify-center gap-4">
<div className="size-12 border-gray-5 bg-gray-3 flex aspect-square items-center justify-center rounded-lg border ">
{props.children}
</div>
<span className="font-mono text-sm">
{"<"}
{props.name}
{"/>"}
</span>
</div>
);
};
Icon.displayName = "Icon";
@@ -1,57 +0,0 @@
"use client";
import { type PropsWithChildren, useState } from "react";
import className from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
type Props = {
customCodeSnippet?: string;
className?: string;
};
export const RenderComponentWithSnippet: React.FC<PropsWithChildren<Props>> = (props) => {
const [open, setOpen] = useState(false);
// const snippet =
// props.customCodeSnippet ??
// reactElementToJSXString(props.children, {
// showFunctions: true,
// useBooleanShorthandSyntax: true,
// displayName: (node) => {
// // @ts-ignore
// return node?.type.displayName ?? node?.type ?? "Unknown";
// },
// });
return (
<div className="border-gray-6 bg-default rounded-lg border">
<div className={className("not-prose p-8 xl:p-12", props.className)}>{props.children}</div>
<div className="bg-gray-3 border-gray-6 flex items-center justify-start border-b border-t p-2">
<Button
color="minimal"
onClick={() => {
setOpen(!open);
}}>
Code (WIP)
</Button>
</div>
<div
className={className(
"bg-gray-2 max-h-96 w-full overflow-y-scroll transition-all",
open ? "block" : "hidden"
)}>
<div className="flex items-start">
{/* <pre className="py-0 text-gray-8 text-right">
{snippet
.split("\n")
.map((_line, i) => i + 1)
.join("\n")}
</pre>
<pre className="py-0 text-gray-12 w-full">{snippet}</pre> */}
</div>
</div>
</div>
);
};
@@ -1,7 +0,0 @@
import type { PropsWithChildren } from "react";
export const Row: React.FC<PropsWithChildren> = (props) => {
return <div className="flex w-full items-center justify-between gap-8">{props.children}</div>;
};
Row.displayName = "Row";
@@ -1,56 +0,0 @@
import { componentSource } from "@/app/source";
import { TooltipProvider } from "@radix-ui/react-tooltip";
import { createTypeTable } from "fumadocs-typescript/ui";
import defaultMdxComponents from "fumadocs-ui/mdx";
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/page";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { Toaster } from "react-hot-toast";
export default async function Page(props: { params: Promise<{ slug?: string[] }> }) {
const params = await props.params;
const page = componentSource.getPage(params.slug);
if (!page) {
notFound();
}
const { AutoTypeTable } = createTypeTable();
const MDX = page.data.body;
return (
<>
{/* @ts-expect-error idk why fumadocs doersnt like this */}
<DocsPage toc={page.data.toc} full={page.data.full}>
<Toaster />
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<TooltipProvider>
{/* @ts-expect-error idk why fumadocs doersnt like this */}
<MDX components={{ ...defaultMdxComponents, AutoTypeTable }} />
</TooltipProvider>
</DocsBody>
</DocsPage>
</>
);
}
export async function generateStaticParams() {
return componentSource.generateParams();
}
export async function generateMetadata({ params }: { params: Promise<{ slug?: string[] }> }) {
const page = componentSource.getPage((await params).slug);
if (!page) {
notFound();
}
return {
title: page.data.title,
description: page.data.description,
} satisfies Metadata;
}
-17
View File
@@ -1,17 +0,0 @@
import { componentSource } from "@/app/source";
import { TooltipProvider } from "@radix-ui/react-tooltip";
import { DocsLayout } from "fumadocs-ui/layouts/notebook";
import type { ReactNode } from "react";
import { IconSprites } from "@calcom/ui/components/icon";
import { baseOptions } from "../layout.config";
export default function Layout({ children }: { children: ReactNode }) {
return (
<DocsLayout tree={componentSource.pageTree} {...baseOptions}>
<TooltipProvider>{children}</TooltipProvider>
<IconSprites />
</DocsLayout>
);
}
File diff suppressed because it is too large Load Diff
-26
View File
@@ -1,26 +0,0 @@
import type { HomeLayoutProps } from "fumadocs-ui/layouts/home";
/**
* Shared layout configurations
*
* you can configure layouts individually from:
* Home Layout: app/(home)/layout.tsx
* Docs Layout: app/docs/layout.tsx
*/
export const baseOptions: HomeLayoutProps = {
nav: {
title: "Cal.com",
},
githubUrl: "https://github.com/calcom/cal.com",
links: [
{
text: "Design",
url: "/design",
active: "nested-url",
},
{
text: "GitHub",
url: "https://github.com/calcom/cal.com",
},
],
};
-36
View File
@@ -1,36 +0,0 @@
import { RootProvider } from "fumadocs-ui/provider";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./global.css";
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<head>
<style>{`
:root {
--font-inter: ${inter.style.fontFamily.replace(/\'/g, "")};
}
`}</style>
</head>
<body className={`${inter.variable} antialiased`}>
<RootProvider>{children}</RootProvider>
</body>
</html>
);
}
-13
View File
@@ -1,13 +0,0 @@
import { components, docs, meta } from "@/.source";
import { loader } from "fumadocs-core/source";
import { createMDXSource } from "fumadocs-mdx";
export const source = loader({
baseUrl: "/docs",
source: createMDXSource(docs, meta),
});
export const componentSource = loader({
baseUrl: "/design",
source: createMDXSource(components, []),
});
@@ -1,19 +0,0 @@
import { Toaster } from "react-hot-toast";
import { Status } from "./Status";
export function MDXLayout({ children, frontmatter }: { children: React.ReactNode; frontmatter: any }) {
return (
<div className="prose max-w-none">
<Toaster />
{frontmatter.airtableId && (
<Status
airtableId={frontmatter.airtableId}
designStatus={frontmatter.designStatus}
devStatus={frontmatter.devStatus}
/>
)}
{children}
</div>
);
}
@@ -1,65 +0,0 @@
"use client";
import React from "react";
import { Toaster } from "react-hot-toast";
import { showToast } from "@calcom/ui/components/toast";
interface BorderRadiusToken {
name: string;
value: string;
className: string;
}
interface BorderRadiusTableProps {
tokens: BorderRadiusToken[];
}
const tokens: BorderRadiusToken[] = [
{ name: "None", value: "0px", className: "rounded-none" },
{ name: "Small", value: "0.125rem", className: "rounded-sm" },
{ name: "Default", value: "0.25rem", className: "rounded" },
{ name: "Medium", value: "0.375rem", className: "rounded-md" },
{ name: "Large", value: "0.5rem", className: "rounded-lg" },
{ name: "XLarge", value: "0.75rem", className: "rounded-xl" },
{ name: "2XLarge", value: "1rem", className: "rounded-2xl" },
{ name: "3XLarge", value: "1.5rem", className: "rounded-3xl" },
{ name: "Full", value: "9999px", className: "rounded-full" },
];
export const BorderRadiusTable: React.FC<BorderRadiusTableProps> = () => {
const handleCopy = async (value: string) => {
try {
await navigator.clipboard.writeText(value);
showToast(`Copied ${value}`, "success");
} catch (error) {
console.error("Failed to copy:", error);
showToast("Failed to copy", "error");
}
};
return (
<>
<Toaster position="bottom-right" />
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{tokens.map((token) => (
<div
key={token.name}
onClick={() => handleCopy(token.className)}
className="border-subtle bg-default hover:bg-subtle group relative cursor-pointer overflow-hidden rounded-lg border p-4">
<div className="space-y-3">
<div className="flex items-center justify-between">
<div>
<p className="text-emphasis text-sm font-medium">{token.name}</p>
<p className="text-subtle mt-1 text-xs">{token.className}</p>
</div>
<p className="text-subtle text-sm">{token.value}</p>
</div>
<div className={`bg-emphasis h-16 w-full ${token.className}`} />
</div>
</div>
))}
</div>
</>
);
};
@@ -1,372 +0,0 @@
"use client";
import React, { useState, useMemo } from "react";
import { Toaster } from "react-hot-toast";
import { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
type IconName =
| "activity"
| "arrow-down"
| "arrow-left"
| "arrow-right"
| "arrow-up-right"
| "arrow-up"
| "asterisk"
| "at-sign"
| "atom"
| "badge-check"
| "ban"
| "bell"
| "binary"
| "blocks"
| "bold"
| "book"
| "book-open-check"
| "book-open"
| "book-user"
| "building"
| "calendar-check-2"
| "calendar-days"
| "calendar-heart"
| "calendar-range"
| "calendar-search"
| "calendar-x-2"
| "calendar"
| "chart-bar"
| "chart-line"
| "check-check"
| "check"
| "chevron-down"
| "chevron-left"
| "chevron-right"
| "chevron-up"
| "chevrons-down-up"
| "chevrons-left"
| "chevrons-right"
| "chevrons-up-down"
| "circle-alert"
| "circle-arrow-up"
| "circle-check-big"
| "circle-check"
| "circle-help"
| "circle-x"
| "circle"
| "clipboard-check"
| "clipboard"
| "clock"
| "code"
| "columns-3"
| "command"
| "contact"
| "copy"
| "corner-down-left"
| "corner-down-right"
| "credit-card"
| "disc"
| "dot"
| "download"
| "ellipsis-vertical"
| "ellipsis"
| "external-link"
| "eye-off"
| "eye"
| "file-down"
| "file-text"
| "file"
| "filter"
| "fingerprint"
| "flag"
| "folder"
| "gift"
| "git-merge"
| "github"
| "globe"
| "grid-3x3"
| "handshake"
| "info"
| "italic"
| "key"
| "layers"
| "layout-dashboard"
| "link-2"
| "link"
| "loader"
| "lock-open"
| "lock"
| "log-out"
| "mail-open"
| "mail"
| "map-pin"
| "map"
| "menu"
| "message-circle"
| "messages-square"
| "moon"
| "paintbrush"
| "paperclip"
| "pencil"
| "phone-call"
| "phone"
| "plus"
| "refresh-ccw"
| "refresh-cw"
| "repeat"
| "rocket"
| "rotate-ccw"
| "rotate-cw"
| "search"
| "send"
| "settings"
| "share-2"
| "shield-check"
| "shield"
| "shuffle"
| "sliders-horizontal"
| "sliders-vertical"
| "smartphone"
| "sparkles"
| "square-check"
| "star"
| "sun"
| "sunrise"
| "sunset"
| "tags"
| "terminal"
| "trash-2"
| "trash"
| "trello"
| "triangle-alert"
| "upload"
| "user-check"
| "user-plus"
| "user-x"
| "user"
| "users"
| "venetian-mask"
| "video"
| "waypoints"
| "webhook"
| "x"
| "zap";
interface IconGridProps {
className?: string;
}
interface CopyMenuProps {
name: IconName;
onCopy: (format: string) => void;
}
const CopyMenu: React.FC<CopyMenuProps> = ({ name, onCopy }) => {
return (
<div className="absolute inset-2 flex items-center justify-center bg-black/10 opacity-0 transition-opacity group-hover:opacity-100">
<div className="bg-default space-y-1 rounded-md p-2 shadow-lg">
<button
onClick={() => onCopy(`<Icon name="${name}" className="h-4 w-4" />`)}
className="hover:bg-subtle w-full rounded px-3 py-1 text-left text-sm">
Copy Component
</button>
</div>
</div>
);
};
export const IconGrid: React.FC<IconGridProps> = ({ className }) => {
const [searchQuery, setSearchQuery] = useState("");
const handleCopy = async (value: string) => {
try {
await navigator.clipboard.writeText(value);
showToast(`Copied ${value}`, "success");
} catch (error) {
console.error("Failed to copy:", error);
showToast("Failed to copy", "error");
}
};
const icons: IconName[] = [
"activity",
"arrow-down",
"arrow-left",
"arrow-right",
"arrow-up-right",
"arrow-up",
"asterisk",
"at-sign",
"atom",
"badge-check",
"ban",
"bell",
"binary",
"blocks",
"bold",
"book-open-check",
"book-open",
"book-user",
"building",
"calendar-check-2",
"calendar-days",
"calendar-heart",
"calendar-range",
"calendar-search",
"calendar-x-2",
"calendar",
"chart-bar",
"chart-line",
"check-check",
"check",
"chevron-down",
"chevron-left",
"chevron-right",
"chevron-up",
"chevrons-down-up",
"chevrons-left",
"chevrons-right",
"chevrons-up-down",
"circle-alert",
"circle-arrow-up",
"circle-check-big",
"circle-check",
"circle-help",
"circle-x",
"circle",
"clipboard-check",
"clipboard",
"clock",
"code",
"columns-3",
"command",
"contact",
"copy",
"corner-down-left",
"corner-down-right",
"credit-card",
"disc",
"dot",
"download",
"ellipsis-vertical",
"ellipsis",
"external-link",
"eye-off",
"eye",
"file-down",
"file-text",
"file",
"filter",
"fingerprint",
"flag",
"folder",
"gift",
"git-merge",
"github",
"globe",
"grid-3x3",
"handshake",
"info",
"italic",
"key",
"layers",
"layout-dashboard",
"link-2",
"link",
"loader",
"lock-open",
"lock",
"log-out",
"mail-open",
"mail",
"map-pin",
"map",
"menu",
"message-circle",
"messages-square",
"moon",
"paintbrush",
"paperclip",
"pencil",
"phone-call",
"phone",
"plus",
"refresh-ccw",
"refresh-cw",
"repeat",
"rocket",
"rotate-ccw",
"rotate-cw",
"search",
"send",
"settings",
"share-2",
"shield-check",
"shield",
"shuffle",
"sliders-horizontal",
"sliders-vertical",
"smartphone",
"sparkles",
"square-check",
"star",
"sun",
"sunrise",
"sunset",
"tags",
"terminal",
"trash-2",
"trash",
"trello",
"triangle-alert",
"upload",
"user-check",
"user-plus",
"user-x",
"user",
"users",
"venetian-mask",
"video",
"waypoints",
"webhook",
"x",
"zap",
];
const filteredIcons = useMemo(() => {
if (!searchQuery) return icons;
const query = searchQuery.toLowerCase();
return icons.filter((icon) => icon.toLowerCase().includes(query));
}, [searchQuery]);
return (
<>
<Toaster position="bottom-right" />
<div className={className}>
<div className="mb-8">
<input
type="text"
placeholder="Search icons..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="border-subtle bg-default text-emphasis focus:ring-emphasis placeholder:text-subtle w-full rounded-md border px-4 py-2 focus:outline-none focus:ring-2"
/>
</div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6">
{filteredIcons.map((iconName) => (
<div
key={iconName}
className="border-subtle bg-default group relative overflow-hidden rounded-lg border p-4">
<div className="flex flex-col items-center space-y-2">
<div className="bg-subtle flex h-12 w-12 items-center justify-center rounded-md">
<Icon name={iconName} className="text-emphasis h-6 w-6" />
</div>
<p className="text-emphasis text-center text-sm">{iconName}</p>
</div>
<CopyMenu name={iconName} onCopy={handleCopy} />
</div>
))}
</div>
</div>
</>
);
};
@@ -1,126 +0,0 @@
"use client";
import React from "react";
import { Toaster } from "react-hot-toast";
import { showToast } from "@calcom/ui/components/toast";
interface ShadowToken {
name: string;
className: string;
description: string;
}
interface ShadowTableProps {
tokens: ShadowToken[];
}
const tokens: ShadowToken[] = [
{
name: "Dropdown",
className: "shadow-dropdown",
description: "Used for dropdown menus and popovers",
},
{
name: "Solid Gray Rested",
className: "shadow-solid-gray-rested",
description: "Default state for solid gray buttons",
},
{
name: "Solid Gray Hover",
className: "shadow-solid-gray-hover",
description: "Hover state for solid gray buttons",
},
{
name: "Solid Gray Active",
className: "shadow-solid-gray-active",
description: "Active/pressed state for solid gray buttons",
},
{
name: "Outline Gray Rested",
className: "shadow-outline-gray-rested",
description: "Default state for outline gray buttons",
},
{
name: "Outline Gray Hover",
className: "shadow-outline-gray-hover",
description: "Hover state for outline gray buttons",
},
{
name: "Outline Gray Active",
className: "shadow-outline-gray-active",
description: "Active/pressed state for outline gray buttons",
},
{
name: "Outline Red Rested",
className: "shadow-outline-red-rested",
description: "Default state for outline red buttons",
},
{
name: "Outline Red Hover",
className: "shadow-outline-red-hover",
description: "Hover state for outline red buttons",
},
{
name: "Outline Red Active",
className: "shadow-outline-red-active",
description: "Active/pressed state for outline red buttons",
},
{
name: "Elevation Low",
className: "shadow-elevation-low",
description: "Subtle elevation for UI elements",
},
{
name: "Button Solid Brand Default",
className: "shadow-button-solid-brand-default",
description: "Default state for solid brand buttons",
},
{
name: "Button Solid Brand Hover",
className: "shadow-button-solid-brand-hover",
description: "Hover state for solid brand buttons",
},
{
name: "Button Solid Brand Active",
className: "shadow-button-solid-brand-active",
description: "Active/pressed state for solid brand buttons",
},
];
export const ShadowTable: React.FC<ShadowTableProps> = () => {
const handleCopy = async (value: string) => {
try {
await navigator.clipboard.writeText(value);
showToast(`Copied ${value}`, "success");
} catch (error) {
console.error("Failed to copy:", error);
showToast("Failed to copy", "error");
}
};
return (
<>
<Toaster position="bottom-right" />
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
{tokens.map((token) => (
<div
key={token.name}
onClick={() => handleCopy(token.className)}
className="group relative cursor-pointer">
<div
className={`border-subtle bg-default hover:bg-subtle relative overflow-hidden rounded-lg border p-4 transition-colors ${token.className}`}>
<div className="space-y-2">
<div>
<p className="text-emphasis text-sm font-medium">{token.name}</p>
<p className="text-subtle mt-1 text-xs">{token.className}</p>
</div>
<p className="text-subtle text-sm">{token.description}</p>
</div>
</div>
</div>
))}
</div>
</>
);
};
@@ -1,45 +0,0 @@
"use client";
import React from "react";
import { Toaster } from "react-hot-toast";
interface SpacingToken {
name: string;
value: string;
pixels: string;
}
interface SpacingTableProps {
tokens: SpacingToken[];
}
export const SpacingTable: React.FC<SpacingTableProps> = ({ tokens }) => {
return (
<>
<Toaster position="bottom-right" />
<div className="space-y-6">
<div className="grid grid-cols-3 gap-4">
{tokens.map((token, index) => (
<div
key={index}
className="border-subtle bg-default group relative overflow-hidden rounded-lg border">
<div className="flex items-center gap-4 p-4">
<div className="w-[100px] space-y-1">
<p className="text-emphasis text-sm font-medium">
{token.name === "px" ? "px" : `${token.name} (${token.pixels})`}
</p>
</div>
<div className="bg-subtle relative h-8 rounded">
<div
className="absolute left-0 top-1/2 h-2 -translate-y-1/2 bg-red-500"
style={{ width: token.pixels }}
/>
</div>
</div>
</div>
))}
</div>
</div>
</>
);
};
@@ -1,103 +0,0 @@
"use client";
import { getColorFormats, copyToClipboard } from "@/lib/colorUtils";
import React from "react";
import { Toaster } from "react-hot-toast";
import { showToast } from "@calcom/ui/components/toast";
interface ColorSwatch {
name: string;
value: string;
className: string;
}
interface ColorSection {
title: string;
description?: string;
colors: ColorSwatch[];
}
interface TypeTableProps {
sections: ColorSection[];
}
interface CopyMenuProps {
color: string;
className: string;
onCopy: (format: string) => void;
}
const CopyMenu: React.FC<CopyMenuProps> = ({ color, className, onCopy }) => {
const formats = getColorFormats(color, className);
if (!formats) return null;
return (
<div className="absolute inset-2 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100">
<div className="bg-default space-y-1 rounded-md p-2 shadow-lg">
<button
onClick={() => onCopy(formats.hex)}
className="hover:bg-subtle w-full rounded px-3 py-1 text-left text-sm">
Copy as HEX
</button>
<button
onClick={() => onCopy(formats.rgb)}
className="hover:bg-subtle w-full rounded px-3 py-1 text-left text-sm">
Copy as RGB
</button>
<button
onClick={() => onCopy(formats.hsl)}
className="hover:bg-subtle w-full rounded px-3 py-1 text-left text-sm">
Copy as HSL
</button>
<button
onClick={() => onCopy(formats.tailwind)}
className="hover:bg-subtle w-full rounded px-3 py-1 text-left text-sm">
Copy Tailwind Class
</button>
</div>
</div>
);
};
export const TypeTable: React.FC<TypeTableProps> = ({ sections }) => {
const handleCopy = async (colorValue: string) => {
try {
await copyToClipboard(colorValue);
showToast(`Copied ${colorValue}`, "success");
} catch (error) {
console.error("Failed to copy color:", error);
showToast("Failed to copy color", "error");
}
};
return (
<>
<Toaster position="bottom-right" />
<div className="space-y-8">
{sections.map((section, sectionIndex) => (
<div key={sectionIndex} className="space-y-4">
<div>
<h3 className="text-emphasis text-lg font-semibold">{section.title}</h3>
{section.description && <p className="text-subtle text-sm">{section.description}</p>}
</div>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{section.colors.map((color, colorIndex) => (
<div key={colorIndex} className="border-subtle bg-default overflow-hidden rounded-lg border">
<div className="group relative">
<div className={`h-36 w-full ${color.className}`} title={color.value} />
<CopyMenu color={color.value} className={color.className} onCopy={handleCopy} />
</div>
<div className="space-y-1 p-2">
<p className="text-emphasis text-sm font-medium">{color.name}</p>
<p className="text-subtle text-xs">{color.value}</p>
</div>
</div>
))}
</div>
</div>
))}
</div>
</>
);
};
@@ -1,109 +0,0 @@
"use client";
import React from "react";
import { Toaster } from "react-hot-toast";
import { showToast } from "@calcom/ui/components/toast";
interface TypographyStyle {
name: string;
className: string;
specs: {
fontFamily: string;
fontSize: string;
lineHeight: string;
weight: string;
};
category?: string;
}
interface TypographySection {
title: string;
description?: string;
styles: TypographyStyle[];
}
interface TypographyTableProps {
sections: TypographySection[];
}
interface CopyMenuProps {
style: TypographyStyle;
onCopy: (format: string) => void;
}
const CopyMenu: React.FC<CopyMenuProps> = ({ style, onCopy }) => {
return (
<div className="absolute inset-2 flex items-center justify-center bg-black/10 opacity-0 transition-opacity group-hover:opacity-100">
<div className="bg-default space-y-1 rounded-md p-2 shadow-lg">
<button
onClick={() => onCopy(style.className)}
className="hover:bg-subtle w-full rounded px-3 py-1 text-left text-sm">
Copy Tailwind Class
</button>
<button
onClick={() =>
onCopy(`font-family: ${style.specs.fontFamily};
font-size: ${style.specs.fontSize}px;
line-height: ${style.specs.lineHeight};
font-weight: ${style.specs.weight};`)
}
className="hover:bg-subtle w-full rounded px-3 py-1 text-left text-sm">
Copy CSS
</button>
</div>
</div>
);
};
export const TypographyTable: React.FC<TypographyTableProps> = ({ sections }) => {
const handleCopy = async (value: string) => {
try {
await navigator.clipboard.writeText(value);
showToast(`Copied to clipboard`, "success");
} catch (error) {
console.error("Failed to copy:", error);
showToast("Failed to copy", "error");
}
};
return (
<>
<Toaster position="bottom-right" />
<div className="space-y-12">
{sections.map((section, sectionIndex) => (
<div key={sectionIndex} className="space-y-6">
<div>
<h3 className="text-emphasis text-2xl font-semibold">{section.title}</h3>
{section.description && <p className="text-subtle mt-2 text-base">{section.description}</p>}
</div>
<div className="grid gap-6">
{section.styles.map((style, styleIndex) => (
<div
key={styleIndex}
className="border-subtle bg-default group relative overflow-hidden rounded-lg border p-6">
<div className="flex items-start justify-between">
<div className="space-y-4">
<div>
<p className="text-emphasis text-sm font-medium">{style.name}</p>
<p className="text-subtle mt-1 text-xs">{style.className}</p>
</div>
<div className={style.className}>The quick brown fox jumps over the lazy dog</div>
</div>
<div className="text-subtle space-y-2 text-sm">
<p>Font: {style.specs.fontFamily}</p>
<p>Size: {style.specs.fontSize}px</p>
<p>Line Height: {style.specs.lineHeight}</p>
<p>Weight: {style.specs.weight}</p>
</div>
</div>
<CopyMenu style={style} onCopy={handleCopy} />
</div>
))}
</div>
</div>
))}
</div>
</>
);
};
@@ -1,32 +0,0 @@
---
title: Border Radius
description: Cal.com's border radius system
---
import { BorderRadiusTable } from '@/components/ui/BorderRadiusTable';
# Border Radius
Our design system uses a consistent set of border radius values to create a cohesive and modern interface. The border radius scale ranges from none (sharp corners) to full (completely rounded).
## Usage
```tsx
// No border radius
<div className="rounded-none">Sharp corners</div>
// Default radius
<div className="rounded">Default rounded corners</div>
// Larger radius
<div className="rounded-lg">Larger rounded corners</div>
// Fully rounded (useful for pills, badges)
<div className="rounded-full">Completely rounded</div>
```
## Border Radius Scale
Click on any radius to copy its Tailwind class name. Each preview shows how the radius appears when applied.
<BorderRadiusTable />
@@ -1,144 +0,0 @@
---
title: Colors
description: Cal.com Color System Documentation
---
import { TypeTable } from '@/components/ui/TypeTable';
export const backgroundTokens = {
title: 'Background Tokens',
colors: [
{ name: 'emphasis', value: 'var(--cal-bg-emphasis)', className: 'bg-emphasis' },
{ name: 'default', value: 'var(--cal-bg, white)', className: 'bg-default' },
{ name: 'subtle', value: 'var(--cal-bg-subtle)', className: 'bg-subtle' },
{ name: 'muted', value: 'var(--cal-bg-muted)', className: 'bg-muted' },
{ name: 'inverted', value: 'var(--cal-bg-inverted)', className: 'bg-inverted' },
],
};
export const primaryBackgroundTokens = {
title: 'Primary Background Tokens',
colors: [
{ name: 'primary.default', value: 'var(--cal-bg-primary)', className: 'bg-primary-default' },
{ name: 'primary.emphasis', value: 'var(--cal-bg-primary-emphasis)', className: 'bg-primary-emphasis' },
{ name: 'primary.muted', value: 'var(--cal-bg-primary-muted)', className: 'bg-primary-muted' },
],
};
export const brandTokens = {
title: 'Brand Colors',
colors: [
{ name: 'brand.default', value: 'var(--cal-brand,#111827)', className: 'bg-brand-default' },
{ name: 'brand.emphasis', value: 'var(--cal-brand-emphasis,#101010)', className: 'bg-brand-emphasis' },
{ name: 'brand.subtle', value: 'var(--cal-brand-subtle,#9CA3AF)', className: 'bg-brand-subtle' },
{ name: 'brand.accent', value: 'var(--cal-brand-accent,white)', className: 'bg-brand-accent' },
],
};
export const semanticBackgroundTokens = {
title: 'Semantic Background Tokens',
colors: [
{ name: 'semantic.info.subtle', value: 'var(--cal-bg-semantic-info-subtle)', className: 'bg-semantic-info-subtle' },
{ name: 'semantic.info.emphasis', value: 'var(--cal-bg-semantic-info-emphasis)', className: 'bg-semantic-info-emphasis' },
{ name: 'semantic.success.subtle', value: 'var(--cal-bg-semantic-success-subtle)', className: 'bg-semantic-success-subtle' },
{ name: 'semantic.success.emphasis', value: 'var(--cal-bg-semantic-success-emphasis)', className: 'bg-semantic-success-emphasis' },
{ name: 'semantic.attention.subtle', value: 'var(--cal-bg-semantic-attention-subtle)', className: 'bg-semantic-attention-subtle' },
{ name: 'semantic.attention.emphasis', value: 'var(--cal-bg-semantic-attention-emphasis)', className: 'bg-semantic-attention-emphasis' },
{ name: 'semantic.error.subtle', value: 'var(--cal-bg-semantic-error-subtle)', className: 'bg-semantic-error-subtle' },
{ name: 'semantic.error.emphasis', value: 'var(--cal-bg-semantic-error-emphasis)', className: 'bg-semantic-error-emphasis' },
],
};
export const borderTokens = {
title: 'Border Tokens',
colors: [
{ name: 'border.emphasis', value: 'var(--cal-border-emphasis)', className: 'border-emphasis' },
{ name: 'border.default', value: 'var(--cal-border)', className: 'border-default' },
{ name: 'border.subtle', value: 'var(--cal-border-subtle)', className: 'border-subtle' },
{ name: 'border.muted', value: 'var(--cal-border-muted)', className: 'border-muted' },
{ name: 'border.semantic.error', value: 'var(--cal-border-semantic-error)', className: 'border-semantic-error' },
],
};
export const textTokens = {
title: 'Text Tokens',
description: 'Semantic text colors for various purposes',
colors: [
{ name: 'text.emphasis', value: 'var(--cal-text-emphasis)', className: 'text-emphasis' },
{ name: 'text.default', value: 'var(--cal-text)', className: 'text-default' },
{ name: 'text.subtle', value: 'var(--cal-text-subtle)', className: 'text-subtle' },
{ name: 'text.muted', value: 'var(--cal-text-muted)', className: 'text-muted' },
{ name: 'text.inverted', value: 'var(--cal-text-inverted)', className: 'text-inverted' },
],
};
# Color System
Our color system is built on semantic tokens that provide consistent theming across light and dark modes. The system is organized into five main categories:
1. Background Colors
2. Primary Colors
3. Brand Colors
4. Semantic Colors
5. Text & Border Colors
## Usage Guidelines
1. **Use Semantic Tokens**: Always prefer semantic tokens over direct color values to maintain consistency and theme support.
2. **Dark Mode Support**: Our semantic tokens automatically handle dark mode transitions.
3. **Component Design**: When creating new components, follow the semantic token system:
- Use `bg-*` tokens for backgrounds
- Use `text-*` tokens for text content
- Use `border-*` tokens for borders
4. **Semantic States**: Use semantic color variations to convey meaning:
- Info: For informational UI elements
- Success: For successful actions and positive states
- Attention: For warnings and important notices
- Error: For error states and destructive actions
5. **Brand Colors**: Use brand colors sparingly and primarily for:
- Primary CTAs
- Brand identity elements
- Key interactive components
## Using Semantic Tokens
Our color tokens are CSS variables that automatically handle light and dark mode transitions. Here's how to use them:
### Background Colors
```tsx
// Basic backgrounds
<div className="bg-default">Default background</div>
<div className="bg-subtle">Subtle background</div>
<div className="bg-emphasis">Emphasis background</div>
// Primary backgrounds
<div className="bg-primary-default">Primary background</div>
<div className="bg-primary-emphasis">Primary emphasis</div>
```
### Semantic Colors
```tsx
// Info states
<div className="bg-semantic-info-subtle">Info subtle</div>
<div className="bg-semantic-info-emphasis">Info emphasis</div>
// Success states
<div className="bg-semantic-success-subtle">Success subtle</div>
<div className="bg-semantic-success-emphasis">Success emphasis</div>
// Error states
<div className="bg-semantic-error-subtle">Error subtle</div>
<div className="bg-semantic-error-emphasis">Error emphasis</div>
```
### Text Colors
```tsx
<p className="text-emphasis">High emphasis text</p>
<p className="text-default">Default text</p>
<p className="text-subtle">Subtle text</p>
<p className="text-muted">Muted text</p>
```
<TypeTable sections={[backgroundTokens, primaryBackgroundTokens, brandTokens, semanticBackgroundTokens, borderTokens, textTokens]} />
@@ -1,106 +0,0 @@
---
title: MultiOptionInput
---
import { MultiOptionInput } from "@calcom/ui/components/form"
import { RenderComponentWithSnippet } from "@/app/components/render"
import { Row } from "@/app/components/row"
import { StatesExample } from "./multiInputField.states"
import { PasteExample } from "./multiInputField.paste"
import { CustomizationExample } from "./multiInputField.customization"
## Overview
The MultiOptionInput component is a flexible form input that allows users to manage a list of options with features like:
- Adding and removing options
- Reordering options with move up/down buttons
- Pasting multiple options with configurable delimiters
- Minimum options requirement
- Customizable placeholders and labels
- Key-value pair mode for environment variables, configuration settings, etc.
## States
MultiOptionInput in different states: default, disabled, and with minimum options.
<StatesExample/>
## Paste Support
Demonstrates the paste functionality with different delimiters. Users can paste comma-separated values, newline-separated values, or use custom delimiters.
In key-value mode, the component can also parse key-value pairs from pasted text. For example:
- `KEY=value`
- `NODE_ENV:production`
<PasteExample/>
## Customization
Examples of different customization options like placeholders, move buttons, remove buttons, and key-value pairs.
<CustomizationExample/>
## Usage
### Standard Options
```tsx
import { MultiOptionInput } from "@calcom/ui/components/form/inputs/MultiOptionInput";
import { useForm, FormProvider } from "react-hook-form";
type FormValues = {
options: Array<{ label: string; id: string }>;
};
const MyForm = () => {
const methods = useForm<FormValues>();
return (
<FormProvider {...methods}>
<form>
<MultiOptionInput<FormValues>
fieldArrayName="options"
optionPlaceholders={["Option 1", "Option 2"]}
defaultNumberOfOptions={2}
pasteDelimiters={["\n", ","]} // Optional
showMoveButtons={true} // Optional
minOptions={1} // Optional
addOptionLabel="Add another option" // Optional
/>
</form>
</FormProvider>
);
};
```
### Key-Value Pairs
```tsx
import { MultiOptionInput } from "@calcom/ui/components/form/inputs/MultiOptionInput";
import { useForm, FormProvider } from "react-hook-form";
type FormValues = {
envVars: Array<{ label: string; value: string; id: string }>;
};
const EnvVarsForm = () => {
const methods = useForm<FormValues>();
return (
<FormProvider {...methods}>
<form>
<MultiOptionInput<FormValues>
fieldArrayName="envVars"
keyValueMode={true}
keyLabel="Environment Variable"
valueLabel="Value"
optionPlaceholders={["NODE_ENV", "PORT"]}
valuePlaceholders={["production", "3000"]}
keyValueDelimiters={[":", "="]} // Optional, defaults to [":", "="]
defaultNumberOfOptions={2}
/>
</form>
</FormProvider>
);
};
```
@@ -1,28 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Alert } from "@calcom/ui/components/alert";
const severities = ["neutral", "info", "warning", "error"] as const;
export const ActionsExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="no-prose space-y-4">
{severities.map((severity) => (
<Alert
key={severity}
severity={severity}
title={`${severity.charAt(0).toUpperCase() + severity.slice(1)} Alert with Actions`}
message={`This is a ${severity} alert with action buttons.`}
actions={
<div className="flex space-x-2">
<button className="rounded-md px-3 py-2 text-sm font-medium hover:opacity-90">Dismiss</button>
<button className="rounded-md px-3 py-2 text-sm font-medium hover:opacity-90">View</button>
</div>
}
/>
))}
</div>
</RenderComponentWithSnippet>
);
@@ -1,22 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Alert } from "@calcom/ui/components/alert";
const severities = ["neutral", "info", "warning", "error"] as const;
export const BasicExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="not-prose space-y-4">
{severities.map((severity) => (
<Alert
key={severity}
severity={severity}
title={`${severity.charAt(0).toUpperCase() + severity.slice(1)} Alert`}
message={`This is a ${severity} alert message to show important information to users.`}
/>
))}
</div>
</RenderComponentWithSnippet>
);
@@ -1,24 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Alert } from "@calcom/ui/components/alert";
const severities = ["neutral", "info", "warning", "error"] as const;
export const CustomIconColorExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="not-prose space-y-4">
{severities.map((severity) => (
<Alert
key={severity}
severity={severity}
CustomIcon="bell"
customIconColor="text-emphasis"
title={`${severity.charAt(0).toUpperCase() + severity.slice(1)} Alert with Custom Icon Color`}
message="This alert uses a custom icon color."
/>
))}
</div>
</RenderComponentWithSnippet>
);
@@ -1,25 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Alert } from "@calcom/ui/components/alert";
const severities = ["neutral", "info", "warning", "error"] as const;
const customIcons = ["info", "alert-triangle", "circle-x", "circle-check", "bell"] as const;
export const CustomIconsExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="not-prose space-y-4">
{severities.map((severity, index) => (
<Alert
key={severity}
severity={severity}
// @ts-expect-error Didnt type this as IconName for CustomIcon
CustomIcon={customIcons[index % customIcons.length]}
title={`${severity.charAt(0).toUpperCase() + severity.slice(1)} Alert with Custom Icon`}
message="This alert uses a custom icon instead of the default one."
/>
))}
</div>
</RenderComponentWithSnippet>
);
@@ -1,42 +0,0 @@
---
title: Alert
airtableId: recnvwaXqOucqLo9Y
---
import { Alert } from "@calcom/ui/components/alert"
import { RenderComponentWithSnippet } from "@/app/components/render"
import { Row } from "@/app/components/row"
import { BasicExample } from "./alert.basic"
import { ActionsExample } from "./alert.actions"
import { CustomIconsExample } from "./alert.customIcons"
import { CustomIconColorExample } from "./alert.customIconColor"
import { NoTitleExample } from "./alert.noTitle"
## Basic Alerts
Alerts come in different severities to convey different levels of importance.
<BasicExample/>
## Alerts with Actions
Alerts can include action buttons to provide users with immediate options.
<ActionsExample/>
## Custom Icons
You can customize the alert icon to better match your message context.
<CustomIconsExample/>
## Custom Icon Colors
The icon color can be customized independently of the alert severity.
<CustomIconColorExample/>
## Without Title
For simpler messages, alerts can be displayed without a title.
<NoTitleExample/>
@@ -1,21 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Alert } from "@calcom/ui/components/alert";
const severities = ["neutral", "info", "warning", "error"] as const;
export const NoTitleExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="not-prose space-y-4">
{severities.map((severity) => (
<Alert
key={severity}
severity={severity}
message={`This is a ${severity} alert without a title to show a more compact version.`}
/>
))}
</div>
</RenderComponentWithSnippet>
);
@@ -1,22 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Avatar } from "@calcom/ui/components/avatar";
const sampleImage = "https://cal.com/stakeholder/peer.jpg";
export const ImageExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="flex items-center gap-8">
<div className="flex flex-col items-center gap-2">
<Avatar size="md" alt="With image" imageSrc={sampleImage} />
<span className="text-subtle text-xs">With Image</span>
</div>
<div className="flex flex-col items-center gap-2">
<Avatar size="md" alt="Without image" imageSrc="https://cal.com/avatar.svg" />
<span className="text-subtle text-xs">Without Image</span>
</div>
</div>
</RenderComponentWithSnippet>
);
@@ -1,25 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Avatar } from "@calcom/ui/components/avatar";
const sampleImage = "https://cal.com/stakeholder/peer.jpg";
export const IndicatorExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="flex items-center gap-4">
<div className="flex flex-col items-center gap-2">
<Avatar
size="md"
alt="With indicator"
imageSrc={sampleImage}
indicator={
<div className="absolute bottom-0 right-0 h-2.5 w-2.5 rounded-full bg-green-500 ring-2 ring-white" />
}
/>
<span className="text-subtle text-xs">With Status Indicator</span>
</div>
</div>
</RenderComponentWithSnippet>
);
@@ -1,18 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Avatar } from "@calcom/ui/components/avatar";
const sampleImage = "https://cal.com/stakeholder/peer.jpg";
export const LinkExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="flex items-center gap-4">
<div className="flex flex-col items-center gap-2">
<Avatar size="md" alt="With link" imageSrc={sampleImage} href="https://cal.com" />
<span className="text-subtle text-xs">Clickable</span>
</div>
</div>
</RenderComponentWithSnippet>
);
@@ -1,103 +0,0 @@
---
title: Avatar
airtableId: reciflFrJo5cVgGrS
---
import { Avatar } from "@calcom/ui/components/avatar"
import { RenderComponentWithSnippet } from "@/app/components/render"
import { Row } from "@/app/components/row"
import { SizesExample } from "./avatar.sizes"
import { ImageExample } from "./avatar.image"
import { TooltipExample } from "./avatar.tooltip"
import { LinkExample } from "./avatar.link"
import { IndicatorExample } from "./avatar.indicator"
import { ShapeExample } from "./avatar.shape"
## Size Variations
Avatars come in different sizes to suit various use cases.
<SizesExample/>
## Shape Variations
Avatars can be displayed in either circular or square shapes.
<ShapeExample/>
## With and Without Image
Avatars can display either an image or a fallback placeholder.
<ImageExample/>
## With Tooltip
Avatars can show additional information on hover using tooltips.
<TooltipExample/>
## With Link
Avatars can be made clickable by providing an href.
<LinkExample/>
## With Indicator
Avatars can include status indicators or badges.
<IndicatorExample/>
## Usage Example
Here's a complete example showing how to use the Avatar component with all its props:
```tsx
import { Avatar } from "@calcom/ui/components/avatar";
function Example() {
return (
<div className="flex flex-col gap-4">
{/* Basic Avatar */}
<Avatar
alt="User Name"
title="User Name"
size="md"
shape="circle"
imageSrc="https://example.com/avatar.jpg"
/>
{/* Avatar with Link */}
<Avatar
alt="User Name"
title="User Name"
size="lg"
shape="square"
imageSrc="https://example.com/avatar.jpg"
href="/profile"
/>
{/* Avatar with Tooltip and Indicator */}
<Avatar
alt="User Name"
title="User Name"
size="md"
shape="circle"
imageSrc="https://example.com/avatar.jpg"
indicator={<div className="absolute bottom-0 right-0 h-3 w-3 rounded-full bg-green-500" />}
/>
{/* Avatar with Fallback */}
<Avatar
alt="User Name"
title="User Name"
size="md"
shape="circle"
fallback={<span>UN</span>}
/>
</div>
);
}
```
@@ -1,24 +0,0 @@
import { Avatar } from "@calcom/ui/components/avatar";
export function ShapeExample() {
return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-4">
<Avatar
alt="Circle Avatar"
title="Circle Avatar"
size="lg"
shape="circle"
imageSrc="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
/>
<Avatar
alt="Square Avatar"
title="Square Avatar"
size="lg"
shape="square"
imageSrc="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
/>
</div>
</div>
);
}
@@ -1,29 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Avatar } from "@calcom/ui/components/avatar";
const sizes = ["xs", "xsm", "sm", "md", "mdLg", "lg", "xl"] as const;
const sampleImage = "https://cal.com/stakeholder/peer.jpg";
export const SizesExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="flex items-center gap-4">
{sizes.map((size) => (
<div key={size} className="flex flex-col items-center gap-2">
<Avatar size={size} alt={`${size} avatar`} imageSrc={sampleImage} />
<span className="text-subtle text-xs">{size}</span>
</div>
))}
</div>
<div className="mt-2 flex items-center gap-4">
{sizes.map((size) => (
<div key={size} className="flex flex-col items-center gap-2">
<Avatar size={size} alt={`${size} avatar`} imageSrc={sampleImage} shape="square" />
<span className="text-subtle text-xs">{size}</span>
</div>
))}
</div>
</RenderComponentWithSnippet>
);
@@ -1,18 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Avatar } from "@calcom/ui/components/avatar";
const sampleImage = "https://cal.com/stakeholder/peer.jpg";
export const TooltipExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="flex items-center gap-4">
<div className="flex flex-col items-center gap-2">
<Avatar size="md" alt="With tooltip" imageSrc={sampleImage} title="Hover me!" />
<span className="text-subtle text-xs">Hover to see tooltip</span>
</div>
</div>
</RenderComponentWithSnippet>
);
@@ -1,33 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Badge } from "@calcom/ui/components/badge";
const variants = [
"default",
"warning",
"orange",
"success",
"green",
"gray",
"blue",
"red",
"error",
"grayWithoutHover",
] as const;
export const DotsExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="flex flex-wrap items-center gap-4">
{variants.map((variant) => (
<div key={variant} className="flex flex-col items-center gap-2">
<Badge variant={variant} withDot>
{variant}
</Badge>
<span className="text-subtle text-xs">Dot</span>
</div>
))}
</div>
</RenderComponentWithSnippet>
);
@@ -1,33 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Badge } from "@calcom/ui/components/badge";
const variants = [
"default",
"warning",
"orange",
"success",
"green",
"gray",
"blue",
"red",
"error",
"grayWithoutHover",
] as const;
export const IconsExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="flex flex-wrap items-center gap-4">
{variants.map((variant) => (
<div key={variant} className="flex flex-col items-center gap-2">
<Badge variant={variant} startIcon="bell">
{variant}
</Badge>
<span className="text-subtle text-xs">Icon</span>
</div>
))}
</div>
</RenderComponentWithSnippet>
);
@@ -1,33 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Badge } from "@calcom/ui/components/badge";
const variants = [
"default",
"warning",
"orange",
"success",
"green",
"gray",
"blue",
"red",
"error",
"grayWithoutHover",
] as const;
export const InteractiveExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="flex flex-wrap items-center gap-4">
{variants.map((variant) => (
<div key={variant} className="flex flex-col items-center gap-2">
<Badge variant={variant} onClick={() => alert(`${variant} badge clicked!`)}>
Click me
</Badge>
<span className="text-subtle text-xs">Clickable</span>
</div>
))}
</div>
</RenderComponentWithSnippet>
);
@@ -1,42 +0,0 @@
---
title: Badge
airtableId: recso7QMfD2xwI04K
---
import { Badge } from "@calcom/ui/components/badge"
import { RenderComponentWithSnippet } from "@/app/components/render"
import { Row } from "@/app/components/row"
import { VariantsExample } from "./badge.variants"
import { IconsExample } from "./badge.icons"
import { DotsExample } from "./badge.dots"
import { InteractiveExample } from "./badge.interactive"
import { RoundedExample } from "./badge.rounded"
## Variants and Sizes
Badges come in different variants and sizes to suit various use cases.
<VariantsExample/>
## With Icons
Badges can include icons to provide additional visual context.
<IconsExample/>
## With Dots
Badges can include dots, useful for showing status or notifications.
<DotsExample/>
## Interactive
Badges can be made interactive with click handlers.
<InteractiveExample/>
## Rounded
Badges can be fully rounded, useful for numerical indicators.
<RoundedExample/>
@@ -1,33 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Badge } from "@calcom/ui/components/badge";
const variants = [
"default",
"warning",
"orange",
"success",
"green",
"gray",
"blue",
"red",
"error",
"grayWithoutHover",
] as const;
export const RoundedExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="flex flex-wrap items-center gap-4">
{variants.map((variant) => (
<div key={variant} className="flex flex-col items-center gap-2">
<Badge variant={variant} rounded>
1
</Badge>
<span className="text-subtle text-xs">{variant}</span>
</div>
))}
</div>
</RenderComponentWithSnippet>
);
@@ -1,42 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Badge } from "@calcom/ui/components/badge";
const variants = [
"default",
"warning",
"orange",
"success",
"green",
"gray",
"blue",
"red",
"error",
"grayWithoutHover",
] as const;
const sizes = ["sm", "md", "lg"] as const;
export const VariantsExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="space-y-8">
{variants.map((variant) => (
<div key={variant} className="space-y-2">
<h4 className="text-default text-sm capitalize">{variant}</h4>
<div className="flex items-center gap-4">
{sizes.map((size) => (
<div key={size} className="flex flex-col items-center gap-2">
<Badge variant={variant} size={size}>
{variant}
</Badge>
<span className="text-subtle text-xs">{size}</span>
</div>
))}
</div>
</div>
))}
</div>
</RenderComponentWithSnippet>
);
@@ -1,63 +0,0 @@
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Button } from "@calcom/ui/components/button";
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
export const ButtonGroupExample = () => {
return (
<RenderComponentWithSnippet className="flex flex-col space-y-6">
{/* Basic Button Group */}
<ButtonGroup>
<Button variant="icon" StartIcon="ellipsis" color="secondary" />
<Button variant="icon" StartIcon="ellipsis" color="secondary" />
<Button variant="icon" StartIcon="ellipsis" color="secondary" />
</ButtonGroup>
{/* Combined Button Group */}
<ButtonGroup combined>
<Button variant="icon" StartIcon="ellipsis" color="secondary" />
<Button variant="icon" StartIcon="ellipsis" color="secondary" />
<Button variant="icon" StartIcon="ellipsis" color="secondary" />
</ButtonGroup>
{/* Button Group with different variants */}
<ButtonGroup combined>
<Button variant="icon" color="secondary" StartIcon="ellipsis" />
<Button variant="icon" color="secondary" StartIcon="ellipsis" />
<Button variant="icon" color="destructive" StartIcon="trash" />
</ButtonGroup>
</RenderComponentWithSnippet>
);
};
export const ButtonGroupSizes = () => {
const sizes = ["xs", "sm", "base", "lg"] as const;
return (
<RenderComponentWithSnippet className="flex flex-col space-y-6">
{sizes.map((size) => (
<ButtonGroup key={size}>
<Button variant="icon" color="secondary" StartIcon="ellipsis" size={size} />
<Button variant="icon" color="secondary" StartIcon="ellipsis" size={size} />
<Button variant="icon" color="secondary" StartIcon="ellipsis" size={size} />
</ButtonGroup>
))}
</RenderComponentWithSnippet>
);
};
export const ButtonGroupSizesCombined = () => {
const sizes = ["xs", "sm", "base", "lg"] as const;
return (
<RenderComponentWithSnippet className="flex flex-col space-y-6">
{sizes.map((size) => (
<ButtonGroup key={size} combined>
<Button variant="icon" color="secondary" StartIcon="ellipsis" size={size} />
<Button variant="icon" color="secondary" StartIcon="ellipsis" size={size} />
<Button variant="icon" color="secondary" StartIcon="ellipsis" size={size} />
</ButtonGroup>
))}
</RenderComponentWithSnippet>
);
};
@@ -1,42 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Row } from "@/app/components/row";
import { Button } from "@calcom/ui/components/button";
const colors = ["primary", "secondary", "minimal", "destructive"] as const;
export const DisabledExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="space-y-8">
<div>
<h3 className="text-default mb-4 text-sm">Disabled State</h3>
<Row>
{colors.map((color) => (
<div key={color} className="flex flex-col items-center gap-2">
<Button color={color} disabled>
{color}
</Button>
<span className="text-subtle text-xs">Disabled</span>
</div>
))}
</Row>
</div>
<div>
<h3 className="text-default mb-4 text-sm">Disabled with Icons</h3>
<Row>
{colors.map((color) => (
<div key={color} className="flex flex-col items-center gap-2">
<Button color={color} disabled StartIcon="calendar">
{color}
</Button>
<span className="text-subtle text-xs">With Icon</span>
</div>
))}
</Row>
</div>
</div>
</RenderComponentWithSnippet>
);
@@ -1,42 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Row } from "@/app/components/row";
import { Button } from "@calcom/ui/components/button";
const colors = ["primary", "secondary", "minimal", "destructive"] as const;
export const IconsExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="space-y-8">
<div>
<h3 className="text-default mb-4 text-sm">Start Icon</h3>
<Row>
{colors.map((color) => (
<div key={color} className="flex flex-col items-center gap-2">
<Button color={color} StartIcon="calendar">
{color}
</Button>
<span className="text-subtle text-xs">Start Icon</span>
</div>
))}
</Row>
</div>
<div>
<h3 className="text-default mb-4 text-sm">End Icon</h3>
<Row>
{colors.map((color) => (
<div key={color} className="flex flex-col items-center gap-2">
<Button color={color} EndIcon="arrow-right">
{color}
</Button>
<span className="text-subtle text-xs">End Icon</span>
</div>
))}
</Row>
</div>
</div>
</RenderComponentWithSnippet>
);
@@ -1,44 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Row } from "@/app/components/row";
import { Button } from "@calcom/ui/components/button";
const colors = ["primary", "secondary", "minimal", "destructive"] as const;
export const LoadingExample: React.FC = () => {
return (
<RenderComponentWithSnippet>
<div className="space-y-8">
<div>
<h3 className="text-default mb-4 text-sm">Loading State</h3>
<Row>
{colors.map((color) => (
<div key={color} className="flex flex-col items-center gap-2">
<Button color={color} loading>
{color}
</Button>
<span className="text-subtle text-xs">Loading</span>
</div>
))}
</Row>
</div>
<div>
<h3 className="text-default mb-4 text-sm">Loading with Icons</h3>
<Row>
{colors.map((color) => (
<div key={color} className="flex flex-col items-center gap-2">
<Button color={color} loading StartIcon="calendar">
{color}
</Button>
<span className="text-subtle text-xs">With Icon</span>
</div>
))}
</Row>
</div>
</div>
</RenderComponentWithSnippet>
);
};
@@ -1,98 +0,0 @@
---
title: Button
airtableId: recEJyzUeMMXCjyTB
---
import { Button } from "@calcom/ui/components/button"
import { RenderComponentWithSnippet } from "@/app/components/render"
import { Row } from "@/app/components/row"
import { OnClickExample } from "./button.onClick"
import { VariantExample } from "./button.variant"
import { IconsExample } from "./button.icons"
import { LoadingExample } from "./button.loading"
import { DisabledExample } from "./button.disabled"
import { ButtonGroupExample,ButtonGroupSizes,ButtonGroupSizesCombined} from "./button.buttongroup"
## Variants and Colors
Buttons come in different variants and colors to suit different purposes. Each variant can be combined with any color and size.
<VariantExample/>
## Icons
Buttons can include icons at the start or end. The icon color and size are automatically handled by the button component.
<IconsExample/>
## Loading State
Buttons can show a loading state to indicate an ongoing action. The loading spinner replaces the button text while maintaining the button's width.
<LoadingExample/>
## Disabled State
Buttons can be disabled to indicate that they are not interactive. The disabled state is visually distinct and prevents any click events.
<DisabledExample/>
## onClick Events
Buttons handle click events like any other button component. You can provide an onClick handler to respond to user interactions.
<OnClickExample/>
Providing a loading state will add a spinner on the left side of the button.
<LoadingExample/>
## Disabled
<DisabledExample/>
## Button Groups
Buttons can be grouped together using the ButtonGroup component. The ButtonGroup component can be used to create a group of related buttons with consistent spacing and optional combined borders.
<ButtonGroupExample/>
### Button Group Sizes
#### Separated
<ButtonGroupSizes/>
#### Combined
<ButtonGroupSizesCombined/>
## Usage
```tsx
// Primary button (default)
<Button>Primary Button</Button>
// Secondary button
<Button color="secondary">Secondary Button</Button>
// Minimal button
<Button color="minimal">Minimal Button</Button>
// Destructive button
<Button color="destructive">Destructive Button</Button>
// With start icon
<Button StartIcon="plus">Add Item</Button>
// With end icon
<Button EndIcon="chevronRight">View More</Button>
// With link
<Button href="/some-path">Navigate</Button>
// Different sizes
<Button size="xs">Extra Small</Button>
<Button size="sm">Small</Button>
<Button size="base">Base (Default)</Button>
<Button size="lg">Large</Button>
```
@@ -1,14 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Row } from "@/app/components/row";
import { Button } from "@calcom/ui/components/button";
export const OnClickExample: React.FC = () => (
<RenderComponentWithSnippet>
<Row>
<Button onClick={() => alert("hello")}>Trigger Alert</Button>
</Row>
</RenderComponentWithSnippet>
);
@@ -1,43 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Row } from "@/app/components/row";
import { Button } from "@calcom/ui/components/button";
const variants = ["button", "icon"] as const;
const colors = ["primary", "secondary", "minimal", "destructive"] as const;
const sizes = ["xs", "sm", "base", "lg"] as const;
export const VariantExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="space-y-8">
{variants.map((variant) => (
<div key={variant} className="space-y-6">
<h3 className="text-default text-sm capitalize">{variant === "button" ? "Default" : variant}</h3>
<div className="space-y-6">
{colors.map((color) => (
<div key={color} className="space-y-2">
<h4 className="text-default text-sm capitalize">{color}</h4>
<Row>
{sizes.map((size) => (
<div key={size} className="flex flex-col items-center gap-2">
<Button
variant={variant}
color={color}
size={size}
StartIcon={variant === "icon" ? "plus" : undefined}>
{variant !== "icon" && `Label`}
</Button>
<span className="text-subtle text-xs">{size}</span>
</div>
))}
</Row>
</div>
))}
</div>
</div>
))}
</div>
</RenderComponentWithSnippet>
);
@@ -1,31 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Checkbox } from "@calcom/ui/components/form";
export const BasicExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="flex flex-wrap gap-4">
<div className="flex flex-col items-center justify-center gap-2">
<Checkbox id="basic-unchecked" />
<span className="text-subtle text-xs">Unchecked</span>
</div>
<div className="flex flex-col items-center justify-center gap-2">
<Checkbox id="basic-checked" defaultChecked />
<span className="text-subtle text-xs">Checked</span>
</div>
<div className="flex flex-col items-center justify-center gap-2">
<Checkbox id="basic-disabled" disabled />
<span className="text-subtle text-xs">Disabled</span>
</div>
<div className="flex flex-col items-center justify-center gap-2">
<Checkbox id="basic-disabled-checked" disabled defaultChecked />
<span className="text-subtle text-xs">Disabled Checked</span>
</div>
</div>
</RenderComponentWithSnippet>
);
@@ -1,15 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { CheckboxField } from "@calcom/ui/components/form";
export const DescriptionExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="space-y-4">
<CheckboxField description="This is a regular description" id="desc-normal" />
<CheckboxField description="This description acts as the label" descriptionAsLabel id="desc-as-label" />
<CheckboxField id="desc-long" label="Label and description" description="Label and description" />
</div>
</RenderComponentWithSnippet>
);
@@ -1,16 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { CheckboxField } from "@calcom/ui/components/form";
export const InfoIconExample: React.FC = () => (
<RenderComponentWithSnippet>
<CheckboxField
description="Checkbox with additional information"
id="info-icon"
label="Information Icon"
informationIconText="This is additional information that appears in a tooltip"
/>
</RenderComponentWithSnippet>
);
@@ -1,24 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { CheckboxField } from "@calcom/ui/components/form";
export const LabelPositionExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="space-y-4">
<CheckboxField
description="Description with label above (default on mobile)"
id="label-above"
label="Label Above"
/>
<div className="sm:min-w-[400px]">
<CheckboxField
description="Description with label to the side (on larger screens)"
id="label-side"
label="Label to the Side"
/>
</div>
</div>
</RenderComponentWithSnippet>
);
@@ -1,35 +0,0 @@
---
title: Checkbox
airtableId: recXGPNAfDJVm8NHo
---
import { Checkbox } from "@calcom/ui/components/form"
import { RenderComponentWithSnippet } from "@/app/components/render"
import { Row } from "@/app/components/row"
import { BasicExample } from "./checkbox.basic"
import { DescriptionExample } from "./checkbox.description"
import { LabelPositionExample } from "./checkbox.labelPosition"
import { InfoIconExample } from "./checkbox.infoIcon"
## Basic Usage
Checkboxes in their various states - unchecked, checked, disabled, and disabled checked.
<BasicExample/>
## With Description
Checkboxes can include descriptions to provide additional context.
<DescriptionExample/>
## Label Positions
Labels can be positioned above or to the side of the checkbox.
<LabelPositionExample/>
## With Information Icon
Checkboxes can include an information icon with a tooltip for additional details.
<InfoIconExample/>
@@ -1,33 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { useState } from "react";
import { Button } from "@calcom/ui/components/button";
import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui/components/dialog";
export const BasicExample: React.FC = () => {
const [open, setOpen] = useState(false);
return (
<RenderComponentWithSnippet>
<div className="space-y-2">
<Button onClick={() => setOpen(true)}>Open Basic Dialog</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent
title="Basic Dialog"
description="This is a basic dialog with a title and description.">
<p className="text-default text-sm">
Dialogs are used to display content that requires user attention or interaction. They appear
above the page content and must be closed before interacting with the page again.
</p>
<DialogFooter>
<DialogClose />
<Button>Action</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</RenderComponentWithSnippet>
);
};
@@ -1,37 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { useState } from "react";
import { Button } from "@calcom/ui/components/button";
import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui/components/dialog";
export const ConfirmationExample: React.FC = () => {
const [open, setOpen] = useState(false);
return (
<RenderComponentWithSnippet>
<div className="space-y-2">
<Button color="destructive" onClick={() => setOpen(true)}>
Delete Item
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent
title="Confirm Deletion"
description="Are you sure you want to delete this item? This action cannot be undone.">
<DialogFooter>
<DialogClose />
<Button
color="destructive"
onClick={() => {
setOpen(false);
}}>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</RenderComponentWithSnippet>
);
};
@@ -1,56 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { useState } from "react";
import { Button } from "@calcom/ui/components/button";
import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui/components/dialog";
import { TextAreaField } from "@calcom/ui/components/form";
export const FormExample: React.FC = () => {
const [open, setOpen] = useState(false);
const [formInput, setFormInput] = useState("");
return (
<RenderComponentWithSnippet>
<div className="space-y-2">
<Button color="secondary" onClick={() => setOpen(true)}>
Open Form Dialog
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent title="Feedback Form" description="Please provide your feedback below">
<div>
<TextAreaField
name="feedback"
label={
<>
Your Feedback
<span className="text-subtle font-normal"> (Optional)</span>
</>
}
value={formInput}
onChange={(e) => setFormInput(e.target.value)}
/>
</div>
<DialogFooter>
<DialogClose />
<Button
onClick={() => {
setOpen(false);
setFormInput("");
}}>
Submit Feedback
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<div className="mt-6">
<h4 className="text-emphasis text-sm font-medium">Current Form Input:</h4>
<pre className="text-default bg-subtle mt-2 rounded-md p-4 text-sm">
{formInput || "No input yet"}
</pre>
</div>
</div>
</RenderComponentWithSnippet>
);
};
@@ -1,58 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { useState } from "react";
import { Button } from "@calcom/ui/components/button";
import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui/components/dialog";
export const LargeContentExample: React.FC = () => {
const [open, setOpen] = useState(false);
return (
<RenderComponentWithSnippet>
<div className="space-y-2">
<Button color="secondary" onClick={() => setOpen(true)}>
Open Large Content Dialog
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent
title="Terms and Conditions"
description="Please review our terms and conditions carefully"
enableOverflow>
<div className="space-y-6">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="space-y-4">
<h3 className="text-emphasis font-semibold">Section {i + 1}</h3>
<p className="text-default text-sm">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam in dui mauris. Vivamus
hendrerit arcu sed erat molestie vehicula. Sed auctor neque eu tellus rhoncus ut eleifend
nibh porttitor. Ut in nulla enim. Phasellus molestie magna non est bibendum non venenatis
nisl tempor.
</p>
<p className="text-default text-sm">
Suspendisse in orci enim. Integer vel sapien at risus ultrices ornare nec sit amet nibh.
Duis blandit lectus ac odio rhoncus non congue diam bibendum. Aliquam erat volutpat.
</p>
{i === 2 && (
<div className="border-subtle rounded-lg border p-4 shadow-sm">
<h4 className="text-emphasis mb-2 font-medium">Important Notice</h4>
<p className="text-default text-sm">
This is a highlighted section within the content to demonstrate how different UI
elements appear within a scrollable dialog.
</p>
</div>
)}
</div>
))}
</div>
<DialogFooter>
<DialogClose>Decline</DialogClose>
<Button>Accept Terms</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</RenderComponentWithSnippet>
);
};
@@ -1,35 +0,0 @@
---
title: Dialog
airtableId: rechCoaoEMZLnLChR
---
import { Dialog } from "@calcom/ui/components/dialog"
import { RenderComponentWithSnippet } from "@/app/components/render"
import { Row } from "@/app/components/row"
import { BasicExample } from "./dialog.basic"
import { LargeContentExample } from "./dialog.largeContent"
import { FormExample } from "./dialog.form"
import { ConfirmationExample } from "./dialog.confirmation"
## Basic Dialog
A simple dialog with a title, description, and action buttons.
<BasicExample/>
## Large Content Dialog
A dialog that can handle scrollable content, such as terms and conditions.
<LargeContentExample/>
## Form Dialog
A dialog containing a form for user input.
<FormExample/>
## Confirmation Dialog
A dialog for confirming destructive actions.
<ConfirmationExample/>
@@ -1,38 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Avatar } from "@calcom/ui/components/avatar";
import {
Dropdown,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownItem,
DropdownMenuLabel,
DropdownMenuSeparator,
} from "@calcom/ui/components/dropdown";
export const AvatarTriggerExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="flex flex-wrap items-center gap-8">
<div className="flex flex-col items-center gap-2">
<Dropdown>
<DropdownMenuTrigger asChild>
<button className="cursor-pointer">
<Avatar size="sm" imageSrc="https://cal.com/stakeholder/peer.jpg" alt="Avatar" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Profile</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownItem>View Profile</DropdownItem>
<DropdownItem>Settings</DropdownItem>
<DropdownMenuSeparator />
<DropdownItem className="text-error">Sign out</DropdownItem>
</DropdownMenuContent>
</Dropdown>
<span className="text-subtle text-xs">Avatar Menu</span>
</div>
</div>
</RenderComponentWithSnippet>
);
@@ -1,84 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import type { IconName } from "@calcom/ui";
import { Button } from "@calcom/ui/components/button";
import {
Dropdown,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownItem,
DropdownMenuLabel,
} from "@calcom/ui/components/dropdown";
const menuItems = [
{ label: "View", icon: "eye", kbd: "V" },
{ label: "Edit", icon: "edit", kbd: "E" },
{ label: "Share", icon: "share", kbd: "⌘S" },
{ label: "Delete", icon: "trash", destructive: true, kbd: "⌘⌫" },
] as {
label: string;
icon: IconName;
destructive?: boolean;
kbd?: string;
}[];
export const ButtonTriggerExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="flex flex-wrap items-center gap-8">
{/* Default Button */}
<div className="flex flex-col items-center gap-2">
<Dropdown>
<DropdownMenuTrigger asChild>
<Button>Menu</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
{menuItems.map((item) => (
<DropdownItem color={item.destructive ? "destructive" : "secondary"} key={item.label}>
{item.label}
</DropdownItem>
))}
</DropdownMenuContent>
</Dropdown>
<span className="text-subtle text-xs">Default</span>
</div>
{/* Button with Icon */}
<div className="flex flex-col items-center gap-2">
<Dropdown>
<DropdownMenuTrigger asChild>
<Button>More Actions</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Quick Actions</DropdownMenuLabel>
{menuItems.map((item) => (
<DropdownItem key={item.label} color={item.destructive ? "destructive" : undefined}>
{item.label}
</DropdownItem>
))}
</DropdownMenuContent>
</Dropdown>
<span className="text-subtle text-xs">With Icon</span>
</div>
{/* Icon Button */}
<div className="flex flex-col items-center gap-2">
<Dropdown>
<DropdownMenuTrigger asChild>
<Button variant="icon" StartIcon="calendar" />
</DropdownMenuTrigger>
<DropdownMenuContent>
{menuItems.map((item) => (
<DropdownItem key={item.label} color={item.destructive ? "destructive" : undefined}>
{item.label}
</DropdownItem>
))}
</DropdownMenuContent>
</Dropdown>
<span className="text-subtle text-xs">Icon Only</span>
</div>
</div>
</RenderComponentWithSnippet>
);
@@ -1,86 +0,0 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Avatar } from "@calcom/ui/components/avatar";
import { Button } from "@calcom/ui/components/button";
import {
Dropdown,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownItem,
DropdownMenuLabel,
DropdownMenuSeparator,
} from "@calcom/ui/components/dropdown";
export const ComplexExample: React.FC = () => (
<RenderComponentWithSnippet>
<div className="flex flex-wrap items-center gap-8">
{/* Example 1: Complex Menu Structure */}
<div className="flex flex-col items-center gap-2">
<Dropdown>
<DropdownMenuTrigger asChild>
<Button color="secondary">Open</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>My account</DropdownMenuLabel>
<DropdownItem>Profile</DropdownItem>
<DropdownItem>Billing</DropdownItem>
<DropdownItem>Settings</DropdownItem>
<DropdownItem>Keyboard Shortcuts</DropdownItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>Team</DropdownMenuLabel>
<DropdownItem>Invite users</DropdownItem>
<DropdownItem>New team</DropdownItem>
<DropdownMenuSeparator />
<DropdownItem>Github</DropdownItem>
<DropdownItem>Support</DropdownItem>
<DropdownItem>API</DropdownItem>
</DropdownMenuContent>
</Dropdown>
<span className="text-subtle text-xs">Complex Menu</span>
</div>
{/* Example 2: Avatar with Icons */}
<div className="flex flex-col items-center gap-2">
<Dropdown>
<DropdownMenuTrigger asChild>
<button className="cursor-pointer">
<Avatar size="md" imageSrc="https://cal.com/stakeholder/peer.jpg" alt="Avatar" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>peer@cal.com</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownItem StartIcon="user">My account</DropdownItem>
<DropdownItem StartIcon="map">Plan</DropdownItem>
<DropdownItem StartIcon="credit-card">Billing</DropdownItem>
<DropdownItem StartIcon="link">Integrations</DropdownItem>
<DropdownItem StartIcon="upload">Module</DropdownItem>
<DropdownMenuSeparator />
<DropdownItem className="text-error" StartIcon="log-out">
Log out
</DropdownItem>
</DropdownMenuContent>
</Dropdown>
<span className="text-subtle text-xs">With Icons</span>
</div>
{/* Example 3: Simple Actions */}
<div className="flex flex-col items-center gap-2">
<Dropdown>
<DropdownMenuTrigger asChild>
<Button variant="icon" color="secondary" StartIcon="calendar" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownItem>Rename</DropdownItem>
<DropdownItem>Duplicate</DropdownItem>
<DropdownItem>Move</DropdownItem>
<DropdownItem color="destructive">Delete</DropdownItem>
</DropdownMenuContent>
</Dropdown>
<span className="text-subtle text-xs">Simple Actions</span>
</div>
</div>
</RenderComponentWithSnippet>
);

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