Compare commits

...
Author SHA1 Message Date
emrysal 70cfe8d693 chore: release v5.6.7
Post release / release (push) Canceled after 0s
2025-08-26 23:13:36 +00:00
Keith WilliamsandDevin AI 520e711ca6 perf: optimize analytics app imports to avoid loading entire app store (#23372)
* perf: optimize analytics app imports to avoid loading entire app store

- Add analytics service generation to app-store-cli build process
- Generate analytics.services.generated.ts with only analytics apps (dub)
- Update getAnalytics.ts to use AnalyticsServiceMap instead of full appStore
- Add NEXT_PUBLIC_IS_E2E to turbo.json globalEnv for generated files
- Reduces import footprint from 100+ apps to only analytics apps with AnalyticsService
- Follows same pattern as calendar services optimization from PR #22450

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

* Reorder cli output

* fix: follow getCalendar pattern in getAnalytics and maintain alphabetical order in turbo.json

- Remove unnecessary object wrapping in getAnalytics.ts to match getCalendar.ts pattern
- Move NEXT_PUBLIC_IS_E2E to correct alphabetical position in turbo.json globalEnv
- Address PR feedback from keithwillcode

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

* chore: update yarn.lock after analytics optimization changes

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-26 18:57:38 -03:00
Keith WilliamsandDevin AI 6d1fe32a39 feat: add self-hosted to ignored labels in team labeler workflow (#23373)
- Add self-hosted to the ignore-labels list in .github/workflows/labeler.yml
- This prevents the team-label-action from automatically applying the self-hosted label to PRs
- Maintains alphabetical ordering in the comma-separated list

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-26 20:21:38 +00:00
Benny JooandDevin AI 1cc548284d fix: remove TypeScript path mapping and standardize @calcom/ui imports (#23359)
- Remove '@calcom/ui/*': ['../../packages/ui/components/*'] path mapping from apps/web/tsconfig.json
- Fix imports from '@calcom/ui/componentName' to '@calcom/ui/components/componentName' format
- Preserve direct exports like '@calcom/ui/classNames' and '@calcom/ui/styles'
- Update 200+ files across apps/web, packages/features, packages/app-store, and packages/ui
- Fix localStorage usage to use safe webstorage import
- Remove forbidden 'as any' type casting in favor of proper TypeScript types
- All changes verified to pass TypeScript compilation

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-26 13:47:06 -03:00
Rajiv Sahal d49843de0b fix: update e2e tests for event types (#23322)
* add data test id for testing

* update tests for creating event type

* add test for creating team event types

* helper function for tests

* update tests

* update tests

* fixup: fix correct data test ids for tabs
2025-08-26 13:02:22 -03:00
Peer Richelsen 42efd95476 chore: fixed typo in recording link (#23363)
* Update common.json

* Update apps/web/public/static/locales/en/common.json
2025-08-26 14:46:37 +00:00
Anik Dhabal Babu 2ec68ef1aa fix: team metadata restriction for stripe (#23327) 2025-08-26 14:24:11 +00:00
6e09489685 perf: implement quick app store loading optimizations (#22450)
* perf: implement quick app store loading optimizations

- Add conditional app store imports in videoClient and handlePayment
- Implement lazy calendar manager pattern in CalendarManager
- Enhance createCachedImport with better concurrency handling
- Create calendar-only registry for common calendar operations
- Add performance instrumentation for debugging

These optimizations reduce initial app store loading time by avoiding
module-level imports and creating smaller, focused registries for
calendar operations.

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

* fix: revert getCalendar to use appStore for proper test mocking

- Reverted getCalendar.ts to use main appStore instead of calendarStore
- This ensures test mocking system works properly with existing appStoreMock
- Fixes unit test failures where Google Calendar references were getting null values
- All collective scheduling tests now pass

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

* perf: implement CalendarServiceMap for optimized calendar loading

- Add CalendarServiceMap generation following CrmServiceMap pattern
- Update getCalendar.ts to use generated calendar service map
- Remove manual calendar-registry.ts in favor of auto-generated approach
- Reduces calendar initialization from loading 48+ apps to ~10 calendar apps

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

* fix: update test mocks for CalendarServiceMap compatibility

- Add missing SelectedCalendar fields (createdAt, updatedAt, lastErrorAt, watchAttempts, etc.)
- Fix CredentialPayload type errors by adding user.email and delegationCredentialId
- Mock CalendarServiceMap to use vi.importActual for real calendar services
- Ensure calendar service tests work with new lazy loading approach

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

* fix: resolve CalendarServiceMap test compatibility issues

- Update getCalendarsEvents.test.ts mocks to work with CalendarServiceMap dynamic imports
- Add missing SelectedCalendar fields (createdAt, updatedAt, lastErrorAt, watchAttempts, etc.)
- Fix CredentialPayload type errors by adding user.email and delegationCredentialId
- Use type assertion in getCalendar.ts to resolve credential type conflicts
- Ensure calendar service tests work with new lazy loading approach

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

* fix: update test mocking for CalendarServiceMap compatibility

- Add comprehensive vi.mock for calendar.services.generated in delegation-credential tests
- Mock GoogleCalendarService and Office365CalendarService with proper return values
- Update all test files to use await with mockCalendarToHaveNoBusySlots
- Ensure calendar events return expected meetingId, meetingPassword, meetingUrl values
- Fix async/await compatibility issues in booking scenario test utilities

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

* fix: resolve TypeScript errors in CalendarServiceMap mocking

- Extract CalendarServiceMap promise to variable to fix 'always true' condition
- Ensure vi.mocked is called on Promise type for proper mockResolvedValue access
- Add await keywords to calendar mock calls in test files
- Maintain existing functionality while making code type-safe

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

* Remove Exchange 2013 and 2016

* Mock Exchange in all tests

* Fix tests

* Remove exchange 2013 and 2016 from app store index

* Fix merge error

* Await when getting calendar service

* Fix selectedSlot test

* Add missing variable

* Update openapi.json

* Updated CalendarService imports

* try again

* WIP migrate calendar apps to ES6

* Revert "WIP migrate calendar apps to ES6"

This reverts commit 15bf2c83305e82050779d0bca3380fa9573db1e0.

* Revert changes back to e23991024a5a455c14aa05f100a5b56288db343a

This reverts all calendar service changes that were causing circular dependency issues during builds and E2E tests.

* Remove circular dependency for location constants

* Update yarn.lock with removed package

* Add empty map when running E2E

* Type fies

* Fix merge conflict

* Remove logging statements

* Throw error and reset state if failing to load app

* Revert "Remove Exchange 2013 and 2016"

This reverts commit fedaf6346bd9e4c63337276d5a6f9e8c3943056c.

* Re-introduce exchange{2013,2016}

Revert the removal in app-store/index.ts also.

* Trying to fix tests

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-08-26 14:35:35 +01:00
Hemant M MehtaandEunjae Lee af07c411ee fix: auto-open filter dropdown after adding a new filter (#23330)
* fix: auto-open-filter-dropdown

closes #23326

* coderabbit suggestion

Signed-off-by: Hemant M Mehta <hemant29mehta@gmail.com>

* coderabbit suggestion

Signed-off-by: Hemant M Mehta <hemant29mehta@gmail.com>

* removing clickable event

Signed-off-by: Hemant M Mehta <hemant29mehta@gmail.com>

* updated filters

Signed-off-by: Hemant M Mehta <hemant29mehta@gmail.com>

* fix e2e tests

---------

Signed-off-by: Hemant M Mehta <hemant29mehta@gmail.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-08-26 13:22:43 +00:00
a22ca4758c test: Performance tests suite (#21226)
* fix: add org email into ics

* perf test

* update

* fix: address bot comments on performance tests

- Add validation to randomSleep function to ensure min <= max
- Replace new Date().getTime() with Date.now() for better performance
- Move hardcoded test credentials to environment variables
- Add HTTP timeout to requests in helpers.js
- Improve DOM element checking to be less brittle
- Fix spike test sleep duration from 0.01s to 0.1s
- Make GitHub workflow BASE_URL configurable with inputs
- Fix README filename reference from booking_flow.js to booking.js
- Add documentation for new environment variables

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

* Add script and small sec check

* chore: fix script path

* chore: fix script path

* chore: change default url

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
2025-08-26 09:16:19 -03:00
Anik Dhabal Babu bb3ba88f18 fix: Skip trial button visibility (#23034)
* fix: flaky e2e

* fix: skip trial button visibility

* fix type error

* fix test

* update

* Update skipTeamTrials.test.ts

* fix test
2025-08-26 17:25:37 +05:30
Udit Takkar ce646c590f chore: update daily dependencies (#23272) 2025-08-26 08:55:18 -03:00
Morgan ba8cca55e8 refactor: convert lucky user to service class (#23329)
* refactor: lucky user to service class with DI

* fix: unit test filterHostsByLeadThreshold.test.ts

* fixup! fix: unit test filterHostsByLeadThreshold.test.ts

* fix: get lucky user test missing mocks

* fix: get lucky user hosts not within interval

* chore: bump platform library

* chore: bump platform library

* chore: add DAILY_API_KEY to turborepo.json db seed

* chore: add DAILY_API_KEY to cache-db action

* fixup! chore: add DAILY_API_KEY to cache-db action

* chore: remove log in fixture

* chore: bump platform library

* chore: refactor test team-emails.e2e

* only failing test

* run all api v2 tests

* chore: bump platform library

* revert change to turbo.json and action in cache-db

* chore: use inPerson location in team-emails.e2e
2025-08-26 08:43:41 -03:00
sean-brydon ae738fae35 revert: "fix: remove fallback permssions on organization Id. (#22769)" (#23013)
* Revert "fix: remove fallback permssions on organization Id. (#22769)"

This reverts commit 30061c0d58.

Removes the manage permission concept and fallback logic to simplify
permission checking back to direct role-based checks.

* remove manage from workflows

* fix type error and remove manage from components
2025-08-26 08:32:49 -03:00
Benny Joo 2922c8fe77 perf: Move runtime redirects to build-time redirects in next.config.js (#23325)
* routing forms

* auth new

* settings route

* apps installed

* next.config.js
2025-08-26 20:20:12 +09:00
emrysal 4599740637 chore: release v5.6.6
Post release / release (push) Canceled after 0s
2025-08-26 11:10:41 +00:00
Benny Joo 8d17ca536e perf: bundle i18n json into code (#23303)
* bundle i18n jsons into code

* refactor

* use absolute url
2025-08-26 12:01:44 +01:00
Rajiv Sahal 3fa7b30485 chore: update playwright config for platform examples (#23352) 2025-08-26 09:55:08 +00:00
Rajiv Sahal d9adb6c1fc feat: atoms e2e tests (#23273)
* e2e tests for availability settings atom

* init tests for booking page

* add data test id for event type card

* update tests for booking page

* add data test ids for playwright tests

* update test coverage for booking page
2025-08-26 07:35:45 +00:00
Anik Dhabal Babu f4944a05c9 fix: recurring event isn't working (#23346) 2025-08-26 12:40:40 +05:30
Sahitya Chandra 6ba49a73df fix: fr and de translation for buttons on the onboarding flow (#23347)
* fix: added french translation for buttons on the onboarding

* added some german also
2025-08-26 01:02:28 +05:30
Anik Dhabal Babu babeb1f31d fix get booking issue (#23344) 2025-08-25 18:42:30 +00:00
Hariom BalharaandDevin AI 13cbb59e5e fix: Custom Event Redirect in Routing Form with Dropdown Based fields (#17870)
Co-authored-by: hariom@cal.com <hariombalhara@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-25 13:46:18 -04:00
Sahitya Chandra 8d2a072645 fix: german translation for button in the onboarding flow (#23336) 2025-08-25 20:33:59 +05:30
Joe Au-YeungandAlex van Andel b2244503b1 fix: Missing jose module in saml-jackson package (#23291)
* Ensure packages are bundled with code

* Update dependencies

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-08-25 15:36:31 +01:00
Alex van AndelandDevin AI 9d67e1c473 feat: enable Stripe Adaptive Currencies for subscription flows (#23328)
* feat: enable Stripe Adaptive Currencies for subscription flows

- Add STRIPE_ADAPTIVE_CURRENCIES_ENABLED environment variable to control feature
- Remove hardcoded USD currency when Adaptive Currencies is enabled
- Update documentation with comprehensive setup instructions
- Focus on team subscriptions, platform billing, and premium username flows
- Maintain backward compatibility with USD default when disabled

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

* refactor: simplify Stripe Adaptive Currencies implementation by removing currency parameter

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-25 14:00:49 +00:00
Benny Joo 489d56dad1 perf: Make /icons page a static page (#23324)
* write _generateMetadataForStaticPage

* refactor /icons/page.tsx
2025-08-25 14:34:46 +02:00
Anik Dhabal Babu 532c2f57a1 fix: show correct input type for location (#23228) 2025-08-25 12:06:33 +00:00
Bandhan MajumderandEunjae Lee efca90507b refactor: improve most booked & least booked (#23192)
* refactor: improve most booked & least booked

* add 2 new charts and trpc handlers

* update arg

* update comment

* completed booking filter

* update

* add change in component level and remove unused translations

* current time and accepted status logic handler

* resize some charts

* restore some i18n

* restore i18n

* restore i18n

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-08-25 11:17:07 +00:00
Hariom BalharaandDevin AI 28ff809739 feat: add startsWith operator to routing forms (#23026)
* feat: add startsWith operator to routing forms

- Implement startsWith operator in BasicConfig.ts for RAQB
- Add custom JsonLogic operation for startsWith evaluation
- Add startsWith support to jsonLogicToPrisma for reporting
- Supports both form field response routing and attribute routing
- Uses case-insensitive matching via normalize function

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

* test: add comprehensive unit tests for startsWith operator

- Add tests for operator availability in FormFieldsBaseConfig and AttributesBaseConfig
- Add tests for JsonLogic configuration validation
- Add tests for text widget operator integration
- All 15 tests pass including new startsWith operator tests

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

* test: move startsWith operator tests to jsonLogic.test.ts

- Add comprehensive startsWith tests to packages/lib/raqb/jsonLogic.test.ts
- Remove startsWith tests from packages/app-store/routing-forms/__tests__/config.test.ts
- Tests verify case-insensitive matching, edge cases, and falsy value handling
- All tests pass: jsonLogic.test.ts (18 passed) and config.test.ts (12 passed)

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

* refactor: remove unused startsWith operator from jsonLogicToPrisma.ts

- Remove startsWith entry from OPERATOR_MAP in jsonLogicToPrisma.ts
- File is currently unused so this cleanup keeps PR focused
- Core startsWith functionality remains intact in jsonLogic.ts and BasicConfig.ts

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

* test: add comprehensive edge case tests for startsWith operator

- Add test cases for non-string first argument (haystack) handling
- Cover null, undefined, numbers, booleans, arrays, and objects
- Update startsWith implementation to safely handle non-string inputs
- Ensure operation returns false without throwing runtime errors
- All tests now pass: 19 passed | 1 skipped

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

* refactor: improve startsWith operation with type guards and String.prototype.startsWith

- Change argument types from string to unknown for better type safety
- Add type guards to return false when either operand is not a string
- Use String.prototype.startsWith instead of indexOf for cleaner implementation
- Keep existing empty-second check and normalize function calls
- All tests continue to pass: 19 passed | 1 skipped

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-25 15:57:34 +05:30
Benny Joo 8e73421094 perf: do not use next/dist/server image optimizer (#23207)
* write image utils

* refactor

* better comment
2025-08-25 15:13:44 +05:30
5bab2af500 chore: some minor QA fixes (#22727)
Post release / release (push) Canceled after 0s
* Various fixes

* Remove tooltip

* another string change

* Fix width of custom event name modal to prevent overflow

* Booking questions fixes

* minor string fix

* checkbox alignment

* padding in reassign dialog

* fix: update FormBuilder tests for checkbox UI changes

- Updated test utilities to work with CheckboxField instead of BooleanToggleGroupField
- Fixed badge expectations to check for 'optional' instead of 'required'
- Updated dialog interaction methods to use checkbox.checked instead of button text
- All 13 FormBuilder tests now pass successfully

Fixes failing tests in PR #22727 caused by UI changes from toggle buttons to checkboxes for required field selection.

Co-Authored-By: amit@cal.com <samit91848@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: amit@cal.com <samit91848@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-08-25 18:11:18 +09:00
Carina WollendorferandCarinaWolli 9b7c6585d8 fix: add variables dropdown UI (#23214)
* Ui fixes for variables dropdown

* remove other translation keys

* fix test

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-08-25 16:42:27 +09:00
Sahitya ChandraandSomay Chauhan 58930c4c5e fix: correct conversion of username parameter (#23203)
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2025-08-25 12:32:16 +05:30
Anirban SinghaandAnik Dhabal Babu 9412226a76 fix: Trim input before submission(availability) (#23317)
* fix: Trim input before submission(availability)

Signed-off-by: Anirban Singha <anirbansingha20@gmail.com>

* refactor: Persist trimmed value in form

Signed-off-by: Anirban Singha <anirbansingha20@gmail.com>

* Update NewScheduleButton.tsx

---------

Signed-off-by: Anirban Singha <anirbansingha20@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-08-25 06:08:10 +00:00
Benny Joo df12581879 perf: reduce build time and deployment size by removing unused routes (#23319) 2025-08-25 04:56:55 +00:00
Alex van Andel 0ec1638ca7 chore: Clean up some code, like console.log, tsx -> ts (#23321) 2025-08-25 01:30:45 +00:00
emrysal 2516e18ebb chore: release v5.6.5 2025-08-24 23:34:40 +00:00
Volnei Munhoz d2f8076c70 perf: Improve memory cache on build (#23286)
* Improve memory cache on build

* Improve memory cache on build
2025-08-22 19:57:25 +00:00
Keith WilliamsandDevin AI cbfb11015e feat: add E2E test to prevent duplicate schedule API calls (#23232)
* feat: add E2E test to prevent duplicate schedule API calls

- Tests individual user events (/{user}/{eventTypeSlug})
- Tests team events (/team/{teamSlug}/{eventTypeSlug})
- Tests organization team events (/org/{orgSlug}/{teamSlug}/{eventTypeSlug})
- Ensures /api/trpc/getSchedule and /v2/slots/available are called only once
- Prevents regression of double API calls in Booker component
- Uses flexible assertions to handle environment issues while detecting duplicates

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

* refactor: rename E2E test file to booking-duplicate-api-calls.e2e.ts

Addresses GitHub comment from keithwillcode to follow naming convention

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

* fix: correct tRPC API route pattern to api/trpc/slots/getSchedule

Addresses GitHub comment from keithwillcode - the route pattern should be
api/trpc/slots/getSchedule instead of api/trpc/viewer.slots.getSchedule
in all page.route() interceptors for the getSchedule endpoint.

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

* fix: revert to correct tRPC API route pattern api/trpc/viewer.slots.getSchedule

The previous change to api/trpc/slots/getSchedule was incorrect. The tRPC router
structure shows that getSchedule is under viewer.slots, which translates to the
HTTP endpoint api/trpc/viewer.slots.getSchedule as used by trpc.viewer.slots.getSchedule.useQuery().

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-22 18:25:40 +00:00
2d90d11e9e feat: Add custom label for organizer default app and link meeting (#23114)
* Create `CalVideoSettings` type

* Create `LocationOptionContainer`

* Extract cal video settings to `CalVideoSettings`

* Refactor `<Locations />` component to use `<CalVideoSettings />`

* Unify naming

* Move location folder back under components

* Type fixes

* Extract types

* Add custom label prop to default app types

* Extract `LocationInput` component

* Create `DefaultLocationSettings` component

* Move `locations` folder

* Add custom label field for link and organizer default locations

* Type fixes

* Save the custom label in db

* Include location custom label when querying db

* Display custom label on booking page

* Add translations

* Type fixes

* Change `customLabel` to `supportsCustomLabel`

* Remove unused code

* Show the customLabel on booking page

* Fix hover tooltip text as well

* Fix displaying exact value on booking page

* Add missing translation

* Remove default label of "Value"

* Replace nullish collescing with OR

* Replace nullish collescing with OR

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-08-22 16:34:14 +00:00
Lauris Skraucis 7ae0d6b7cc refactor: run atoms e2e when e2e label added (#23281)
* refactor: run atoms e2e when e2e label added

* refactor: rename atoms-e2e to e2e-atoms
2025-08-22 14:32:19 +00:00
Lauris Skraucis e0dc2ba03a feat: authentication secured event types (#23217)
* feat: EventType bookingRequiresAuthentication column

* feat: toggle EventType bookingRequiresAuthentication via api

* feat: check bookingRequiresAuthentication when booking

* docs: v2 swagger

* fix: ts error

* refactor: use Forbidden exception instead of Unauthorized

* refactor: use findFirst instead of unique

* fix: only count accepted memberships

* fix: orgs schedules docs

* regenerate docs

* chore: update platform libraries

* fix: unit test

* fix: e2e test
2025-08-22 15:22:50 +01:00
288 changed files with 6037 additions and 3512 deletions
+7 -1
View File
@@ -74,8 +74,14 @@ jobs:
uses: ./.github/workflows/e2e-embed-react.yml
secrets: inherit
e2e-atoms:
name: Tests
needs: [lint, build, build-atoms, build-api-v2]
uses: ./.github/workflows/e2e-atoms.yml
secrets: inherit
required:
needs: [lint, type-check, unit-test, integration-test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
needs: [lint, type-check, unit-test, integration-test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store, e2e-atoms]
if: always()
runs-on: buildjet-2vcpu-ubuntu-2204
steps:
@@ -1,15 +1,7 @@
name: Atoms E2E Tests
on:
pull_request:
branches:
- main
paths:
- 'packages/platform/atoms/**'
- 'packages/platform/examples/base/**'
- 'apps/api/v2/**'
- '.github/workflows/atoms-e2e.yml'
workflow_call:
permissions:
actions: write
contents: read
@@ -25,7 +17,7 @@ env:
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
jobs:
atoms-e2e:
e2e-atoms:
timeout-minutes: 15
name: Atoms E2E Tests
runs-on: buildjet-4vcpu-ubuntu-2204
@@ -45,13 +37,13 @@ jobs:
uses: actions/upload-artifact@v4
if: always()
with:
name: atoms-e2e-test-results
name: e2e-atoms-test-results
path: test-results
retention-days: 7
- name: Upload Playwright Report
uses: actions/upload-artifact@v4
if: always()
with:
name: atoms-e2e-playwright-report
name: e2e-atoms-playwright-report
path: playwright-report
retention-days: 7
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
with:
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
organization-name: calcom
ignore-labels: "admin, app-store, ai, authentication, automated-testing, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, dba, devops, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
ignore-labels: "admin, app-store, ai, authentication, automated-testing, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, dba, devops, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, self-hosted, teams, webhooks, workflows, zapier"
apply-labels-from-issue:
runs-on: ubuntu-latest
+65
View File
@@ -0,0 +1,65 @@
name: Performance Tests
on:
release:
types: [created]
workflow_dispatch:
inputs:
base_url:
description: 'Base URL for performance tests'
required: false
default: 'https://cal.dev'
jobs:
performance-tests:
runs-on: ubuntu-latest
env:
BASE_URL: ${{ github.event.inputs.base_url || 'https://cal.dev' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup K6
uses: grafana/setup-k6-action@v1
with:
k6-version: '1.0.0'
- name: Run Smoke Tests
uses: grafana/run-k6-action@v1
env:
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
K6_CLOUD_PROJECT_ID: ${{ secrets.K6_CLOUD_PROJECT_ID }}
with:
path: tests/performance/smoke/*.js
cloud-run-locally: true
fail-fast: true
- name: Run Load Tests
uses: grafana/run-k6-action@v1
env:
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
K6_CLOUD_PROJECT_ID: ${{ secrets.K6_CLOUD_PROJECT_ID }}
with:
path: tests/performance/load/*.js
cloud-run-locally: true
fail-fast: true
- name: Run Stress Tests
uses: grafana/run-k6-action@v1
env:
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
K6_CLOUD_PROJECT_ID: ${{ secrets.K6_CLOUD_PROJECT_ID }}
with:
path: tests/performance/stress/*.js
cloud-run-locally: true
fail-fast: true
- name: Run Spike Tests
uses: grafana/run-k6-action@v1
env:
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
K6_CLOUD_PROJECT_ID: ${{ secrets.K6_CLOUD_PROJECT_ID }}
with:
path: tests/performance/spike/*.js
cloud-run-locally: true
fail-fast: true
+9 -1
View File
@@ -195,6 +195,13 @@ jobs:
uses: ./.github/workflows/e2e-embed-react.yml
secrets: inherit
e2e-atoms:
name: Tests
needs: [changes, check-label, build, build-atoms, build-api-v2]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e-atoms.yml
secrets: inherit
analyze:
name: Analyze Build
needs: [build]
@@ -204,7 +211,7 @@ jobs:
merge-reports:
name: Merge reports
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
needs: [changes, check-label, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
needs: [changes, check-label, e2e, e2e-embed, e2e-embed-react, e2e-app-store, e2e-atoms]
uses: ./.github/workflows/merge-reports.yml
secrets: inherit
@@ -249,6 +256,7 @@ jobs:
e2e-embed,
e2e-embed-react,
e2e-app-store,
e2e-atoms,
]
if: always()
runs-on: buildjet-2vcpu-ubuntu-2204
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
+1 -1
View File
@@ -38,7 +38,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.320",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.333",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
"@calcom/prisma": "*",
@@ -22,6 +22,7 @@ import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { KyselyModule } from "@/modules/kysely/kysely.module";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
@@ -53,6 +54,7 @@ import { Module } from "@nestjs/common";
StripeModule,
TeamsModule,
TeamsEventTypesModule,
MembershipsModule,
],
providers: [
TokensRepository,
@@ -13,10 +13,14 @@ import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.ser
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import {
AuthOptionalUser,
GetOptionalUser,
} from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
@@ -96,6 +100,7 @@ export class BookingsController_2024_08_13 {
) {}
@Post("/")
@UseGuards(OptionalApiAuthGuard)
@ApiOperation({
summary: "Create a booking",
description: `
@@ -138,9 +143,10 @@ export class BookingsController_2024_08_13 {
async createBooking(
@Body(new CreateBookingInputPipe())
body: CreateBookingInput,
@Req() request: Request
@Req() request: Request,
@GetOptionalUser() user: AuthOptionalUser
): Promise<CreateBookingOutput_2024_08_13> {
const booking = await this.bookingsService.createBooking(request, body);
const booking = await this.bookingsService.createBooking(request, body, user);
if (Array.isArray(booking)) {
await this.bookingsService.billBookings(booking);
File diff suppressed because it is too large Load Diff
@@ -3,6 +3,7 @@ import { AppModule } from "@/app.module";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { Locales } from "@/lib/enums/locales";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import {
CreateManagedUserData,
CreateManagedUserOutput,
@@ -80,7 +81,7 @@ describe("Managed user bookings 2024-08-13", () => {
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule],
imports: [AppModule, UsersModule, MembershipsModule],
}).compile();
app = moduleRef.createNestApplication();
@@ -772,6 +773,77 @@ describe("Managed user bookings 2024-08-13", () => {
});
});
describe("event type booking requires authentication", () => {
let eventTypeRequiringAuthenticationId: number;
let body: CreateBookingInput_2024_08_13;
beforeAll(async () => {
const eventTypeRequiringAuthentication = await eventTypesRepositoryFixture.create(
{
title: `event-type-requiring-authentication-${randomString()}`,
slug: `event-type-requiring-authentication-${randomString()}`,
length: 60,
requiresConfirmation: true,
bookingRequiresAuthentication: true,
},
secondManagedUser.user.id
);
eventTypeRequiringAuthenticationId = eventTypeRequiringAuthentication.id;
body = {
start: new Date(Date.UTC(2030, 0, 9, 15, 0, 0)).toISOString(),
eventTypeId: eventTypeRequiringAuthenticationId,
attendee: {
email: "external@example.com",
name: "External Attendee",
timeZone: "Europe/Rome",
},
};
});
it("can't be booked without credentials", async () => {
await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(401);
});
it("can't be booked with managed user credentials who is not admin and not event type owner", async () => {
await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(403);
});
it("can be booked with managed user credentials who is event type owner", async () => {
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(201);
const bookingId = response.body.data.id;
await bookingsRepositoryFixture.deleteById(bookingId);
});
it("can be booked with managed user credentials who is admin", async () => {
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
.expect(201);
const bookingId = response.body.data.id;
await bookingsRepositoryFixture.deleteById(bookingId);
});
});
afterAll(async () => {
await userRepositoryFixture.delete(firstManagedUser.user.id);
await userRepositoryFixture.delete(secondManagedUser.user.id);
@@ -6,9 +6,12 @@ import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/servi
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { getPagination } from "@/lib/pagination/pagination";
import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
import { BillingService } from "@/modules/billing/services/billing.service";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { MembershipsService } from "@/modules/memberships/services/memberships.service";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
@@ -18,7 +21,14 @@ import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-eve
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { UsersService } from "@/modules/users/services/users.service";
import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository";
import { ConflictException, Injectable, Logger, NotFoundException } from "@nestjs/common";
import {
ConflictException,
ForbiddenException,
Injectable,
Logger,
NotFoundException,
UnauthorizedException,
} from "@nestjs/common";
import { BadRequestException } from "@nestjs/common";
import { Request } from "express";
import { DateTime } from "luxon";
@@ -95,10 +105,12 @@ export class BookingsService_2024_08_13 {
private readonly organizationsRepository: OrganizationsRepository,
private readonly teamsRepository: TeamsRepository,
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
private readonly membershipsRepository: MembershipsRepository,
private readonly membershipsService: MembershipsService,
private readonly errorsBookingsService: ErrorsBookingsService_2024_08_13
) {}
async createBooking(request: Request, body: CreateBookingInput) {
async createBooking(request: Request, body: CreateBookingInput, authUser: AuthOptionalUser) {
let bookingTeamEventType = false;
try {
const eventType = await this.getBookedEventType(body);
@@ -108,6 +120,7 @@ export class BookingsService_2024_08_13 {
if (!eventType) {
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
}
await this.checkBookingRequiresAuthenticationSetting(eventType, authUser);
if (eventType.schedulingType === "MANAGED") {
throw new BadRequestException(
@@ -142,6 +155,56 @@ export class BookingsService_2024_08_13 {
}
}
async checkBookingRequiresAuthenticationSetting(
eventType: EventTypeWithOwnerAndTeam,
authUser: AuthOptionalUser
) {
if (!eventType.bookingRequiresAuthentication) return true;
if (!authUser) {
throw new UnauthorizedException(
"checkBookingRequiresAuthentication - request must be authenticated by passing credentials belonging to event type owner, host or team or org admin or owner."
);
}
const authUserId = authUser.id;
const authUserRole = authUser.role;
const eventTypeId = eventType.id;
const teamId = eventType.teamId;
const bookingUserId = eventType.owner?.id || null;
if (authUserRole === "ADMIN") return;
if (bookingUserId === authUserId) return;
if (eventTypeId) {
const [isUserHost, isUserAssigned] = await Promise.all([
this.eventTypesRepository.isUserHostOfEventType(authUserId, eventTypeId),
this.eventTypesRepository.isUserAssignedToEventType(authUserId, eventTypeId),
]);
if (isUserHost || isUserAssigned) return;
}
if (teamId) {
const membership = await this.membershipsRepository.getUserAdminOrOwnerTeamMembership(
authUserId,
teamId
);
if (membership) return;
}
if (
bookingUserId &&
(await this.membershipsService.isUserOrgAdminOrOwnerOfAnotherUser(authUserId, bookingUserId))
) {
return;
}
throw new ForbiddenException(
"checkBookingRequiresAuthentication - user is not authorized to access this event type. User has to be either event type owner, host, team admin or owner or org admin or owner."
);
}
async getBookedEventType(body: CreateBookingInput) {
if (body.eventTypeId) {
return await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(body.eventTypeId);
@@ -498,6 +498,7 @@ describe("Event types Endpoints", () => {
lightThemeHex: "#fafafa",
},
customName: `{Event type title} between {Organiser} and {Scheduler}`,
bookingRequiresAuthentication: true,
};
return request(app.getHttpServer())
@@ -570,6 +571,7 @@ describe("Event types Endpoints", () => {
];
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
expect(createdEventType.bookingRequiresAuthentication).toEqual(true);
eventType = responseBody.data;
});
});
@@ -1026,6 +1028,7 @@ describe("Event types Endpoints", () => {
lightThemeHex: "#fafafa",
},
customName: `{Event type title} betweennnnnnnnnnn {Organiser} and {Scheduler}`,
bookingRequiresAuthentication: false,
};
return request(app.getHttpServer())
@@ -1120,6 +1123,8 @@ describe("Event types Endpoints", () => {
eventType.color = updatedEventType.color;
eventType.bookingFields = updatedEventType.bookingFields;
eventType.calVideoSettings = updatedEventType.calVideoSettings;
expect(updatedEventType.bookingRequiresAuthentication).toEqual(false);
});
});
@@ -2028,7 +2033,6 @@ describe("Event types Endpoints", () => {
const username = name;
let user: User;
let legacyEventTypeId1: number;
let legacyEventTypeId2: number;
beforeAll(async () => {
const moduleRef = await withApiAuth(
@@ -132,4 +132,26 @@ export class EventTypesRepository_2024_06_14 {
async deleteEventType(eventTypeId: number) {
return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } });
}
async isUserHostOfEventType(userId: number, eventTypeId: number) {
const eventType = await this.dbRead.prisma.eventType.findFirst({
where: {
id: eventTypeId,
hosts: { some: { userId: userId } },
},
select: { id: true },
});
return !!eventType;
}
async isUserAssignedToEventType(userId: number, eventTypeId: number) {
const eventType = await this.dbRead.prisma.eventType.findFirst({
where: {
id: eventTypeId,
users: { some: { id: userId } },
},
select: { id: true },
});
return !!eventType;
}
}
@@ -90,6 +90,7 @@ type Input = Pick<
| "hideOrganizerEmail"
| "calVideoSettings"
| "hidden"
| "bookingRequiresAuthentication"
>;
@Injectable()
@@ -129,6 +130,7 @@ export class OutputEventTypesService_2024_06_14 {
hideOrganizerEmail,
calVideoSettings,
hidden,
bookingRequiresAuthentication,
} = databaseEventType;
const locations = this.transformLocations(databaseEventType.locations);
@@ -206,6 +208,7 @@ export class OutputEventTypesService_2024_06_14 {
hideOrganizerEmail,
calVideoSettings,
hidden,
bookingRequiresAuthentication,
};
}
@@ -0,0 +1,12 @@
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { PrismaAttributeRepository as PrismaAttributeRepositoryLib } from "@calcom/platform-libraries/repositories";
import { PrismaClient } from "@calcom/prisma";
@Injectable()
export class PrismaAttributeRepository extends PrismaAttributeRepositoryLib {
constructor(private readonly dbWrite: PrismaWriteService) {
super(dbWrite.prisma as unknown as PrismaClient);
}
}
@@ -0,0 +1,12 @@
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { PrismaHostRepository as PrismaHostRepositoryLib } from "@calcom/platform-libraries/repositories";
import { PrismaClient } from "@calcom/prisma";
@Injectable()
export class PrismaHostRepository extends PrismaHostRepositoryLib {
constructor(private readonly dbWrite: PrismaWriteService) {
super(dbWrite.prisma as unknown as PrismaClient);
}
}
@@ -0,0 +1,27 @@
import { PrismaAttributeRepository } from "@/lib/repositories/prisma-attribute.repository";
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
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 { Injectable } from "@nestjs/common";
import { LuckyUserService as BaseLuckyUserService } from "@calcom/platform-libraries/bookings";
@Injectable()
export class LuckyUserService extends BaseLuckyUserService {
constructor(
bookingRepository: PrismaBookingRepository,
hostRepository: PrismaHostRepository,
oooRepository: PrismaOOORepository,
userRepository: PrismaUserRepository,
attributeRepository: PrismaAttributeRepository
) {
super({
bookingRepository: bookingRepository,
hostRepository: hostRepository,
oooRepository: oooRepository,
userRepository: userRepository,
attributeRepository: attributeRepository,
});
}
}
@@ -2,6 +2,8 @@ import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.st
import { ExecutionContext } from "@nestjs/common";
import { createParamDecorator } from "@nestjs/common";
export type AuthOptionalUser = ApiAuthGuardUser | null;
export const GetOptionalUser = createParamDecorator<
keyof ApiAuthGuardUser | (keyof ApiAuthGuardUser)[],
ExecutionContext
@@ -89,7 +89,6 @@ export class BillingService implements OnModuleDestroy {
teamId: teamId.toString(),
plan: plan.toString(),
},
currency: "usd",
subscription_data: {
metadata: {
teamId: teamId.toString(),
@@ -117,7 +116,6 @@ export class BillingService implements OnModuleDestroy {
teamId: teamId.toString(),
plan: plan.toString(),
},
currency: "usd",
});
if (!url) throw new InternalServerErrorException("Failed to create Stripe session.");
@@ -403,7 +401,6 @@ export class BillingService implements OnModuleDestroy {
fromReschedule?: string | null;
}
) {
if (this.configService.get("e2e")) {
return true;
}
@@ -465,7 +462,6 @@ export class BillingService implements OnModuleDestroy {
async onModuleDestroy() {
try {
await this.billingQueue.close();
} catch (err) {
this.logger.error(err);
@@ -1,7 +1,7 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { Injectable } from "@nestjs/common";
import { MembershipRole } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
@Injectable()
export class MembershipsRepository {
@@ -90,6 +90,26 @@ export class MembershipsRepository {
return !!adminMembership;
}
async getOrgIdsWhereUserIsAdminOrOwner(userId: number) {
const userOrgAdminMemberships = await this.dbRead.prisma.membership.findMany({
where: {
userId: userId,
accepted: true,
role: {
in: ["OWNER", "ADMIN"],
},
team: {
parentId: null,
},
},
select: {
teamId: true,
},
});
return userOrgAdminMemberships.map((m) => m.teamId);
}
async createMembership(teamId: number, userId: number, role: MembershipRole, accepted: boolean) {
const membership = await this.dbRead.prisma.membership.create({
data: {
@@ -103,4 +123,46 @@ export class MembershipsRepository {
return membership;
}
async getUserAdminOrOwnerTeamMembership(userId: number, teamId: number) {
return this.dbRead.prisma.membership.findFirst({
where: {
userId: userId,
teamId: teamId,
accepted: true,
role: {
in: ["OWNER", "ADMIN"],
},
},
});
}
async getUserMembershipInOneOfOrgs(userId: number, orgIds: number[]) {
return this.dbRead.prisma.membership.findFirst({
where: {
userId: userId,
accepted: true,
teamId: {
in: orgIds,
},
team: {
parentId: null,
},
},
});
}
async getUserMembershipInOneOfOrgsTeams(userId: number, orgIds: number[]) {
return this.dbRead.prisma.membership.findFirst({
where: {
userId: userId,
accepted: true,
team: {
parentId: {
in: orgIds,
},
},
},
});
}
}
@@ -21,4 +21,28 @@ export class MembershipsService {
"teamId"
);
}
async isUserOrgAdminOrOwnerOfAnotherUser(userId: number, anotherUserId: number) {
const orgIdsWhereUserIsAdminOrOwner = await this.membershipsRepository.getOrgIdsWhereUserIsAdminOrOwner(
userId
);
if (orgIdsWhereUserIsAdminOrOwner.length === 0) {
return false;
}
const anotherUserOrgMembership = await this.membershipsRepository.getUserMembershipInOneOfOrgs(
anotherUserId,
orgIdsWhereUserIsAdminOrOwner
);
if (anotherUserOrgMembership) return true;
const anotherUserOrgTeamMembership = await this.membershipsRepository.getUserMembershipInOneOfOrgsTeams(
anotherUserId,
orgIdsWhereUserIsAdminOrOwner
);
return !!anotherUserOrgTeamMembership;
}
}
@@ -75,6 +75,7 @@ type Input = Pick<
| "team"
| "calVideoSettings"
| "hidden"
| "bookingRequiresAuthentication"
>;
@Injectable()
@@ -43,6 +43,8 @@ export class OrganizationsTeamsSchedulesController {
@DocsTags("Orgs / Teams / Schedules")
@ApiOperation({ summary: "Get all team member schedules" })
async getTeamSchedules(
// note(Lauirs): putting orgId so swagger is generacted correctly
@Param("orgId", ParseIntPipe) _orgId: number,
@Param("teamId", ParseIntPipe) teamId: number,
@Query() queryParams: SkipTakePagination
): Promise<GetSchedulesOutput_2024_06_11> {
@@ -65,6 +67,9 @@ export class OrganizationsTeamsSchedulesController {
summary: "Get schedules of a team member",
})
async getUserSchedules(
// note(Lauirs): putting orgId and teamId so swagger is generacted correctly
@Param("orgId", ParseIntPipe) _orgId: number,
@Param("teamId", ParseIntPipe) _teamId: number,
@Param("userId", ParseIntPipe) userId: number
): Promise<GetSchedulesOutput_2024_06_11> {
const schedules = await this.schedulesService.getUserSchedules(userId);
@@ -1,6 +1,9 @@
import { VERSION_2024_09_04 } from "@/lib/api-versions";
import { OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER, OPTIONAL_X_CAL_CLIENT_ID_HEADER } from "@/lib/docs/headers";
import { GetOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
import {
AuthOptionalUser,
GetOptionalUser,
} from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
import { GetReservedSlotOutput_2024_09_04 } from "@/modules/slots/slots-2024-09-04/outputs/get-reserved-slot.output";
import { GetSlotsOutput_2024_09_04 } from "@/modules/slots/slots-2024-09-04/outputs/get-slots.output";
@@ -26,7 +29,6 @@ import {
ApiResponse as DocsResponse,
ApiQuery,
} from "@nestjs/swagger";
import { User } from "@prisma/client";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -261,7 +263,7 @@ export class SlotsController_2024_09_04 {
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
async reserveSlot(
@Body() body: ReserveSlotInput_2024_09_04,
@GetOptionalUser() user: User
@GetOptionalUser() user: AuthOptionalUser
): Promise<ReserveSlotOutputResponse_2024_09_04> {
const reservedSlot = await this.slotsService.reserveSlot(body, user?.id);
+257
View File
@@ -5547,6 +5547,97 @@
]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/schedules": {
"get": {
"operationId": "OrganizationsTeamsSchedulesController_getTeamSchedules",
"summary": "Get all team member schedules",
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-secret-key",
"in": "header",
"description": "For platform customers - OAuth client secret key",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-client-id",
"in": "header",
"description": "For platform customers - OAuth client ID",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "take",
"required": false,
"in": "query",
"description": "Maximum number of items to return",
"example": 25,
"schema": {
"minimum": 1,
"maximum": 250,
"default": 250,
"type": "number"
}
},
{
"name": "skip",
"required": false,
"in": "query",
"description": "Number of items to skip",
"example": 0,
"schema": {
"minimum": 0,
"default": 0,
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11"
}
}
}
}
},
"tags": [
"Orgs / Teams / Schedules"
]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/stripe/connect": {
"get": {
"operationId": "OrganizationsStripeController_getTeamStripeConnectUrl",
@@ -5720,6 +5811,22 @@
"type": "string"
}
},
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "userId",
"required": true,
@@ -13215,6 +13322,71 @@
]
}
},
"/v2/teams/{teamId}/schedules": {
"get": {
"operationId": "TeamsSchedulesController_getTeamSchedules",
"summary": "Get all team member schedules",
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "take",
"required": false,
"in": "query",
"description": "Maximum number of items to return",
"example": 25,
"schema": {
"minimum": 1,
"maximum": 250,
"default": 250,
"type": "number"
}
},
{
"name": "skip",
"required": false,
"in": "query",
"description": "Number of items to skip",
"example": 0,
"schema": {
"minimum": 0,
"default": 0,
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11"
}
}
}
}
},
"tags": [
"Teams / Schedules"
]
}
},
"/v2/teams/{teamId}/verified-resources/emails/verification-code/request": {
"post": {
"operationId": "TeamsVerifiedResourcesController_requestEmailVerificationCode",
@@ -16504,6 +16676,14 @@
}
]
},
"hidden": {
"type": "boolean"
},
"bookingRequiresAuthentication": {
"type": "boolean",
"default": false,
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
},
"locations": {
"type": "array",
"description": "Locations where the event will take place. If not provided, cal video link will be used as the location.",
@@ -18166,6 +18346,13 @@
}
]
},
"hidden": {
"type": "boolean"
},
"bookingRequiresAuthentication": {
"type": "boolean",
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
},
"ownerId": {
"type": "number",
"example": 10
@@ -18195,6 +18382,8 @@
"successRedirectUrl",
"isInstantEvent",
"scheduleId",
"hidden",
"bookingRequiresAuthentication",
"ownerId",
"users"
]
@@ -18515,6 +18704,14 @@
}
]
},
"hidden": {
"type": "boolean"
},
"bookingRequiresAuthentication": {
"type": "boolean",
"default": false,
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
},
"locations": {
"type": "array",
"description": "Locations where the event will take place. If not provided, cal video link will be used as the location.",
@@ -20343,6 +20540,14 @@
}
]
},
"hidden": {
"type": "boolean"
},
"bookingRequiresAuthentication": {
"type": "boolean",
"default": false,
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
},
"schedulingType": {
"type": "string",
"enum": [
@@ -20777,6 +20982,13 @@
}
]
},
"hidden": {
"type": "boolean"
},
"bookingRequiresAuthentication": {
"type": "boolean",
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
},
"teamId": {
"type": "number"
},
@@ -21223,6 +21435,14 @@
}
]
},
"hidden": {
"type": "boolean"
},
"bookingRequiresAuthentication": {
"type": "boolean",
"default": false,
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
},
"hosts": {
"type": "array",
"items": {
@@ -22589,6 +22809,11 @@
},
"unit": {
"type": "object",
"enum": [
"hour",
"minute",
"day"
],
"description": "Unit for the offset time",
"example": "hour"
}
@@ -22612,6 +22837,9 @@
"type": {
"type": "string",
"default": "beforeEvent",
"enum": [
"beforeEvent"
],
"description": "Trigger type for the workflow",
"example": "beforeEvent"
}
@@ -22635,6 +22863,9 @@
"type": {
"type": "string",
"default": "afterEvent",
"enum": [
"afterEvent"
],
"description": "Trigger type for the workflow",
"example": "afterEvent"
}
@@ -22650,6 +22881,9 @@
"type": {
"type": "string",
"default": "eventCancelled",
"enum": [
"eventCancelled"
],
"description": "Trigger type for the workflow"
}
},
@@ -22663,6 +22897,9 @@
"type": {
"type": "string",
"default": "newEvent",
"enum": [
"newEvent"
],
"description": "Trigger type for the workflow"
}
},
@@ -22676,6 +22913,9 @@
"type": {
"type": "string",
"default": "rescheduleEvent",
"enum": [
"rescheduleEvent"
],
"description": "Trigger type for the workflow"
}
},
@@ -22697,6 +22937,9 @@
"type": {
"type": "string",
"default": "afterGuestsCalVideoNoShow",
"enum": [
"afterGuestsCalVideoNoShow"
],
"description": "Trigger type for the workflow",
"example": "afterGuestsCalVideoNoShow"
}
@@ -22720,6 +22963,9 @@
"type": {
"type": "string",
"default": "afterHostsCalVideoNoShow",
"enum": [
"afterHostsCalVideoNoShow"
],
"description": "Trigger type for the workflow",
"example": "afterHostsCalVideoNoShow"
}
@@ -23314,6 +23560,17 @@
"properties": {
"type": {
"type": "object",
"enum": [
[
"beforeEvent",
"eventCancelled",
"newEvent",
"afterEvent",
"rescheduleEvent",
"afterHostsCalVideoNoShow",
"afterGuestsCalVideoNoShow"
]
],
"description": "Trigger type for the workflow"
}
},
@@ -15,7 +15,6 @@ export class ApiKeysRepositoryFixture {
async createApiKey(userId: number, expiresAt: Date | null, teamId?: number) {
const keyString = randomBytes(16).toString("hex");
const hashedKey = createHash("sha256").update(keyString).digest("hex");
console.log("createApiKey Fixutre", { hashedKey, keyString });
const apiKey = await this.prismaWriteClient.apiKey.create({
data: {
userId,
@@ -1,32 +0,0 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { Button } from "@calcom/ui/components/button";
export const generateMetadata = () =>
_generateMetadata(
(t) => `${t("access_denied")} | ${APP_NAME}`,
() => "",
undefined,
undefined,
"/403"
);
async function Error403() {
const t = await getTranslate();
return (
<div className="bg-subtle flex h-screen">
<div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
<h1 className="font-cal text-emphasis text-6xl">403</h1>
<h2 className="text-emphasis mt-6 text-2xl font-medium">{t("dont_have_access_this_page")}</h2>
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">
{t("you_need_admin_or_owner_privileges_to_access")}
</p>
<Button href={WEBAPP_URL}>{t("go_back_home")}</Button>
</div>
</div>
);
}
export default Error403;
@@ -1,22 +0,0 @@
"use client";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui/components/button";
import { showToast } from "@calcom/ui/components/toast";
export default function CopyButton({ error }: { error: string }) {
const { t } = useLocale();
return (
<Button
color="secondary"
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
StartIcon="copy"
onClick={() => {
navigator.clipboard.writeText(error);
showToast("Link copied!", "success");
}}>
{t("copy")}
</Button>
);
}
@@ -1,47 +0,0 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { APP_NAME } from "@calcom/lib/constants";
import { Button } from "@calcom/ui/components/button";
import CopyButton from "./copy-button";
export const generateMetadata = () =>
_generateMetadata(
(t) => `${t("something_unexpected_occurred")} | ${APP_NAME}`,
() => "",
undefined,
undefined,
"/500"
);
async function Error500({ searchParams }: { searchParams: { error?: string } }) {
const t = await getTranslate();
return (
<div className="bg-subtle flex h-screen">
<div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
<h1 className="font-cal text-emphasis text-6xl">500</h1>
<h2 className="text-emphasis mt-6 text-2xl font-medium">{t("500_error_message")}</h2>
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">{t("something_went_wrong_on_our_end")}</p>
{searchParams?.error && (
<div className="mb-8 flex flex-col">
<p className="text-default mb-4 max-w-2xl text-sm">
{t("please_provide_following_text_to_suppport")}:
</p>
<pre className="bg-emphasis text-emphasis w-full max-w-2xl whitespace-normal break-words rounded-md p-4">
{searchParams.error}
<br />
<CopyButton error={searchParams.error} />
</pre>
</div>
)}
<Button href="mailto:support@cal.com">{t("contact_support")}</Button>
<Button color="secondary" href="javascript:history.back()" className="ml-2">
{t("go_back")}
</Button>
</div>
</div>
);
}
export default Error500;
@@ -1,7 +0,0 @@
import { redirect } from "next/navigation";
const Page = () => {
redirect("/apps/installed/calendar");
};
export default Page;
@@ -1,7 +0,0 @@
import { redirect } from "next/navigation";
const ServerPage = async () => {
redirect("/apps/routing-forms/forms");
};
export default ServerPage;
@@ -1,17 +0,0 @@
import { _generateMetadata } from "app/_utils";
import { redirect } from "next/navigation";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("apps"),
(t) => t("admin_apps_description"),
undefined,
undefined,
"/settings/admin/apps"
);
const Page = () => {
redirect("/settings/admin/apps/calendar");
};
export default Page;
@@ -6,9 +6,9 @@ import type { Resource } from "@calcom/features/pbac/domain/types/permission-reg
import { PERMISSION_REGISTRY, CrudAction } from "@calcom/features/pbac/domain/types/permission-registry";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@calcom/ui/classNames";
import { Checkbox, Label } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { Tooltip } from "@calcom/ui/components/tooltip";
import { Checkbox, Label } from "@calcom/ui/form";
import { usePermissions } from "./usePermissions";
@@ -1,7 +1,7 @@
"use client";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui/button";
import { Button } from "@calcom/ui/components/button";
import { useRoleSheetState } from "../hooks/useRoleQueryStates";
@@ -14,10 +14,10 @@ import {
} from "@calcom/features/pbac/domain/types/permission-registry";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/button";
import { Button } from "@calcom/ui/components/button";
import { Form, TextField, Checkbox, Label } from "@calcom/ui/components/form";
import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from "@calcom/ui/components/sheet";
import { showToast } from "@calcom/ui/components/toast";
import { Form, TextField, Checkbox, Label } from "@calcom/ui/form";
import { revalidateTeamRoles } from "../actions";
import { AdvancedPermissionGroup } from "./AdvancedPermissionGroup";
@@ -2,9 +2,9 @@
import type { Scope } from "@calcom/features/pbac/domain/types/permission-registry";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge } from "@calcom/ui/badge";
import { Button } from "@calcom/ui/button";
import classNames from "@calcom/ui/classNames";
import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import {
Dropdown,
DropdownMenuContent,
@@ -44,17 +44,5 @@ describe("usePermissions", () => {
expect(getResourcePermissionLevel("eventType", permissions)).toBe("all");
});
it("should return 'all' for resource with manage permission", () => {
const permissions = ["eventType.manage"];
expect(getResourcePermissionLevel("eventType", permissions)).toBe("all");
});
it("should return 'all' for resource with manage permission even if other permissions are missing", () => {
const permissions = ["eventType.manage", "eventType.read"]; // Has manage and read, but missing create, update, delete
expect(getResourcePermissionLevel("eventType", permissions)).toBe("all");
});
});
});
@@ -51,20 +51,14 @@ export function usePermissions(): UsePermissionsReturn {
return "all";
}
// Check if user has manage permission for this resource
const hasManagePermission = permissions.includes(`${resource}.manage`);
if (hasManagePermission) {
return "all";
}
// Filter out internal keys like _resource and manage when checking for individual permissions
const crudPermissions = Object.keys(resourceConfig)
.filter((action) => !action.startsWith("_") && action !== "manage")
// Filter out internal keys like _resource when checking permissions
const allResourcePerms = Object.keys(resourceConfig)
.filter((action) => !action.startsWith("_"))
.map((action) => `${resource}.${action}`);
const hasAllCrudPerms = crudPermissions.every((p) => permissions.includes(p));
const hasAllPerms = allResourcePerms.every((p) => permissions.includes(p));
const hasReadPerm = permissions.includes(`${resource}.${CrudAction.Read}`);
if (hasAllCrudPerms) return "all";
if (hasAllPerms) return "all";
if (hasReadPerm) return "read";
return "none";
};
+42
View File
@@ -80,6 +80,48 @@ export const _generateMetadata = async (
};
};
export const _generateMetadataForStaticPage = (
title: string,
description: string,
hideBranding?: boolean,
origin?: string,
pathname?: string
) => {
const _pathname = pathname ?? "";
const canonical = buildCanonical({ path: _pathname, origin: origin ?? CAL_URL });
const titleSuffix = `| ${APP_NAME}`;
const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`;
const metadataBase = new URL(IS_CALCOM ? getCalcomUrl() : WEBAPP_URL);
const metadata = {
title: title.length === 0 ? APP_NAME : displayedTitle,
description,
alternates: { canonical },
openGraph: {
description: truncateOnWord(description, 158),
url: canonical,
type: "website",
siteName: APP_NAME,
title: displayedTitle,
},
metadataBase,
};
const image =
SEO_IMG_OGIMG +
constructGenericImage({
title: metadata.title,
description: metadata.description,
});
return {
...metadata,
openGraph: {
...metadata.openGraph,
images: [image],
},
};
};
export const generateMeetingMetadata = async (
meeting: MeetingImageProps,
getTitle: (t: TFunction<string, undefined>) => string,
@@ -1,7 +1,9 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { parseRequestData } from "app/api/parseRequestData";
import * as jose from "jose";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import * as dummy from "openid-client";
import { uuid } from "short-uuid";
import jackson from "@calcom/features/ee/sso/lib/jackson";
@@ -9,6 +11,10 @@ import type { OAuthTokenReq } from "@calcom/features/ee/sso/lib/jackson";
import logger from "@calcom/lib/logger";
async function handler(req: NextRequest) {
// Need these imports to fix import errors with jackson
// https://github.com/ory/polis/blob/main/pages/api/import-hack.ts
const unused = dummy; // eslint-disable-line @typescript-eslint/no-unused-vars
const unused2 = jose; // eslint-disable-line @typescript-eslint/no-unused-vars
const { oauthController } = await jackson();
const log = logger.getSubLogger({ prefix: ["[SAML token]"] });
+10 -8
View File
@@ -193,25 +193,27 @@ async function getHandler(request: NextRequest) {
const response = await fetch(filteredLogo);
const arrayBuffer = await response.arrayBuffer();
let buffer: Buffer = Buffer.from(arrayBuffer);
let contentType = response.headers.get("content-type") || "image/png";
// If we need to resize the team logos (via Next.js' built-in image processing)
// Resize the team logos if needed
if (teamLogos[logoDefinition.source] && logoDefinition.w) {
const { detectContentType, optimizeImage } = await import("next/dist/server/image-optimizer");
buffer = await optimizeImage({
const { resizeImage } = await import("@calcom/lib/server/imageUtils");
const { buffer: outBuffer, contentType: outContentType } = await resizeImage({
buffer,
contentType: (await detectContentType(buffer)) ?? "image/jpeg",
quality: 100,
width: logoDefinition.w,
height: logoDefinition.h, // optional
height: logoDefinition.h,
quality: 100,
contentType,
});
buffer = outBuffer;
contentType = outContentType;
}
// Create a new response with the image buffer
const imageResponse = new NextResponse(buffer as BodyInit);
// Set the appropriate headers
imageResponse.headers.set("Content-Type", response.headers.get("content-type") || "image/png");
imageResponse.headers.set("Content-Type", contentType);
imageResponse.headers.set("Cache-Control", "s-maxage=86400, stale-while-revalidate=60");
return imageResponse;
@@ -12,7 +12,7 @@ import stripe from "@calcom/features/ee/payments/server/stripe";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -50,7 +50,7 @@ async function getHandler(req: NextRequest, { params }: { params: Promise<Params
if (!team) {
const prevTeam = await prisma.team.findFirstOrThrow({ where: { id } });
metadata = teamMetadataSchema.safeParse(prevTeam.metadata);
metadata = teamMetadataStrictSchema.safeParse(prevTeam.metadata);
if (!metadata.success) {
throw new HttpError({ statusCode: 400, message: "Invalid team metadata" });
}
@@ -80,7 +80,7 @@ async function getHandler(req: NextRequest, { params }: { params: Promise<Params
}
if (!metadata) {
metadata = teamMetadataSchema.safeParse(team.metadata);
metadata = teamMetadataStrictSchema.safeParse(team.metadata);
if (!metadata.success) {
throw new HttpError({ statusCode: 400, message: "Invalid team metadata" });
}
-5
View File
@@ -1,5 +0,0 @@
import { redirect } from "next/navigation";
export default function Page() {
redirect(process.env.NEXT_PUBLIC_WEBAPP_URL || "https://app.cal.com");
}
+14 -13
View File
@@ -1,4 +1,5 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { _generateMetadataForStaticPage } from "app/_utils";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import localFont from "next/font/local";
@@ -8,6 +9,15 @@ import type { IconName } from "@calcom/ui/components/icon";
import { lucideIconList } from "../../../../packages/ui/components/icon/icon-list.mjs";
import { IconGrid } from "./IconGrid";
export const dynamic = "force-static";
export const metadata: Metadata = _generateMetadataForStaticPage(
"Icons Showcase",
"",
undefined,
undefined,
"/icons"
);
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
const calFont = localFont({
src: "../../fonts/CalSans-SemiBold.woff2",
@@ -16,25 +26,16 @@ const calFont = localFont({
display: "swap",
weight: "600",
});
export const generateMetadata = async () => {
return await _generateMetadata(
(t) => t("icon_showcase"),
() => "",
undefined,
undefined,
"/icons"
);
};
export default async function IconsPage() {
export default function IconsPage() {
const icons = Array.from(lucideIconList).sort() as IconName[];
const t = await getTranslate();
return (
<div className={`${interFont.variable} ${calFont.variable}`}>
<div className="bg-subtle flex h-screen">
<IconSprites />
<div className="bg-default m-auto min-w-full rounded-md p-10 text-right ltr:text-left">
<h1 className="text-emphasis font-cal text-2xl font-medium">{t("icons_showcase")}</h1>
<h1 className="text-emphasis font-cal text-2xl font-medium">Icons Showcase</h1>
<IconGrid title="Regular Icons" icons={icons} />
<IconGrid
title="Filled Icons"
+4 -1
View File
@@ -2,6 +2,7 @@
import { TrpcProvider } from "app/_trpc/trpc-provider";
import { SessionProvider } from "next-auth/react";
import { usePathname } from "next/navigation";
import CacheProvider from "react-inlinesvg/provider";
import { WebPushProvider } from "@calcom/features/notifications/WebPushContext";
@@ -17,11 +18,13 @@ type ProvidersProps = {
};
export function Providers({ isEmbed, children, nonce }: ProvidersProps) {
const isBookingPage = useIsBookingPage();
const pathname = usePathname();
const isOnboardingPage = pathname?.startsWith("/getting-started");
return (
<SessionProvider>
<TrpcProvider>
{!isBookingPage ? <PlainChat nonce={nonce} /> : null}
{!isBookingPage && !isOnboardingPage ? <PlainChat nonce={nonce} /> : null}
{!isEmbed && !isBookingPage && <NotificationSoundHandler />}
{/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */}
<CacheProvider>
-7
View File
@@ -1,7 +0,0 @@
import { redirect } from "next/navigation";
const Page = () => {
redirect("/apps/routing-forms/forms");
};
export default Page;
@@ -218,7 +218,7 @@ export const ReassignDialog = ({
placeholder={t("search")}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<div className="scroll-bar flex h-[150px] flex-col gap-0.5 overflow-y-scroll rounded-md border p-1">
<div className="scroll-bar mt-2 flex h-[150px] flex-col gap-0.5 overflow-y-scroll rounded-md border p-1">
{teamMemberOptions.map((member) => (
<label
key={member.value}
@@ -539,18 +539,11 @@ const NewRoutingManager = ({
url: chosenEventUrls.eventBookingAbsoluteUrl,
});
console.log("SETTING NEW VALUE", {
type: "reschedule_to_different_event_new_tab",
reschedulerWindow,
newBooking: null,
});
reroutingState.setValue({
type: "reschedule_to_different_event_new_tab",
reschedulerWindow,
newBooking: null,
});
console.log("VALUE NOW", JSON.stringify(reroutingState));
}
function reroutingPreview() {
@@ -84,6 +84,7 @@ const AppConnectionItem = (props: IAppConnectionItem) => {
{t("this_app_requires_connected_account", {
appName: title,
dependencyName: dependency.name,
interpolation: { escapeValue: false },
})}
</span>
@@ -92,7 +92,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
loading={isPageLoading}
onClick={() => nextStep()}
disabled={disabledNextButton}>
{firstCalendar ? `${t("continue")}` : `${t("next_step_text")}`}
{firstCalendar ? `${t("connect_your_video")}` : `${t("connect_calendar_first")}`}
</Button>
</>
);
@@ -105,7 +105,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
disabled={!hasAnyInstalledVideoApps}
loading={isPageLoading}
onClick={() => nextStep()}>
{t("next_step_text")}
{t("set_availability")}
</Button>
</>
);
@@ -81,7 +81,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
className="mt-2 w-full justify-center p-2 text-sm sm:mt-8"
loading={availabilityForm.formState.isSubmitting}
disabled={availabilityForm.formState.isSubmitting}>
{t("next_step_text")}
{t("complete_profile")}
</Button>
</div>
</Form>
@@ -164,7 +164,7 @@ const UserProfile = ({ user }: UserProfileProps) => {
EndIcon="arrow-right"
type="submit"
className="mt-8 w-full items-center justify-center">
{t("finish")}
{t("finish_and_start")}
</Button>
</form>
);
@@ -88,6 +88,7 @@ const UserSettings = (props: IUserSettingsProps) => {
id="name"
name="name"
type="text"
placeholder="John Doe"
autoComplete="off"
autoCorrect="off"
/>
@@ -121,7 +122,7 @@ const UserSettings = (props: IUserSettingsProps) => {
className="mt-8 flex w-full flex-row justify-center"
loading={mutation.isPending}
disabled={mutation.isPending}>
{t("next_step_text")}
{t("connect_your_calendar")}
</Button>
</form>
);
@@ -22,7 +22,6 @@ export function NotificationSoundHandler() {
const response = await fetch("/ring.mp3");
const arrayBuffer = await response.arrayBuffer();
audioBufferRef.current = await audioContextRef.current.decodeAudioData(arrayBuffer);
console.log("Audio file loaded and decoded");
}
return true;
@@ -62,7 +61,6 @@ export function NotificationSoundHandler() {
source.loop = true;
source.start(0);
console.log("Sound started playing");
setTimeout(() => {
if (sourceRef.current === source) {
@@ -101,7 +99,7 @@ export function NotificationSoundHandler() {
useEffect(() => {
if (!("serviceWorker" in navigator)) {
console.log("ServiceWorker not available");
console.warn("ServiceWorker not available");
return;
}
@@ -120,6 +120,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
<TextField
ref={usernameRef}
name="username"
placeholder="john"
value={inputUsernameValue}
autoComplete="none"
autoCapitalize="none"
@@ -6,9 +6,9 @@ import { APP_NAME } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants/permissions";
import { Icon } from "@calcom/ui/components/icon";
import { Avatar } from "@calcom/ui/components/avatar";
import { Button } from "@calcom/ui/components/button";
import { Icon } from "@calcom/ui/components/icon";
import { hasPermission } from "../../../../../packages/platform/utils/permissions";
@@ -32,8 +32,6 @@ export default function Authorize() {
permissions: 7,
};
console.log("These are the search params:", queryString);
const permissions = Object.values(PERMISSIONS_GROUPED_MAP).map((value) => {
let permissionsMessage = "";
const hasReadPermission = hasPermission(client.permissions, value.read);
@@ -42,7 +42,7 @@ const getStepsAndHeadersForUser = (t: TFunction) => {
}[] = [
{
title: t("welcome_to_cal_header", { appName: APP_NAME }),
subtitle: [t("we_just_need_basic_info"), t("edit_form_later_subtitle")],
subtitle: [t("we_just_need_basic_info")],
},
{
title: t("connect_your_calendar"),
@@ -57,8 +57,9 @@ const getStepsAndHeadersForUser = (t: TFunction) => {
{
title: t("set_availability"),
subtitle: [
t("set_availability_getting_started_subtitle_1"),
t("set_availability_getting_started_subtitle_2"),
`${t("set_availability_getting_started_subtitle_1")} ${t(
"set_availability_getting_started_subtitle_2"
)}`,
],
},
{
+10 -5
View File
@@ -19,6 +19,8 @@ import {
LowestRatedMembersTable,
MostBookedTeamMembersTable,
MostCancelledBookingsTables,
MostCompletedTeamMembersTable,
LeastCompletedTeamMembersTable,
PopularEventsTable,
RecentFeedbackTable,
TimezoneBadge,
@@ -82,20 +84,23 @@ function InsightsPageContent() {
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
<div className="sm:col-span-2">
<PopularEventsTable />
</div>
<MostBookedTeamMembersTable />
<LeastBookedTeamMembersTable />
<MostCompletedTeamMembersTable />
<LeastCompletedTeamMembersTable />
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
<MostCancelledBookingsTables />
<HighestNoShowHostTable />
<div className="sm:col-span-2">
<PopularEventsTable />
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
<HighestRatedMembersTable />
<LowestRatedMembersTable />
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
<div className="sm:col-span-2">
<RecentFeedbackTable />
</div>
@@ -16,7 +16,7 @@ import { Button } from "@calcom/ui/components/button";
import { Select } from "@calcom/ui/components/form";
import { TextField, Label, InputError } from "@calcom/ui/components/form";
import { ProgressBar } from "@calcom/ui/components/progress-bar";
import { showToast } from "@calcom/ui/toast";
import { showToast } from "@calcom/ui/components/toast";
import { BillingCreditsSkeleton } from "./BillingCreditsSkeleton";
+3 -1
View File
@@ -197,6 +197,7 @@ export default function Signup({
useEffect(() => {
if (redirectUrl) {
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
localStorage.setItem("onBoardingRedirect", redirectUrl);
}
}, [redirectUrl]);
@@ -216,7 +217,6 @@ export default function Signup({
if (err.checkoutSessionId) {
const stripe = await getStripe();
if (stripe) {
console.log("Redirecting to stripe checkout");
const { error } = await stripe.redirectToCheckout({
sessionId: err.checkoutSessionId,
});
@@ -470,6 +470,7 @@ export default function Signup({
showToast("error", t("username_required"));
return;
}
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
localStorage.setItem("username", username);
const sp = new URLSearchParams();
// @NOTE: don't remove username query param as it's required right now for stripe payment page
@@ -539,6 +540,7 @@ export default function Signup({
if (prepopulateFormValues?.username) {
// If username is present we save it in query params to check for premium
searchQueryParams.set("username", prepopulateFormValues.username);
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
localStorage.setItem("username", prepopulateFormValues.username);
}
if (token) {
+26 -2
View File
@@ -196,6 +196,8 @@ const nextConfig = (phase) => {
experimental: {
// externalize server-side node_modules with size > 1mb, to improve dev mode performance/RAM usage
optimizePackageImports: ["@calcom/ui"],
webpackMemoryOptimizations: true,
webpackBuildWorker: true,
},
productionBrowserSourceMaps: true,
/* We already do type check on GH actions */
@@ -231,7 +233,15 @@ const nextConfig = (phase) => {
images: {
unoptimized: true,
},
webpack: (config, { webpack, buildId, isServer }) => {
webpack: (config, { webpack, buildId, isServer, dev }) => {
if (!dev) {
if (config.cache) {
config.cache = Object.freeze({
type: "memory",
});
}
}
if (isServer) {
// Module not found fix @see https://github.com/boxyhq/jackson/issues/1535#issuecomment-1704381612
config.plugins.push(
@@ -285,6 +295,10 @@ const nextConfig = (phase) => {
source: "/routing/:path*",
destination: "/apps/routing-forms/:path*",
},
{
source: "/routing-forms",
destination: "/apps/routing-forms/forms",
},
{
source: "/success/:path*",
has: [
@@ -538,7 +552,7 @@ const nextConfig = (phase) => {
},
{
source: "/apps/routing-forms",
destination: "/routing/forms",
destination: "/apps/routing-forms/forms",
permanent: false,
},
{
@@ -546,6 +560,11 @@ const nextConfig = (phase) => {
destination: "/app-store/:path*",
permanent: true,
},
{
source: "/auth/new",
destination: process.env.NEXT_PUBLIC_WEBAPP_URL || "https://app.cal.com",
permanent: true,
},
{
source: "/auth/signup",
destination: "/signup",
@@ -643,6 +662,11 @@ const nextConfig = (phase) => {
destination: "/settings/platform",
permanent: true,
},
{
source: "/settings/admin/apps",
destination: "/settings/admin/apps/calendar",
permanent: true,
},
// OAuth callbacks when sent to localhost:3000(w would be expected) should be redirected to corresponding to WEBAPP_URL
...(process.env.NODE_ENV === "development" &&
// Safer to enable the redirect only when the user is opting to test out organizations
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "5.6.4",
"version": "5.6.7",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -44,8 +44,8 @@
"@calcom/trpc": "*",
"@calcom/tsconfig": "*",
"@calcom/ui": "*",
"@daily-co/daily-js": "^0.76.0",
"@daily-co/daily-react": "^0.22.0",
"@daily-co/daily-js": "^0.83.1",
"@daily-co/daily-react": "^0.23.2",
"@dub/analytics": "^0.0.27",
"@dub/embed-core": "^0.0.10",
"@dub/embed-react": "^0.0.10",
@@ -114,6 +114,7 @@
"next-themes": "^0.2.0",
"nodemailer": "^6.7.8",
"nuqs": "^1.20.0",
"openid-client": "6.5.0",
"otplib": "^12.0.1",
"posthog-js": "^1.164.1",
"posthog-node": "^4.2.0",
@@ -0,0 +1,145 @@
import { expect } from "@playwright/test";
import { createTeamEventType } from "./fixtures/users";
import { test } from "./lib/fixtures";
test.describe("Duplicate API Calls Prevention", () => {
test.afterEach(({ users }) => users.deleteAll());
test("should detect when schedule endpoints are called multiple times for individual user events", async ({
page,
users,
}) => {
const user = await users.create();
const eventType = user.eventTypes.find((e) => e.slug === "30-min");
const trpcCalls: string[] = [];
const apiV2Calls: string[] = [];
// Intercept tRPC getSchedule calls - pattern matches trpc.viewer.slots.getSchedule.useQuery()
await page.route("**/api/trpc/viewer.slots.getSchedule**", async (route) => {
trpcCalls.push(route.request().url());
await route.continue();
});
await page.route("**/api/v2/slots/available**", async (route) => {
apiV2Calls.push(route.request().url());
await route.continue();
});
await page.goto(`/${user.username}/${eventType?.slug}`);
await page.waitForTimeout(5000);
const totalCalls = trpcCalls.length + apiV2Calls.length;
if (totalCalls === 0) {
console.log("No API calls detected - environment may have issues preventing page load");
expect(totalCalls).toBeGreaterThanOrEqual(0);
} else {
expect(totalCalls).toBeLessThanOrEqual(1);
expect(trpcCalls.length).toBeLessThanOrEqual(1);
expect(apiV2Calls.length).toBeLessThanOrEqual(1);
}
});
test("should detect when schedule endpoints are called multiple times for team events", async ({
page,
users,
}) => {
const teamOwner = await users.create(
{ name: "Team Owner" },
{
hasTeam: true,
teammates: [{ name: "teammate-1" }],
}
);
const { team } = await teamOwner.getFirstTeamMembership();
const teamEvent = await createTeamEventType(
{ id: teamOwner.id },
{ id: team.id },
{ teamEventSlug: "team-event-test", teamEventTitle: "Team Event Test" }
);
const trpcCalls: string[] = [];
const apiV2Calls: string[] = [];
await page.route("**/api/trpc/viewer.slots.getSchedule**", async (route) => {
trpcCalls.push(route.request().url());
await route.continue();
});
await page.route("**/api/v2/slots/available**", async (route) => {
apiV2Calls.push(route.request().url());
await route.continue();
});
await page.goto(`/team/${team.slug}/${teamEvent.slug}`);
await page.waitForTimeout(5000);
const totalCalls = trpcCalls.length + apiV2Calls.length;
if (totalCalls === 0) {
console.log("No API calls detected - environment may have issues preventing page load");
expect(totalCalls).toBeGreaterThanOrEqual(0);
} else {
expect(totalCalls).toBeLessThanOrEqual(1);
expect(trpcCalls.length).toBeLessThanOrEqual(1);
expect(apiV2Calls.length).toBeLessThanOrEqual(1);
}
});
test("should detect when schedule endpoints are called multiple times for organization team events", async ({
page,
users,
}) => {
const orgOwner = await users.create(
{ name: "Org Owner" },
{
hasTeam: true,
teammates: [{ name: "teammate-1" }],
isOrg: true,
isOrgVerified: true,
hasSubteam: true,
}
);
const { team } = await orgOwner.getFirstTeamMembership();
const { team: org } = await orgOwner.getOrgMembership();
const teamEvent = await createTeamEventType(
{ id: orgOwner.id },
{ id: team.id },
{ teamEventSlug: "org-team-event", teamEventTitle: "Org Team Event" }
);
const trpcCalls: string[] = [];
const apiV2Calls: string[] = [];
await page.route("**/api/trpc/viewer.slots.getSchedule**", async (route) => {
trpcCalls.push(route.request().url());
await route.continue();
});
await page.route("**/api/v2/slots/available**", async (route) => {
apiV2Calls.push(route.request().url());
await route.continue();
});
await page.goto(`/org/${org.slug}/${team.slug}/${teamEvent.slug}`);
await page.waitForTimeout(5000);
const totalCalls = trpcCalls.length + apiV2Calls.length;
if (totalCalls === 0) {
console.log("No API calls detected - environment may have issues preventing page load");
expect(totalCalls).toBeGreaterThanOrEqual(0);
} else {
expect(totalCalls).toBeLessThanOrEqual(1);
expect(trpcCalls.length).toBeLessThanOrEqual(1);
expect(apiV2Calls.length).toBeLessThanOrEqual(1);
}
});
});
+1 -5
View File
@@ -5,7 +5,7 @@ import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/client";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { addFilter, openFilter } from "./filter-helpers";
import { addFilter } from "./filter-helpers";
import { createTeamEventType } from "./fixtures/users";
import type { Fixtures } from "./lib/fixtures";
import { test } from "./lib/fixtures";
@@ -78,7 +78,6 @@ test.describe("Bookings", () => {
await bookingsGetResponse;
await addFilter(page, "dateRange");
await openFilter(page, "dateRange");
await expect(page.locator('[data-testid="date-range-options-c"]')).toBeHidden();
await expect(page.locator('[data-testid="date-range-options-w"]')).toBeHidden();
@@ -244,7 +243,6 @@ test.describe("Bookings", () => {
await bookingsGetResponse;
await addFilter(page, "dateRange");
await openFilter(page, "dateRange");
await expect(page.locator('[data-testid="date-range-options-c"]')).toBeVisible();
await expect(page.locator('[data-testid="date-range-options-w"]')).toBeVisible();
@@ -354,7 +352,6 @@ test.describe("Bookings", () => {
await bookingsGetResponse;
await addFilter(page, "userId");
await openFilter(page, "userId");
const bookingsGetResponse2 = page.waitForResponse(
(response) => response.url().includes("/api/trpc/bookings/get?batch=1") && response.status() === 200
@@ -463,7 +460,6 @@ test.describe("Bookings", () => {
await bookingsGetResponse1;
await addFilter(page, "userId");
await openFilter(page, "userId");
const bookingsGetResponse2 = page.waitForResponse((response) =>
/\/api\/trpc\/bookings\/get.*/.test(response.url())
);
@@ -109,10 +109,7 @@ test("multiple duration selection updates event length correctly", async ({ page
await page.waitForSelector('[data-testid="event-types"]');
await page.click(`text=Multiple duration`);
await page.waitForSelector('[data-testid="event-title"]');
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
"aria-current",
"page"
);
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page");
await page.getByTestId("vertical-tab-event_advanced_tab_title").click();
await page.fill('[name="eventName"]', "{Event duration} event btwn {Organiser} {Scheduler}");
await page.locator('[data-testid="update-eventtype"]').click();
+32 -4
View File
@@ -77,10 +77,7 @@ test.describe("Event Types tests", () => {
// fix the race condition
await page.waitForSelector('[data-testid="event-title"]');
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
"aria-current",
"page"
);
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page");
await page.click("[data-testid=vertical-tab-recurring]");
await expect(page.locator("[data-testid=recurring-event-collapsible]")).toBeHidden();
@@ -447,6 +444,37 @@ test.describe("Event Types tests", () => {
await expect(currentTimezone).toHaveClass(/cursor-not-allowed/);
await expect(page.getByText("New York")).toBeVisible();
});
test("should create recurring event and successfully book multiple occurrences", async ({ page }) => {
const nonce = randomString(3);
const eventTitle = `Recurring Event Test ${nonce}`;
await createNewUserEventType(page, { eventTitle });
await page.waitForSelector('[data-testid="event-title"]');
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page");
await page.click("[data-testid=vertical-tab-recurring]");
await expect(page.locator("[data-testid=recurring-event-collapsible]")).toBeHidden();
await page.click("[data-testid=recurring-event-check]");
await expect(page.locator("[data-testid=recurring-event-collapsible]")).toBeVisible();
await page.locator("[data-testid=recurring-event-collapsible] input[type=number]").nth(1).fill("3");
await saveEventType(page);
await gotoBookingPage(page);
await expect(page.locator("[data-testid=occurrence-input]")).toHaveValue("3");
await selectFirstAvailableTimeSlotNextMonth(page);
await expect(page.locator("[data-testid=recurring-dates]")).toBeVisible();
await bookTimeSlot(page, { isRecurringEvent: true });
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
await expect(page.locator("text=3 occurrences")).toBeVisible();
});
});
test.describe("Interface Language Tests", () => {
+12 -15
View File
@@ -17,15 +17,20 @@ export async function openFilter(page: Page, columnId: string) {
await page.getByTestId(`filter-popover-trigger-${columnId}`).click();
}
export async function addOrOpenFilter(page: Page, columnId: string) {
const existingFilter = page.getByTestId(`filter-popover-trigger-${columnId}`);
if (await existingFilter.isVisible()) {
await openFilter(page, columnId);
} else {
await addFilter(page, columnId);
}
}
/**
* Apply a filter with a specific value
*/
export async function applySelectFilter(page: Page, columnId: string, value: string) {
const existingFilter = page.getByTestId(`filter-popover-trigger-${columnId}`);
if (!(await existingFilter.isVisible())) {
await addFilter(page, columnId);
}
await openFilter(page, columnId);
await addOrOpenFilter(page, columnId);
await selectOptionValue(page, columnId, value);
await page.keyboard.press("Escape");
}
@@ -36,11 +41,7 @@ export async function selectOptionValue(page: Page, columnId: string, value: str
}
export async function applyTextFilter(page: Page, columnId: string, operator: string, operand?: string) {
const existingFilter = page.getByTestId(`filter-popover-trigger-${columnId}`);
if (!(await existingFilter.isVisible())) {
await addFilter(page, columnId);
}
await openFilter(page, columnId);
await addOrOpenFilter(page, columnId);
await page.getByTestId(`text-filter-options-select-${columnId}`).click();
await page
@@ -58,11 +59,7 @@ export async function applyTextFilter(page: Page, columnId: string, operator: st
}
export async function applyNumberFilter(page: Page, columnId: string, operator: string, operand: number) {
const existingFilter = page.getByTestId(`filter-popover-trigger-${columnId}`);
if (!(await existingFilter.isVisible())) {
await addFilter(page, columnId);
}
await openFilter(page, columnId);
await addOrOpenFilter(page, columnId);
await page.getByTestId(`number-filter-options-select-${columnId}`).click();
await page
+1 -1
View File
@@ -123,7 +123,7 @@ export function createAppsFixture(page: Page) {
},
verifyAppsInfoNew: async (app: string, eventTypeId: number) => {
await page.goto(`event-types/${eventTypeId}?tabName=apps`);
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toContainText("Event Setup"); // fix the race condition
await expect(page.getByTestId("vertical-tab-basics")).toContainText("Basics"); // fix the race condition
await expect(page.locator(`[data-testid='${app}-app-switch'][data-state="checked"]`)).toBeVisible();
},
};
+1 -1
View File
@@ -69,7 +69,7 @@ test.describe("private links creation and usage", () => {
// Ensure that private URL is enabled after modifying the event type.
// Additionally, if the slug is changed, ensure that the private URL is updated accordingly.
await page.getByTestId("vertical-tab-event_setup_tab_title").click();
await page.getByTestId("vertical-tab-basics").click();
await page.locator("[data-testid=event-title]").first().fill("somethingrandom");
await page.locator("[data-testid=event-slug]").first().fill("somethingrandom");
await expect(page.locator('[data-testid="event-slug"]').first()).toHaveValue("somethingrandom");
@@ -288,7 +288,6 @@ test.describe("Insights > Routing Filters", () => {
await page.goto(`/insights/routing`);
await addFilter(page, "bookingAttendees");
await openFilter(page, "bookingAttendees");
await page.getByPlaceholder("Filter attendees by name or email").fill("John");
await page.keyboard.press("Enter");
@@ -297,7 +296,6 @@ test.describe("Insights > Routing Filters", () => {
await clearFilters(page);
await addFilter(page, "bookingAttendees");
await openFilter(page, "bookingAttendees");
await page.getByPlaceholder("Filter attendees by name or email").fill("jane.smith@example");
await page.keyboard.press("Enter");
@@ -306,7 +304,6 @@ test.describe("Insights > Routing Filters", () => {
await clearFilters(page);
await addFilter(page, "bookingAttendees");
await openFilter(page, "bookingAttendees");
await page.getByPlaceholder("Filter attendees by name or email").fill("JANE");
await page.keyboard.press("Enter");
+2 -2
View File
@@ -9,7 +9,7 @@ import type { Team, User } from "@calcom/prisma/client";
import { RedirectType } from "@calcom/prisma/client";
import { Prisma } from "@calcom/prisma/client";
import type { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataSchema, teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
const log = logger.getSubLogger({ prefix: ["orgMigration"] });
@@ -588,7 +588,7 @@ async function dbRemoveTeamFromOrg({ teamId }: { teamId: number }) {
});
}
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
const teamMetadata = teamMetadataStrictSchema.parse(team?.metadata);
try {
return await prisma.team.update({
where: {
+5 -6
View File
@@ -155,6 +155,7 @@ export const bookTimeSlot = async (
title?: string;
attendeePhoneNumber?: string;
expectedStatusCode?: number;
isRecurringEvent?: boolean;
}
) => {
// --- fill form
@@ -166,8 +167,9 @@ export const bookTimeSlot = async (
if (opts?.attendeePhoneNumber) {
await page.fill('[name="attendeePhoneNumber"]', opts.attendeePhoneNumber ?? "+918888888888");
}
await submitAndWaitForResponse(page, "/api/book/event", {
action: () => page.locator('[name="email"]').press("Enter"),
const url = opts?.isRecurringEvent ? "/api/book/recurring-event" : "/api/book/event";
await submitAndWaitForResponse(page, url, {
action: () => page.locator('[data-testid="confirm-book-button"]').click(),
expectedStatusCode: opts?.expectedStatusCode,
});
};
@@ -222,10 +224,7 @@ export const createNewSeatedEventType = async (page: Page, args: { eventTitle: s
const eventTitle = args.eventTitle;
await createNewUserEventType(page, { eventTitle });
await page.waitForSelector('[data-testid="event-title"]');
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
"aria-current",
"page"
);
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page");
await page.locator('[data-testid="vertical-tab-event_advanced_tab_title"]').click();
await page.locator('[data-testid="offer-seats-toggle"]').click();
await page.locator('[data-testid="update-eventtype"]').click();
@@ -70,10 +70,7 @@ test.describe("Manage Booking Questions", () => {
const firstEventTypeElement = $eventTypes.first();
await firstEventTypeElement.click();
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
"aria-current",
"page"
);
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page");
await page.getByTestId("vertical-tab-event_advanced_tab_title").click();
});
@@ -117,10 +114,7 @@ test.describe("Manage Booking Questions", () => {
const firstEventTypeElement = $eventTypes.first();
await firstEventTypeElement.click();
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
"aria-current",
"page"
);
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page");
});
await test.step("Open the 'Name' field dialog", async () => {
@@ -642,8 +636,8 @@ async function toggleQuestionRequireStatusAndSave({
await page.locator(`[data-testid="field-${name}"]`).locator('[data-testid="edit-field-action"]').click();
await page
.locator('[data-testid="edit-field-dialog"]')
.locator('[data-testid="field-required"] button')
.locator(`text=${required ? "Yes" : "No"}`)
.locator('[data-testid="field-required"]')
.first()
.click();
await page.locator('[data-testid="field-add-save"]').click();
await saveEventType(page);
@@ -754,11 +748,8 @@ test.describe("Text area min and max characters text", () => {
await page.waitForSelector('[data-testid="event-types"]');
await createNewUserEventType(page, { eventTitle });
await page.waitForSelector('[data-testid="event-title"]');
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toContainText("Event Setup"); //fix the race condition
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
"aria-current",
"page"
);
await expect(page.getByTestId("vertical-tab-basics")).toContainText("Basics"); //fix the race condition
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page");
await page.getByTestId("vertical-tab-event_advanced_tab_title").click();
const insertQuestion = async (questionName: string) => {
const element = page.locator('[data-testid="add-field"]');
@@ -79,11 +79,8 @@ test.describe("Managed Event Types", () => {
const { adminUser, managedEvent } = await setupManagedEvent({ users });
await adminUser.apiLogin();
await page.goto(`/event-types/${managedEvent.id}?tabName=setup`);
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
"aria-current",
"page"
); // fix the race condition
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toContainText("Event Setup"); //fix the race condition
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page"); // fix the race condition
await expect(page.getByTestId("vertical-tab-basics")).toContainText("Basics"); //fix the race condition
await page.locator("#location-select").click();
const optionText = await getByKey(page, "organizer_default_conferencing_app");
await expect(optionText).toBeVisible();
+1 -1
View File
@@ -21,7 +21,7 @@ test.describe("Onboarding", () => {
// tests whether the user makes it to /getting-started
// after login with completedOnboarding false
await page.waitForURL("/getting-started");
await expect(page.locator('text="Next Step"')).toBeVisible(); // Fix race condition
await expect(page.locator('text="Connect your calendar"')).toBeVisible(); // Fix race condition
await test.step("step 1 - User Settings", async () => {
// Check required fields
@@ -780,8 +780,8 @@ const markPhoneNumberAsRequiredAndEmailAsOptional = async (page: Page, eventId:
// Make email as not required
await page.locator('[data-testid="field-email"] [data-testid="edit-field-action"]').click();
const emailRequiredFiled = await page.locator('[data-testid="field-required"]');
await emailRequiredFiled.locator("> :nth-child(2)").click();
const emailRequiredFiled = await page.locator('[data-testid="field-required"]').first();
await emailRequiredFiled.click();
await page.getByTestId("field-add-save").click();
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
@@ -790,12 +790,12 @@ const markPhoneNumberAsRequiredAndEmailAsOptional = async (page: Page, eventId:
const markPhoneNumberAsRequiredField = async (page: Page, eventId: number) => {
await page.goto(`/event-types/${eventId}?tabName=advanced`);
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toContainText("Event Setup"); // fix the race condition
await expect(page.getByTestId("vertical-tab-basics")).toContainText("Basics"); // fix the race condition
await page.locator('[data-testid="field-attendeePhoneNumber"] [data-testid="toggle-field"]').click();
await page.locator('[data-testid="field-attendeePhoneNumber"] [data-testid="edit-field-action"]').click();
const phoneRequiredFiled = await page.locator('[data-testid="field-required"]');
await phoneRequiredFiled.locator("> :nth-child(1)").click();
const phoneRequiredFiled = await page.locator('[data-testid="field-required"]').first();
await phoneRequiredFiled.click();
await page.getByTestId("field-add-save").click();
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
+1 -3
View File
@@ -7,7 +7,7 @@ import { randomString } from "@calcom/lib/random";
import prisma from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { addFilter, openFilter } from "./filter-helpers";
import { addFilter } from "./filter-helpers";
import { test } from "./lib/fixtures";
import { localize } from "./lib/localize";
import { submitAndWaitForResponse } from "./lib/testUtils";
@@ -673,7 +673,6 @@ test.describe("Out of office", () => {
(response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
);
await addFilter(page, "dateRange");
await openFilter(page, "dateRange");
await expect(page.locator('[data-testid="date-range-options-tdy"]')).toBeVisible(); //Today
await expect(page.locator('[data-testid="date-range-options-w"]')).toBeVisible(); //Last 7 Days
@@ -843,7 +842,6 @@ test.describe("Out of office", () => {
);
await entriesListRespPromise1;
await openFilter(page, "dateRange");
const entriesListRespPromise2 = page.waitForResponse(
(response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
);
@@ -1489,7 +1489,7 @@
"connect_your_calendar_and_link": "يمكنك ربط رزنامتك من <1>هنا</1>.",
"default_calendar_selected": "الرزنامة الافتراضية",
"hide_from_profile": "إخفاء من الملف الشخصي",
"event_setup_tab_title": "إعداد الحدث",
"basics": "إعداد الحدث",
"availability_not_found_in_schedule_error": "لا يوجد مواعيد متاحة",
"event_limit_tab_title": "الحدود",
"event_limit_tab_description": "ما تواتر مرات الحجز",
@@ -1489,7 +1489,7 @@
"connect_your_calendar_and_link": "Təqviminizi <1>buradan</1> əlaqələndirə bilərsiniz.",
"default_calendar_selected": "Varsayılan təqvim seçildi",
"hide_from_profile": "Profildən gizlət",
"event_setup_tab_title": "Tədbir Quraşdırması",
"basics": "Tədbir Quraşdırması",
"availability_not_found_in_schedule_error": "Cədvəldə mövcudluq tapılmadı",
"event_limit_tab_title": "Limitlər",
"event_limit_tab_description": "Nə qədər tez-tez rezervasiya edilə bilərsiniz",
@@ -1489,7 +1489,7 @@
"connect_your_calendar_and_link": "Можете да свържете календара си от <1>тук</1>.",
"default_calendar_selected": "Календар по подразбиране",
"hide_from_profile": "Скрий от профила",
"event_setup_tab_title": "Настройка на събитие",
"basics": "Настройка на събитие",
"availability_not_found_in_schedule_error": "Не е намерена свободна заетост в графика",
"event_limit_tab_title": "Лимити",
"event_limit_tab_description": "Колко често можете да бъдете резервиран",
@@ -1489,7 +1489,7 @@
"connect_your_calendar_and_link": "আপনি আপনার ক্যালেন্ডার থেকে সংযুক্ত করতে পারেন <1>এখানে</1>।",
"default_calendar_selected": "ডিফল্ট ক্যালেন্ডার",
"hide_from_profile": "প্রোফাইল থেকে লুকান",
"event_setup_tab_title": "ইভেন্ট সেটআপ",
"basics": "ইভেন্ট সেটআপ",
"availability_not_found_in_schedule_error": "তফসিলের মধ্যে কোনও প্রাপ্যতা পাওয়া যায় না",
"event_limit_tab_title": "সীমা",
"event_limit_tab_description": "আপনি কতবার বুক করা যায়",
@@ -1489,7 +1489,7 @@
"connect_your_calendar_and_link": "Pots connectar el teu calendari des d'<1>aquí</1>.",
"default_calendar_selected": "Calendari predeterminat",
"hide_from_profile": "Amaga del perfil",
"event_setup_tab_title": "Configuració de l'esdeveniment",
"basics": "Configuració de l'esdeveniment",
"availability_not_found_in_schedule_error": "No s'ha trobat disponibilitat a l'agenda",
"event_limit_tab_title": "Límits",
"event_limit_tab_description": "Amb quina freqüència et poden reservar",
@@ -1489,7 +1489,7 @@
"connect_your_calendar_and_link": "Svůj kalendář můžete připojit <1>zde</1>.",
"default_calendar_selected": "Výchozí kalendář",
"hide_from_profile": "Nezobrazovat v profilu",
"event_setup_tab_title": "Nastavení události",
"basics": "Nastavení události",
"availability_not_found_in_schedule_error": "V rozvrhu nebyla nalezena žádná dostupnost",
"event_limit_tab_title": "Omezení",
"event_limit_tab_description": "Jak často lze u vás provést rezervaci",
@@ -1489,7 +1489,7 @@
"connect_your_calendar_and_link": "Du kan forbinde din kalender fra <1>her</1>.",
"default_calendar_selected": "Standard kalender",
"hide_from_profile": "Skjul fra profil",
"event_setup_tab_title": "Opsætning af Begivenheder",
"basics": "Opsætning af Begivenheder",
"availability_not_found_in_schedule_error": "Ingen tilgængelighed fundet i tidsplanen",
"event_limit_tab_title": "Grænser",
"event_limit_tab_description": "Hvor ofte du kan bookes",
@@ -1489,7 +1489,7 @@
"connect_your_calendar_and_link": "Sie können Ihren Kalender von <1>hier</1> verbinden.",
"default_calendar_selected": "Standardkalender",
"hide_from_profile": "Im Profil ausblenden",
"event_setup_tab_title": "Termin-Einrichtung",
"basics": "Termin-Einrichtung",
"availability_not_found_in_schedule_error": "Keine Verfügbarkeit im Zeitplan gefunden",
"event_limit_tab_title": "Limits",
"event_limit_tab_description": "Wie oft können Sie gebucht werden",
@@ -1527,6 +1527,8 @@
"delete_my_account": "Meinen Account löschen",
"start_of_week": "Wochenstart",
"recordings_title": "Aufnahmen",
"finish_and_start": "Einrichtung abschließen und loslegen",
"complete_profile": "Vervollständigen Sie Ihr Profil",
"recording": "Aufnahme",
"happy_scheduling": "Fröhliche Terminplanung",
"select_calendars": "Wählen Sie aus, in welchen Kalendern Sie nach Konflikten suchen wollen, um Doppelbuchungen zu vermeiden.",
@@ -1489,7 +1489,7 @@
"connect_your_calendar_and_link": "Μπορείτε να συνδέσετε το ημερολόγιό σας από <1>εδώ</1>.",
"default_calendar_selected": "Προεπιλεγμένο ημερολόγιο",
"hide_from_profile": "Απόκρυψη από το προφίλ",
"event_setup_tab_title": "Ρύθμιση εκδήλωσης",
"basics": "Ρύθμιση εκδήλωσης",
"availability_not_found_in_schedule_error": "Δεν βρέθηκε διαθεσιμότητα στο πρόγραμμα",
"event_limit_tab_title": "Όρια",
"event_limit_tab_description": "Πόσο συχνά μπορείτε να κάνετε κράτηση",
+36 -19
View File
@@ -278,6 +278,7 @@
"number": "Number",
"checkbox": "Checkbox",
"is_required": "Is required",
"make_field_required": "Make this field required",
"required": "Required",
"optional": "Optional",
"input_type": "Input type",
@@ -354,7 +355,7 @@
"check_email_reset_password": "Check your email. We sent you a link to reset your password.",
"finish": "Finish",
"organization_general_description": "Manage settings for your team language and timezone",
"few_sentences_about_yourself": "A few sentences about yourself. This will appear on your personal url page.",
"few_sentences_about_yourself": "A few sentences about yourself. This will appear on your scheduling link.",
"nearly_there": "Nearly there!",
"nearly_there_instructions": "Last thing, a brief description about you and a photo really helps you get bookings and let people know who they're booking with.",
"set_availability_instructions": "Define ranges of time when you are available on a recurring basis. You can create more of these later and assign them to different calendars.",
@@ -367,7 +368,7 @@
"continue_with": "Continue with {{appName}}",
"connect_your_calendar": "Connect your calendar",
"connect_your_video_app": "Connect your video apps",
"connect_your_video_app_instructions": "Connect your video apps to use them on your event types.",
"connect_your_video_app_instructions": "Connect your preferred video conferencing app to use it for your meetings.",
"connect_your_calendar_instructions": "Connect your calendar to automatically check for busy times and new events as they're scheduled.",
"set_up_later": "Set up later",
"current_time": "Current time",
@@ -931,7 +932,7 @@
"add_attendees": "Add attendees",
"show_advanced_settings": "Show advanced settings",
"event_name": "Event Name",
"event_name_in_calendar": "With event name",
"event_name_in_calendar": "Calendar event name",
"event_name_tooltip": "The name that will appear in calendars",
"meeting_with_user": "{Event type title} between {Organiser} & {Scheduler}",
"additional_inputs": "Additional Inputs",
@@ -947,7 +948,7 @@
"add_input": "Add an Input",
"disable_notes": "Hide notes in calendar",
"disable_notes_description": "For privacy reasons, additional inputs and notes will be hidden in the calendar entry. They will still be sent to your email.",
"requires_confirmation_description": "The booking needs to be manually confirmed before it is pushed to the integrations and a confirmation mail is sent.",
"requires_confirmation_description": "The booking needs to be manually confirmed before it is pushed to your calendar and a confirmation is sent.",
"requires_confirmation_will_block_slot_description": "Unconfirmed bookings still block calendar slots.",
"recurring_event": "Recurring Event",
"recurring_event_description": "People can subscribe for recurring events",
@@ -998,7 +999,7 @@
"quick_chat": "Quick Chat",
"add_new_team_event_type": "Add a new team event type",
"add_new_event_type": "Add a new event type",
"new_event_type_to_book_description": "Create a new event type for people to book times with.",
"new_event_type_to_book_description": "Set up event types to offer different types of meetings.",
"length": "Length",
"minimum_booking_notice": "Minimum Notice",
"offset_toggle": "Offset start times",
@@ -1023,6 +1024,7 @@
"category_apps": "{{category}} apps",
"app_store": "App Store",
"app_store_description": "Connecting people, technology and the workplace.",
"connect_your_video": "Connect your video conferencing app",
"settings": "Settings",
"event_type_moved_successfully": "Event type has been moved successfully",
"next_step_text": "Next Step",
@@ -1238,7 +1240,7 @@
"make_team_private_description": "Your team members won't be able to see other team members when this is turned on.",
"you_cannot_see_team_members": "You cannot see all the team members of a private team.",
"you_cannot_see_teams_of_org": "You cannot see teams of a private organization.",
"allow_booker_to_select_duration": "Allow booker to select duration",
"allow_booker_to_select_duration": "Allow booker to select from multiple durations",
"impersonate_user_tip": "All uses of this feature is audited.",
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
"impersonating_stop_instructions": "Click here to stop",
@@ -1494,7 +1496,7 @@
"connect_your_calendar_and_link": "You can connect your calendar from <1>here</1>.",
"default_calendar_selected": "Default calendar",
"hide_from_profile": "Hide from profile",
"event_setup_tab_title": "Event Setup",
"basics": "Basics",
"availability_not_found_in_schedule_error": "No availability found in schedule",
"event_limit_tab_title": "Limits",
"event_limit_tab_description": "How often you can be booked",
@@ -1524,7 +1526,8 @@
"settings_updated_successfully": "Settings updated successfully",
"error_updating_settings": "Error updating settings",
"personal_cal_url": "My personal {{appName}} URL",
"bio_hint": "A few sentences about yourself. this will appear on your personal url page.",
"bio_hint": "A few sentences about yourself. this will appear on your scheduling link.",
"finish_and_start": "Finish setup and get started",
"user_has_no_bio": "This user has not added a bio yet.",
"bio": "Bio",
"delete_account_modal_title": "Delete Account",
@@ -1580,8 +1583,10 @@
"we_just_need_basic_info": "We just need some basic info to get your profile setup.",
"skip": "Skip",
"do_this_later": "Do this later",
"set_availability_getting_started_subtitle_1": "Define ranges of time when you are available",
"set_availability_getting_started_subtitle_2": "You can customise all of this later in the availability page.",
"set_availability_getting_started_subtitle_1": "Define ranges of time when you are available.",
"set_availability_getting_started_subtitle_2": "Customize it later in the availability page.",
"connect_calendar_first": "Connect a calendar first",
"complete_profile": "Complete your profile",
"connect_calendars_from_app_store": "You can add more calendars from the app store",
"connect_conference_apps": "Connect conference apps",
"connect_calendar_apps": "Connect calendar apps",
@@ -1602,7 +1607,9 @@
"event_duration_info": "The event duration",
"event_time_info": "The event start time",
"event_type_not_found": "EventType not Found",
"location_variable": "Location",
"location_info": "The location of the event",
"additional_notes_variable": "Additional notes",
"additional_notes_info": "The additional notes of booking",
"attendee_name_info": "The person booking's name",
"organizer_name_info": "Organizer's name",
@@ -1621,7 +1628,7 @@
"transcript_from_previous_call": "Transcript from your recent call on {{appName}} is ready to download. Links are valid only for 1 Hour",
"you_can_download_transcript_from_attachments": "You can also download transcript from attachments",
"link_valid_for_12_hrs": "Note: The download link is valid only for 12 hours. You can generate new download link by following instructions <1>here</1>.",
"link_valid_for_12_hrs_description": "Note: The download link is valid only for 12 hours. You can generate new download link by following instructionss.",
"link_valid_for_12_hrs_description": "Note: The download link is only valid for 12 hours. You can generate new download link by following the instructions",
"create_your_first_form": "Create your first form",
"create_your_first_form_description": "With Routing Forms you can ask qualifying questions and route to the correct person or event type.",
"create_your_first_webhook": "Create your first Webhook",
@@ -2115,6 +2122,7 @@
"can_you_try_again": "Can you try again with a different time?",
"verify": "Verify",
"timezone_info": "The timezone of the person receiving",
"timezone_variable": "Timezone",
"event_end_time_variable": "Event end time",
"event_end_time_info": "The event end time",
"cancel_url_variable": "Cancel URL",
@@ -2179,6 +2187,10 @@
"bookings_by_hour": "Bookings by Hour",
"most_booked_members": "Most Booked",
"least_booked_members": "Least Booked",
"most_bookings_scheduled": "Most Bookings Scheduled",
"least_bookings_scheduled": "Least Bookings Scheduled",
"most_bookings_completed": "Most Bookings Completed",
"least_bookings_completed": "Least Bookings Completed",
"events_created": "Events Created",
"events_completed": "Events Completed",
"events_cancelled": "Events Cancelled",
@@ -2730,7 +2742,9 @@
"review": "Review",
"reviewed": "Reviewed",
"unreviewed": "Unreviewed",
"rating_url_variable": "Rating url",
"rating_url_info": "The URL for Rating Feedback Form",
"no_show_url_variable": "No Show url",
"no_show_url_info": "The URL for No Show Feedback",
"no_support_needed": "No Support Needed?",
"hide_support": "Hide Support",
@@ -2920,8 +2934,10 @@
"row": "row",
"rows": "rows",
"attendee_timezone_variable": "Attendee Timezone",
"event_start_time_in_attendee_timezone_variable": "Event Start Time In Attendee Timezone",
"event_start_time_in_attendee_timezone_info": "Event Start Time In Attendee Timezone",
"event_end_time_in_attendee_timezone_info": "Event End Time In Attendee Timezone",
"event_end_time_in_attendee_timezone_variable": "Event End Time In Attendee Timezone",
"salesforce_create_event_on_contact": "Create event on contact, if it exists. Else fallback to lead",
"salesforce_owner_name_to_change": "Owner name to change",
"no_filter_set": "No filter set",
@@ -2985,8 +3001,8 @@
"edit_team_member_weights": "Edit Weights",
"weights_updated_successfully": "Weights updated successfully",
"require_confirmation_for_free_email": "Only require confirmation for free email providers (Ex. @gmail.com, @outlook.com)",
"exclude_emails_that_contain": "Exclude emails that contain ...",
"require_emails_that_contain": "Require emails that contain ...",
"exclude_emails_that_contain": "Exclude emails that contain",
"require_emails_that_contain": "Require emails that contain",
"exclude_emails_match_found_error_message": "Please enter a valid work email address",
"require_emails_no_match_found_error_message": "You are not allowed to book with this email domain. Please contact the owner of this link.",
"search_columns": "Search columns",
@@ -3121,7 +3137,7 @@
"inactive_team_plan_description": "Your team plan is inactive. Check your subcription or reach out to customer support",
"limited_access_trial_mode": "Limited access during trial. Feature available after trial ends.",
"referral_program": "Referral Program",
"referral_text": "Earn 20% referral",
"referral_text": "Refer and earn",
"dub_disabled_error_message": "You need a Dub API Key and Program Id to use this page.",
"uid": "UID",
"link": "Link",
@@ -3320,9 +3336,8 @@
"pbac_desc_view_roles": "View roles",
"pbac_desc_update_roles": "Update roles",
"pbac_desc_delete_roles": "Delete roles",
"pbac_desc_manage_roles": "All actions on roles across organization teams",
"pbac_desc_create_workflows": "Create and set up new workflows",
"pbac_desc_view_workflows": "View existing workflows and their configurations",
"pbac_desc_view_workflows": "View existing workflows and their configurations",
"pbac_desc_update_workflows": "Edit and modify workflow settings",
"pbac_desc_delete_workflows": "Remove workflows from the system",
"pbac_desc_manage_workflows": "Full management access to all workflows",
@@ -3330,7 +3345,7 @@
"pbac_desc_view_event_types": "View event types",
"pbac_desc_update_event_types": "Update event types",
"pbac_desc_delete_event_types": "Delete event types",
"pbac_desc_manage_event_types": "All actions on event types across organization teams",
"pbac_desc_manage_event_types": "All actions on event types",
"pbac_desc_create_teams": "Create teams",
"pbac_desc_view_team_details": "View team details",
"pbac_desc_update_team_settings": "Update team settings",
@@ -3338,7 +3353,7 @@
"pbac_desc_invite_team_members": "Invite team members",
"pbac_desc_remove_team_members": "Remove team members",
"pbac_desc_change_team_member_role": "Change role of team members",
"pbac_desc_manage_teams": "All actions on teams across organization teams",
"pbac_desc_manage_teams": "All actions on teams",
"pbac_desc_create_organization": "Create organization",
"pbac_desc_view_organization_details": "View organization details",
"pbac_desc_list_organization_members": "List organization members",
@@ -3353,7 +3368,7 @@
"pbac_desc_view_organization_bookings": "View organization bookings",
"pbac_desc_view_booking_recordings": "View booking recordings",
"pbac_desc_update_bookings": "Update bookings",
"pbac_desc_manage_bookings": "All actions on bookings across organization teams",
"pbac_desc_manage_bookings": "All actions on bookings",
"pbac_desc_view_team_insights": "View team insights",
"pbac_desc_manage_team_insights": "Manage team insights",
"read_permission_auto_enabled_tooltip": "Read permission is automatically enabled when creating, updating, or deleting a resource",
@@ -3472,6 +3487,8 @@
"webhook_metadata": "Metadata",
"stats": "Stats",
"booking_status": "Booking status",
"location_custom_label_input_label": "Custom label on booking page",
"meeting_link": "Meeting link",
"my_bookings": "My Bookings",
"phone": "Phone",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
@@ -1489,7 +1489,7 @@
"connect_your_calendar_and_link": "Puedes conectar tu calendario desde <1>aquí</1>.",
"default_calendar_selected": "Calendario predeterminado",
"hide_from_profile": "Ocultar del perfil",
"event_setup_tab_title": "Configuración del evento",
"basics": "Configuración del evento",
"availability_not_found_in_schedule_error": "No se encontró disponibilidad en el horario",
"event_limit_tab_title": "Límites",
"event_limit_tab_description": "Con qué frecuencia puedes ser reservado",
@@ -1489,7 +1489,7 @@
"connect_your_calendar_and_link": "Puede conectar su calendario desde <1>aquí</1>.",
"default_calendar_selected": "Calendario predeterminado",
"hide_from_profile": "Ocultar del perfil",
"event_setup_tab_title": "Configuración del evento",
"basics": "Configuración del evento",
"availability_not_found_in_schedule_error": "No se encontró disponibilidad en el horario",
"event_limit_tab_title": "Límites",
"event_limit_tab_description": "Con qué frecuencia se puede reservar",
@@ -1489,7 +1489,7 @@
"connect_your_calendar_and_link": "Saate oma kalendri ühendada <1>siit</1>.",
"default_calendar_selected": "Vaikekalender",
"hide_from_profile": "Peida profiilist",
"event_setup_tab_title": "Sündmuse seadistamine",
"basics": "Sündmuse seadistamine",
"availability_not_found_in_schedule_error": "Ajakavas ei leitud saadavust",
"event_limit_tab_title": "Piirangud",
"event_limit_tab_description": "Kui sageli saate broneerida",

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