Compare commits

...
Author SHA1 Message Date
Hariom BalharaandDevin AI bec49ef417 fix: provide meetingStartTime for test preview to prevent 'Meeting start time is required' crash (#24379)
Post release / release (push) Canceled after 0s
* fix: provide meetingStartTime for test preview to prevent RR timestamp error

When Round Robin timestamp basis is set to START_TIME, the system requires
a meeting start time to calculate the interval for load balancing. Test
Preview doesn't have a real meeting time since it's just simulating the
routing logic.

This fix provides the current time (new Date()) as the meetingStartTime
when calling getOrderedListOfLuckyUsers in test preview mode. This allows
the Round Robin calculation to proceed using the current interval for
realistic ordering while still being in preview mode.

Fixes the 'Meeting start time is required' error that occurs when users
click Test Preview on routing forms with Round Robin timestamp basis set
to START_TIME.

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

* add comment

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-09 08:58:57 +00:00
Kartik LabhshetwarandDevanshu Sharma c13aa69565 fix: improve cross button alignment and spacing in FormBuilder Options (#24247)
* fix: improve cross button alignment and spacing in FormBuilder Options

* fix: revert eslint errors

---------

Co-authored-by: Devanshu Sharma <devanshusharma658@gmail.com>
2025-10-09 08:18:19 +00:00
emrysal 1470adb159 chore: release v5.7.10 2025-10-09 08:17:36 +00:00
emrysal bf89991eef chore: release v5.9.10 2025-10-09 00:22:21 +00:00
iqram magdon-ismail 4dfab874e0 refactor: update JellyPhone icon (#24341)
change the icon for jellyphone
2025-10-08 14:38:54 +00:00
Joe Au-Yeung 4303fb8726 feat: Salesforce - add urql/retry-exchange (#24302)
* Add .env variables

* Add `urql/exchange-retry`

* Add retry mechanic to graphql client

* Only add retries if .env variables are set

* Remove console.log
2025-10-08 14:03:40 +00:00
64f811ed0b feat: show organization username in impersonation banner (#24313)
- Update ImpersonatingBanner to use orgAwareUsername field from session
- Falls back to regular username for non-organization users
- Uses existing session data without additional queries

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-10-08 11:31:59 +00:00
4d51873140 feat: remove Cal.ai tab from navigation while preserving URL access (#23894)
Co-authored-by: peer@cal.com <peer@cal.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-07 20:38:05 +00:00
3c0fc2e88b fix: reschedule issue when limited booking is enabled in the event settings. (#23717)
* fix

* fix fix

* cleanup

* Update useBookings.ts

* Update store.ts

---------

Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
Co-authored-by: Devanshu Sharma <devanshusharma658@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-10-07 19:23:43 +00:00
c702da0334 feat: Add team billing tables (#24148)
* Init billing tables

* Create `IBillingRepository` types

* Create billing repositories

* Create billingRepositoryFactory

* Eslint fix - remove unused organizationOnboarding

* internal-team-billing create saveTeamBilling method using repositories

* On new teams write to team billing table

* On new org write to org billing table

* Change fields to organizationId

* Add todo comment

* Revert "Change fields to organizationId"

This reverts commit bbb2e5dfa6b4c20a8a395f5730848a492cd70d68.

* test: add comprehensive tests for team billing tables

- Fix credit-service.test.ts Prisma mock to export prisma object
- Replace any types with proper TypeScript types in credit-service.test.ts
- Add unit tests for PrismaTeamBillingRepository covering record creation, enum casting, and error handling
- Add unit tests for PrismaOrganizationBillingRepository with same coverage
- Add unit tests for BillingRepositoryFactory to verify correct repository selection
- Add unit tests for InternalTeamBilling.saveTeamBilling() method testing delegation to correct repositories
- All 53 tests pass with TZ=UTC yarn test
- Type checking passes with yarn type-check:ci --force

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

* refactor: update saveTeamBilling tests to mock repository interface

- Replace prismaMock usage with BillingRepositoryFactory mock
- Mock IBillingRepository interface instead of Prisma directly
- Follow repository mocking pattern from handleResponse.test.ts
- All tests passing (53 total)

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

* Remove log statement

* Remove repository tests

* Address feedback

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-07 14:25:31 -04:00
Bhavya Bhardwaj 92169eb0d2 fix: resolve AI assistant button overlap and mobile padding on Teams page (#24286)
* Fix padding in UpgradeTip component

Adjusted padding in UpgradeTip component.

* Adjust bottom positioning of main navigation component

* Adjust bottom positioning for main navigation
2025-10-07 23:11:39 +05:30
kusiewicz 3d74357e96 fix: polish sign-in translation (#24263) 2025-10-07 23:06:07 +05:30
Parship Chowdhury 6593291d00 Proper error handling in PrismaAppRepository.findAppStore with Sentry logging (#24212)
Signed-off-by: Parship Chowdhury <i.am.parship@gmail.com>
2025-10-07 21:23:27 +05:30
Kartik LabhshetwarandKartik Saini c48fb44a8d fix: make empty screen template grid mobile responsive (#24259)
* fix: make empty screen template grid mobile responsive

* refactor: adjust EmptyScreen layout classes for responsive grid

---------

Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
2025-10-07 21:19:58 +05:30
Peer Richelsen e7c908bc80 feat: framer app (#24204)
* added framer app

* new 1.jpeg

* Update packages/app-store/framer/config.json
2025-10-07 15:12:22 +00:00
Peer RichelsenandRodrigo Ehlers ad35e19f8e feat: fonio.ai app (#23874)
* added fonio

* small hitpay update

* added fonio screenshots

* added description

* fix: rename 1.jpg -> 1.jpeg in fonio-ai app

---------

Co-authored-by: Rodrigo Ehlers <rodrigoehlers@outlook.com>
2025-10-07 14:42:29 +00:00
Keith Williams 3fc8c6c84e chore: Remove unused next dependency in @calcom/ui (#24311)
* chore: Remove unused Next.js package

* update yarn.lock
2025-10-07 01:34:53 +00:00
emrysal da7aeb4a21 chore: release v5.7.9
Post release / release (push) Canceled after 0s
2025-10-06 23:57:19 +00:00
Keith Williams 6197a2a5c5 chore: Upgrade Next.js to 15.5.4 (#24307) 2025-10-06 21:39:26 +00:00
Syed Ali ShahbazandAlex van Andel de186b95c1 fix: create missing migration for dropping obsolete indexes (#24304)
* Create migration.sql

* Drop index if exists, otherwise do nada

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-10-06 21:33:00 +00:00
Syed Ali Shahbaz 6b24513073 Update how-to-setup-api-in-a-local-instance.mdx (#24300) 2025-10-06 17:39:55 +00:00
Kinbaum 55ea18128a fix: timeZone variable name does not match webhook payload (#24279) 2025-10-06 22:49:28 +05:30
Calcom BotandLingo.dev d5557fa512 feat: update translations via @LingoDotDev (#24301)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-10-06 12:59:10 -03:00
Hariom BalharaandDevin AI 9c97b2aa6d feat: add usernameInOrg field to webhook organizer payload for organization users (#23246)
* feat: add usernameInOrg field to webhook organizer payload for organization users

- Add usernameInOrg field to CalendarEventBuilder organizer interface
- Update handleNewBooking to pass organizerOrganizationProfile.username as usernameInOrg
- Include usernameInOrg in webhook payload generation (sendPayload.ts)
- Add webhook form variable for usernameInOrg with translation
- Update Person type to include usernameInOrg field
- Add test case for organization user webhook verification
- Maintain backward compatibility by keeping existing username field unchanged

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

* feat: add usernameInOrg to additional webhook sending locations

- Update handleCancelBooking.ts to include usernameInOrg in organizer payload
- Update confirm.handler.ts to include usernameInOrg for booking confirmations
- Update getBooking.ts to include usernameInOrg in payment-related webhooks
- Maintain backward compatibility with existing username field
- All webhook sending locations now include organization profile username

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

* fixes

* Add subteam event test for usernameInOrg

* fix eslitn issues

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-06 19:51:04 +05:30
sean-brydonandClaude 1e226a40d1 Add next-auth-options.ts to Foundation code owners (#24295)
Added packages/features/auth/lib/next-auth-options.ts to CODEOWNERS for the Foundation team to ensure proper review and oversight of authentication configuration changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-06 11:55:58 +00:00
Hariom Balhara bbe46eaa7d Restrict the length of emails as per RFC (#24269) 2025-10-06 10:22:58 +00:00
sean-brydon 7799b191ec feat: botid enabled on api/book/event api route (#24207)
* wip

* WIP

* restore event

* testing without instrument client

* Add conditional for botID init

* Bump BotID version + pass in header

* botID yarn lock changes

* feat: Add feature flag checks + tidy up into service

* rely on env var also

* use eventType repo instead of passing in prisma

* remove slug from audit

* rename botId feature to botid

* add unit tests for bot service
2025-10-06 11:16:13 +01:00
Anik Dhabal Babu f3269a3ddf fix: contentType is added in worng format (#24284) 2025-10-06 06:29:54 +00:00
emrysal a4be822636 chore: release v5.7.8
Post release / release (push) Canceled after 0s
2025-10-04 14:20:17 +00:00
Syed Ali Shahbaz 7cd8dbf7d8 chore: Watchlist schema update (#24246)
* watchlist schema update

* changes addressed

* no backfill audit

* fix type

* fix flow of migration

* fix err

* type err fix

* add if exists check in migration
2025-10-04 01:44:37 +01:00
654a865007 refactor: Salesforce getContacts (#24235)
* lint fixes

* Create getContactOrLeadFromEmail method

* Use getContactOrLeadFromEmail in getContacts

* Move normal getContact query

* fix: correct return type in getContactOrLeadFromEmail and add search mock

- Fix getContactOrLeadFromEmail to return single ContactRecord instead of array
- Add conn.search() implementation to salesforceMock for integration tests
- Ensure id field is always string with proper fallbacks for AccountId and Id
- Fix Owner.Email to use undefined instead of null in mock

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

* test: add comprehensive tests for getContactOrLeadFromEmail method

- Add 6 new unit tests covering all key scenarios
- Test contact lookup, lead fallback, and preference logic
- Test both roundRobinSkipFallbackToLeadOwner and createEventOnLeadCheckForContact code paths
- Add search mock support to mockConnection in beforeEach
- All 34 tests passing, type-check clean

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

* Implement change to error from #24090

* Address feedback

* Type fix

* test: update SOQL query expectations to include Account.OwnerId

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-03 14:53:00 -04:00
Alex van AndelandDevin AI 409a0277fb fix: Resolve BillingPlan tree-shaking issue causing runtime error (#24229)
* fix: Convert BillingPlan enum to const object to prevent webpack tree-shaking

TypeScript enums compile to IIFEs which can be incorrectly tree-shaken by webpack
when 'sideEffects: false' is set in package.json. Converting to a const object
with 'as const' avoids the IIFE pattern while maintaining full type safety.

This fixes the 'TRPCError: BillingPlan is not defined' error that occurred in
the hasTeamPlan tRPC handler.

Follows the existing CHECKOUT_SESSION_TYPES pattern in the same file.

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

* fix: Re-export BillingPlan from billing-plans to prevent tree-shaking

The issue was a module initialization order problem. When BillingPlan was
imported from constants.ts into billing-plans.ts and used inside the
BillingPlanService class, webpack's tree-shaker (with sideEffects: false)
couldn't properly track the value dependency across the package boundary.

By re-exporting BillingPlan from the same module that exports
BillingPlanService, we ensure the constant is bundled together with the
class that uses it, preventing tree-shaking.

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

* revert: Restore BillingPlan enum format in constants.ts

The const object conversion didn't fix the tree-shaking issue. The real
fix is re-exporting BillingPlan from billing-plans.ts to ensure it's
part of the same module as BillingPlanService.

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

* fix: Add BillingPlan as private static member to prevent tree-shaking

By making BillingPlan a private static member of BillingPlanService, webpack
now sees it as part of the class definition rather than just used inside
methods. This creates a strong reference that prevents webpack's tree-shaker
from removing the enum initialization IIFE when 'sideEffects: false' is set.

With the previous approach where BillingPlan was only imported and used inside
methods, webpack's static analysis couldn't properly track the enum usage
across package boundaries (@calcom/features -> @calcom/trpc), causing it to
incorrectly determine the enum initialization was unused code.

Fixes the TRPCError: BillingPlan is not defined runtime error.

Thread: https://calendso.slack.com/archives/C08LT9BLEET/p1759420015428149
Co-Authored-By: alex@cal.com <me@alexvanandel.com>

* fix: Clean up BillingPlan references and add explanatory comment

- Remove unnecessary re-export statement
- Use direct BillingPlan references instead of BillingPlanService.BillingPlan
- Add detailed comment explaining webpack tree-shaking workaround
- Keep import from constants.ts and private static member for webpack reference

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

* fix: Remove Slack reference from comment and restore static usage

- Remove Slack URL from explanatory comment
- Restore BillingPlanService.BillingPlan usage throughout class methods
- This static member usage is essential for webpack to track the enum reference

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

* Fix BillingPlan issue by finding root cause

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-03 16:37:45 +01:00
Anik Dhabal Babu 7adab1f65f fix: event not showing up on outlook through ics file (#24249) 2025-10-03 13:52:25 +00:00
Calcom BotandLingo.dev 3b92c86582 feat: update translations via @LingoDotDev (#24248)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-10-03 12:42:14 +00:00
Devanshu Sharma de195a1c0f feat: add email filtering to team memberships endpoint (#23923)
* Controller Layer updates.

* adding email filtering and pagination to team memberships endpoint..

* Minor enhancements.

* Improve addressed.

* refactor: update team memberships input to handle comma-separated emails

- Replace array format with comma-separated string handling
- Add proper email validation with BadRequestException
- Remove ArrayMaxSize constraint for better flexibility
- Update API documentation and examples
- Align with codebase patterns from get-managed-users.input.ts

* Morgan suggestions addressed.

* More improvements......
2025-10-03 18:09:47 +05:30
an_ifrah24andDevanshu Sharma d7d2487f6d fix: hydration error warning in scheduleListItem component (#24240)
Co-authored-by: Devanshu Sharma <devanshusharma658@gmail.com>
2025-10-03 17:52:32 +05:30
sean-brydon 3e31945157 Fix nits (#24244) 2025-10-03 11:03:07 +00:00
Lauris Skraucis c11814c342 fix: managed user timeZone validation (#24226)
* fix: managed user timeZone validation

* fix: managed user timeZone validation

* fix: managed user timeZone validation
2025-10-03 08:23:54 +00:00
Syed Ali Shahbaz 60e3244088 Migrate watchlist severity to action(#24195) 2025-10-03 11:18:14 +04:00
Rajiv Sahal a1ac5f36f0 feat: add atoms section inside developing guides (#24220) 2025-10-02 21:59:43 +05:30
Lauris SkraucisandMorgan 6f38d40613 refactor: dont allow scheduling team event type without hosts (#24206)
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-10-02 15:58:42 +00:00
Lauris Skraucis 6b7d22de3f refactor: fail breaking change check on error only (#24221) 2025-10-02 14:42:49 +00:00
Anik Dhabal Babu 2f763dceda fix: Getting an unauthorized error when accessing the workflow tab for a child-managed event (#24219)
* Update get.handler.ts

* fix: Ignore userIds form filter segment if no permission

* add tests

* Update booking-filters.e2e.ts

* Update booking-filters.e2e.ts

* fix test

* fix: got unautorized error when viewing workflows

* fix
2025-10-02 13:38:21 +00:00
Benny Joo 96468c4083 refactor: move @calcom/lib/di folder to @calcom/features (#24199)
* mv di folder

* update imports

* fix

* fix

* fix test
2025-10-02 08:12:06 -03:00
sean-brydonandBenny Joo 858d52563f feat: pbac - org pages check update permissions (#24164)
* pbac - org pages check update permissions

* restore permision service

* fix type check

* more type fixes

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-10-02 20:04:21 +09:00
7505b3b271 feat: add missing sensitive fields to redactSensitiveData (#24121)
- Add private_key, encrypted_credentials, tenant_id, client_email, serviceAccountKey to SENSITIVE_FIELDS array
- Add comprehensive test cases for delegation credential sensitive fields
- Prevents logging of sensitive delegation credential data including private keys

Fixes security issue where delegation credential sensitive fields were not being redacted from logs

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: morgan@cal.com <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-10-01 20:26:58 -03:00
Calcom BotandLingo.dev 758dc88b55 feat: update translations via @LingoDotDev (#24203)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-10-01 23:24:17 +00:00
Udit Takkar 9be418dc65 chore: update description (#24189) 2025-10-01 23:35:41 +05:30
Volnei Munhoz 2ac310456e fix: Selected calendar delegation credentials (#24190)
* Fix selected calendar delegation credentials
2025-10-01 14:20:36 +00:00
ff264d6f7a fix: allow team with same slug for diff cases (#24029)
* fix: aalow team with slug for diff cases

* addressed review

* fix type error

* update test

* addressed review

* fix test

* Update team.ts

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-10-01 14:04:20 +00:00
8019c414e1 chore: CSRF protect forgot-password functionality (#22361)
* chore: CSRF protect forgot-password functionality

* feat: implement conditional sameSite cookie setting for CSRF tokens

- Use WEBAPP_URL to determine secure cookie settings
- Set sameSite to 'none' for HTTPS environments to support cross-origin scenarios
- Fall back to 'lax' for HTTP development environments
- Follows patterns from PRs #23439 and #23556

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

* update

* change

* NIT

* minor

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-10-01 13:38:29 +00:00
chauhan_s a3fbe3731e feat: add host validation for team event types (#24170)
* feat: add host validation for team event types

* refactor: remove hosts validator in favor of documentation-based validation approach

* fix: remove HostsOrAssignAllValidator from event type update inputs

* docs: improve clarity of hosts and assignAllTeamMembers field descriptions in event type inputs
2025-10-01 16:15:40 +05:30
sean-brydonandcoderabbitai[bot] e3742dbc66 fix: Always enforce one owner with pbac (#24144)
* Always enforce one owner

* updates from merge

* remove redudant eslint rules

* Update packages/features/pbac/services/pbac-role-manager.service.ts

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

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-01 11:06:18 +01:00
Rajiv SahalandMorgan 83bf717d6d fix: customReplyEmailTo feedback (#23738)
* fix: move validateRoundRobinSlotAvailability to core libraries

* fix: implement PR feedback

* fix: merge conflicts

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-10-01 15:32:32 +05:30
Peer RichelsenandDevin AI 7fcdaa1c46 feat: add created_at field display to admin user edit page (#23805)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-01 09:58:20 +00:00
Kartik LabhshetwarandKartik Saini a8154f9405 feat(ui/api-keys): add confirmation dialog for API key deletion (#24118)
* feat(api-keys): add confirmation dialog for API key deletion

* fix: changed text color to subtle

---------

Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
2025-10-01 09:56:55 +00:00
chauhan_sandLauris Skraucis 52a5afeba5 fix: different calendar hosts (#24000)
* fis: pass the missing `platformClientId` to `handleNewBooking` in api/v2

* Update formatCalendarEvent.ts

* only add the externald in case of same calendar, otherwise add if from the .ics file sent in email

* Revert "fis: pass the missing `platformClientId` to `handleNewBooking` in api/v2"

This reverts commit 030ac0f0981c1135d4973fbdafa62d58e8985831.

* fix: alow the host change logic to run for collective envets as well

* fix: run the changedOrganizer logic for both collective and round-robin events

* Update handleNewBooking.ts

* fix: failing unit test

---------

Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
2025-10-01 12:15:34 +03:00
Anik Dhabal Babu 43cc40d7fe fix: add permissions to workflows (#24192) 2025-10-01 06:53:08 +00:00
Hariom Balhara 42a98f000e fix: 405 method not allowed error for queuedFormResponseCleanup (#24191) 2025-10-01 04:55:48 +00:00
Joe Au-Yeung f003ef935b Remove safeStringify from graphql errors (#24188) 2025-10-01 09:50:15 +05:30
Volnei Munhoz e2b2a184c8 fix: Calendar Cache sync page (#24182)
* fix

* fix calendar sync page
2025-09-30 17:10:51 +01:00
Calcom BotandLingo.dev 094595c435 feat: update translations via @LingoDotDev (#24178)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-09-30 13:51:13 +00:00
Benny Joo 4118a86c0d refactor: do a permission check in removeHostsFromEventTypes trpc handler (#24176) 2025-09-30 13:30:12 +00:00
sean-brydon 928d6f3821 chore: PBAC routingform entity permissions to favour pbac (#24130)
* Remove routingform entity permissions to favour pbac

* push typo

* fix types

* update returns

* Remove unused function

* nits

* fix hariom feedback

* fix correct write permissions
2025-09-30 18:55:43 +05:30
Benny Joo b2ade568c8 refactor: more pbac replacements (#24135)
Post release / release (push) Canceled after 0s
* getUserConnectedApps

* create teams

* use permission check in team deletion

* team read

* revert

* publish handler

* wip

* revert

* wip

* wip
2025-09-30 13:11:02 +00:00
27820ce897 feat: auto-accept team invitations for existing users (#24091)
* feat: auto-accept team invitations for existing users

- Change email button text from 'View Invitation' to 'Accept Invite'
- Implement auto-accept flow when clicking email CTA
- Update TeamService.inviteMemberByToken to support auto-acceptance
- Add new autoAcceptInvite tRPC endpoint for handling auto-acceptance
- Update invitation link generation to include autoAccept parameter
- Handle both team and organization invitation scenarios
- Maintain payment/billing flow integration with TeamBilling.updateQuantity
- Preserve backward compatibility with existing manual flow
- Update all locale files with new 'Accept Invite' button text

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

* revert: locale changes except English

- Keep only English 'Accept Invite' translation
- Revert all other locale files to original 'View Invitation' translations
- Maintain core auto-accept invitation functionality

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

* simplify: remove autoAccept parameter and make auto-acceptance default

- Remove autoAccept parameter from TeamService.inviteMemberByToken
- Always auto-accept invitations for existing users clicking email links
- Remove autoAccept logic from teams server-page.tsx
- Remove autoAccept=true from invitation URLs
- Delete autoAcceptInvite handler and schema files
- Remove autoAcceptInvite endpoint from tRPC router
- Simplify invitation flow to match new user pattern

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

* test: update teamService test to expect auto-accepted memberships

- Change expectation from accepted: false to accepted: true
- Update test description to reflect auto-accept behavior
- Fix TypeScript type casting to use Pick<TeamRepository, 'deleteById'>
- Aligns with new default auto-acceptance for team invitations

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

* update

* Update utils.ts

* fix type error

* delete token

* add prisma transaction

* update

* update param

* test: fix mock objects in teamService tests with realistic data

- Fix duplicate property assignments in mock user objects
- Use proper email format (user@example.com) for email fields
- Use proper username format (testuser) for username fields
- Fix logic error in acceptInvitationByToken (|| to &&)
- Add autoAccept parameter to resendInvitation.handler.ts
- All 16 tests passing with proper TypeScript types

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

* test: add e2e tests for team invitation auto-accept flow

- Add test for existing user auto-accepting team invitation via email link
- Add test for error handling when wrong user tries to use invitation link
- Verify proper user identity validation and database state changes
- Follow existing e2e test patterns with browser context isolation
- Fix ESLint warnings: replace conditional with assertion and remove unused browser parameter

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

* fix

* fix

* fix: update team owner creation in e2e tests to include proper names

- Fix email subject mismatch in auto-accept invitation tests
- Team owners now created with explicit names instead of undefined
- Matches pattern used in other working team invitation tests

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

* fix: update organization invitation test helper to match new auto-accept link format

- Change expectExistingUserToBeInvitedToOrganization to look for 'teams?token' instead of 'settings/team'
- Fixes 'Invite link not found' error in organization booking e2e test
- Aligns with auto-accept invitation URL changes that use /teams?token= format
- Fix eslint disable comment for playwright rule

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

* address coderrabit review

* fix failing test

* addressed review

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-30 13:07:21 +00:00
emrysal fea7e66996 chore: release v5.7.7 2025-09-30 12:57:27 +00:00
sean-brydon 474d1571ae Remove checkAdminOrOwner from settings layout client (#24162) 2025-09-30 12:08:05 +01:00
Rajiv Sahal 2ef627b87e fix: css issue for 12/24 styles (#24141) 2025-09-30 08:38:17 +00:00
sean-brydon 2dfb82b623 Bring back correct routing form teams (#24158) 2025-09-30 08:37:16 +00:00
sean-brydon ccd7fdfd52 refactor role manage (#24146) 2025-09-30 08:35:26 +01:00
Keith Williams f92e59ed34 chore: Update yarn.lock (#24153) 2025-09-30 02:19:30 +00:00
Keith WilliamsandDevin AI d29477349a test: replace prismaMock with BookingRepository mock in checkBookingLimits tests (#24152)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-29 23:34:20 +00:00
Alex van Andel 259e480baa chore: Handle stripe refunds slightly differently to reduce hard errors (#24150)
* chore: Handle stripe refunds slightly differently to reduce hard errors

* Remove 'Received and discarded' error when a credential is not found for a triggered subscription

* Fix handling of no credential found (do show in stripe debugger)

* Minimal overhaul of delete handling

* Fix doc typo
2025-09-29 23:07:08 +00:00
Benny Joo 0055dd1142 perf: dynamically import sentry in Next.js api routes' wrapper (#23710)
* dynamically import sentry in nextjs api routes wrapper

* refactor

* same for pages router

* fixes

* address feedback

* refactor

* address feedback
2025-09-30 02:13:50 +05:30
Volnei Munhoz 61c44780f0 chore: Optional skip warnings for lint-staged (#24102)
* add optional skip warnings

* remove test changes

* allow underscore variables pattern being ignored by eslint (#24103)
2025-09-29 19:51:18 +00:00
Calcom BotandLingo.dev 9710d3b15c feat: update translations via @LingoDotDev (#23991)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-09-29 19:23:59 +00:00
sean-brydon 04cefebd75 add permissions.canUpdateOrganization to show/hide items in org navbar (#24142) 2025-09-29 15:11:39 +00:00
Amit Sharma dbd927c002 fix: intercom zindex (#24096) 2025-09-29 11:43:11 -03:00
e6b2116a2b feat: Calendar Cache and Sync (#23876)
* feat: calendar cache and sync - wip

* Add env.example

* refactor on CalendarCacheEventService

* remove test console.log

* Fix type checks errors

* chore: remove pt comment

* add route.ts

* chore: fix tests

* Improve cache impl

* chore: update recurring event id

* chore: small improvements

* calendar cache improvements

* Fix remove dynamic imports

* Add cleanup stale cache

* Fix tests

* add event update

* type fixes

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

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

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

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

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

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

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

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

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

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

* Fix tests

* Fix tests

* type fix

* Fix coderabbit comments

* Fix types

* Fix test

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

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

* Fixes by first review

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

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

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

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

* only google-calendar for now

* docs: add Calendar Cache and Sync feature documentation

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

Addresses PR #23876 documentation requirements

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

* docs: update calendar subscription README with comprehensive documentation

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

Addresses PR #23876 documentation requirements

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

* fix docs

* Fix test to available calendars

* Fix test to available calendars

* add migration and sync boilerplate

* fix typo

* remove double log

* sync boilerplate

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-09-29 14:26:14 +00:00
Udit Takkar 855a02091f chore: update tooltip text (#24143) 2025-09-29 11:20:28 -03:00
Volnei MunhozandDevin AI 358434429e feat: add booking flags for skipAvailabilityCheck, skipEventLimitsCheck, skipCalendarSyncTaskCreation (#24122)
- Add three optional boolean flags to BookingHandlerInput interface
- Implement conditional logic to skip availability checks when skipAvailabilityCheck is true
- Implement conditional logic to skip event limits checks when skipEventLimitsCheck is true
- Implement conditional logic to skip calendar sync when skipCalendarSyncTaskCreation is true
- Add comprehensive tests for all three flags and combined scenarios
- Maintain backward compatibility with default false values
- Fix linting issues (unused variables and expressions)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-29 14:02:31 +00:00
sean-brydonandhbjORbj b00061d85e chore: Add addMembersToTeams logic from 24099 (#24140)
* Add addMembersToTeams logic from 24099

* remove import

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-09-29 12:54:35 +00:00
Udit Takkarandsean-brydon 022f08c05e feat: add 10 minute cooldown in instant meetings (#24119)
* feat: add 10 minute

* feat: add 10 minute

* chore: change name

* chore: change name

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2025-09-29 16:03:03 +04:00
emrysal 6ff5c8ce5f chore: release v5.7.6
Post release / release (push) Canceled after 0s
2025-09-29 11:22:43 +00:00
Lauris Skraucis 3be7fb9dd8 fix: v2 create org user with time zone (#24133)
* fix: tzdata undefined import

* refactor: capitalize org user timezone

* test: org user creation with time zone
2025-09-29 11:12:18 +00:00
Benny Joo 72dd970b6e refactor: use permission check service in /organizations endpoints (#24099)
* getUserConnectedApps

* create teams

* use permission check in team deletion

* team read

* revert

* publish handler

* wip
2025-09-29 11:07:19 +00:00
sean-brydon f4d09ece43 chore: (PBAC) readonly eventTypes (#24131)
* Chore(PBAC): readonly eventTypes

* Set readonly value
2025-09-29 11:06:55 +00:00
sean-brydonandEunjae Lee 239e6085ba feat: pbac - private teams (#23998)
* Add permission + migration for listMembersPrivate + depends on lsitMember

* Add legacy list members PBAC private logic

* Add checks in team pages + handler

* add list memeber checks for private orgs

* Add permision visibility scope to loading permissions

* getMembers handler

* fix fallback permission

* Verify private teams work with private org

* Apply suggestion from @eunjae-lee

Co-authored-by: Eunjae Lee <hey@eunjae.dev>

* Remove and private scope function and add param to old

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-09-29 11:30:54 +01:00
sean-brydon b17eab3aeb chore: Use enums instead of hardcoded strings (#24132)
* Use enums instead of hardcoded strings

* use membership role
2025-09-29 10:57:36 +01:00
752f7fcde0 refactor: replace checkAdminOrOwner with team.create permission in teams page (#24116)
* refactor: replace checkAdminOrOwner with team.create permission in teams page

- Replace role-based access control with PBAC using team.create permission
- Follow PBAC refactoring guide pattern for server-side permission checking
- Use permission-specific variable naming (canCreateTeam)
- Maintain existing TeamsListing component interface

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

* refactor: implement permissions object pattern for TeamsListing

- Replace isOrgAdmin prop with permissions object containing canCreateTeam
- Follow PBAC refactoring guide pattern for UI components
- Update TeamsListing component to use permissions.canCreateTeam
- Maintain existing functionality while improving code structure
- All type checking and linting verification passed

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

* use different method

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2025-09-29 14:11:42 +05:30
f6b061f787 fix: update handler to not delete banner (#24106)
* Fix update handler to not delete banner

* code cleanup

* add banner-upload.e2e

* fix: add slug fallback in banner-upload e2e test

- Use org.slug as fallback when requestedSlug is undefined
- Addresses GitHub comment about guarding against undefined slug
- Prevents test failures when requestedSlug is not set
- Fix ESLint warnings: remove unnecessary escape characters and unused variable

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

* test: add banner preservation test for profile updates

- Verify that updating organization profile without banner changes preserves existing banner
- Ensures update handler doesn't overwrite banner when undefined is passed
- Addresses user request to test banner preservation during profile saves

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

* remove banner e2e wip

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-29 09:31:49 +01:00
Eunjae LeeandDevin AI cc8a1290ee docs: add comprehensive DataTable usage guide (#24107)
* docs: add comprehensive DataTable usage guide

- Document DataTableProvider, DataTableWrapper, and DataTable components
- Cover filter system with all filter types and operators
- Explain segment system (system vs user segments)
- Document pagination modes (infinite vs standard)
- Include real-world usage examples from UserListTable, BookingsListingView, MemberList
- Add TypeScript types reference and best practices

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

* docs: update DataTable guide to recommend standard pagination

- De-emphasize virtualized rendering and infinite mode
- Position standard pagination as the recommended approach
- Document known issues with infinite loading mode
- Reorder pagination section to prioritize standard mode
- Update best practices to recommend standard pagination

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

* docs: add missing DataTable guide topics

- Document getFacetedUniqueValues for faceted filters with real examples
- Add useSegments prop requirement for filter segment enablement
- Enhance server-side filtering documentation with comprehensive patterns
- Include examples from useFacetedUniqueValues and useInsightsRoutingParameters

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

* docs: add missing DataTable guide topics

- Document getFacetedUniqueValues for faceted filters with real examples
- Add useSegments prop requirement for filter segment enablement
- Enhance server-side filtering documentation with comprehensive patterns
- Include examples from useFacetedUniqueValues and useInsightsRoutingParameters
- Correct server-side vs client-side filtering rationale

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

* docs: enhance DataTable guide with troubleshooting and testing

- Remove duplicate Key Hooks section (lines 783-803)
- Add visual ASCII diagrams for architecture and component hierarchy
- Add comprehensive troubleshooting section with 7 common issues and solutions
- Add testing patterns section with Vitest examples and best practices
- Update table of contents to include new sections
- Maintain existing high-quality documentation standards

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

* remove some sections

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-29 07:59:55 +00:00
Eunjae LeeandDevin AI b7cf893169 refactor: replace checkAdminOrOwner with PBAC availability.read permission (#24115)
- Replace role-based check with PermissionCheckService
- Use availability.read permission with OWNER/ADMIN fallback roles
- Maintain existing privacy logic for organizations
- Follow established PBAC pattern from bookings implementation

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-29 07:59:23 +00:00
Lauris Skraucis 1458cacdbb fix: v2 api event type splitName booking field (#24111)
* fix: v2 api event type splitName booking field

* fix: test
2025-09-29 09:19:24 +02:00
401 changed files with 13398 additions and 2066 deletions
+13
View File
@@ -144,6 +144,12 @@ GOOGLE_WEBHOOK_TOKEN=
# Optional URL to override for tunelling webhooks. Defaults to NEXT_PUBLIC_WEBAPP_URL.
GOOGLE_WEBHOOK_URL=
# Token to verify incoming webhooks from Microsoft Calendar
MICROSOFT_WEBHOOK_TOKEN=
# Optional URL to override for tunelling webhooks. Defaults to NEXT_PUBLIC_WEBAPP_URL.
MICROSOFT_WEBHOOK_URL=
# Inbox to send user feedback
SEND_FEEDBACK_EMAIL=
@@ -261,6 +267,9 @@ NEXT_PUBLIC_CLOUDFLARE_SITEKEY=
NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER=
CLOUDFLARE_TURNSTILE_SECRET=
# 0 = false, 1=true
NEXT_PUBLIC_VERCEL_USE_BOTID_IN_BOOKER=
# Set the following value to true if you wish to enable Team Impersonation
NEXT_PUBLIC_TEAM_IMPERSONATION=false
@@ -473,3 +482,7 @@ DATABASE_CHUNK_SIZE=
# Service Account Encryption Key for encrypting/decrypting service account keys
# You can use: `openssl rand -base64 24` to generate one
CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY=
SALESFORCE_GRAPHQL_DELAY_MS=500
SALESFORCE_GRAPHQL_MAX_DELAY_MS=2000
SALESFORCE_GRAPHQL_MAX_RETRIES=3
+1
View File
@@ -23,6 +23,7 @@
/packages/app-store/office365calendar/**/* @calcom/Foundation
/packages/app-store/zohocalendar/**/* @calcom/Foundation
/packages/embeds/**/* @calcom/Foundation
/packages/features/auth/lib/next-auth-options.ts @calcom/Foundation
/packages/features/bookings/lib/**/* @calcom/Foundation
/packages/lib/getAggregatedAvailability.ts @calcom/Foundation
/packages/lib/getUserAvailability.ts @calcom/Foundation
+1 -1
View File
@@ -82,7 +82,7 @@ jobs:
with:
base: https://raw.githubusercontent.com/calcom/cal.com/refs/heads/main/docs/api-reference/v2/openapi.json
revision: docs/api-reference/v2/openapi.json
fail-on: WARN
fail-on: ERR
- name: Run Tests
working-directory: apps/api/v2
View File
View File
View File
View File
View File
+2
View File
@@ -0,0 +1,2 @@
undecided:
- calcom-monorepo
+1 -1
View File
@@ -1,7 +1,7 @@
import type { NextApiRequest } from "next";
import { z } from "zod";
import { getUserAvailabilityService } from "@calcom/lib/di/containers/GetUserAvailability";
import { getUserAvailabilityService } from "@calcom/features/di/containers/GetUserAvailability";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
+1 -1
View File
@@ -4,7 +4,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs";
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
import { getAvailableSlotsService } from "@calcom/lib/di/containers/AvailableSlots";
import { getAvailableSlotsService } from "@calcom/features/di/containers/AvailableSlots";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import { createContext } from "@calcom/trpc/server/createContext";
+20 -19
View File
@@ -4,6 +4,7 @@ import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/pay
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import { TeamRepository } from "@calcom/lib/server/repository/team";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
@@ -61,24 +62,24 @@ export async function patchHandler(req: NextApiRequest) {
const { teamId } = schemaQueryTeamId.parse(req.query);
/** Only OWNERS and ADMINS can edit teams */
const _team = await prisma.team.findFirst({
const team = await prisma.team.findFirst({
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
include: { members: true },
where: { id: teamId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
});
if (!_team) throw new HttpError({ statusCode: 401, message: "Unauthorized: OWNER or ADMIN required" });
if (!team) throw new HttpError({ statusCode: 401, message: "Unauthorized: OWNER or ADMIN required" });
const slugAlreadyExists = await prisma.team.findFirst({
where: {
slug: {
mode: "insensitive",
equals: data.slug,
},
},
});
if (slugAlreadyExists && data.slug !== _team.slug)
throw new HttpError({ statusCode: 409, message: "Team slug already exists" });
if (data.slug) {
const teamRepository = new TeamRepository(prisma);
const isSlugAvailable = await teamRepository.isSlugAvailableForUpdate({
slug: data.slug,
teamId: team.id,
parentId: team.parentId,
});
if (!isSlugAvailable) {
throw new HttpError({ statusCode: 409, message: "Team slug already exists" });
}
}
// Check if parentId is related to this user
if (data.parentId && data.parentId === teamId) {
@@ -99,16 +100,16 @@ export async function patchHandler(req: NextApiRequest) {
}
let paymentUrl;
if (_team.slug === null && data.slug) {
if (team.slug === null && data.slug) {
data.metadata = {
...(_team.metadata as Prisma.JsonObject),
...(team.metadata as Prisma.JsonObject),
requestedSlug: data.slug,
};
delete data.slug;
if (IS_TEAM_BILLING_ENABLED) {
const checkoutSession = await purchaseTeamOrOrgSubscription({
teamId: _team.id,
seatsUsed: _team.members.length,
teamId: team.id,
seatsUsed: team.members.length,
userId,
pricePerSeat: null,
});
@@ -131,9 +132,9 @@ export async function patchHandler(req: NextApiRequest) {
bookingLimits: data.bookingLimits === null ? {} : data.bookingLimits,
metadata: data.metadata === null ? {} : data.metadata || undefined,
};
const team = await prisma.team.update({ where: { id: teamId }, data: cloneData });
const updatedTeam = await prisma.team.update({ where: { id: teamId }, data: cloneData });
const result = {
team: schemaTeamReadPublic.parse(team),
team: schemaTeamReadPublic.parse(updatedTeam),
paymentUrl,
};
if (!paymentUrl) {
+1 -1
View File
@@ -76,7 +76,7 @@ vi.mock("@calcom/features/watchlist/operations/check-if-users-are-blocked.contro
checkIfUsersAreBlocked: vi.fn().mockResolvedValue(false),
}));
vi.mock("@calcom/lib/di/containers/QualifiedHosts", () => ({
vi.mock("@calcom/features/di/containers/QualifiedHosts", () => ({
getQualifiedHostsService: vi.fn().mockReturnValue({
findQualifiedHostsWithDelegationCredentials: vi.fn().mockResolvedValue({
qualifiedRRHosts: [],
@@ -6,6 +6,7 @@ import { createMocks } from "node-mocks-http";
import { describe, expect, test } from "vitest";
import { HttpError } from "@calcom/lib/http-error";
import type { User } from "@calcom/prisma/client";
import handler from "../../../pages/api/selected-calendars/_post";
@@ -72,7 +73,7 @@ describe("POST /api/selected-calendars", () => {
prismaMock.user.findFirstOrThrow.mockResolvedValue({
id: 444444,
} as any);
} as User);
prismaMock.selectedCalendar.create.mockResolvedValue({
credentialId: 1,
@@ -95,6 +96,16 @@ describe("POST /api/selected-calendars", () => {
unwatchAttempts: 0,
createdAt: new Date(),
updatedAt: new Date(),
channelId: null,
channelKind: null,
channelResourceId: null,
channelResourceUri: null,
channelExpiration: null,
syncSubscribedAt: null,
syncToken: null,
syncedAt: null,
syncErrorAt: null,
syncErrorCount: null,
});
await handler(req, res);
@@ -140,6 +151,16 @@ describe("POST /api/selected-calendars", () => {
unwatchAttempts: 0,
createdAt: new Date(),
updatedAt: new Date(),
channelId: null,
channelKind: null,
channelResourceId: null,
channelResourceUri: null,
channelExpiration: null,
syncSubscribedAt: null,
syncToken: null,
syncedAt: null,
syncErrorAt: null,
syncErrorCount: null,
});
await handler(req, res);
@@ -1,9 +1,8 @@
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
import type { NextApiRequest } from "next";
import { describe, expect, it, beforeEach } from "vitest";
import { WatchlistSeverity } from "@calcom/prisma/enums";
import { isLockedOrBlocked } from "../../../lib/utils/isLockedOrBlocked";
describe("isLockedOrBlocked", () => {
@@ -18,7 +17,6 @@ describe("isLockedOrBlocked", () => {
{
type: "DOMAIN",
value: "blocked.com",
severity: WatchlistSeverity.CRITICAL,
createdById: 1,
},
],
@@ -26,13 +24,13 @@ describe("isLockedOrBlocked", () => {
});
it("should return false if no user in request", async () => {
const req = { userId: null, user: null } as any;
const req = { userId: null, user: null } as unknown as NextApiRequest;
const result = await isLockedOrBlocked(req);
expect(result).toBe(false);
});
it("should return false if user has no email", async () => {
const req = { userId: 123, user: { email: null } } as any;
const req = { userId: 123, user: { email: null } } as unknown as NextApiRequest;
const result = await isLockedOrBlocked(req);
expect(result).toBe(false);
});
@@ -44,7 +42,7 @@ describe("isLockedOrBlocked", () => {
locked: true,
email: "test@example.com",
},
} as any;
} as unknown as NextApiRequest;
const result = await isLockedOrBlocked(req);
expect(result).toBe(true);
@@ -57,7 +55,7 @@ describe("isLockedOrBlocked", () => {
locked: false,
email: "test@blocked.com",
},
} as any;
} as unknown as NextApiRequest;
const result = await isLockedOrBlocked(req);
expect(result).toBe(true);
@@ -70,7 +68,7 @@ describe("isLockedOrBlocked", () => {
locked: false,
email: "test@example.com",
},
} as any;
} as unknown as NextApiRequest;
const result = await isLockedOrBlocked(req);
expect(result).toBe(false);
@@ -83,7 +81,7 @@ describe("isLockedOrBlocked", () => {
locked: false,
email: "test@BLOCKED.COM",
},
} as any;
} as unknown as NextApiRequest;
const result = await isLockedOrBlocked(req);
expect(result).toBe(true);
@@ -65,6 +65,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
let team1EventTypeId: number;
let team2EventTypeId: number;
let phoneOnlyEventTypeId: number;
let collectiveEventWithoutHostsId: number;
let roundRobinEventWithoutHostsId: number;
const team1EventTypeSlug = `team-bookings-event-type-${randomString()}`;
const team2EventTypeSlug = `team-bookings-event-type-${randomString()}`;
@@ -223,6 +225,28 @@ describe("Bookings Endpoints 2024-08-13", () => {
team1EventTypeId = team1EventType.id;
const team1CollectiveEventTypeWithoutHosts = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "COLLECTIVE",
team: {
connect: { id: team1.id },
},
title: `team-bookings-2024-08-13-collective-event-type-without-hosts-${randomString()}`,
slug: `team-bookings-2024-08-13-collective-event-type-without-hosts-${randomString()}`,
length: 60,
});
collectiveEventWithoutHostsId = team1CollectiveEventTypeWithoutHosts.id;
const team1RoundRobinEventTypeWithoutHosts = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "ROUND_ROBIN",
team: {
connect: { id: team1.id },
},
title: `team-bookings-2024-08-13-round-robin-event-type-without-hosts-${randomString()}`,
slug: `team-bookings-2024-08-13-round-robin-event-type-without-hosts-${randomString()}`,
length: 60,
});
roundRobinEventWithoutHostsId = team1RoundRobinEventTypeWithoutHosts.id;
const phoneOnlyEventType = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "ROUND_ROBIN",
team: {
@@ -363,6 +387,52 @@ describe("Bookings Endpoints 2024-08-13", () => {
await app.init();
});
describe("cant book event types without hosts", () => {
it("should fail to book a collective event type without hosts", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: collectiveEventWithoutHostsId,
attendee: {
name: "alice",
timeZone: "Europe/Madrid",
email: "alice@gmail.com",
},
};
const response = await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(response.status).toBe(422);
expect(response.body.error.message).toBe(
`Can't book this team event type because it has no hosts. Please, add at least 1 host to event type with id=${collectiveEventWithoutHostsId} belonging to team with id=${team1.id} and try again.`
);
});
it("should fail to book a round robin event type without hosts", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: roundRobinEventWithoutHostsId,
attendee: {
name: "alice",
timeZone: "Europe/Madrid",
email: "alice@gmail.com",
},
};
const response = await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(response.status).toBe(422);
expect(response.body.error.message).toBe(
`Can't book this team event type because it has no hosts. Please, add at least 1 host to event type with id=${roundRobinEventWithoutHostsId} belonging to team with id=${team1.id} and try again.`
);
});
});
describe("create team bookings", () => {
it("should create a team 1 booking", async () => {
const body: CreateBookingInput_2024_08_13 = {
@@ -765,8 +835,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
return client;
}
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data !== null && data && "id" in data;
}
afterAll(async () => {
@@ -29,6 +29,7 @@ import {
Logger,
NotFoundException,
UnauthorizedException,
UnprocessableEntityException,
} from "@nestjs/common";
import { BadRequestException } from "@nestjs/common";
import { Request } from "express";
@@ -132,6 +133,10 @@ export class BookingsService_2024_08_13 {
);
}
if (eventType.schedulingType === "COLLECTIVE" || eventType.schedulingType === "ROUND_ROBIN") {
await this.checkEventTypeHasHosts(eventType.id);
}
body.eventTypeId = eventType.id;
if ("instant" in body && body.instant) {
@@ -159,6 +164,15 @@ export class BookingsService_2024_08_13 {
}
}
async checkEventTypeHasHosts(eventTypeId: number) {
const eventType = await this.eventTypesRepository.getEventTypeWithHosts(eventTypeId);
if (!eventType?.hosts?.length) {
throw new UnprocessableEntityException(
`Can't book this team event type because it has no hosts. Please, add at least 1 host to event type with id=${eventTypeId} belonging to team with id=${eventType?.teamId} and try again.`
);
}
}
async checkBookingRequiresAuthenticationSetting(
eventType: EventTypeWithOwnerAndTeam,
authUser: AuthOptionalUser,
@@ -38,6 +38,7 @@ import type {
GuestsDefaultFieldOutput_2024_06_14,
NameDefaultFieldInput_2024_06_14,
NotesDefaultFieldInput_2024_06_14,
SplitNameDefaultFieldOutput_2024_06_14,
UpdateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import type { PlatformOAuthClient, Team, User, Schedule, EventType } from "@calcom/prisma/client";
@@ -1447,23 +1448,23 @@ describe("Event types Endpoints", () => {
try {
await eventTypesRepositoryFixture.delete(eventType.id);
} catch (e) {
// Event type might have been deleted by the test
console.log(e);
}
try {
await userRepositoryFixture.delete(user.id);
} catch (e) {
// User might have been deleted by the test
console.log(e);
}
try {
await userRepositoryFixture.delete(falseTestUser.id);
} catch (e) {
// User might have been deleted by the test
console.log(e);
}
try {
await userRepositoryFixture.delete(orgUser.id);
} catch (e) {
// User might have been deleted by the test
console.log(e);
}
await app.close();
});
@@ -2187,6 +2188,47 @@ describe("Event types Endpoints", () => {
});
});
describe("split name booking field", () => {
it("should create an event type with split name booking field", async () => {
const splitNameBookingField: SplitNameDefaultFieldOutput_2024_06_14 = {
isDefault: true,
type: "splitName",
slug: "splitName",
firstNameLabel: "First name",
firstNamePlaceholder: "",
lastNameLabel: "last name",
lastNamePlaceholder: "",
lastNameRequired: false,
disableOnPrefill: false,
};
const body: CreateEventTypeInput_2024_06_14 = {
title: "Coding class 9",
slug: "coding-class-9",
description: "Let's learn how to code like a pro.",
lengthInMinutes: 60,
bookingFields: [splitNameBookingField],
};
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const createdEventType = responseBody.data;
const splitNameBookingFieldResponse = createdEventType.bookingFields.find(
(field) => field.type === "splitName"
) as SplitNameDefaultFieldOutput_2024_06_14 | undefined;
expect(splitNameBookingFieldResponse).toEqual(splitNameBookingField);
await eventTypesRepositoryFixture.delete(createdEventType.id);
});
});
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
@@ -2194,12 +2236,12 @@ describe("Event types Endpoints", () => {
await eventTypesRepositoryFixture.delete(legacyEventTypeId1);
await eventTypesRepositoryFixture.delete(legacyEventTypeId2);
} catch (e) {
// Event type might have been deleted by the test
console.log(e);
}
try {
await userRepositoryFixture.delete(user.id);
} catch (e) {
// User might have been deleted by the test
console.log(e);
}
await app.close();
});
@@ -2379,7 +2421,7 @@ describe("Event types Endpoints", () => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const updatedEventType = responseBody.data;
+expect(updatedEventType.hidden).toBe(false);
expect(updatedEventType.hidden).toBe(false);
});
});
@@ -2389,12 +2431,12 @@ describe("Event types Endpoints", () => {
try {
await eventTypesRepositoryFixture.delete(legacyEventTypeId1);
} catch (e) {
// Event type might have been deleted by the test
console.log(e);
}
try {
await userRepositoryFixture.delete(user.id);
} catch (e) {
// User might have been deleted by the test
console.log(e);
}
await app.close();
});
@@ -0,0 +1,51 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
@Injectable()
export class AtomsSecondaryEmailsRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
async getSecondaryEmailsVerified(userId: number) {
return await this.dbRead.prisma.secondaryEmail.findMany({
where: {
userId,
emailVerified: {
not: null,
},
},
});
}
async getExistingSecondaryEmailByUserAndEmail(userId: number, email: string) {
const existingSecondaryEmailRecord = await this.dbRead.prisma.secondaryEmail.findUnique({
where: {
userId_email: { userId, email },
},
});
return existingSecondaryEmailRecord?.email;
}
async getExistingSecondaryEmail(email: string) {
const existingSecondaryEmailRecord = await this.dbRead.prisma.secondaryEmail.findUnique({
where: {
email,
},
});
return existingSecondaryEmailRecord?.email;
}
async addSecondaryEmailVerified(userId: number, email: string) {
const existingSecondaryEmailRecord = await this.dbWrite.prisma.secondaryEmail.create({
data: {
userId,
email,
emailVerified: new Date(),
},
});
return existingSecondaryEmailRecord?.email;
}
}
@@ -1,4 +1,5 @@
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { AtomsSecondaryEmailsRepository } from "@/modules/atoms/atoms-secondary-emails.repository";
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
import { AtomsConferencingAppsController } from "@/modules/atoms/controllers/atoms.conferencing-apps.controller";
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
@@ -33,6 +34,7 @@ import { Module } from "@nestjs/common";
CredentialsRepository,
UsersRepository,
AtomsRepository,
AtomsSecondaryEmailsRepository,
UsersService,
SchedulesAtomsService,
VerificationAtomsService,
@@ -64,47 +64,4 @@ export class AtomsRepository {
return userTeams;
}
async getSecondaryEmails(userId: number) {
return await this.dbRead.prisma.secondaryEmail.findMany({
where: {
userId,
emailVerified: {
not: null,
},
},
});
}
async getExistingSecondaryEmailByUserAndEmail(userId: number, email: string) {
const existingSecondaryEmailRecord = await this.dbRead.prisma.secondaryEmail.findUnique({
where: {
userId_email: { userId, email },
},
});
return existingSecondaryEmailRecord?.email;
}
async getExistingSecondaryEmail(email: string) {
const existingSecondaryEmailRecord = await this.dbRead.prisma.secondaryEmail.findUnique({
where: {
email,
},
});
return existingSecondaryEmailRecord?.email;
}
async addSecondaryEmail(userId: number, email: string) {
const existingSecondaryEmailRecord = await this.dbWrite.prisma.secondaryEmail.create({
data: {
userId,
email,
emailVerified: new Date(),
},
});
return existingSecondaryEmailRecord?.email;
}
}
@@ -1,10 +1,10 @@
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
import { AtomsSecondaryEmailsRepository } from "@/modules/atoms/atoms-secondary-emails.repository";
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
import { GetVerifiedEmailsInput } from "@/modules/atoms/inputs/get-verified-emails-params";
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { UserWithProfile } from "@/modules/users/users.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable, BadRequestException, UnauthorizedException } from "@nestjs/common";
import {
@@ -17,8 +17,8 @@ import {
@Injectable()
export class VerificationAtomsService {
constructor(
private readonly atomsRepository: AtomsRepository,
private readonly teamsRepository: TeamsRepository
private readonly atomsSecondaryEmailsRepository: AtomsSecondaryEmailsRepository,
private readonly usersRepository: UsersRepository
) {}
async checkEmailVerificationRequired(input: CheckEmailVerificationRequiredParams) {
@@ -76,7 +76,8 @@ export class VerificationAtomsService {
if (teamId) {
const verifiedEmails: string[] = [];
const teamMembers = await this.teamsRepository.getTeamMemberEmails(teamId);
const teamMembers = await this.usersRepository.getUserEmailsVerifiedForTeam(teamId);
if (teamMembers.length === 0) {
return verifiedEmails;
@@ -96,7 +97,7 @@ export class VerificationAtomsService {
let verifiedEmails = [userEmailWithoutOauthClientId];
const secondaryEmails = await this.atomsRepository.getSecondaryEmails(userId);
const secondaryEmails = await this.atomsSecondaryEmailsRepository.getSecondaryEmailsVerified(userId);
verifiedEmails = verifiedEmails.concat(
secondaryEmails.map((secondaryEmail) => this.removeClientIdFromEmail(secondaryEmail.email))
);
@@ -113,11 +114,9 @@ export class VerificationAtomsService {
existingPrimaryEmail: string;
email: string;
}): Promise<boolean> {
const existingSecondaryEmail = await this.atomsRepository.getExistingSecondaryEmailByUserAndEmail(
userId,
email
);
const alreadyExistingEmail = await this.atomsRepository.getExistingSecondaryEmail(email);
const existingSecondaryEmail =
await this.atomsSecondaryEmailsRepository.getExistingSecondaryEmailByUserAndEmail(userId, email);
const alreadyExistingEmail = await this.atomsSecondaryEmailsRepository.getExistingSecondaryEmail(email);
if (alreadyExistingEmail) {
throw new BadRequestException("Email already exists");
@@ -127,7 +126,7 @@ export class VerificationAtomsService {
return true;
}
await this.atomsRepository.addSecondaryEmail(userId, email);
await this.atomsSecondaryEmailsRepository.addSecondaryEmailVerified(userId, email);
return true;
}
@@ -705,6 +705,111 @@ describe("OAuth Client Users Endpoints", () => {
.expect(200);
});
describe("managed user time zone", () => {
describe("negative tests", () => {
it("should not allow '' time zone", async () => {
const requestBody = {
email: "whatever2@gmail.com",
timeZone: "",
name: "Bob Smithson",
};
await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(400);
});
it("should not allow 'invalid-timezone' time zone", async () => {
const requestBody = {
email: "whatever2@gmail.com",
timeZone: "invalid-timezone",
name: "Bob Smithson",
};
await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(400);
});
});
describe("positive tests", () => {
it("should allow null timezone", async () => {
const requestBody = {
email: "whatever1@gmail.com",
timeZone: null,
name: "Bob Smithson",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.data.user.timeZone).toEqual("Europe/London");
await userRepositoryFixture.delete(responseBody.data.user.id);
});
it("should allow undefined time zone", async () => {
const requestBody = {
email: "whatever3@gmail.com",
timeZone: undefined,
name: "Bob Smithson",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.data.user.timeZone).toEqual("Europe/London");
await userRepositoryFixture.delete(responseBody.data.user.id);
});
it("should allow valid time zone", async () => {
const requestBody = {
email: "whatever4@gmail.com",
timeZone: "Europe/Rome",
name: "Bob Smithson",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.data.user.timeZone).toBe("Europe/Rome");
await userRepositoryFixture.delete(responseBody.data.user.id);
});
it("should allow without any time zone", async () => {
const requestBody = {
email: "whatever5@gmail.com",
name: "Bob Smithson",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.data.user.timeZone).toEqual("Europe/London");
await userRepositoryFixture.delete(responseBody.data.user.id);
});
});
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await oauthClientRepositoryFixture.delete(oAuthClientEventTypesDisabled.id);
@@ -712,17 +817,17 @@ describe("OAuth Client Users Endpoints", () => {
try {
await userRepositoryFixture.delete(postResponseData.user.id);
} catch (e) {
// User might have been deleted by the test
console.log(e);
}
try {
await userRepositoryFixture.delete(postResponseData2.user.id);
} catch (e) {
// User might have been deleted by the test
console.log(e);
}
try {
await userRepositoryFixture.delete(platformAdmin.id);
} catch (e) {
// User might have been deleted by the test
console.log(e);
}
await app.close();
});
@@ -931,7 +1036,7 @@ describe("OAuth Client Users Endpoints", () => {
try {
await userRepositoryFixture.delete(postResponseData.user.id);
} catch (e) {
// User might have been deleted by the test
console.log(e);
}
await app.close();
});
@@ -351,6 +351,7 @@ describe("Organizations Users Endpoints", () => {
email: `organizations-users-new-member-${randomString()}@api.com`,
bio,
metadata,
timeZone: "Europe/Rome",
};
const emailSpy = jest
@@ -367,6 +368,7 @@ describe("Organizations Users Endpoints", () => {
expect(userData.email).toBe(newOrgUser.email);
expect(userData.bio).toBe(newOrgUser.bio);
expect(userData.metadata).toEqual(newOrgUser.metadata);
expect(userData.timeZone).toBe(newOrgUser.timeZone);
expect(emailSpy).toHaveBeenCalledWith({
usernameOrEmail: newOrgUser.email,
orgName: org.name,
@@ -328,6 +328,131 @@ describe("Teams Memberships Endpoints", () => {
return request(app.getHttpServer()).get(`/v2/teams/${team.id}/memberships/123132145`).expect(404);
});
it("should filter memberships by single email", async () => {
return request(app.getHttpServer())
.get(`/v2/teams/${team.id}/memberships?emails=${teamAdminEmail}`)
.expect(200)
.then((response) => {
const responseBody: GetTeamMembershipsOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data.length).toEqual(1);
expect(responseBody.data[0].user.email).toEqual(teamAdminEmail);
expect(responseBody.data[0].userId).toEqual(teamAdmin.id);
expect(responseBody.data[0].role).toEqual("ADMIN");
});
});
it("should filter memberships by multiple emails", async () => {
return request(app.getHttpServer())
.get(`/v2/teams/${team.id}/memberships?emails=${teamAdminEmail},${teamMemberEmail}`)
.expect(200)
.then((response) => {
const responseBody: GetTeamMembershipsOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data.length).toEqual(2);
const emails = responseBody.data.map((membership) => membership.user.email);
expect(emails).toContain(teamAdminEmail);
expect(emails).toContain(teamMemberEmail);
const adminMembership = responseBody.data.find((m) => m.user.email === teamAdminEmail);
const memberMembership = responseBody.data.find((m) => m.user.email === teamMemberEmail);
expect(adminMembership).toBeDefined();
expect(memberMembership).toBeDefined();
expect(adminMembership?.role).toEqual("ADMIN");
expect(memberMembership?.role).toEqual("MEMBER");
});
});
it("should return empty array when filtering by non-existent email", async () => {
const nonExistentEmail = `nonexistent-${randomString()}@test.com`;
return request(app.getHttpServer())
.get(`/v2/teams/${team.id}/memberships?emails=${nonExistentEmail}`)
.expect(200)
.then((response) => {
const responseBody: GetTeamMembershipsOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data.length).toEqual(0);
});
});
it("should return partial results when filtering by mix of existing and non-existent emails", async () => {
const nonExistentEmail = `nonexistent-${randomString()}@test.com`;
return request(app.getHttpServer())
.get(`/v2/teams/${team.id}/memberships?emails=${teamAdminEmail},${nonExistentEmail}`)
.expect(200)
.then((response) => {
const responseBody: GetTeamMembershipsOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data.length).toEqual(1);
expect(responseBody.data[0].user.email).toEqual(teamAdminEmail);
});
});
it("should work with pagination and email filtering combined", async () => {
return request(app.getHttpServer())
.get(`/v2/teams/${team.id}/memberships?emails=${teamAdminEmail},${teamMemberEmail}&skip=1&take=1`)
.expect(200)
.then((response) => {
const responseBody: GetTeamMembershipsOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data.length).toEqual(1);
const returnedEmail = responseBody.data[0].user.email;
expect([teamAdminEmail, teamMemberEmail]).toContain(returnedEmail);
});
});
it("should handle empty emails array gracefully", async () => {
return request(app.getHttpServer())
.get(`/v2/teams/${team.id}/memberships?emails=`)
.expect(200)
.then((response) => {
const responseBody: GetTeamMembershipsOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data.length).toEqual(2);
});
});
it("should handle URL encoded email addresses in filter", async () => {
const encodedEmail = encodeURIComponent(teamAdminEmail);
return request(app.getHttpServer())
.get(`/v2/teams/${team.id}/memberships?emails=${encodedEmail}`)
.expect(200)
.then((response) => {
const responseBody: GetTeamMembershipsOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data.length).toEqual(1);
expect(responseBody.data[0].user.email).toEqual(teamAdminEmail);
});
});
it("should filter by email and maintain all user properties", async () => {
return request(app.getHttpServer())
.get(`/v2/teams/${team.id}/memberships?emails=${teamMemberEmail}`)
.expect(200)
.then((response) => {
const responseBody: GetTeamMembershipsOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data.length).toEqual(1);
const membership = responseBody.data[0];
expect(membership.user.email).toEqual(teamMemberEmail);
expect(membership.user.bio).toEqual(teamMember.bio);
expect(membership.user.metadata).toEqual(teamMember.metadata);
expect(membership.user.username).toEqual(teamMember.username);
expect(membership.teamId).toEqual(team.id);
expect(membership.userId).toEqual(teamMember.id);
expect(membership.role).toEqual("MEMBER");
});
});
it("should validate email array size limits", async () => {
const tooManyEmails = Array.from({ length: 21 }, (_, i) => `test${i}@example.com`).join(",");
return request(app.getHttpServer())
.get(`/v2/teams/${team.id}/memberships?emails=${tooManyEmails}`)
.expect(400);
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(teamAdmin.email);
await userRepositoryFixture.deleteByEmail(teammateInvitedViaApi.email);
@@ -4,6 +4,7 @@ import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
import { GetTeamMembershipsInput } from "@/modules/teams/memberships/inputs/get-team-memberships.input";
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output";
import { DeleteTeamMembershipOutput } from "@/modules/teams/memberships/outputs/delete-team-membership.output";
@@ -32,7 +33,6 @@ import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries/event-types";
import { SkipTakePagination } from "@calcom/platform-types";
@Controller({
path: "/v2/teams/:teamId/memberships",
@@ -85,16 +85,20 @@ export class TeamsMembershipsController {
}
@Get("/")
@ApiOperation({ summary: "Get all memberships" })
@ApiOperation({
summary: "Get all memberships",
description: "Retrieve team memberships with optional filtering by email addresses. Supports pagination.",
})
@Roles("TEAM_ADMIN")
@HttpCode(HttpStatus.OK)
async getTeamMemberships(
@Param("teamId", ParseIntPipe) teamId: number,
@Query() queryParams: SkipTakePagination
@Query() queryParams: GetTeamMembershipsInput
): Promise<GetTeamMembershipsOutput> {
const { skip, take } = queryParams;
const { skip, take, emails } = queryParams;
const orgTeamMemberships = await this.teamsMembershipsService.getPaginatedTeamMemberships(
teamId,
emails,
skip ?? 0,
take ?? 250
);
@@ -0,0 +1,32 @@
import { GetUsersInput } from "@/modules/users/inputs/get-users.input";
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { ArrayMaxSize, ArrayNotEmpty, IsEmail, IsOptional } from "class-validator";
export class GetTeamMembershipsInput extends GetUsersInput {
@IsOptional()
@Transform(({ value }) => {
if (value == null) return undefined;
const rawValues = (Array.isArray(value) ? value : [value]).flatMap((entry) =>
typeof entry === "string" ? entry.split(",") : []
);
const normalized = rawValues
.map((email) => email.trim())
.filter((email) => email.length > 0)
.map((email) => email.toLowerCase());
const deduplicated = [...new Set(normalized)];
return deduplicated.length > 0 ? deduplicated : undefined;
})
@ArrayNotEmpty({ message: "emails cannot be empty." })
@ArrayMaxSize(20, {
message: "emails array cannot contain more than 20 email addresses for team membership filtering",
})
@IsEmail({}, { each: true, message: "Each email must be a valid email address" })
@ApiPropertyOptional({
type: [String],
description:
"Filter team memberships by email addresses. If you want to filter by multiple emails, separate them with a comma (max 20 emails for performance).",
example: "?emails=user1@example.com,user2@example.com",
})
emails?: string[];
}
@@ -23,13 +23,15 @@ export class TeamsMembershipsService {
return teamMembership;
}
async getPaginatedTeamMemberships(teamId: number, skip = 0, take = 250) {
const teamMemberships = await this.teamsMembershipsRepository.findTeamMembershipsPaginated(
async getPaginatedTeamMemberships(teamId: number, emails?: string[], skip = 0, take = 250) {
const emailArray = !emails ? [] : emails;
return await this.teamsMembershipsRepository.findTeamMembershipsPaginatedWithFilters(
teamId,
{ emails: emailArray },
skip,
take
);
return teamMemberships;
}
async getTeamMembership(teamId: number, membershipId: number) {
@@ -6,6 +6,10 @@ import { Injectable } from "@nestjs/common";
import type { Prisma } from "@calcom/prisma/client";
export interface TeamMembershipFilters {
emails?: string[];
}
export const MembershipUserSelect: Prisma.UserSelect = {
username: true,
email: true,
@@ -41,6 +45,30 @@ export class TeamsMembershipsRepository {
});
}
async findTeamMembershipsPaginatedWithFilters(
teamId: number,
filters: TeamMembershipFilters,
skip: number,
take: number
) {
const whereClause: Prisma.MembershipWhereInput = {
teamId: teamId,
};
if (filters.emails && filters.emails.length > 0) {
whereClause.user = {
email: { in: filters.emails },
};
}
return await this.dbRead.prisma.membership.findMany({
where: whereClause,
include: { user: { select: MembershipUserSelect } },
skip,
take,
});
}
async findTeamMembership(teamId: number, membershipId: number) {
return this.dbRead.prisma.membership.findUnique({
where: {
@@ -120,30 +120,4 @@ export class TeamsRepository {
},
});
}
async getTeamMemberEmails(teamId: number) {
return this.dbRead.prisma.user.findMany({
where: {
teams: {
some: {
teamId,
},
},
},
select: {
id: true,
email: true,
secondaryEmails: {
where: {
emailVerified: {
not: null,
},
},
select: {
email: true,
},
},
},
});
}
}
@@ -1,6 +1,7 @@
import { Locales } from "@/lib/enums/locales";
import { CapitalizeTimeZone } from "@/lib/inputs/capitalize-timezone";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsOptional, IsTimeZone, IsString, IsEnum, IsIn, IsUrl, IsObject } from "class-validator";
import { ValidateMetadata } from "@calcom/platform-types";
@@ -29,6 +30,7 @@ export class CreateManagedUserInput {
})
weekStart?: WeekDay;
@Transform(({ value }) => (value === null ? undefined : value))
@IsTimeZone()
@IsOptional()
@CapitalizeTimeZone()
@@ -1,3 +1,4 @@
import { CapitalizeTimeZone } from "@/lib/inputs/capitalize-timezone";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Expose, Transform } from "class-transformer";
import {
@@ -112,6 +113,7 @@ export class CreateUserInput {
@ApiProperty({ type: String, required: false, description: "Time zone", example: "America/New_York" })
@IsOptional()
@IsString()
@CapitalizeTimeZone()
@Validate(TimeZoneValidator)
@Expose()
timeZone?: string;
@@ -28,6 +28,7 @@ export class GetUsersInput {
@ApiPropertyOptional({
type: [String],
description: "The email address or an array of email addresses to filter by",
example: ["user1@example.com", "user2@example.com"],
})
emails?: string[];
}
@@ -408,4 +408,30 @@ export class UsersRepository {
},
});
}
async getUserEmailsVerifiedForTeam(teamId: number) {
return this.dbRead.prisma.user.findMany({
where: {
teams: {
some: {
teamId,
},
},
},
select: {
id: true,
email: true,
secondaryEmails: {
where: {
emailVerified: {
not: null,
},
},
select: {
email: true,
},
},
},
});
}
}
@@ -1,6 +1,6 @@
import type { ValidatorConstraintInterface } from "class-validator";
import { ValidatorConstraint } from "class-validator";
import tzdata from "tzdata";
import * as tzdata from "tzdata";
@ValidatorConstraint({ name: "timezoneValidator", async: false })
export class TimeZoneValidator implements ValidatorConstraintInterface {
-8
View File
@@ -636,10 +636,6 @@ select:focus {
@apply !border-r-default left-0.5 !border-y-0 !border-l-0;
}
.intercom-lightweight-app {
@apply z-40 !important;
}
@import "../../web/styles/tokens.css";
@layer base {
@@ -1027,10 +1023,6 @@ select:focus {
@apply !border-r-default left-0.5 !border-y-0 !border-l-0;
}
.intercom-lightweight-app {
@apply z-40 !important;
}
.prose :where(a:not([data-card])):not(:where([class~="not-prose"] *)) {
text-decoration: none;
}
@@ -45,7 +45,7 @@ export function ShellMainAppDir(props: LayoutProps) {
className={classNames(
props.backPath
? "relative"
: "pwa:bottom-[max(7rem,_calc(5rem_+_env(safe-area-inset-bottom)))] fixed bottom-20 z-40 ltr:right-4 rtl:left-4 md:z-auto md:ltr:right-0 md:rtl:left-0",
: "pwa:bottom-[max(7rem,_calc(5rem_+_env(safe-area-inset-bottom)))] fixed bottom-32 z-40 ltr:right-4 rtl:left-4 md:z-auto md:ltr:right-0 md:rtl:left-0",
"flex-shrink-0 [-webkit-app-region:no-drag] md:relative md:bottom-auto md:right-auto"
)}>
{props.CTA}
@@ -5,10 +5,11 @@ import { unstable_cache } from "next/cache";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
import { MembershipRole } from "@calcom/prisma/enums";
import { availabilityRouter } from "@calcom/trpc/server/routers/viewer/availability/_router";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -72,8 +73,14 @@ const Page = async ({ searchParams: _searchParams }: PageProps) => {
orgId: organizationId,
})
: false;
const isOrgAdminOrOwner = checkAdminOrOwner(session?.user?.org?.role);
const canViewTeamAvailability = isOrgAdminOrOwner || !isOrgPrivate;
const permissionService = new PermissionCheckService();
const teamIdsWithPermission = await permissionService.getTeamIdsWithPermission({
userId: session.user.id,
permission: "availability.read",
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
});
const canViewTeamAvailability = teamIdsWithPermission.length > 0 || !isOrgPrivate;
return (
<ShellMainAppDir
@@ -23,7 +23,8 @@ const ServerPage = async ({ searchParams: _searchParams }: ServerPageProps) => {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const searchParams = await _searchParams;
const token = Array.isArray(searchParams?.token) ? searchParams.token[0] : searchParams?.token;
const callbackUrl = token ? `/teams?token=${encodeURIComponent(token)}` : null;
const autoAccept = Array.isArray(searchParams?.autoAccept) ? searchParams.autoAccept[0] : searchParams?.autoAccept;
const callbackUrl = token ? `/teams?token=${encodeURIComponent(token)}${autoAccept ? `&autoAccept=${encodeURIComponent(autoAccept)}` : ''}` : null;
if (!session) {
redirect(callbackUrl ? `/auth/login?callbackUrl=${callbackUrl}` : "/auth/login");
@@ -2,11 +2,12 @@ import type { SearchParams } from "app/_types";
import type { Session } from "next-auth";
import { unstable_cache } from "next/cache";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { TeamsListing } from "@calcom/features/ee/teams/components/TeamsListing";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { TeamRepository } from "@calcom/lib/server/repository/team";
import { TeamService } from "@calcom/lib/server/service/teamService";
import prisma from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
@@ -32,14 +33,21 @@ export const ServerTeamsListing = async ({
session: Session;
}) => {
const token = Array.isArray(searchParams?.token) ? searchParams.token[0] : searchParams?.token;
const autoAccept = Array.isArray(searchParams?.autoAccept) ? searchParams.autoAccept[0] : searchParams?.autoAccept;
const userId = session.user.id;
let invitationAccepted = false;
let teamNameFromInvite,
errorMsgFromInvite = null;
if (token) {
try {
teamNameFromInvite = await TeamService.inviteMemberByToken(token, userId);
if (autoAccept === "true") {
await TeamService.acceptInvitationByToken(token, userId);
invitationAccepted = true;
} else {
teamNameFromInvite = await TeamService.inviteMemberByToken(token, userId);
}
} catch (e) {
errorMsgFromInvite = "Error while fetching teams";
if (e instanceof TRPCError) errorMsgFromInvite = e.message;
@@ -49,21 +57,30 @@ export const ServerTeamsListing = async ({
const teams = await getCachedTeams(userId);
const userProfile = session?.user?.profile;
const orgId = userProfile?.organizationId ?? session?.user.org?.id;
const orgRole =
session?.user?.org?.role ??
userProfile?.organization?.members.find((m: { userId: number }) => m.userId === userId)?.role;
const isOrgAdminOrOwner = checkAdminOrOwner(orgRole);
const permissionCheckService = new PermissionCheckService();
const canCreateTeam = orgId
? await permissionCheckService.checkPermission({
userId: session.user.id,
teamId: orgId,
permission: "team.create",
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
})
: false;
return {
Main: (
<TeamsListing
invitationAccepted={invitationAccepted}
teams={teams}
orgId={orgId ?? null}
isOrgAdmin={isOrgAdminOrOwner}
permissions={{
canCreateTeam: canCreateTeam,
}}
teamNameFromInvite={teamNameFromInvite ?? null}
errorMsgFromInvite={errorMsgFromInvite}
/>
),
CTA: !orgId || isOrgAdminOrOwner ? <TeamsCTA /> : null,
CTA: !orgId || canCreateTeam ? <TeamsCTA /> : null,
};
};
@@ -18,6 +18,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import { ArrowButton } from "@calcom/ui/components/arrow-button";
import { Badge } from "@calcom/ui/components/badge";
@@ -46,6 +47,10 @@ function NewFormButton({ setNewFormDialogState }: { setNewFormDialogState: SetNe
createFunction={(teamId) => {
setNewFormDialogState({ action: "new", target: teamId ? String(teamId) : "" });
}}
withPermission={{
permission: "routingForm.create",
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
}}
/>
);
}
@@ -175,6 +175,7 @@ const organizationAdminKeys = ["privacy", "OAuth Clients", "SSO", "directory_syn
export interface SettingsPermissions {
canViewRoles?: boolean;
canViewOrganizationBilling?: boolean;
canUpdateOrganization?: boolean;
}
const useTabs = ({
@@ -190,7 +191,6 @@ const useTabs = ({
const { data: user } = trpc.viewer.me.get.useQuery({ includePasswordAdded: true });
const orgBranding = useOrgBranding();
const isAdmin = session.data?.user.role === UserPermissionRole.ADMIN;
const isOrgAdminOrOwner = checkAdminOrOwner(orgBranding?.role);
const processTabsMemod = useMemo(() => {
const processedTabs = getTabs(orgBranding).map((tab) => {
@@ -203,10 +203,10 @@ const useTabs = ({
};
} else if (tab.href === "/settings/organizations") {
const newArray = (tab?.children ?? []).filter(
(child) => isOrgAdminOrOwner || !organizationAdminKeys.includes(child.name)
(child) => permissions?.canUpdateOrganization || !organizationAdminKeys.includes(child.name)
);
if (isOrgAdminOrOwner) {
if (permissions?.canUpdateOrganization) {
newArray.splice(4, 0, {
name: "attributes",
href: "/settings/organizations/attributes",
@@ -238,7 +238,7 @@ const useTabs = ({
});
}
} else {
if (isOrgAdminOrOwner) {
if (permissions?.canUpdateOrganization) {
newArray.push({
name: "billing",
href: "/settings/organizations/billing",
@@ -264,7 +264,7 @@ const useTabs = ({
return { ...tab, children: filtered };
} else if (tab.href === "/settings/developer") {
const filtered = tab?.children?.filter(
(childTab) => isOrgAdminOrOwner || childTab.name !== "admin_api"
(childTab) => permissions?.canUpdateOrganization || childTab.name !== "admin_api"
);
return { ...tab, children: filtered };
}
@@ -274,12 +274,12 @@ const useTabs = ({
// check if name is in adminRequiredKeys
return processedTabs.filter((tab) => {
if (organizationRequiredKeys.includes(tab.name)) return !!orgBranding;
if (tab.name === "other_teams" && !isOrgAdminOrOwner) return false;
if (tab.name === "other_teams" && !permissions?.canUpdateOrganization) return false;
if (isAdmin) return true;
return !adminRequiredKeys.includes(tab.name);
});
}, [isAdmin, orgBranding, isOrgAdminOrOwner, user, isDelegationCredentialEnabled, isPbacEnabled, permissions]);
}, [isAdmin, orgBranding, user, isDelegationCredentialEnabled, isPbacEnabled, permissions]);
return processTabsMemod;
};
@@ -560,8 +560,6 @@ const SettingsSidebarContainer = ({
}
}, [searchParams?.get("id"), otherTeams]);
const isOrgAdminOrOwner = checkAdminOrOwner(orgBranding?.role);
return (
<nav
style={{ maxHeight: `calc(100vh - ${bannersHeight}px)`, top: `${bannersHeight}px` }}
@@ -638,12 +636,12 @@ const SettingsSidebarContainer = ({
as="p"
className="text-subtle truncate text-sm font-medium leading-5"
loadingClassName="ms-3">
{t(isOrgAdminOrOwner ? "my_teams" : tab.name)}
{t("my_teams")}
</Skeleton>
</div>
</Link>
<TeamListCollapsible teamFeatures={teamFeatures} />
{(!orgBranding?.id || isOrgAdminOrOwner) && (
{(!orgBranding?.id || permissions?.canUpdateOrganization) && (
<VerticalTabItem
name={t("add_a_team")}
href={`${WEBAPP_URL}/settings/teams/new`}
@@ -47,6 +47,7 @@ export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) {
let teamFeatures: Record<number, TeamFeatures> | null = null;
let canViewRoles = false;
let canViewOrganizationBilling = false;
let canUpdateOrganization = false;
const orgId = session?.user?.profile?.organizationId ?? session?.user.org?.id;
// For now we only grab organization features but it would be nice to fetch these on the server side for specific team feature flags
@@ -68,6 +69,11 @@ export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) {
canViewRoles = roleActions[CrudAction.Read] ?? false;
const orgActions = PermissionMapper.toActionMap(organizationPermissions, Resource.Organization);
canViewOrganizationBilling = orgActions[CustomAction.ManageBilling] ?? isOrgAdminOrOwner;
canUpdateOrganization = orgActions[CrudAction.Update] ?? isOrgAdminOrOwner;
} else {
canViewRoles = false;
canViewOrganizationBilling = isOrgAdminOrOwner;
canUpdateOrganization = isOrgAdminOrOwner;
}
}
@@ -76,7 +82,7 @@ export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) {
<SettingsLayoutAppDirClient
{...props}
teamFeatures={teamFeatures ?? {}}
permissions={{ canViewRoles, canViewOrganizationBilling }}
permissions={{ canViewRoles, canViewOrganizationBilling, canUpdateOrganization }}
/>
</>
);
@@ -1,8 +1,9 @@
import { _generateMetadata } from "app/_utils";
import OrgAttributesEditPage from "@calcom/ee/organizations/pages/settings/attributes/attributes-edit-view";
import { MembershipRole } from "@calcom/prisma/enums";
import { validateUserHasOrgAdmin } from "../../../../actions/validateUserHasOrgAdmin";
import { validateUserHasOrgPerms } from "../../../../actions/validateUserHasOrgPerms";
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
await _generateMetadata(
@@ -14,7 +15,10 @@ export const generateMetadata = async ({ params }: { params: Promise<{ id: strin
);
const OrgAttributesEditPageWrapper = async () => {
await validateUserHasOrgAdmin();
await validateUserHasOrgPerms({
permission: "organization.attributes.update",
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
});
return <OrgAttributesEditPage />;
};
@@ -1,8 +1,9 @@
import { _generateMetadata } from "app/_utils";
import OrgAttributesCreatePage from "@calcom/ee/organizations/pages/settings/attributes/attributes-create-view";
import { MembershipRole } from "@calcom/prisma/enums";
import { validateUserHasOrgAdmin } from "../../../actions/validateUserHasOrgAdmin";
import { validateUserHasOrgPerms } from "../../../actions/validateUserHasOrgPerms";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -14,7 +15,10 @@ export const generateMetadata = async () =>
);
const OrgAttributesCreatePageWrapper = async () => {
await validateUserHasOrgAdmin();
await validateUserHasOrgPerms({
permission: "organization.attributes.create",
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
});
return <OrgAttributesCreatePage />;
};
@@ -7,7 +7,7 @@ import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permi
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
import { validateUserHasOrg } from "../../actions/validateUserHasOrg";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -20,7 +20,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
const session = await validateUserHasOrgAdmin();
const session = await validateUserHasOrg();
const { canRead, canEdit, canDelete, canCreate } = await getResourcePermissions({
userId: session.user.id,
@@ -2,8 +2,9 @@ import { _generateMetadata, getTranslate } from "app/_utils";
import DelegationCredentialList from "@calcom/features/ee/organizations/pages/settings/delegationCredential";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
import { validateUserHasOrgPerms } from "../../actions/validateUserHasOrgPerms";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -17,7 +18,10 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
await validateUserHasOrgAdmin();
await validateUserHasOrgPerms({
permission: "organization.update",
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
});
return (
<SettingsHeader
@@ -6,7 +6,7 @@ import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permi
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
import { validateUserHasOrg } from "../../actions/validateUserHasOrg";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -19,7 +19,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
const session = await validateUserHasOrgAdmin();
const session = await validateUserHasOrg();
const { canEdit } = await getResourcePermissions({
userId: session.user.id,
@@ -7,7 +7,7 @@ import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permi
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
import { validateUserHasOrg } from "../../actions/validateUserHasOrg";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -21,7 +21,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
const session = await validateUserHasOrgAdmin();
const session = await validateUserHasOrg();
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
return redirect("/settings/profile");
@@ -7,7 +7,7 @@ import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permi
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
import { validateUserHasOrg } from "../../actions/validateUserHasOrg";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -20,7 +20,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
const session = await validateUserHasOrgAdmin();
const session = await validateUserHasOrg();
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
return redirect("/settings/organizations/general");
@@ -22,6 +22,7 @@ interface AdvancedPermissionGroupProps {
onChange: (permissions: string[]) => void;
disabled?: boolean;
scope?: Scope;
isPrivate?: boolean;
}
const INTERNAL_DATAACCESS_KEY = "_resource";
@@ -32,10 +33,11 @@ export function AdvancedPermissionGroup({
onChange,
disabled,
scope = Scope.Organization,
isPrivate = false,
}: AdvancedPermissionGroupProps) {
const { t } = useLocale();
const { toggleSinglePermission, toggleResourcePermissionLevel } = usePermissions(scope);
const scopedRegistry = getPermissionsForScope(scope);
const scopedRegistry = getPermissionsForScope(scope, isPrivate);
const resourceConfig = scopedRegistry[resource];
const [isExpanded, setIsExpanded] = useState(false);
@@ -56,9 +56,17 @@ interface RoleSheetProps {
onOpenChange: (open: boolean) => void;
teamId: number;
scope?: Scope;
isPrivate?: boolean; // Add isPrivate prop to control permission visibility
}
export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Organization }: RoleSheetProps) {
export function RoleSheet({
role,
open,
onOpenChange,
teamId,
scope = Scope.Organization,
isPrivate = false,
}: RoleSheetProps) {
const { t } = useLocale();
const router = useRouter();
const isEditing = Boolean(role);
@@ -108,7 +116,8 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
const { isAdvancedMode, permissions, color } = form.watch();
const { filteredResources, scopedRegistry } = useMemo(() => {
const scopedRegistry = getPermissionsForScope(scope);
// Use privacy-aware filtering if we have privacy information
const scopedRegistry = getPermissionsForScope(scope, isPrivate);
const filteredResources = Object.keys(scopedRegistry).filter((resource) =>
t(
scopedRegistry[resource as Resource][CrudAction.All as keyof (typeof scopedRegistry)[Resource]]
@@ -118,7 +127,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
.includes(searchQuery.toLowerCase())
);
return { filteredResources, scopedRegistry };
}, [searchQuery, t, scope]);
}, [searchQuery, t, scope, isPrivate]);
const createMutation = trpc.viewer.pbac.createRole.useMutation({
onSuccess: async () => {
@@ -182,6 +191,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
{...form.register("name")}
placeholder={t("role_name_placeholder")}
disabled={isSystemRole}
maxLength={50}
/>
</div>
<RoleColorPicker
@@ -199,10 +209,13 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
<Label className="mb-0">{t("permissions")}</Label>
<div className="flex items-center gap-2">
<Checkbox
id="advanced_mode_checkbox"
checked={form.watch("isAdvancedMode")}
onCheckedChange={(checked: boolean) => form.setValue("isAdvancedMode", checked)}
/>
<span className="text-sm">{t("advanced")}</span>
<label htmlFor="advanced_mode_checkbox" className="text-sm">
{t("advanced")}
</label>
</div>
</div>
<TextField
@@ -221,6 +234,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
onChange={(newPermissions) => form.setValue("permissions", newPermissions)}
disabled={isSystemRole}
scope={scope}
isPrivate={isPrivate}
/>
))}{" "}
</div>
@@ -245,6 +259,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
onChange={(newPermissions) => form.setValue("permissions", newPermissions)}
disabled={isSystemRole}
scope={scope}
isPrivate={isPrivate}
/>
))}
</div>{" "}
@@ -48,6 +48,7 @@ interface RolesListProps {
initialSheetOpen?: boolean;
teamId: number;
scope?: Scope;
isPrivate?: boolean;
}
export function RolesList({
@@ -57,6 +58,7 @@ export function RolesList({
initialSheetOpen,
teamId,
scope,
isPrivate,
}: RolesListProps) {
const { t } = useLocale();
const { isOpen, setIsOpen, selectedRoleId, setSelectedRoleId, handleSheetOpenChange } = useRoleStates(
@@ -113,6 +115,7 @@ export function RolesList({
onOpenChange={handleSheetOpenChange}
teamId={teamId}
scope={scope}
isPrivate={isPrivate}
/>
</>
);
@@ -18,6 +18,7 @@ interface SimplePermissionItemProps {
onChange: (permissions: string[]) => void;
disabled?: boolean;
scope?: Scope;
isPrivate?: boolean;
}
export function SimplePermissionItem({
@@ -26,10 +27,11 @@ export function SimplePermissionItem({
onChange,
disabled,
scope = Scope.Organization,
isPrivate = false,
}: SimplePermissionItemProps) {
const { t } = useLocale();
const { getResourcePermissionLevel, toggleResourcePermissionLevel } = usePermissions(scope);
const scopedRegistry = getPermissionsForScope(scope);
const scopedRegistry = getPermissionsForScope(scope, isPrivate);
const registry = scopedRegistry || PERMISSION_REGISTRY;
const isAllResources = resource === "*";
@@ -44,6 +44,18 @@ const getCachedResourcePermissions = unstable_cache(
{ revalidate: 3600 }
);
const getCachedTeamPrivacy = unstable_cache(
async (teamId: number) => {
const team = await prisma.team.findUnique({
where: { id: teamId },
select: { isPrivate: true },
});
return team?.isPrivate ?? false;
},
["team-privacy"],
{ revalidate: 3600 }
);
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("roles_and_permissions"),
@@ -69,9 +81,10 @@ const Page = async ({ searchParams }: { searchParams: Record<string, string | st
roleSearchParamsCache.parse(searchParams);
const [roles, rolePermissions] = await Promise.all([
const [roles, rolePermissions, isPrivate] = await Promise.all([
getCachedTeamRoles(session.user.org.id),
getCachedResourcePermissions(session.user.id, session.user.org.id, Resource.Role),
getCachedTeamPrivacy(session.user.org.id),
]);
// NOTE: this approach of fetching permssions per resource does not account for fall back roles.
@@ -108,6 +121,7 @@ const Page = async ({ searchParams }: { searchParams: Record<string, string | st
}}
initialSelectedRole={selectedRole}
initialSheetOpen={isSheetOpen}
isPrivate={isPrivate}
/>
</SettingsHeader>
);
@@ -102,6 +102,7 @@ const Page = async ({ params }: { params: Promise<{ id: string }> }) => {
CustomAction.ChangeMemberRole,
CustomAction.Remove,
CustomAction.ListMembers,
CustomAction.ListMembersPrivate,
CustomAction.Impersonate,
],
fallbackRoles: {
@@ -120,12 +121,17 @@ const Page = async ({ params }: { params: Promise<{ id: string }> }) => {
[CustomAction.Impersonate]: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
[CustomAction.ListMembersPrivate]: {
roles: fallbackRolesCanListMembers,
},
},
});
// Map specific permissions to member actions
const memberPermissions = {
canListMembers: permissions[CustomAction.ListMembers],
canListMembers: team.isPrivate
? permissions[CustomAction.ListMembersPrivate]
: permissions[CustomAction.ListMembers],
canInvite: permissions[CustomAction.Invite],
canChangeMemberRole: permissions[CustomAction.ChangeMemberRole],
canRemove: permissions[CustomAction.Remove],
@@ -4,6 +4,7 @@ import { cookies, headers } from "next/headers";
import { notFound } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getTeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
import type { AppFlags } from "@calcom/features/flags/config";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { PermissionMapper } from "@calcom/features/pbac/domain/mappers/PermissionMapper";
@@ -11,7 +12,6 @@ import { Resource, CrudAction, Scope } from "@calcom/features/pbac/domain/types/
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { RoleService } from "@calcom/features/pbac/services/role.service";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { getTeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
import { prisma } from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -51,6 +51,19 @@ const getCachedResourcePermissions = (userId: number, teamId: number, resource:
{ revalidate: 3600, tags: [`resource-permissions-${teamId}`] }
);
const getCachedTeamPrivacy = (teamId: number) =>
unstable_cache(
async () => {
const team = await prisma.team.findUnique({
where: { id: teamId },
select: { isPrivate: true },
});
return team?.isPrivate ?? false;
},
[`team-privacy-${teamId}`],
{ revalidate: 3600, tags: [`team-privacy-${teamId}`] }
);
const getCachedTeam = (teamId: string, userId: number) =>
unstable_cache(
async () => {
@@ -111,9 +124,10 @@ const Page = async ({
roleSearchParamsCache.parse(searchParams);
const [roles, rolePermissions] = await Promise.all([
const [roles, rolePermissions, isPrivate] = await Promise.all([
getCachedTeamRoles(team.id)(),
getCachedResourcePermissions(session.user.id, team.id, Resource.Role)(),
getCachedTeamPrivacy(team.id)(),
]);
// NOTE: this approach of fetching permssions per resource does not account for fall back roles.
@@ -153,6 +167,7 @@ const Page = async ({
initialSelectedRole={selectedRole}
initialSheetOpen={isSheetOpen}
scope={Scope.Team}
isPrivate={isPrivate}
/>
</SettingsHeader>
);
@@ -70,6 +70,7 @@ const Page = async () => {
userRole: session.user.org.role,
actions: [
CustomAction.ListMembers,
CustomAction.ListMembersPrivate,
CustomAction.Invite,
CustomAction.ChangeMemberRole,
CustomAction.Remove,
@@ -79,6 +80,9 @@ const Page = async () => {
[CustomAction.ListMembers]: {
roles: fallbackRolesThatCanSeeMembers,
},
[CustomAction.ListMembersPrivate]: {
roles: fallbackRolesThatCanSeeMembers,
},
[CustomAction.Invite]: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
@@ -96,7 +100,9 @@ const Page = async () => {
// Map specific permissions to member actions
const memberPermissions = {
canListMembers: permissions[CustomAction.ListMembers],
canListMembers: org.isPrivate
? permissions[CustomAction.ListMembersPrivate]
: permissions[CustomAction.ListMembers],
canInvite: permissions[CustomAction.Invite],
canChangeMemberRole: permissions[CustomAction.ChangeMemberRole],
canRemove: permissions[CustomAction.Remove],
+17 -1
View File
@@ -1,5 +1,6 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { parseRequestData } from "app/api/parseRequestData";
import { cookies } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
@@ -10,6 +11,7 @@ import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
const passwordResetRequestSchema = z.object({
csrfToken: z.string(),
password: z.string().refine(validPassword, () => ({
message: "Password does not meet the requirements",
})),
@@ -18,8 +20,22 @@ const passwordResetRequestSchema = z.object({
async function handler(req: NextRequest) {
const body = await parseRequestData(req);
const {
password: rawPassword,
requestId: rawRequestId,
csrfToken: submittedToken,
} = passwordResetRequestSchema.parse(body);
const cookieStore = await cookies();
const cookieToken = cookieStore.get("calcom.csrf_token")?.value;
if (submittedToken !== cookieToken) {
return NextResponse.json({ error: "Invalid CSRF token" }, { status: 403 });
}
// token verified, delete the cookie / a resubmit on failure requires a new csrf token.
cookieStore.delete("calcom.csrf_token");
const { password: rawPassword, requestId: rawRequestId } = passwordResetRequestSchema.parse(body);
// rate-limited there is a low, very low chance that a password request stays valid long enough
// to brute force 3.8126967e+40 options.
const maybeRequest = await prisma.resetPasswordRequest.findFirstOrThrow({
@@ -0,0 +1,213 @@
import { NextRequest } from "next/server";
import { describe, test, expect, vi, beforeEach } from "vitest";
import { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
vi.mock("next/server", () => ({
NextRequest: class MockNextRequest {
url: string;
method: string;
nextUrl: { searchParams: URLSearchParams };
private _headers: Map<string, string>;
constructor(url: string, options: { method?: string } = {}) {
this.url = url;
this.method = options.method || "GET";
this._headers = new Map();
this.nextUrl = { searchParams: new URLSearchParams(url.split("?")[1] || "") };
}
headers = {
get: (key: string): string | null => this._headers.get(key.toLowerCase()) || null,
set: (key: string, value: string): void => {
this._headers.set(key.toLowerCase(), value);
},
has: (key: string): boolean => this._headers.has(key.toLowerCase()),
};
},
NextResponse: {
json: vi.fn((body, init) => ({
json: vi.fn().mockResolvedValue(body),
status: init?.status || 200,
})),
},
}));
vi.mock("@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService");
vi.mock("@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository");
vi.mock("@calcom/lib/logger", () => ({
default: {
getSubLogger: vi.fn(() => ({
info: vi.fn(),
error: vi.fn(),
})),
},
}));
vi.mock("@calcom/lib/server/perfObserver", () => ({
performance: {
mark: vi.fn(),
measure: vi.fn(),
},
}));
vi.mock("@sentry/nextjs", () => ({
wrapApiHandlerWithSentry: vi.fn((handler) => handler),
captureException: vi.fn(),
}));
vi.mock("@calcom/lib/server/getServerErrorFromUnknown", () => ({
getServerErrorFromUnknown: vi.fn((error) => ({
message: error instanceof Error ? error.message : "Unknown error",
statusCode: 500,
url: "test-url",
method: "GET",
})),
}));
vi.mock("../../defaultResponderForAppDir", () => ({
defaultResponderForAppDir: vi.fn((handler) => handler),
}));
vi.mock("@calcom/prisma", () => ({
prisma: {},
}));
const mockCalendarCacheEventService = vi.mocked(CalendarCacheEventService);
describe("/api/cron/calendar-subscriptions-cleanup", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.stubEnv("CRON_API_KEY", "test-cron-key");
vi.stubEnv("CRON_SECRET", "test-cron-secret");
});
describe("Authentication", () => {
test("should return 403 when no API key is provided", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions-cleanup");
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(403);
const body = await response.json();
expect(body.message).toBe("Forbidden");
});
test("should return 403 when invalid API key is provided", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions-cleanup");
request.headers.set("authorization", "invalid-key");
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(403);
const body = await response.json();
expect(body.message).toBe("Forbidden");
});
test("should accept CRON_API_KEY in authorization header", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions-cleanup");
request.headers.set("authorization", "test-cron-key");
const mockCleanupStaleCache = vi.fn().mockResolvedValue(undefined);
mockCalendarCacheEventService.prototype.cleanupStaleCache = mockCleanupStaleCache;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(200);
expect(mockCleanupStaleCache).toHaveBeenCalled();
});
test("should accept CRON_SECRET as Bearer token", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions-cleanup");
request.headers.set("authorization", "Bearer test-cron-secret");
const mockCleanupStaleCache = vi.fn().mockResolvedValue(undefined);
mockCalendarCacheEventService.prototype.cleanupStaleCache = mockCleanupStaleCache;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(200);
expect(mockCleanupStaleCache).toHaveBeenCalled();
});
test("should accept API key as query parameter", async () => {
const request = new NextRequest(
"http://localhost/api/cron/calendar-subscriptions-cleanup?apiKey=test-cron-key"
);
const mockCleanupStaleCache = vi.fn().mockResolvedValue(undefined);
mockCalendarCacheEventService.prototype.cleanupStaleCache = mockCleanupStaleCache;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(200);
expect(mockCleanupStaleCache).toHaveBeenCalled();
});
});
describe("Cleanup functionality", () => {
test("should successfully cleanup stale cache", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions-cleanup");
request.headers.set("authorization", "test-cron-key");
const mockCleanupStaleCache = vi.fn().mockResolvedValue(undefined);
mockCalendarCacheEventService.prototype.cleanupStaleCache = mockCleanupStaleCache;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(200);
const body = await response.json();
expect(body.ok).toBe(true);
expect(mockCleanupStaleCache).toHaveBeenCalledOnce();
});
test("should handle cleanup errors gracefully", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions-cleanup");
request.headers.set("authorization", "test-cron-key");
const mockError = new Error("Database connection failed");
const mockCleanupStaleCache = vi.fn().mockRejectedValue(mockError);
mockCalendarCacheEventService.prototype.cleanupStaleCache = mockCleanupStaleCache;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(500);
const body = await response.json();
expect(body.message).toBe("Database connection failed");
});
test("should handle non-Error exceptions", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions-cleanup");
request.headers.set("authorization", "test-cron-key");
const mockCleanupStaleCache = vi.fn().mockRejectedValue("String error");
mockCalendarCacheEventService.prototype.cleanupStaleCache = mockCleanupStaleCache;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(500);
const body = await response.json();
expect(body.message).toBe("Unknown error");
});
});
describe("Service instantiation", () => {
test("should instantiate CalendarCacheEventService with correct dependencies", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions-cleanup");
request.headers.set("authorization", "test-cron-key");
const mockCleanupStaleCache = vi.fn().mockResolvedValue(undefined);
mockCalendarCacheEventService.prototype.cleanupStaleCache = mockCleanupStaleCache;
const { GET } = await import("../route");
await GET(request, { params: Promise.resolve({}) });
expect(mockCalendarCacheEventService).toHaveBeenCalledWith({
calendarCacheEventRepository: expect.any(Object),
});
});
});
});
@@ -0,0 +1,39 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { CalendarCacheEventRepository } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository";
import { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
import { prisma } from "@calcom/prisma";
import { defaultResponderForAppDir } from "@calcom/web/app/api/defaultResponderForAppDir";
/**
* Cron webhook
* Cleanup stale calendar cache
*
* @param request
* @returns
*/
async function getHandler(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (![process.env.CRON_API_KEY, `Bearer ${process.env.CRON_SECRET}`].includes(`${apiKey}`)) {
return NextResponse.json({ message: "Forbidden" }, { status: 403 });
}
// instantiate dependencies
const calendarCacheEventRepository = new CalendarCacheEventRepository(prisma);
const calendarCacheEventService = new CalendarCacheEventService({
calendarCacheEventRepository,
});
try {
await calendarCacheEventService.cleanupStaleCache();
return NextResponse.json({ ok: true });
} catch (e: unknown) {
const message = e instanceof Error ? e.message : "Unknown error";
console.error(`[calendar-subscriptions-cleanup] ${message}:`, e);
return NextResponse.json({ message }, { status: 500 });
}
}
export const GET = defaultResponderForAppDir(getHandler);
@@ -0,0 +1,229 @@
import { NextRequest } from "next/server";
import { describe, test, expect, vi, beforeEach } from "vitest";
import { CalendarSubscriptionService } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionService";
vi.mock("next/server", () => ({
NextRequest: class MockNextRequest {
url: string;
method: string;
nextUrl: { searchParams: URLSearchParams };
private _headers: Map<string, string>;
constructor(url: string, options: { method?: string } = {}) {
this.url = url;
this.method = options.method || "GET";
this._headers = new Map();
this.nextUrl = { searchParams: new URLSearchParams(url.split("?")[1] || "") };
}
headers = {
get: (key: string): string | null => this._headers.get(key.toLowerCase()) || null,
set: (key: string, value: string): void => {
this._headers.set(key.toLowerCase(), value);
},
has: (key: string): boolean => this._headers.has(key.toLowerCase()),
};
},
NextResponse: {
json: vi.fn((body, init) => ({
json: vi.fn().mockResolvedValue(body),
status: init?.status || 200,
})),
},
}));
vi.mock("@calcom/features/calendar-subscription/lib/CalendarSubscriptionService");
vi.mock("@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService");
vi.mock("@calcom/features/calendar-subscription/lib/sync/CalendarSyncService");
vi.mock("@calcom/prisma", () => ({
prisma: {},
}));
const mockCalendarSubscriptionService = vi.mocked(CalendarSubscriptionService);
describe("/api/cron/calendar-subscriptions", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
vi.stubEnv("CRON_API_KEY", "test-cron-key");
vi.stubEnv("CRON_SECRET", "test-cron-secret");
});
describe("Authentication", () => {
test("should return 403 when no API key is provided", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions");
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(403);
const body = await response.json();
expect(body.message).toBe("Forbiden");
}, 10000);
test("should return 403 when invalid API key is provided", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions");
request.headers.set("authorization", "invalid-key");
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(403);
const body = await response.json();
expect(body.message).toBe("Forbiden");
});
test("should accept valid API key", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions");
request.headers.set("authorization", "test-cron-key");
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(true);
const mockCheckForNewSubscriptions = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.checkForNewSubscriptions = mockCheckForNewSubscriptions;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(200);
});
});
describe("Feature flag checks", () => {
test("should return early when cache AND sync are disabled", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions");
request.headers.set("authorization", "test-cron-key");
const mockIsCacheEnabled = vi.fn().mockResolvedValue(false);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(false);
const mockCheckForNewSubscriptions = vi.fn();
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.checkForNewSubscriptions = mockCheckForNewSubscriptions;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(200);
const body = await response.json();
expect(body.ok).toBe(true);
expect(mockCheckForNewSubscriptions).not.toHaveBeenCalled();
});
test("should proceed when both cache and sync are enabled", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions");
request.headers.set("authorization", "test-cron-key");
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(true);
const mockCheckForNewSubscriptions = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.checkForNewSubscriptions = mockCheckForNewSubscriptions;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(200);
const body = await response.json();
expect(body.ok).toBe(true);
expect(mockCheckForNewSubscriptions).toHaveBeenCalledOnce();
});
});
describe("Subscription checking functionality", () => {
test("should successfully check for new subscriptions", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions");
request.headers.set("authorization", "test-cron-key");
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(true);
const mockCheckForNewSubscriptions = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.checkForNewSubscriptions = mockCheckForNewSubscriptions;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(200);
const body = await response.json();
expect(body.ok).toBe(true);
expect(mockCheckForNewSubscriptions).toHaveBeenCalledOnce();
});
test("should handle subscription checking errors gracefully", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions");
request.headers.set("authorization", "test-cron-key");
const mockError = new Error("Subscription service unavailable");
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(true);
const mockCheckForNewSubscriptions = vi.fn().mockRejectedValue(mockError);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.checkForNewSubscriptions = mockCheckForNewSubscriptions;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(500);
const body = await response.json();
expect(body.message).toBe("Subscription service unavailable");
});
test("should handle non-Error exceptions", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions");
request.headers.set("authorization", "test-cron-key");
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(true);
const mockCheckForNewSubscriptions = vi.fn().mockRejectedValue("String error");
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.checkForNewSubscriptions = mockCheckForNewSubscriptions;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(500);
const body = await response.json();
expect(body.message).toBe("Unknown error");
});
});
describe("Service instantiation", () => {
test("should instantiate all services with correct dependencies", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions");
request.headers.set("authorization", "test-cron-key");
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(true);
const mockCheckForNewSubscriptions = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.checkForNewSubscriptions = mockCheckForNewSubscriptions;
const { GET } = await import("../route");
await GET(request, { params: Promise.resolve({}) });
expect(mockCalendarSubscriptionService).toHaveBeenCalledWith({
adapterFactory: expect.any(Object),
selectedCalendarRepository: expect.any(Object),
featuresRepository: expect.any(Object),
calendarSyncService: expect.any(Object),
calendarCacheEventService: expect.any(Object),
});
});
});
});
@@ -0,0 +1,66 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { DefaultAdapterFactory } from "@calcom/features/calendar-subscription/adapters/AdaptersFactory";
import { CalendarSubscriptionService } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionService";
import { CalendarCacheEventRepository } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository";
import { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
import { CalendarSyncService } from "@calcom/features/calendar-subscription/lib/sync/CalendarSyncService";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/SelectedCalendarRepository";
import { BookingRepository } from "@calcom/lib/server/repository/booking";
import { prisma } from "@calcom/prisma";
import { defaultResponderForAppDir } from "@calcom/web/app/api/defaultResponderForAppDir";
/**
* Cron webhook
* Checks for new calendar subscriptions (rollouts)
*
* @param request
* @returns
*/
async function getHandler(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (![process.env.CRON_API_KEY, `Bearer ${process.env.CRON_SECRET}`].includes(`${apiKey}`)) {
return NextResponse.json({ message: "Forbiden" }, { status: 403 });
}
// instantiate dependencies
const bookingRepository = new BookingRepository(prisma);
const calendarSyncService = new CalendarSyncService({
bookingRepository,
});
const calendarCacheEventRepository = new CalendarCacheEventRepository(prisma);
const calendarCacheEventService = new CalendarCacheEventService({
calendarCacheEventRepository,
});
const calendarSubscriptionService = new CalendarSubscriptionService({
adapterFactory: new DefaultAdapterFactory(),
selectedCalendarRepository: new SelectedCalendarRepository(prisma),
featuresRepository: new FeaturesRepository(prisma),
calendarSyncService,
calendarCacheEventService,
});
// are features globally enabled
const [isCacheEnabled, isSyncEnabled] = await Promise.all([
calendarSubscriptionService.isCacheEnabled(),
calendarSubscriptionService.isSyncEnabled(),
]);
if (!isCacheEnabled && !isSyncEnabled) {
return NextResponse.json({ ok: true });
}
try {
await calendarSubscriptionService.checkForNewSubscriptions();
return NextResponse.json({ ok: true });
} catch (e) {
const message = e instanceof Error ? e.message : "Unknown error";
return NextResponse.json({ message }, { status: 500 });
}
}
export const GET = defaultResponderForAppDir(getHandler);
@@ -2,4 +2,4 @@ import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { handleQueuedFormResponseCleanup } from "@calcom/app-store/routing-forms/cron/queuedFormResponseCleanup";
export const POST = defaultResponderForAppDir(handleQueuedFormResponseCleanup);
export const GET = defaultResponderForAppDir(handleQueuedFormResponseCleanup);
+13 -7
View File
@@ -3,19 +3,25 @@ import { NextResponse } from "next/server";
import { WEBAPP_URL } from "@calcom/lib/constants";
export async function GET() {
const token = randomBytes(32).toString("hex");
export async function GET(req: Request) {
const url = new URL(req.url);
const sameSiteParam = url.searchParams.get("sameSite");
const useSecureCookies = WEBAPP_URL.startsWith("https://");
// Validate the param, default to "lax"
let sameSite: "lax" | "strict" | "none" = "lax";
if (sameSiteParam === "strict" || (sameSiteParam === "none" && useSecureCookies)) {
sameSite = sameSiteParam;
}
const token = randomBytes(32).toString("hex");
const res = NextResponse.json({ csrfToken: token });
// We need this cookie to be accessible from embeds where the booking flow is displayed within an iframe on a different origin.
// For third‑party iframe contexts (embeds on other sites), browsers require SameSite=None and Secure to make the cookie available.
// For local development on http://localhost we fall back to SameSite=Lax to avoid requiring https during development.
const useSecureCookies = WEBAPP_URL.startsWith("https://");
res.cookies.set("calcom.csrf_token", token, {
httpOnly: true,
secure: useSecureCookies,
sameSite: useSecureCookies ? "none" : "lax",
sameSite,
path: "/",
});
@@ -1,5 +1,3 @@
import { wrapApiHandlerWithSentry } from "@sentry/nextjs";
import { captureException } from "@sentry/nextjs";
import type { Params } from "app/_types";
import { ApiError } from "next/dist/server/api-utils";
import type { NextRequest } from "next/server";
@@ -21,10 +19,13 @@ export const defaultResponderForAppDir = <T extends NextResponse | Response = Ne
let ok = false;
try {
performance.mark("Start");
const result = endpointRoute
? await wrapApiHandlerWithSentry(async () => await handler(req, { params }), endpointRoute)()
: await handler(req, { params });
let result: T | undefined;
if (process.env.NODE_ENV === "development" || !endpointRoute) {
result = await handler(req, { params });
} else {
const { wrapApiHandlerWithSentry } = await import("@sentry/nextjs");
result = await wrapApiHandlerWithSentry(async () => await handler(req, { params }), endpointRoute)();
}
ok = true;
if (result) {
@@ -49,6 +50,7 @@ export const defaultResponderForAppDir = <T extends NextResponse | Response = Ne
// Don't report 400-499 errors to Sentry/console
if (!(serverError.statusCode >= 400 && serverError.statusCode < 500)) {
console.error(serverError);
const { captureException } = await import("@sentry/nextjs");
captureException(error);
}
+16 -1
View File
@@ -4,9 +4,11 @@ import { NextResponse } from "next/server";
import type Stripe from "stripe";
import { z } from "zod";
import { Plan, SubscriptionStatus } from "@calcom/features/ee/billing/repository/IBillingRepository";
import { InternalTeamBilling } from "@calcom/features/ee/billing/teams/internal-team-billing";
import stripe from "@calcom/features/ee/payments/server/stripe";
import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { MembershipSchema } from "@calcom/prisma/zod/modelSchema/MembershipSchema";
import { TeamSchema } from "@calcom/prisma/zod/modelSchema/TeamSchema";
@@ -54,6 +56,19 @@ async function handler(request: NextRequest) {
include: { members: true },
});
if (checkoutSessionSubscription) {
const internalBillingService = new InternalTeamBilling(finalizedTeam);
await internalBillingService.saveTeamBilling({
teamId: finalizedTeam.id,
subscriptionId: checkoutSessionSubscription.id,
subscriptionItemId: checkoutSessionSubscription.items.data[0].id,
customerId: checkoutSessionSubscription.customer as string,
// TODO: Implement true subscription status when webhook events are implemented
status: SubscriptionStatus.ACTIVE,
planName: Plan.TEAM,
});
}
const response = {
message: `Team created successfully. We also made user with ID=${checkoutSessionMetadata.ownerId} the owner of this team.`,
team: schemaTeamReadPublic.parse(finalizedTeam),
+15
View File
@@ -4,6 +4,8 @@ import { NextResponse } from "next/server";
import type Stripe from "stripe";
import { z } from "zod";
import { Plan, SubscriptionStatus } from "@calcom/features/ee/billing/repository/IBillingRepository";
import { InternalTeamBilling } from "@calcom/features/ee/billing/teams/internal-team-billing";
import stripe from "@calcom/features/ee/payments/server/stripe";
import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
@@ -84,6 +86,19 @@ async function getHandler(req: NextRequest) {
},
});
if (checkoutSession && subscription) {
const internalBillingService = new InternalTeamBilling(team);
await internalBillingService.saveTeamBilling({
teamId: team.id,
subscriptionId: subscription.id,
subscriptionItemId: subscription.items.data[0].id,
customerId: subscription.customer as string,
// TODO: Implement true subscription status when webhook events are implemented
status: SubscriptionStatus.ACTIVE,
planName: Plan.TEAM,
});
}
// redirect to team screen
return NextResponse.redirect(
new URL(`/settings/teams/${team.id}/onboard-members?event=team_created`, req.nextUrl.origin),
@@ -0,0 +1,315 @@
import { NextRequest } from "next/server";
import { describe, test, expect, vi, beforeEach } from "vitest";
import { CalendarSubscriptionService } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionService";
vi.mock("next/server", () => ({
NextRequest: class MockNextRequest {
url: string;
method: string;
nextUrl: { searchParams: URLSearchParams };
private _headers: Map<string, string>;
constructor(url: string, options: { method?: string } = {}) {
this.url = url;
this.method = options.method || "POST";
this._headers = new Map();
this.nextUrl = { searchParams: new URLSearchParams(url.split("?")[1] || "") };
}
headers = {
get: (key: string): string | null => this._headers.get(key.toLowerCase()) || null,
set: (key: string, value: string): void => {
this._headers.set(key.toLowerCase(), value);
},
has: (key: string): boolean => this._headers.has(key.toLowerCase()),
};
},
NextResponse: {
json: vi.fn((body, init) => ({
json: vi.fn().mockResolvedValue(body),
status: init?.status || 200,
})),
},
}));
vi.mock("@calcom/features/calendar-subscription/lib/CalendarSubscriptionService");
vi.mock("@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService");
vi.mock("@calcom/features/calendar-subscription/lib/sync/CalendarSyncService");
vi.mock("@calcom/prisma", () => ({
prisma: {},
}));
const mockCalendarSubscriptionService = vi.mocked(CalendarSubscriptionService);
describe("/api/webhooks/calendar-subscription/[provider]", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
});
describe("Provider validation", () => {
test("should accept google_calendar provider", async () => {
const request = new NextRequest("http://localhost/api/webhooks/calendar-subscription/google_calendar", {
method: "POST",
});
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(false);
const mockProcessWebhook = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.processWebhook = mockProcessWebhook;
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "google_calendar" }),
});
expect(response.status).toBe(200);
expect(mockProcessWebhook).toHaveBeenCalledWith("google_calendar", request);
}, 10000);
test("should accept office365_calendar provider", async () => {
const request = new NextRequest(
"http://localhost/api/webhooks/calendar-subscription/office365_calendar",
{
method: "POST",
}
);
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(false);
const mockProcessWebhook = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.processWebhook = mockProcessWebhook;
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "office365_calendar" }),
});
expect(response.status).toBe(200);
expect(mockProcessWebhook).toHaveBeenCalledWith("office365_calendar", request);
});
test("should reject unsupported provider", async () => {
const request = new NextRequest(
"http://localhost/api/webhooks/calendar-subscription/unsupported_calendar",
{
method: "POST",
}
);
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "unsupported_calendar" }),
});
expect(response.status).toBe(400);
const body = await response.json();
expect(body.message).toBe("Unsupported provider");
});
});
describe("Feature flag handling", () => {
test("should return 200 when neither cache nor sync is enabled", async () => {
const request = new NextRequest("http://localhost/api/webhooks/calendar-subscription/google_calendar", {
method: "POST",
});
const mockIsCacheEnabled = vi.fn().mockResolvedValue(false);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(false);
const mockProcessWebhook = vi.fn();
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.processWebhook = mockProcessWebhook;
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "google_calendar" }),
});
expect(response.status).toBe(200);
const body = await response.json();
expect(body.message).toBe("No cache or sync enabled");
expect(mockProcessWebhook).not.toHaveBeenCalled();
});
test("should process webhook when cache is enabled", async () => {
const request = new NextRequest("http://localhost/api/webhooks/calendar-subscription/google_calendar", {
method: "POST",
});
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(false);
const mockProcessWebhook = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.processWebhook = mockProcessWebhook;
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "google_calendar" }),
});
expect(response.status).toBe(200);
const body = await response.json();
expect(body.message).toBe("Webhook processed");
expect(mockProcessWebhook).toHaveBeenCalledWith("google_calendar", request);
});
test("should process webhook when sync is enabled", async () => {
const request = new NextRequest("http://localhost/api/webhooks/calendar-subscription/google_calendar", {
method: "POST",
});
const mockIsCacheEnabled = vi.fn().mockResolvedValue(false);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(true);
const mockProcessWebhook = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.processWebhook = mockProcessWebhook;
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "google_calendar" }),
});
expect(response.status).toBe(200);
const body = await response.json();
expect(body.message).toBe("Webhook processed");
expect(mockProcessWebhook).toHaveBeenCalledWith("google_calendar", request);
});
test("should process webhook when both cache and sync are enabled", async () => {
const request = new NextRequest("http://localhost/api/webhooks/calendar-subscription/google_calendar", {
method: "POST",
});
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(true);
const mockProcessWebhook = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.processWebhook = mockProcessWebhook;
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "google_calendar" }),
});
expect(response.status).toBe(200);
const body = await response.json();
expect(body.message).toBe("Webhook processed");
expect(mockProcessWebhook).toHaveBeenCalledWith("google_calendar", request);
});
});
describe("Error handling", () => {
test("should handle webhook processing errors gracefully", async () => {
const request = new NextRequest("http://localhost/api/webhooks/calendar-subscription/google_calendar", {
method: "POST",
});
const mockError = new Error("Webhook validation failed");
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(false);
const mockProcessWebhook = vi.fn().mockRejectedValue(mockError);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.processWebhook = mockProcessWebhook;
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "google_calendar" }),
});
expect(response.status).toBe(500);
const body = await response.json();
expect(body.message).toBe("Webhook validation failed");
});
test("should handle non-Error exceptions", async () => {
const request = new NextRequest("http://localhost/api/webhooks/calendar-subscription/google_calendar", {
method: "POST",
});
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(false);
const mockProcessWebhook = vi.fn().mockRejectedValue("String error");
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.processWebhook = mockProcessWebhook;
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "google_calendar" }),
});
expect(response.status).toBe(500);
const body = await response.json();
expect(body.message).toBe("Unknown error");
});
test("should handle feature flag check errors", async () => {
const request = new NextRequest("http://localhost/api/webhooks/calendar-subscription/google_calendar", {
method: "POST",
});
const mockError = new Error("Feature flag service unavailable");
const mockIsCacheEnabled = vi.fn().mockRejectedValue(mockError);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(false);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "google_calendar" }),
});
expect(response.status).toBe(500);
const body = await response.json();
expect(body.message).toBe("Feature flag service unavailable");
});
});
describe("Service instantiation", () => {
test("should instantiate all services with correct dependencies", async () => {
const request = new NextRequest("http://localhost/api/webhooks/calendar-subscription/google_calendar", {
method: "POST",
});
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(false);
const mockProcessWebhook = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.processWebhook = mockProcessWebhook;
const { POST } = await import("../route");
await POST(request, {
params: Promise.resolve({ provider: "google_calendar" }),
});
expect(mockCalendarSubscriptionService).toHaveBeenCalledWith({
adapterFactory: expect.any(Object),
selectedCalendarRepository: expect.any(Object),
featuresRepository: expect.any(Object),
calendarSyncService: expect.any(Object),
calendarCacheEventService: expect.any(Object),
});
});
});
});
@@ -0,0 +1,87 @@
import type { Params } from "app/_types";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import type { CalendarSubscriptionProvider } from "@calcom/features/calendar-subscription/adapters/AdaptersFactory";
import { DefaultAdapterFactory } from "@calcom/features/calendar-subscription/adapters/AdaptersFactory";
import { CalendarSubscriptionService } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionService";
import { CalendarCacheEventRepository } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository";
import { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
import { CalendarSyncService } from "@calcom/features/calendar-subscription/lib/sync/CalendarSyncService";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import logger from "@calcom/lib/logger";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/SelectedCalendarRepository";
import { BookingRepository } from "@calcom/lib/server/repository/booking";
import { prisma } from "@calcom/prisma";
import { defaultResponderForAppDir } from "@calcom/web/app/api/defaultResponderForAppDir";
const log = logger.getSubLogger({ prefix: ["calendar-webhook"] });
function extractAndValidateProviderFromParams(params: Params): CalendarSubscriptionProvider | null {
if (!("provider" in params)) {
return null;
}
const { provider } = params;
if (provider === "google_calendar" || provider === "office365_calendar") {
return provider;
}
return null;
}
/**
* Handles incoming POST requests for calendar webhooks.
* It processes the webhook based on the calendar provider specified in the URL.
* If the provider is unsupported, it returns a 400 response.
* If the webhook is processed successfully, it returns a 200 response.
* In case of errors during processing, it returns a 500 response with the error message.
* @param {NextRequest} request - The incoming request object.
* @param {Object} context - The context object containing route parameters.
* @param {Promise<Params>} context.params - A promise that resolves to the route parameters.
* @returns {Promise<NextResponse>} - A promise that resolves to the response object.
*/
async function postHandler(request: NextRequest, ctx: { params: Promise<Params> }) {
const providerFromParams = extractAndValidateProviderFromParams(await ctx.params);
if (!providerFromParams) {
return NextResponse.json({ message: "Unsupported provider" }, { status: 400 });
}
try {
// instantiate dependencies
const bookingRepository = new BookingRepository(prisma);
const calendarSyncService = new CalendarSyncService({
bookingRepository,
});
const calendarCacheEventRepository = new CalendarCacheEventRepository(prisma);
const calendarCacheEventService = new CalendarCacheEventService({
calendarCacheEventRepository,
});
const calendarSubscriptionService = new CalendarSubscriptionService({
adapterFactory: new DefaultAdapterFactory(),
selectedCalendarRepository: new SelectedCalendarRepository(prisma),
featuresRepository: new FeaturesRepository(prisma),
calendarSyncService,
calendarCacheEventService,
});
// are features globally enabled
const [isCacheEnabled, isSyncEnabled] = await Promise.all([
calendarSubscriptionService.isCacheEnabled(),
calendarSubscriptionService.isSyncEnabled(),
]);
if (!isCacheEnabled && !isSyncEnabled) {
log.debug("No cache or sync enabled");
return NextResponse.json({ message: "No cache or sync enabled" }, { status: 200 });
}
await calendarSubscriptionService.processWebhook(providerFromParams, request);
return NextResponse.json({ message: "Webhook processed" }, { status: 200 });
} catch (error) {
log.error("Error processing webhook", { error });
const message = error instanceof Error ? error.message : "Unknown error";
return NextResponse.json({ message }, { status: 500 });
}
}
export const POST = defaultResponderForAppDir(postHandler);
@@ -266,7 +266,7 @@ export default function CancelBooking(props: Props) {
telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
const response = await fetch("/api/csrf", { cache: "no-store" });
const response = await fetch("/api/csrf?sameSite=none", { cache: "no-store" });
const { csrfToken } = await response.json();
const res = await fetch("/api/cancel", {
Regular → Executable
+2 -2
View File
@@ -23,9 +23,9 @@ try {
"*/5 * * * * *",
async function () {
await Promise.allSettled([
fetchCron("/calendar-cache/cron"),
fetchCron("/cron/calendar-subscriptions"),
// fetchCron("/calendar-cache/cron"),
// fetchCron("/cron/calVideoNoShowWebhookTriggers"),
//
fetchCron("/tasks/cron"),
]);
},
+11
View File
@@ -2,6 +2,7 @@
// The added config here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
import { initBotId } from "botid/client/core";
if (process.env.NODE_ENV === "production") {
Sentry.init({
@@ -48,3 +49,13 @@ export function onRouterTransitionStart(url: string, navigationType: "push" | "r
Sentry.captureRouterTransitionStart(url, navigationType);
}
}
process.env.NEXT_PUBLIC_VERCEL_USE_BOTID_IN_BOOKER === "1" &&
initBotId({
protect: [
{
path: "/api/book/event",
method: "POST",
},
],
});
@@ -5,13 +5,13 @@ import {
getOrganizationSettings,
getVerifiedDomain,
} from "@calcom/features/ee/organizations/lib/orgSettings";
import { getTeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { IS_CALCOM } from "@calcom/lib/constants";
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
import logger from "@calcom/lib/logger";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { getTeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
import slugify from "@calcom/lib/slugify";
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
import prisma from "@calcom/prisma";
@@ -1,7 +1,8 @@
"use client";
import Link from "next/link";
import type { CSSProperties } from "react";
import { useEffect, useReducer, type CSSProperties } from "react";
import type { UseFormReturn } from "react-hook-form";
import { useForm } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -16,121 +17,176 @@ import type { getServerSideProps } from "@server/lib/auth/forgot-password/[id]/g
export type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function Page({ requestId, isRequestExpired, csrfToken }: PageProps) {
function Success() {
const { t } = useLocale();
const formMethods = useForm<{ new_password: string }>();
const success = formMethods.formState.isSubmitSuccessful;
const loading = formMethods.formState.isSubmitting;
const passwordValue = formMethods.watch("new_password");
const isEmpty = passwordValue?.length === 0;
return (
<>
<div className="space-y-6">
<div>
<h2 className="font-cal text-emphasis mt-6 text-center text-3xl font-extrabold">
{t("password_updated")}
</h2>
</div>
<Button href="/auth/login" className="w-full justify-center">
{t("login")}
</Button>
</div>
</>
);
}
const submitChangePassword = async ({ password, requestId }: { password: string; requestId: string }) => {
function Expired() {
const { t } = useLocale();
return (
<>
<div className="space-y-6">
<div>
<h2 className="font-cal text-emphasis mt-6 text-center text-3xl font-extrabold">{t("whoops")}</h2>
<h2 className="text-emphasis text-center text-3xl font-extrabold">{t("request_is_expired")}</h2>
</div>
<p>{t("request_is_expired_instructions")}</p>
<Link href="/auth/forgot-password" passHref legacyBehavior>
<button
type="button"
className="flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
{t("try_again")}
</button>
</Link>
</div>
</>
);
}
type FormValues = {
newPassword: string;
csrfToken: string;
};
function PasswordResetForm({
form: formMethods,
requestId,
}: {
form: UseFormReturn<FormValues>;
requestId: string;
}) {
const { t } = useLocale();
const [refreshToken, forceRefresh] = useReducer((x) => x + 1, 0);
const {
watch,
setValue,
setError,
formState: { isSubmitting: loading },
} = formMethods;
useEffect(() => {
fetch("/api/csrf", { cache: "no-store" })
.then((res) => res.json())
.then(({ csrfToken }) => setValue("csrfToken", csrfToken))
.catch(() => setValue("csrfToken", ""));
}, [setValue, refreshToken]);
const submitChangePassword = async ({
password,
requestId,
csrfToken,
}: {
password: string;
requestId: string;
csrfToken: string;
}) => {
const res = await fetch("/api/auth/reset-password", {
method: "POST",
body: JSON.stringify({ requestId, password }),
body: JSON.stringify({ requestId, password, csrfToken }),
headers: {
"Content-Type": "application/json",
},
});
const json = await res.json();
if (!res.ok) return formMethods.setError("new_password", { type: "server", message: json.message });
if (!res.ok) {
// if the request fails, we want to force refresh of the CSRF token - this allows resubmit
forceRefresh();
return setError("newPassword", { type: "server", message: json.message });
}
};
const Success = () => {
return (
<>
<div className="space-y-6">
<div>
<h2 className="font-cal text-emphasis mt-6 text-center text-3xl font-extrabold">
{t("password_updated")}
</h2>
</div>
<Button href="/auth/login" className="w-full justify-center">
{t("login")}
</Button>
</div>
</>
);
};
const passwordValue = watch("newPassword");
const isEmpty = passwordValue?.length === 0;
const Expired = () => {
return (
<Form
className="space-y-6"
form={formMethods}
style={
{
"--cal-brand": "#111827",
"--cal-brand-emphasis": "#101010",
"--cal-brand-text": "white",
"--cal-brand-subtle": "#9CA3AF",
} as CSSProperties
}
handleSubmit={async (values) => {
await submitChangePassword({
password: values.newPassword,
csrfToken: values.csrfToken,
requestId,
});
}}>
<input {...formMethods.register("csrfToken")} name="csrfToken" type="hidden" hidden />
<div className="mt-1">
<PasswordField
{...formMethods.register("newPassword", {
minLength: {
message: t("password_hint_min"),
value: 7, // We don't have user here so we can't check if they are admin or not
},
pattern: {
message: "Should contain a number, uppercase and lowercase letters",
value: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).*$/gm,
},
})}
label={t("new_password")}
/>
</div>
<div>
<Button
loading={loading}
color="primary"
type="submit"
disabled={loading || isEmpty}
className="w-full justify-center">
{t("reset_password")}
</Button>
</div>
</Form>
);
}
export default function Page({ requestId, isRequestExpired }: PageProps) {
const { t } = useLocale();
const formMethods = useForm<FormValues>({
defaultValues: {
newPassword: "",
csrfToken: "",
},
});
const {
formState: { isSubmitSuccessful: success },
} = formMethods;
if (isRequestExpired) {
return (
<>
<div className="space-y-6">
<div>
<h2 className="font-cal text-emphasis mt-6 text-center text-3xl font-extrabold">{t("whoops")}</h2>
<h2 className="text-emphasis text-center text-3xl font-extrabold">{t("request_is_expired")}</h2>
</div>
<p>{t("request_is_expired_instructions")}</p>
<Link href="/auth/forgot-password" passHref legacyBehavior>
<button
type="button"
className="flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
{t("try_again")}
</button>
</Link>
</div>
</>
<AuthContainer showLogo heading={t("reset_password")}>
<Expired />
</AuthContainer>
);
};
}
return (
<AuthContainer showLogo heading={!success ? t("reset_password") : undefined}>
{isRequestExpired && <Expired />}
{!isRequestExpired && !success && (
<>
<Form
className="space-y-6"
form={formMethods}
style={
{
"--cal-brand": "#111827",
"--cal-brand-emphasis": "#101010",
"--cal-brand-text": "white",
"--cal-brand-subtle": "#9CA3AF",
} as CSSProperties
}
handleSubmit={async (values) => {
await submitChangePassword({
password: values.new_password,
requestId,
});
}}>
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
<div className="mt-1">
<PasswordField
{...formMethods.register("new_password", {
minLength: {
message: t("password_hint_min"),
value: 7, // We don't have user here so we can't check if they are admin or not
},
pattern: {
message: "Should contain a number, uppercase and lowercase letters",
value: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).*$/gm,
},
})}
label={t("new_password")}
/>
</div>
<div>
<Button
loading={loading}
color="primary"
type="submit"
disabled={loading || isEmpty}
className="w-full justify-center">
{t("reset_password")}
</Button>
</div>
</Form>
</>
)}
{!isRequestExpired && success && (
<>
<Success />
</>
)}
{success ? <Success /> : <PasswordResetForm form={formMethods} requestId={requestId} />}
</AuthContainer>
);
}
+6
View File
@@ -1,6 +1,7 @@
require("dotenv").config({ path: "../../.env" });
const englishTranslation = require("./public/static/locales/en/common.json");
const { withAxiom } = require("next-axiom");
const { withBotId } = require("botid/next/config");
const { version } = require("./package.json");
const { PrismaPlugin } = require("@prisma/nextjs-monorepo-workaround-plugin");
const {
@@ -117,6 +118,11 @@ if (process.env.ANALYZE === "true") {
}
plugins.push(withAxiom);
if (process.env.NEXT_PUBLIC_VERCEL_USE_BOTID_IN_BOOKER === "1") {
plugins.push(withBotId);
}
const orgDomainMatcherConfig = {
root: nextJsOrgRewriteConfig.disableRootPathRewrite
? null
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "5.7.5",
"version": "5.7.10",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -87,6 +87,7 @@
"async": "^3.2.4",
"bcp-47-match": "^2.0.3",
"bcryptjs": "^2.4.3",
"botid": "^1.5.7",
"classnames": "^2.3.1",
"dompurify": "^3.1.7",
"dotenv-cli": "^6.0.0",
@@ -108,7 +109,7 @@
"memory-cache": "^0.2.0",
"micro": "^10.0.1",
"mime-types": "^2.1.35",
"next": "^15.4.5",
"next": "15.5.4",
"next-auth": "^4.22.1",
"next-axiom": "^0.17.0",
"next-collect": "^0.2.1",
@@ -138,7 +139,7 @@
"react-select": "^5.7.0",
"react-timezone-select": "^1.4.0",
"react-turnstile": "^1.1.3",
"react-use-intercom": "1.5.1",
"react-use-intercom": "^5.5.0",
"recoil": "^0.7.7",
"remove-markdown": "^0.5.0",
"retell-client-js-sdk": "^2.0.0",
+14
View File
@@ -2,11 +2,15 @@ import type { NextApiRequest } from "next";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
import { BotDetectionService } from "@calcom/features/bot-detection";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import getIP from "@calcom/lib/getIP";
import { piiHasher } from "@calcom/lib/server/PiiHasher";
import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import { EventTypeRepository } from "@calcom/lib/server/repository/eventTypeRepository";
import prisma from "@calcom/prisma";
import { CreationSource } from "@calcom/prisma/enums";
async function handler(req: NextApiRequest & { userId?: number }) {
@@ -19,6 +23,16 @@ async function handler(req: NextApiRequest & { userId?: number }) {
});
}
// Check for bot detection using feature flag
const featuresRepository = new FeaturesRepository(prisma);
const eventTypeRepository = new EventTypeRepository(prisma);
const botDetectionService = new BotDetectionService(featuresRepository, eventTypeRepository);
await botDetectionService.checkBotDetection({
eventTypeId: req.body.eventTypeId,
headers: req.headers,
});
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: piiHasher.hash(userIp),
+1 -1
View File
@@ -12,7 +12,7 @@ async function handler(req: NextApiRequest & { userId?: number }) {
const userIp = getIP(req);
await checkRateLimitAndThrowError({
rateLimitingType: "core",
rateLimitingType: "instantMeeting",
identifier: `instant.event-${piiHasher.hash(userIp)}`,
});
@@ -5,7 +5,7 @@ import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { ZGetRetellLLMSchema } from "@calcom/features/calAIPhone/zod-utils";
import type { TGetRetellLLMSchema } from "@calcom/features/calAIPhone/zod-utils";
import { getAvailableSlotsService } from "@calcom/lib/di/containers/AvailableSlots";
import { getAvailableSlotsService } from "@calcom/features/di/containers/AvailableSlots";
import { fetcher } from "@calcom/lib/retellAIFetcher";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import prisma from "@calcom/prisma";
@@ -6,7 +6,7 @@ import stripe from "@calcom/features/ee/payments/server/stripe";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { HttpError as HttpCode } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
export const config = {
api: {
@@ -27,7 +27,10 @@ const handleSubscriptionUpdate = async (event: Stripe.Event) => {
});
if (!app) {
throw new HttpCode({ statusCode: 202, message: "Received and discarded" });
throw new HttpCode({
statusCode: 202,
message: `No credential found with subscription ID ${subscription.id}`,
});
}
await prisma.credential.update({
@@ -51,7 +54,10 @@ const handleSubscriptionDeleted = async (event: Stripe.Event) => {
});
if (!app) {
throw new HttpCode({ statusCode: 202, message: "Received and discarded" });
throw new HttpCode({
statusCode: 202,
message: `No credential found with subscription ID ${subscription.id}`,
});
}
// should we delete the credential here rather than marking as inactive?
@@ -103,7 +109,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
} catch (_err) {
const err = getErrorFromUnknown(_err);
console.error(`Webhook Error: ${err.message}`);
if (!err.message.includes("No credential found with subscription ID")) {
console.error(`Webhook Error: ${err.message}`);
}
res.status(err.statusCode ?? 500).send({
message: err.message,
stack: IS_PRODUCTION ? undefined : err.stack,
@@ -70,7 +70,7 @@ test.describe("Forgot password", async () => {
// Wait for page to fully load
await page.waitForSelector("text=Reset Password");
await page.fill('input[name="new_password"]', newPassword);
await page.fill('input[name="newPassword"]', newPassword);
await page.click('button[type="submit"]');
await page.waitForSelector("text=Password updated");
+33 -2
View File
@@ -160,6 +160,7 @@ const createTeamAndAddUser = async (
orgRequestedSlug,
schedulingType,
assignAllTeamMembersForSubTeamEvents,
teamSlug,
}: {
user: { id: number; email: string; username: string | null; role?: MembershipRole };
isUnpublished?: boolean;
@@ -172,12 +173,15 @@ const createTeamAndAddUser = async (
orgRequestedSlug?: string;
schedulingType?: SchedulingType;
assignAllTeamMembersForSubTeamEvents?: boolean;
teamSlug?: string;
},
workerInfo: WorkerInfo
) => {
const slugIndex = index ? `-count-${index}` : "";
const slug =
orgRequestedSlug ?? `${isOrg ? "org" : "team"}-${workerInfo.workerIndex}-${Date.now()}${slugIndex}`;
teamSlug ??
orgRequestedSlug ??
`${isOrg ? "org" : "team"}-${workerInfo.workerIndex}-${Date.now()}${slugIndex}`;
const data: PrismaType.TeamCreateInput = {
name: `user-id-${user.id}'s ${isOrg ? "Org" : "Team"}`,
isOrganization: isOrg,
@@ -283,6 +287,7 @@ export const createUsersFixture = (
schedulingType?: SchedulingType;
teamEventTitle?: string;
teamEventSlug?: string;
teamSlug?: string;
teamEventLength?: number;
isOrg?: boolean;
isOrgVerified?: boolean;
@@ -367,6 +372,7 @@ export const createUsersFixture = (
orgRequestedSlug: scenario.orgRequestedSlug,
schedulingType: scenario.schedulingType,
assignAllTeamMembersForSubTeamEvents: scenario.assignAllTeamMembersForSubTeamEvents,
teamSlug: scenario?.teamSlug,
},
workerInfo
);
@@ -465,7 +471,7 @@ export const createUsersFixture = (
},
data: {
orgProfiles: _user.profiles.length
? {
? {
connect: _user.profiles.map((profile) => ({ id: profile.id })),
}
: {
@@ -711,6 +717,31 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
}
return membership;
},
getAllTeamMembership: async () => {
const memberships = await prisma.membership.findMany({
where: {
userId: user.id,
team: {
isOrganization: false,
},
},
include: { team: true, user: true },
});
const filteredMemberships = memberships.map((membership) => ({
...membership,
team: {
...membership.team,
metadata: teamMetadataSchema.parse(membership.team.metadata),
},
}));
if (filteredMemberships.length === 0) {
throw new Error("No team memberships found for user");
}
return filteredMemberships;
},
getOrgMembership: async () => {
const membership = await prisma.membership.findFirstOrThrow({
where: {
+1 -2
View File
@@ -15,7 +15,6 @@ export async function expectInvitationEmailToBeReceived(
) {
if (!emails) return null;
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(2000);
const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail });
expect(receivedEmails?.total).toBe(1);
@@ -36,5 +35,5 @@ export async function expectExistingUserToBeInvitedToOrganization(
userEmail: string,
subject?: string | null
) {
return expectInvitationEmailToBeReceived(page, emails, userEmail, subject, "settings/team");
return expectInvitationEmailToBeReceived(page, emails, userEmail, subject, "teams?token");
}
+126 -3
View File
@@ -47,8 +47,7 @@ test.describe("Team", () => {
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
).toHaveCount(1);
// eslint-disable-next-line playwright/no-conditional-in-test
if (!inviteLink) return null;
expect(inviteLink).toBeTruthy();
// Follow invite link to new window
const context = await browser.newContext();
@@ -103,7 +102,7 @@ test.describe("Team", () => {
});
});
test("Invitation (verified)", async ({ browser, page, users, emails }) => {
test("Invitation (verified)", async ({ page, users, emails }) => {
const t = await localize("en");
const teamOwner = await users.create({ name: `team-owner-${Date.now()}` }, { hasTeam: true });
const { team } = await teamOwner.getFirstTeamMembership();
@@ -192,4 +191,128 @@ test.describe("Team", () => {
});
expect(parentEventType?.children.find((et) => et.userId === invitedMember.id)).toBeTruthy();
});
test("Auto-accept invitation for existing user", async ({ browser, page, users, emails }) => {
const t = await localize("en");
const teamOwner = await users.create({ name: "Invited User" }, { hasTeam: true });
const { team } = await teamOwner.getFirstTeamMembership();
const invitedUser = await users.create({
email: `invited-user-${Date.now()}@example.com`,
name: "Invited User",
});
await teamOwner.apiLogin();
await page.goto(`/settings/teams/${team.id}/members`);
let inviteLink: string;
await test.step("Send invitation to existing user", async () => {
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(500);
await page.getByTestId("new-member-button").click();
await page.locator('input[name="inviteUser"]').fill(invitedUser.email);
await page.getByText(t("send_invite")).click();
inviteLink = await expectInvitationEmailToBeReceived(
page,
emails,
invitedUser.email,
`${teamOwner.name} invited you to join the team ${team.name} on Cal.com`,
"teams?token"
);
expect(inviteLink).toContain("autoAccept=true");
const membership = await prisma.membership.findFirst({
where: {
userId: invitedUser.id,
teamId: team.id,
},
});
expect(membership?.accepted).toBe(false);
});
await test.step("Auto-accept invitation by clicking link", async () => {
const [secondContext, secondPage] = await invitedUser.apiLoginOnNewBrowser(browser);
await secondPage.goto(inviteLink);
await expect(secondPage.getByText("Successfully joined")).toBeVisible();
const membership = await prisma.membership.findFirst({
where: {
userId: invitedUser.id,
teamId: team.id,
},
});
expect(membership?.accepted).toBe(true);
await secondPage.close();
await secondContext.close();
});
});
test("Error when wrong user tries to use invitation link", async ({ browser, page, users, emails }) => {
const t = await localize("en");
const teamOwner = await users.create({ name: "Wrong User" }, { hasTeam: true });
const { team } = await teamOwner.getFirstTeamMembership();
const invitedUser = await users.create({
email: `invited-user-${Date.now()}@example.com`,
name: "Invited User",
});
const wrongUser = await users.create({
email: `wrong-user-${Date.now()}@example.com`,
name: "Wrong User",
});
await teamOwner.apiLogin();
await page.goto(`/settings/teams/${team.id}/members`);
let inviteLink: string;
await test.step("Send invitation to specific user", async () => {
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(500);
await page.getByTestId("new-member-button").click();
await page.locator('input[name="inviteUser"]').fill(invitedUser.email);
await page.getByText(t("send_invite")).click();
inviteLink = await expectInvitationEmailToBeReceived(
page,
emails,
invitedUser.email,
`${teamOwner.name} invited you to join the team ${team.name} on Cal.com`,
"teams?token"
);
expect(inviteLink).toContain("autoAccept=true");
});
await test.step("Wrong user tries to use invitation link", async () => {
const [secondContext, secondPage] = await wrongUser.apiLoginOnNewBrowser(browser);
await secondPage.goto(inviteLink);
await expect(secondPage.getByText("This invitation is not for your account")).toBeVisible();
const membership = await prisma.membership.findFirst({
where: {
userId: wrongUser.id,
teamId: team.id,
},
});
expect(membership).toBeNull();
const invitedMembership = await prisma.membership.findFirst({
where: {
userId: invitedUser.id,
teamId: team.id,
},
});
expect(invitedMembership?.accepted).toBe(false);
await secondPage.close();
await secondContext.close();
});
});
});
+106
View File
@@ -10,6 +10,7 @@ import {
confirmReschedule,
fillStripeTestCheckout,
selectFirstAvailableTimeSlotNextMonth,
submitAndWaitForResponse,
testName,
} from "./lib/testUtils";
@@ -301,3 +302,108 @@ test.describe("Teams - NonOrg", () => {
});
todo("Reschedule a Round Robin EventType booking");
});
test.describe("Team Slug Validation", () => {
test.afterEach(({ users, orgs }) => {
users.deleteAll();
orgs.deleteAll();
});
test("Teams in different organizations can have the same slug", async ({ page, users, orgs }) => {
const org1 = await orgs.create({ name: "Organization 1" });
const org2 = await orgs.create({ name: "Organization 2" });
const owner1 = await users.create(
{
organizationId: org1.id,
roleInOrganization: "OWNER",
},
{
hasTeam: true,
teamSlug: "cal",
teamRole: "OWNER",
}
);
const owner2 = await users.create(
{
organizationId: org2.id,
roleInOrganization: "OWNER",
},
{
hasTeam: true,
teamSlug: "calCom",
teamRole: "OWNER",
}
);
const { team: team1 } = await owner1.getFirstTeamMembership();
await owner1.apiLogin();
await page.goto(`/settings/teams/${team1.id}/profile`);
await page.locator('input[name="slug"]').fill("calCom");
await submitAndWaitForResponse(page, "/api/trpc/teams/update?batch=1", {
action: () => page.locator("[data-testid=update-team-profile]").click(),
});
});
test("Teams within same organization cannot have duplicate slugs", async ({ page, users, orgs }) => {
const org = await orgs.create({ name: "Organization 1" });
const owner = await users.create(
{
organizationId: org.id,
roleInOrganization: "OWNER",
},
{
hasTeam: true,
numberOfTeams: 2,
teamRole: "OWNER",
}
);
const teams = await owner.getAllTeamMembership();
await owner.apiLogin();
await page.goto(`/settings/teams/${teams[0].team.id}/profile`);
if (!teams[1].team.slug) throw new Error("Slug not found for team 2");
await page.locator('input[name="slug"]').fill(teams[1].team.slug);
await submitAndWaitForResponse(page, "/api/trpc/teams/update?batch=1", {
action: () => page.locator("[data-testid=update-team-profile]").click(),
expectedStatusCode: 409,
});
});
test("Teams without organization can have same slug as teams in organizations", async ({
page,
users,
orgs,
}) => {
const org = await orgs.create({ name: "Organization 1" });
const orgOwner = await users.create(
{
organizationId: org.id,
roleInOrganization: "OWNER",
},
{
hasTeam: true,
teamSlug: "calCom",
teamRole: "OWNER",
}
);
const teamOwner = await users.create(
{ username: "pro-user", name: "pro-user" },
{
hasTeam: true,
}
);
const { team } = await teamOwner.getFirstTeamMembership();
await teamOwner.apiLogin();
await page.goto(`/settings/teams/${team.id}/profile`);
await page.locator('input[name="slug"]').fill("calCom");
await submitAndWaitForResponse(page, "/api/trpc/teams/update?batch=1", {
action: () => page.locator("[data-testid=update-team-profile]").click(),
});
});
});
+30 -5
View File
@@ -111,6 +111,7 @@
"no_available_users_found_error": "لم يتم العثور على مستخدمين متاحين. هل يمكنك تجرِبة وقت آخر؟",
"timeslot_unavailable_book_a_new_time": "الفترة الزمنية المحددة لم تعد متاحة. <0>يرجى اختيار وقت جديد</0>",
"timeslot_unavailable_short": "محجوز",
"just_connected_description": "لقد اتصلت للتو. يرجى الحجز من الفترات المتاحة أعلاه أو إعادة المحاولة لاحقًا.",
"unavailable_timeslot_title": "فترة زمنية غير متاحة",
"booking_time_out_of_bounds_error": "لا يمكن حجز هذا النوع من الفعاليات في هذا الوقت. هل يمكنك تجربة وقت آخر؟",
"request_body_end_time_internal_error": "خطأ داخلي. محتوى الطلب لا يحتوي على وقت النهاية",
@@ -321,6 +322,7 @@
"secret_meeting": "اجتماع سري",
"login_instead": "تسجيل الدخول بدلًا من ذلك",
"create_account": "إنشاء حساب",
"get_started": "ابدأ الآن",
"confirm_password": "تأكيد كلمة المرور",
"reset_your_password": "عيّن كلمة المرور الجديدة متبعاً الإرشادات المرسلة إلى عنوان بريدك الإلكتروني.",
"org_banner_instructions": "الرجاء رفع صورة بعرض {{width}} وطول {{height}}.",
@@ -1019,6 +1021,7 @@
"attendees": "الحضور",
"add_attendees": "إضافة الحضور",
"show_advanced_settings": "عرض الإعدادات المتقدمة",
"hide_advanced_settings": "إخفاء الإعدادات المتقدمة",
"event_name": "اسم الحدث",
"event_name_in_calendar": "اسم الحدث في التقويم",
"event_name_tooltip": "الاسم الذي سيظهر في التقاويم",
@@ -1231,7 +1234,7 @@
"contact_sales": "تواصل مع قسم المبيعات",
"error_404": "خطأ 404",
"default": "الافتراضي",
"set_to_default": "تعيين إلى الافتراضي",
"set_to_default": "تعيين كافتراضي",
"new_schedule_btn": "جدول جديد",
"add_new_schedule": "إضافة جدول جديد",
"add_new_calendar": "إضافة تقويم جديد",
@@ -1329,7 +1332,7 @@
"make_team_private_description": "لن يتمكن أعضاء فريقك من رؤية أعضاء الفريق الآخرين عند تشغيل هذا الإعداد.",
"you_cannot_see_team_members": "لا يمكنك رؤية جميع أعضاء الفريق السري.",
"you_cannot_see_teams_of_org": "لا يمكنك رؤية فرق المؤسسة الخاصة.",
"allow_booker_to_select_duration": "السماح لمن يقوم بالحجز بتحديد المدة",
"allow_multiple_durations": "السماح بمدد متعددة",
"impersonate_user_tip": "يتم مراجعة جميع استخدامات هذه الميزة.",
"impersonating_user_warning": "انتحال اسم مستخدم \"{{user}}\".",
"impersonating_stop_instructions": "انقر هنا للإيقاف",
@@ -2150,6 +2153,9 @@
"delete_sso_configuration_confirmation_description": "هل تريد بالتأكيد حذف تكوين {{connectionType}}؟ لن يتمكن أعضاء فريقك الذين يستخدمون معلومات تسجيل الدخول إلى {{connectionType}} من الوصول إلى Cal.com بعد الآن.",
"organizer_timezone": "منظم المناطق الزمنية",
"email_user_cta": "عرض الدعوة",
"accept_invite": "قبول الدعوة",
"join_now": "انضم الآن",
"successfully_joined": "تم الانضمام بنجاح",
"email_no_user_invite_heading_subteam": "تمت دعوتك للانضمام إلى فريق من منظمة {{parentTeamName}}",
"email_no_user_invite_heading_org": "تمت دعوتك للانضمام إلى منظمة {{appName}}",
"email_no_user_invite_subheading": "دعاك {{invitedBy}} للانضمام إلى فريقه على {{appName}}. إن {{appName}} هو برنامج جدولة الأحداث الذي يمكّنك أنت وفريقك من جدولة الاجتماعات دون الحاجة إلى المراسلة عبر البريد الإلكتروني.",
@@ -2202,6 +2208,7 @@
"booking_confirmation_failed": "فشل تأكيد الحجز",
"not_enough_seats": "لا توجد مقاعد كافية",
"form_builder_field_already_exists": "يوجد حقل بهذا الاسم بالفعل",
"guests_field_must_be_multiemail": "يجب أن يكون حقل الضيوف من نوع 'بريد إلكتروني متعدد'",
"show_on_booking_page": "إظهار في صفحة الحجز",
"visit_cancelled_booking": "يمكنك زيارة صفحة الحجز الملغى",
"get_started_zapier_templates": "البدء في استخدام قوالب Zapier",
@@ -2379,6 +2386,7 @@
"invite_link_copied": "تم نسخ رابط الدعوة",
"invite_link_deleted": "تم حذف رابط الدعوة",
"api_key_deleted": "تم حذف مفتاح API",
"delete_api_key_warning": "سيؤدي هذا إلى حذف مفتاح API بشكل دائم. ستفقد أي تطبيقات تستخدم هذا المفتاح الوصول إلى حسابك على الفور. لا يمكن التراجع عن هذا الإجراء.",
"invite_link_updated": "تم حفظ إعدادات رابط الدعوة",
"link_expires_after": "تم تعيين انتهاء صلاحية الروابط بعد...",
"one_day": "1 يوم",
@@ -2922,6 +2930,7 @@
"proceeding_agreement": "بالمتابعة، فإنك توافق على",
"terms": "الشروط",
"always_show_x_days": "دائمًا {{x}} أيام متاحة",
"always_show_x_days_description": "ينقل تلقائيًا نافذة الحجز لمدة {{x}} يوم إلى الأمام كل يوم، مما يضمن أن تقويمك يعرض دائمًا الفترات المتاحة للـ {{x}} يومًا القادمة.",
"team_subscription_cancelled_successfully": "تم إلغاء اشتراك الفريق بنجاح",
"team_subscription_cancellation_error": "حدث خطأ أثناء إلغاء اشتراك الفريق",
"unable_to_subscribe_to_the_platform": "حدث خطأ أثناء محاولة الاشتراك في خطة المنصة، يرجى المحاولة مرة أخرى لاحقًا",
@@ -3143,7 +3152,8 @@
"filter_operator_ends_with": "ينتهي بـ",
"filter_operator_is_empty": "فارغ",
"filter_operator_not_empty": "غير فارغ",
"translate_description_button": "ترجمة العنوان/الوصف إلى لغة متصفح الزائر باستخدام الذكاء الاصطناعي",
"translate_description_button": "ترجمة العنوان والوصف تلقائيًا",
"translate_description_button_description": "ترجمة العناوين والأوصاف تلقائيًا إلى لغة متصفح الزائر باستخدام الذكاء الاصطناعي.",
"rr_distribution_method": "التوزيع",
"rr_distribution_method_description": "يتيح تحسين التوزيع لتحقيق أقصى قدر من التوفر أو لتحقيق توزيع أكثر توازناً.",
"rr_distribution_method_availability_title": "تعظيم التوفر",
@@ -3309,6 +3319,7 @@
"seats_doesnt_support_recurring": "ميزة المقاعد لا تدعم الفعاليات المتكررة. قم بتعطيل ميزة المقاعد لجعلها متكررة.",
"recurring_event_seats_error": "الفعالية المتكررة لا تدعم ميزة المقاعد. قم بتعطيل ميزة المقاعد أو اجعل الفعالية غير متكررة.",
"segment": "شريحة",
"saved_filters": "الفلاتر المحفوظة",
"rename": "إعادة تسمية",
"rename_segment": "إعادة تسمية الشريحة",
"delete_segment": "حذف الشريحة",
@@ -3338,6 +3349,7 @@
"picklist": "قائمة الاختيار",
"most_cancelled_bookings": "الأكثر إلغاءً للحجوزات",
"interface_language": "لغة الواجهة",
"interface_language_description": "اختر لغتك المفضلة لواجهة الحجز",
"visitors_browser_language": "لغة متصفح الزائر",
"name_or_email": "الاسم أو البريد الإلكتروني",
"salesforce_round_robin_skip_fallback_to_lead_owner": "إذا لم يتم العثور على جهة اتصال، يتم الرجوع إلى مالك العميل المحتمل إذا كان موجودًا",
@@ -3452,6 +3464,7 @@
"roles_and_permissions_description": "إدارة الأدوار والصلاحيات لمؤسستك",
"learn_more_permissions": "تعرف على المزيد حول كيفية عمل الصلاحيات",
"role_name": "اسم الدور",
"role_name_required": "اسم الدور مطلوب",
"role_name_placeholder": "مثال: فريق المبيعات، موظفو الدعم",
"role_description": "وصف الدور",
"role_description_placeholder": "صف الغرض من هذا الدور والصلاحيات التي يحتاجها",
@@ -3490,7 +3503,7 @@
"role_created_successfully": "تم إنشاء الدور بنجاح",
"role_updated_successfully": "تم تحديث الدور بنجاح",
"delete_role": "حذف الدور",
"confirm_delete_role": "هل أنت متأكد من رغبتك في حذف هذا الدور؟ لا يمكن التراجع عن هذا الإجراء.",
"confirm_delete_role": "هل أنت متأكد أنك تريد حذف الدور: {{roleName}}. لا يمكن التراجع عن هذا الإجراء.",
"role_deleted_successfully": "تم حذف الدور بنجاح",
"error_creating_role": "خطأ في إنشاء الدور",
"error_updating_role": "خطأ في تحديث الدور",
@@ -3509,10 +3522,20 @@
"pbac_desc_update_workflows": "تعديل وتغيير إعدادات سير العمل",
"pbac_desc_delete_workflows": "إزالة سير العمل من النظام",
"pbac_resource_webhook": "ويب هوك",
"pbac_resource_availability": "الأوقات المتاحة",
"pbac_resource_out_of_office": "خارج المكتب",
"pbac_desc_create_webhooks": "إنشاء ويب هوكس",
"pbac_desc_view_webhooks": "عرض ويب هوكس",
"pbac_desc_update_webhooks": "تحديث الويب هوك",
"pbac_desc_delete_webhooks": "حذف الويب هوك",
"pbac_desc_create_availability": "إنشاء وقت متاح",
"pbac_desc_view_availability": "عرض الأوقات المتاحة",
"pbac_desc_update_availability": "تحديث الأوقات المتاحة",
"pbac_desc_delete_availability": "حذف الأوقات المتاحة",
"pbac_desc_create_out_of_office": "إنشاء حالة خارج المكتب",
"pbac_desc_view_out_of_office": "عرض حالة خارج المكتب",
"pbac_desc_update_out_of_office": "تحديث حالة خارج المكتب",
"pbac_desc_delete_out_of_office": "حذف حالة خارج المكتب",
"pbac_desc_manage_workflows": "وصول كامل لإدارة جميع سير العمل",
"pbac_desc_create_event_types": "إنشاء أنواع الأحداث",
"pbac_desc_view_event_types": "عرض أنواع الأحداث",
@@ -3654,6 +3677,8 @@
"webhook_organizer_email": "البريد الإلكتروني للمنظم",
"webhook_organizer_timezone": "المنطقة الزمنية للمنظم (مثل 'America/New_York'، 'Asia/Kolkata')",
"webhook_organizer_locale": "اللغة المحلية للمنظم (مثل 'en'، 'fr')",
"webhook_organizer_username": "اسم المستخدم للمنظم (مثال: 'john.doe')",
"webhook_organizer_username_in_org": "اسم المستخدم للمنظم في مؤسسته (مثال: 'john.doe')",
"webhook_attendee_name": "اسم الحاضر الأول",
"webhook_attendee_email": "البريد الإلكتروني للحاضر الأول",
"webhook_attendee_timezone": "المنطقة الزمنية للحاضر الأول",
@@ -3732,4 +3757,4 @@
"use_voice": "استخدم الصوت",
"current_voice": "الصوت الحالي",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
+32 -7
View File
@@ -111,6 +111,7 @@
"no_available_users_found_error": "Mövcud istifadəçilər tapılmadı. Başqa bir vaxt aralığını yoxlaya bilərsinizmi?",
"timeslot_unavailable_book_a_new_time": "Seçilmiş vaxt artıq mövcud deyil. <0>Zəhmət olmasa yeni vaxt seçin</0>",
"timeslot_unavailable_short": "Tutulub",
"just_connected_description": "Siz indicə qoşuldunuz. Zəhmət olmasa, yuxarıdakı slotlardan birini seçin və ya sonra yenidən cəhd edin.",
"unavailable_timeslot_title": "Mövcud olmayan vaxt intervalı",
"booking_time_out_of_bounds_error": "Tədbir növünü bu vaxtda rezerv etmək mümkün deyil. Başqa vaxt aralığını sınamaq istərdinizmi?",
"request_body_end_time_internal_error": "Daxili səhv. Sorğu bədənində bitmə vaxtı yoxdur",
@@ -321,6 +322,7 @@
"secret_meeting": "Gizli Görüş",
"login_instead": "Əvəzinə daxil olun",
"create_account": "Hesab yaradın",
"get_started": "Başlayın",
"confirm_password": "Şifrəni təsdiqləyin",
"reset_your_password": "E-poçt ünvanınıza göndərilən təlimatlarla yeni şifrənizi təyin edin.",
"org_banner_instructions": "Zəhmət olmasa {{width}} enində və {{height}} hündürlüyündə şəkil yükləyin.",
@@ -958,7 +960,7 @@
"light_event_type_color": "Tədbir Tipi Rəngi (İşıq Mövzusu)",
"dark_event_type_color": "Tədbir Tipi Rəngi (Qaranlıq Mövzu)",
"file_not_named": "Fayl adlandırılmayıb [idOrSlug]/[user]",
"create_team": "Komanda Yarat",
"create_team": "Komanda yaradın",
"name": "Ad",
"nameless_team": "Adsız Komanda",
"oauth_clients": "OAuth Müştəriləri",
@@ -1019,6 +1021,7 @@
"attendees": "İştirakçılar",
"add_attendees": "İştirakçı əlavə et",
"show_advanced_settings": "Ətraflı parametrləri göstər",
"hide_advanced_settings": "Ətraflı parametrləri gizlədin",
"event_name": "Tədbirin Adı",
"event_name_in_calendar": "Tədbirin adı ilə",
"event_name_tooltip": "Təqvimlərdə görünəcək ad",
@@ -1329,7 +1332,7 @@
"make_team_private_description": "Bu aktiv olduqda komanda üzvləri digər komanda üzvlərini görə bilməyəcək.",
"you_cannot_see_team_members": "Gizli komandanın bütün üzvlərini görə bilməzsiniz.",
"you_cannot_see_teams_of_org": "Gizli təşkilatın komandalarını görə bilməzsiniz.",
"allow_booker_to_select_duration": "Rezervasiya edənə müddəti seçməyə icazə ver",
"allow_multiple_durations": "Çoxsaylı müddətlərə icazə verin",
"impersonate_user_tip": "Bu xüsusiyyətin bütün istifadələri auditi olunur.",
"impersonating_user_warning": "\"{{user}}\" istifadəçi adını təqlid edirsiniz.",
"impersonating_stop_instructions": "Dayandırmaq üçün bura klikləyin",
@@ -2150,6 +2153,9 @@
"delete_sso_configuration_confirmation_description": "{{connectionType}} konfiqurasiyasını silmək istədiyinizə əminsinizmi? {{connectionType}} girişi istifadə edən komanda üzvləriniz artıq Cal.com-a daxil ola bilməyəcəklər.",
"organizer_timezone": "Təşkilatçının saat qurşağı",
"email_user_cta": "Dəvəti Görüntülə",
"accept_invite": "Dəvəti qəbul edin",
"join_now": "İndi qoşulun",
"successfully_joined": "Uğurla qoşuldunuz",
"email_no_user_invite_heading_subteam": "{{parentTeamName}} təşkilatının komandasına qoşulmağa dəvət olunmusunuz",
"email_no_user_invite_heading_org": "{{appName}} təşkilatına qoşulmağa dəvət olunmusunuz",
"email_no_user_invite_subheading": "{{invitedBy}} sizi {{appName}} üzərindəki komandasına qoşulmağa dəvət edib. {{appName}} sizə və komandanıza görüşləri e-poçt mübadiləsi olmadan planlaşdırmağa imkan verən tədbir idarəetmə planlayıcısıdır.",
@@ -2202,6 +2208,7 @@
"booking_confirmation_failed": "Rezervasiya təsdiqi uğursuz oldu",
"not_enough_seats": "Kifayət qədər oturacaq yoxdur",
"form_builder_field_already_exists": "Bu adla bir sahə artıq mövcuddur",
"guests_field_must_be_multiemail": "Qonaqlar sahəsi 'Çoxsaylı e-poçtlar' tipində olmalıdır",
"show_on_booking_page": "Rezervasiya səhifəsində göstər",
"visit_cancelled_booking": "Ləğv edilmiş rezervasiya səhifəsinə baxa bilərsiniz",
"get_started_zapier_templates": "Zapier şablonları ilə başlayın",
@@ -2379,6 +2386,7 @@
"invite_link_copied": "Dəvət linki kopyalandı",
"invite_link_deleted": "Dəvət linki silindi",
"api_key_deleted": "API açarı silindi",
"delete_api_key_warning": "Bu, API açarını həmişəlik siləcək. Bu açardan istifadə edən bütün tətbiqlər dərhal hesabınıza girişi itirəcək. Bu əməliyyat geri qaytarıla bilməz.",
"invite_link_updated": "Dəvət linki parametrləri yadda saxlanıldı",
"link_expires_after": "Linklər bu müddətdən sonra bitir...",
"one_day": "1 gün",
@@ -2688,7 +2696,7 @@
"enterprise": "Müəssisə",
"enterprise_description": "Təşkilatınızı yaratmaq üçün Enterprise-ə yüksəldin",
"create_your_org": "Təşkilatınızı Yaradın",
"create_org": "Təşkilat Yarat",
"create_org": "Təşkilat yaradın",
"create_your_org_description": "Təşkilatlara yüksəlin və subdomen, birləşdirilmiş ödəniş, Göstəricilər, geniş ağ etiketləmə və daha çoxunu əldə edin",
"create_your_enterprise_description": "Enterprise-ə yüksəlin və Active Directory Sync, SCIM Avtomatik İstifadəçi təminatı, Cal.ai Səs Agentləri, Admin API-ləri və daha çoxuna giriş əldə edin!",
"other_payment_app_enabled": "Hər tədbir növü üçün yalnız bir ödəniş tətbiqini aktivləşdirə bilərsiniz",
@@ -2922,6 +2930,7 @@
"proceeding_agreement": "Davam etməklə, bizim",
"terms": "Şərtlər",
"always_show_x_days": "Həmişə {{x}} gün mövcuddur",
"always_show_x_days_description": "Avtomatik olaraq {{x}}-günlük rezervasiya pəncərəsini hər gün irəli aparır, təqviminizin həmişə növbəti {{x}} günün əlçatan slotlarını göstərməsini təmin edir.",
"team_subscription_cancelled_successfully": "Komanda abunəliyi uğurla ləğv edildi",
"team_subscription_cancellation_error": "Komanda abunəliyini ləğv edərkən xəta baş verdi",
"unable_to_subscribe_to_the_platform": "Platforma planına abunə olmağa çalışarkən bir səhv baş verdi, zəhmət olmasa bir az sonra yenidən cəhd edin",
@@ -3143,7 +3152,8 @@
"filter_operator_ends_with": "İlə bitir",
"filter_operator_is_empty": "Boşdur",
"filter_operator_not_empty": "Boş deyil",
"translate_description_button": "Süni intellekt vasitəsilə başlıq/təsviri ziyarətçinin brauzer dilinə tərcümə et",
"translate_description_button": "Başlıq və təsviri auto tərcümə et",
"translate_description_button_description": "Başlıqları və təsvirləri ziyarətçinin brauzer dilinə AI vasitəsilə avtomatik tərcümə edin.",
"rr_distribution_method": "Paylama",
"rr_distribution_method_description": "Maksimum əlçatanlıq üçün paylamanı optimallaşdırmağa və ya daha balanslı bir təyinat üçün çalışmağa imkan verir.",
"rr_distribution_method_availability_title": "Əlçatanlığı maksimuma çatdırmaq",
@@ -3309,6 +3319,7 @@
"seats_doesnt_support_recurring": "Yerlər funksiyası təkrarlanan tədbiri dəstəkləmir. Təkrarlanma funksiyasını aktivləşdirmək üçün yerlər funksiyasını deaktiv edin.",
"recurring_event_seats_error": "Təkrarlanan tədbir yerlər funksiyasını dəstəkləmir. Yerlər funksiyasını deaktiv edin və ya tədbiri təkrarlanmayan edin.",
"segment": "Seqment",
"saved_filters": "Saxlanılmış filtrlər",
"rename": "Adını dəyişdir",
"rename_segment": "Seqmentin adını dəyişdir",
"delete_segment": "Seqmenti sil",
@@ -3338,6 +3349,7 @@
"picklist": "Seçim siyahısı",
"most_cancelled_bookings": "Ən Çox Ləğv Edilən",
"interface_language": "İnterfeys dili",
"interface_language_description": "Rezervasiya interfeysi üçün üstünlük verdiyiniz dili təyin edin",
"visitors_browser_language": "Ziyarətçinin brauzer dili",
"name_or_email": "Ad və ya E-poçt",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Əgər əlaqə tapılmazsa, mövcud olduğu təqdirdə potensial müştəri sahibinə qayıt",
@@ -3452,6 +3464,7 @@
"roles_and_permissions_description": "Təşkilatınız üçün rolları və icazələri idarə edin",
"learn_more_permissions": "İcazələrin necə işlədiyini daha ətraflı öyrənin",
"role_name": "Rol adı",
"role_name_required": "Rol adı tələb olunur",
"role_name_placeholder": "məs. Satış Komandası, Dəstək İşçiləri",
"role_description": "Rol təsviri",
"role_description_placeholder": "Bu rolun nə üçün olduğunu və hansı icazələrə ehtiyacı olduğunu təsvir edin",
@@ -3490,7 +3503,7 @@
"role_created_successfully": "Rol uğurla yaradıldı",
"role_updated_successfully": "Rol uğurla yeniləndi",
"delete_role": "Rolu sil",
"confirm_delete_role": "Bu rolu silmək istədiyinizə əminsiniz? Bu əməliyyat geri qaytarıla bilməz.",
"confirm_delete_role": "{{roleName}} rolunu silmək istədiyinizə əminsiniz? Bu əməliyyat geri qaytarıla bilməz.",
"role_deleted_successfully": "Rol uğurla silindi",
"error_creating_role": "Rol yaradılarkən xəta baş verdi",
"error_updating_role": "Rol yenilənərkən xəta baş verdi",
@@ -3509,10 +3522,20 @@
"pbac_desc_update_workflows": "İş axını parametrlərini redaktə etmək və dəyişdirmək",
"pbac_desc_delete_workflows": "İş axınlarını sistemdən silmək",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Mövcudluq",
"pbac_resource_out_of_office": "Ofisdən kənarda",
"pbac_desc_create_webhooks": "Webhook yaradın",
"pbac_desc_view_webhooks": "Webhook-lara baxın",
"pbac_desc_update_webhooks": "Webhook-ləri yeniləyin",
"pbac_desc_delete_webhooks": "Webhook-ləri silin",
"pbac_desc_create_availability": "Mövcudluq yaratmaq",
"pbac_desc_view_availability": "Mövcudluğa baxmaq",
"pbac_desc_update_availability": "Mövcudluğu yeniləmək",
"pbac_desc_delete_availability": "Mövcudluğu silmək",
"pbac_desc_create_out_of_office": "Ofisdən kənar yaratmaq",
"pbac_desc_view_out_of_office": "Ofisdən kənara baxmaq",
"pbac_desc_update_out_of_office": "Ofisdən kənarı yeniləmək",
"pbac_desc_delete_out_of_office": "Ofisdən kənarı silmək",
"pbac_desc_manage_workflows": "Bütün iş axınlarına tam idarəetmə girişi",
"pbac_desc_create_event_types": "Tədbir növləri yaratmaq",
"pbac_desc_view_event_types": "Tədbir növlərinə baxmaq",
@@ -3561,7 +3584,7 @@
"round_robin_settings_description": "Bu komanda üçün standart növbəli təqvim parametrlərini fərdiləşdirin",
"round_robin_settings_updated_successfully": "Növbəli təqvim parametrləri uğurla yeniləndi",
"booking_creation_time": "Rezervasiya yaradılma vaxtı",
"meeting_start_time": "Görüş başlama vaxtı",
"meeting_start_time": "Görüşün başlama vaxtı",
"members_affected_by_disabling_delegation_credential": "Təsir edilən üzvlər",
"no_members_affected_by_disabling_delegation_credential": "Nümayəndəlik etimadnaməsinin deaktiv edilməsindən təsirlənən üzv yoxdur",
"download_expense_log": "Xərc jurnalını yüklə",
@@ -3654,6 +3677,8 @@
"webhook_organizer_email": "Təşkilatçının e-poçtu",
"webhook_organizer_timezone": "Təşkilatçının saat qurşağı (məs., 'America/New_York', 'Asia/Kolkata')",
"webhook_organizer_locale": "Təşkilatçının lokalı (məs., 'en', 'fr')",
"webhook_organizer_username": "Təşkilatçının istifadəçi adı (məsələn, 'john.doe')",
"webhook_organizer_username_in_org": "Təşkilatçının öz təşkilatındakı istifadəçi adı (məsələn, 'john.doe')",
"webhook_attendee_name": "Birinci iştirakçının adı",
"webhook_attendee_email": "Birinci iştirakçının e-poçtu",
"webhook_attendee_timezone": "Birinci iştirakçının saat qurşağı",
@@ -3732,4 +3757,4 @@
"use_voice": "Səsdən istifadə edin",
"current_voice": "Cari səs",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni sətirləri bura əlavə edin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
+31 -6
View File
@@ -111,6 +111,7 @@
"no_available_users_found_error": "Не са намерени свободни потребители. Бихте ли опитали друг времеви интервал?",
"timeslot_unavailable_book_a_new_time": "Избраният времеви интервал вече не е наличен. <0>Моля, изберете ново време</0>",
"timeslot_unavailable_short": "Заето",
"just_connected_description": "Току-що се свързахте. Моля, резервирайте от наличните часове по-горе или опитайте отново по-късно.",
"unavailable_timeslot_title": "Недостъпен времеви интервал",
"booking_time_out_of_bounds_error": "Този тип събитие не може да бъде резервирано в този час. Бихте ли опитали друг часови интервал?",
"request_body_end_time_internal_error": "Вътрешна грешка. Заявката не съдържа краен час",
@@ -321,6 +322,7 @@
"secret_meeting": "Тайна среща",
"login_instead": "Вместо това влезте",
"create_account": "Създаване на акаунт",
"get_started": "Започнете",
"confirm_password": "Потвърдете паролата",
"reset_your_password": "Задайте новата си парола с инструкциите, изпратени на вашия имейл адрес.",
"org_banner_instructions": "Моля, качете изображение с ширина {{width}} и височина {{height}}.",
@@ -958,7 +960,7 @@
"light_event_type_color": "Цвят на типа събитие (светла тема)",
"dark_event_type_color": "Цвят на типа събитие (тъмна тема)",
"file_not_named": "Файлът не е именуван [idOrSlug]/[user]",
"create_team": "Създай екип",
"create_team": "Създаване на екип",
"name": "Име",
"nameless_team": "Безименен екип",
"oauth_clients": "OAuth клиенти",
@@ -1019,6 +1021,7 @@
"attendees": "Участници",
"add_attendees": "Добавяне на участници",
"show_advanced_settings": "Показване на разширени настройки",
"hide_advanced_settings": "Скриване на разширените настройки",
"event_name": "Име на събитието",
"event_name_in_calendar": "С име на събитието",
"event_name_tooltip": "Името, което ще се показва в календарите",
@@ -1231,7 +1234,7 @@
"contact_sales": "Свържи се с отдел продажби",
"error_404": "Грешка 404",
"default": "По подразбиране",
"set_to_default": "Задай по подразбиране",
"set_to_default": "Задаване по подразбиране",
"new_schedule_btn": "Нов график",
"add_new_schedule": "Добави нов график",
"add_new_calendar": "Добави нов календар",
@@ -1329,7 +1332,7 @@
"make_team_private_description": "Когато това е включено, членовете на екипа няма да могат да виждат другите членове.",
"you_cannot_see_team_members": "Не можеш да видиш всички членове на частния екип.",
"you_cannot_see_teams_of_org": "Не можеш да видиш екипите на частна организация.",
"allow_booker_to_select_duration": "Позволи на резервиращия да избере продължителност",
"allow_multiple_durations": "Разрешаване на множество продължителности",
"impersonate_user_tip": "Всяко използване на тази функция се одитира.",
"impersonating_user_warning": "Представяш се като потребител \"{{user}}\".",
"impersonating_stop_instructions": "Кликни тук, за да спреш",
@@ -2150,6 +2153,9 @@
"delete_sso_configuration_confirmation_description": "Сигурни ли сте, че искате да изтриете {{connectionType}} конфигурацията? Членовете на вашия екип, които използват {{connectionType}} вход, вече няма да имат достъп до Cal.com.",
"organizer_timezone": "Часова зона на организатора",
"email_user_cta": "Преглед на поканата",
"accept_invite": "Приемане на покана",
"join_now": "Присъединете се сега",
"successfully_joined": "Успешно се присъединихте",
"email_no_user_invite_heading_subteam": "Поканени сте да се присъедините към екип от организацията {{parentTeamName}}",
"email_no_user_invite_heading_org": "Поканени сте да се присъедините към организация в {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} ви покани да се присъедините към техния екип в {{appName}}. {{appName}} е платформа за управление на събития, която позволява на вас и вашия екип да планирате срещи без безкрайна размяна на имейли.",
@@ -2202,6 +2208,7 @@
"booking_confirmation_failed": "Потвърждението на резервацията е неуспешно",
"not_enough_seats": "Няма достатъчно места",
"form_builder_field_already_exists": "Поле с това име вече съществува",
"guests_field_must_be_multiemail": "Полето за гости трябва да бъде от тип 'Множество имейли'",
"show_on_booking_page": "Покажи на страницата за резервации",
"visit_cancelled_booking": "Можете да посетите страницата с отменената резервация",
"get_started_zapier_templates": "Започнете със Zapier шаблони",
@@ -2379,6 +2386,7 @@
"invite_link_copied": "Линкът за покана е копиран",
"invite_link_deleted": "Линкът за покана е изтрит",
"api_key_deleted": "API ключът е изтрит",
"delete_api_key_warning": "Това ще изтрие API ключа завинаги. Всички приложения, използващи този ключ, незабавно ще загубят достъп до вашия акаунт. Това действие не може да бъде отменено.",
"invite_link_updated": "Настройките на линка са запазени",
"link_expires_after": "Линковете изтичат след...",
"one_day": "1 ден",
@@ -2922,6 +2930,7 @@
"proceeding_agreement": "С продължаването си, вие се съгласявате с нашите",
"terms": "Условия",
"always_show_x_days": "Винаги показвай {{x}} свободни дни",
"always_show_x_days_description": "Автоматично премества {{x}}-дневния прозорец за резервации напред всеки ден, гарантирайки, че вашият календар винаги показва следващите {{x}} дни с налични слотове.",
"team_subscription_cancelled_successfully": "Абонаментът на екипа е отменен успешно",
"team_subscription_cancellation_error": "Грешка при отмяна на абонамента на екипа",
"unable_to_subscribe_to_the_platform": "Възникна грешка при опит за абониране за плана на платформата, моля опитайте отново по-късно",
@@ -3143,7 +3152,8 @@
"filter_operator_ends_with": "Завършва с",
"filter_operator_is_empty": "Е празно",
"filter_operator_not_empty": "Не е празно",
"translate_description_button": "Преведи заглавието/описанието на езика на браузъра на посетителя чрез AI",
"translate_description_button": "Автоматичен превод на заглавие и описание",
"translate_description_button_description": "Автоматично превеждане на заглавия и описания на езика на браузъра на посетителя с помощта на изкуствен интелект.",
"rr_distribution_method": "Разпределение",
"rr_distribution_method_description": "Позволява оптимизиране на разпределението за максимална наличност или за по-балансирано разпределение.",
"rr_distribution_method_availability_title": "Максимизиране на наличността",
@@ -3309,6 +3319,7 @@
"seats_doesnt_support_recurring": "Функцията за места не поддържа повтарящи се събития. Деактивирайте функцията за места, за да го направите повтарящо се.",
"recurring_event_seats_error": "Повтарящото се събитие не поддържа функцията за места. Деактивирайте функцията за места или направете събитието неповтарящо се.",
"segment": "Сегмент",
"saved_filters": "Запазени филтри",
"rename": "Преименуване",
"rename_segment": "Преименуване на сегмент",
"delete_segment": "Изтриване на сегмент",
@@ -3338,6 +3349,7 @@
"picklist": "Списък за избор",
"most_cancelled_bookings": "Най-много отменени",
"interface_language": "Език на интерфейса",
"interface_language_description": "Задайте предпочитания от вас език за интерфейса за резервации",
"visitors_browser_language": "Език на браузъра на посетителя",
"name_or_email": "Име или имейл",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Ако не е намерен контакт, връщане към собственика на лийда, ако съществува",
@@ -3452,6 +3464,7 @@
"roles_and_permissions_description": "Управлявайте роли и разрешения за вашата организация",
"learn_more_permissions": "Научете повече за това как работят разрешенията",
"role_name": "Име на ролята",
"role_name_required": "Името на ролята е задължително",
"role_name_placeholder": "напр. Търговски екип, Екип за поддръжка",
"role_description": "Описание на ролята",
"role_description_placeholder": "Опишете за какво е тази роля и какви разрешения са ѝ необходими",
@@ -3490,7 +3503,7 @@
"role_created_successfully": "Ролята е създадена успешно",
"role_updated_successfully": "Ролята е актуализирана успешно",
"delete_role": "Изтрий роля",
"confirm_delete_role": "Сигурни ли сте, че искате да изтриете тази роля? Това действие не може да бъде отменено.",
"confirm_delete_role": "Сигурни ли сте, че искате да изтриете ролята: {{roleName}}. Това действие не може да бъде отменено.",
"role_deleted_successfully": "Ролята е изтрита успешно",
"error_creating_role": "Грешка при създаване на роля",
"error_updating_role": "Грешка при актуализиране на роля",
@@ -3509,10 +3522,20 @@
"pbac_desc_update_workflows": "Редактиране и модифициране на настройките на работните процеси",
"pbac_desc_delete_workflows": "Премахване на работни процеси от системата",
"pbac_resource_webhook": "Уебхук",
"pbac_resource_availability": "Наличност",
"pbac_resource_out_of_office": "Извън офиса",
"pbac_desc_create_webhooks": "Създаване на уебхуци",
"pbac_desc_view_webhooks": "Преглед на уебхуци",
"pbac_desc_update_webhooks": "Актуализиране на уебхукове",
"pbac_desc_delete_webhooks": "Изтриване на уебхукове",
"pbac_desc_create_availability": "Създаване на наличност",
"pbac_desc_view_availability": "Преглед на наличност",
"pbac_desc_update_availability": "Актуализиране на наличност",
"pbac_desc_delete_availability": "Изтриване на наличност",
"pbac_desc_create_out_of_office": "Създаване на статус \"Извън офиса\"",
"pbac_desc_view_out_of_office": "Преглед на статус \"Извън офиса\"",
"pbac_desc_update_out_of_office": "Актуализиране на статус \"Извън офиса\"",
"pbac_desc_delete_out_of_office": "Изтриване на статус \"Извън офиса\"",
"pbac_desc_manage_workflows": "Пълен достъп за управление на всички работни процеси",
"pbac_desc_create_event_types": "Създаване на типове събития",
"pbac_desc_view_event_types": "Преглед на типове събития",
@@ -3654,6 +3677,8 @@
"webhook_organizer_email": "Имейл на организатора",
"webhook_organizer_timezone": "Часова зона на организатора (напр. 'America/New_York', 'Asia/Kolkata')",
"webhook_organizer_locale": "Локал на организатора (напр. 'en', 'fr')",
"webhook_organizer_username": "Потребителско име на организатора (напр. 'john.doe')",
"webhook_organizer_username_in_org": "Потребителско име на организатора в неговата организация (напр. 'john.doe')",
"webhook_attendee_name": "Име на първия участник",
"webhook_attendee_email": "Имейл на първия участник",
"webhook_attendee_timezone": "Часова зона на първия участник",
@@ -3732,4 +3757,4 @@
"use_voice": "Използвай глас",
"current_voice": "Текущ глас",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавете новите си низове над този ред ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
+31 -6
View File
@@ -111,6 +111,7 @@
"no_available_users_found_error": "কোনও উপলভ্য ব্যবহারকারী খুঁজে পাওয়া যায় নি। ",
"timeslot_unavailable_book_a_new_time": "নির্বাচিত সময়সীমা আর উপলব্ধ নেই। <0>অনুগ্রহ করে একটি নতুন সময় নির্বাচন করুন</0>",
"timeslot_unavailable_short": "নেওয়া হয়েছে",
"just_connected_description": "আপনি সবেমাত্র সংযুক্ত হয়েছেন। অনুগ্রহ করে উপরের স্লট থেকে বুক করুন অথবা পরে আবার চেষ্টা করুন।",
"unavailable_timeslot_title": "অনুপলব্ধ সময়সীমা",
"booking_time_out_of_bounds_error": "এই সময়ে ইভেন্ট টাইপ বুক করা যাবে না। আপনি কি অন্য সময়সীমা চেষ্টা করতে পারেন?",
"request_body_end_time_internal_error": "অভ্যন্তরীণ ত্রুটি। ",
@@ -321,6 +322,7 @@
"secret_meeting": "গোপন সভা",
"login_instead": "পরিবর্তে লগইন",
"create_account": "অ্যাকাউন্ট তৈরি করুন",
"get_started": "শুরু করুন",
"confirm_password": "পাসওয়ার্ড নিশ্চিত করুন",
"reset_your_password": "আপনার ইমেল ঠিকানায় প্রেরিত নির্দেশাবলী সহ আপনার নতুন পাসওয়ার্ড সেট করুন।",
"org_banner_instructions": "এর চিত্র আপলোড করুন {{width}} প্রস্থ এবং {{height}} উচ্চতা।",
@@ -1019,6 +1021,7 @@
"attendees": "অংশগ্রহণকারীরা",
"add_attendees": "অংশগ্রহণকারীদের যোগ করুন",
"show_advanced_settings": "উন্নত সেটিংস দেখান",
"hide_advanced_settings": "উন্নত সেটিংস লুকান",
"event_name": "ইভেন্টের নাম",
"event_name_in_calendar": "ইভেন্টের নাম সহ",
"event_name_tooltip": "নাম যে ক্যালেন্ডারে প্রদর্শিত হবে",
@@ -1231,7 +1234,7 @@
"contact_sales": "যোগাযোগ বিক্রয়",
"error_404": "ত্রুটি 404",
"default": "ডিফল্ট",
"set_to_default": "ডিফল্ট সেট",
"set_to_default": "ডিফল্ট হিসাবে সেট করুন",
"new_schedule_btn": "নতুন সময়সূচী",
"add_new_schedule": "একটি নতুন সময়সূচি যুক্ত করুন",
"add_new_calendar": "একটি নতুন ক্যালেন্ডার যুক্ত করুন",
@@ -1329,7 +1332,7 @@
"make_team_private_description": "আপনার দলের সদস্যরা এটি চালু থাকলে অন্য দলের সদস্যদের দেখতে সক্ষম হবেন না।",
"you_cannot_see_team_members": "আপনি কোনও বেসরকারী দলের সমস্ত দলের সদস্য দেখতে পারবেন না।",
"you_cannot_see_teams_of_org": "আপনি কোনও বেসরকারী সংস্থার দল দেখতে পাচ্ছেন না।",
"allow_booker_to_select_duration": "বুকারকে সময়কাল নির্বাচন করার অনুমতি দিন",
"allow_multiple_durations": "একাধিক সময়কাল অনুমতি দিন",
"impersonate_user_tip": "এই বৈশিষ্ট্যের সমস্ত ব্যবহার নিরীক্ষণ করা হয়।",
"impersonating_user_warning": "ব্যবহারকারীর নাম \"{{user}}\"।",
"impersonating_stop_instructions": "থামাতে এখানে ক্লিক করুন",
@@ -2150,6 +2153,9 @@
"delete_sso_configuration_confirmation_description": "আপনি কি নিশ্চিত যে আপনি এটি মুছতে চান? {{connectionType}} কনফিগারেশন? {{connectionType}} লগইন আর Cal.com অ্যাক্সেস করতে সক্ষম হবে না।",
"organizer_timezone": "আয়োজক টাইমজোন",
"email_user_cta": "আমন্ত্রণ দেখুন",
"accept_invite": "আমন্ত্রণ গ্রহণ করুন",
"join_now": "এখনই যোগ দিন",
"successfully_joined": "সফলভাবে যোগ দেওয়া হয়েছে",
"email_no_user_invite_heading_subteam": "আপনাকে {{parentTeamName}} সংস্থার একটি দলে যোগদানের আমন্ত্রণ জানানো হয়েছে",
"email_no_user_invite_heading_org": "আপনাকে একটি {{appName}} সংস্থায় যোগদানের আমন্ত্রণ জানানো হয়েছে",
"email_no_user_invite_subheading": "{{invitedBy}} আপনাকে তাদের দলে যোগদানের জন্য আমন্ত্রণ জানিয়েছে {{appName}}। {{appName}} ইভেন্ট-জ্যাকলিং শিডিয়ুলার যা আপনাকে এবং আপনার দলকে ইমেল টেনিস ছাড়াই সভাগুলির সময়সূচী করতে সক্ষম করে।",
@@ -2202,6 +2208,7 @@
"booking_confirmation_failed": "বুকিং নিশ্চিতকরণ ব্যর্থ হয়েছে",
"not_enough_seats": "পর্যাপ্ত আসন নেই",
"form_builder_field_already_exists": "এই নাম সহ একটি ক্ষেত্র ইতিমধ্যে বিদ্যমান",
"guests_field_must_be_multiemail": "অতিথি ক্ষেত্রটি অবশ্যই 'একাধিক ইমেইল' ধরনের হতে হবে",
"show_on_booking_page": "বুকিং পৃষ্ঠায় দেখান",
"visit_cancelled_booking": "আপনি বাতিল হওয়া বুকিং পৃষ্ঠাটি দেখতে পারেন",
"get_started_zapier_templates": "জ্যাপিয়ার টেম্পলেটগুলি দিয়ে শুরু করুন",
@@ -2379,6 +2386,7 @@
"invite_link_copied": "আমন্ত্রণ লিঙ্ক অনুলিপি",
"invite_link_deleted": "লিঙ্ক মুছে ফেলা আমন্ত্রণ",
"api_key_deleted": "এপিআই কী মুছে ফেলা হয়েছে",
"delete_api_key_warning": "এটি স্থায়ীভাবে API কী মুছে ফেলবে। এই কী ব্যবহার করে যেকোনো অ্যাপ্লিকেশন অবিলম্বে আপনার অ্যাকাউন্টে অ্যাক্সেস হারাবে। এই কাজটি পূর্বাবস্থায় ফেরানো যাবে না।",
"invite_link_updated": "সংরক্ষণের লিঙ্ক সেটিংসকে আমন্ত্রণ করুন",
"link_expires_after": "লিঙ্কগুলি পরে শেষ হওয়ার জন্য সেট ...",
"one_day": "1 দিন",
@@ -2688,7 +2696,7 @@
"enterprise": "এন্টারপ্রাইজ",
"enterprise_description": "আপনার সংস্থা তৈরি করতে এন্টারপ্রাইজে আপগ্রেড করুন",
"create_your_org": "আপনার সংস্থা তৈরি করুন",
"create_org": "সংস্থা তৈরি করুন",
"create_org": "সংগঠন তৈরি করুন",
"create_your_org_description": "সংস্থাগুলিতে আপগ্রেড করুন এবং একটি সাবডোমেন, ইউনিফাইড বিলিং, অন্তর্দৃষ্টি, বিস্তৃত হোয়াইটেলবেলিং এবং আরও অনেক কিছু পান",
"create_your_enterprise_description": "এন্টারপ্রাইজে আপগ্রেড করুন এবং অ্যাক্টিভ ডিরেক্টরি সিঙ্ক, এসসিআইএম স্বয়ংক্রিয় ব্যবহারকারীর প্রভিশনিং, ক্যাল.এই ভয়েস এজেন্টস, অ্যাডমিন এপিআই এবং আরও অনেক কিছুতে অ্যাক্সেস পান!",
"other_payment_app_enabled": "আপনি প্রতি ইভেন্টের জন্য কেবল একটি পেমেন্ট অ্যাপ্লিকেশন সক্ষম করতে পারেন",
@@ -2922,6 +2930,7 @@
"proceeding_agreement": "এগিয়ে যাওয়ার মাধ্যমে, আপনি আমাদের নিম্নলিখিত শর্তাবলীতে সম্মত হচ্ছেন",
"terms": "শর্তাবলী",
"always_show_x_days": "সর্বদা {{x}} দিন উপলব্ধ",
"always_show_x_days_description": "প্রতিদিন স্বয়ংক্রিয়ভাবে {{x}}-দিনের বুকিং উইন্ডো সামনে সরিয়ে দেয়, যা নিশ্চিত করে যে আপনার ক্যালেন্ডার সবসময় পরবর্তী {{x}} দিনের উপলব্ধ স্লট দেখায়।",
"team_subscription_cancelled_successfully": "দলের সাবস্ক্রিপশন সফলভাবে বাতিল করা হয়েছে",
"team_subscription_cancellation_error": "দলের সাবস্ক্রিপশন বাতিল করার সময় ত্রুটি",
"unable_to_subscribe_to_the_platform": "প্ল্যাটফর্ম পরিকল্পনায় সাবস্ক্রাইব করার চেষ্টা করার সময় একটি ত্রুটি ঘটেছে, দয়া করে পরে আবার চেষ্টা করুন",
@@ -3143,7 +3152,8 @@
"filter_operator_ends_with": "সাথে শেষ",
"filter_operator_is_empty": "খালি আছে",
"filter_operator_not_empty": "খালি নয়",
"translate_description_button": "এআই ব্যবহার করে দর্শনার্থীর ব্রাউজার ভাষায় বর্ণনা অনুবাদ করুন",
"translate_description_button": "শিরোনাম এবং বিবরণ অটো অনুবাদ করুন",
"translate_description_button_description": "AI ব্যবহার করে দর্শকের ব্রাউজার ভাষায় শিরোনাম এবং বিবরণ স্বয়ংক্রিয়ভাবে অনুবাদ করুন।",
"rr_distribution_method": "বিতরণ",
"rr_distribution_method_description": "সর্বাধিক প্রাপ্যতার জন্য বিতরণকে অনুকূলকরণের জন্য বা আরও সুষম অ্যাসাইনমেন্টের জন্য লক্ষ্য করার অনুমতি দেয়।",
"rr_distribution_method_availability_title": "প্রাপ্যতা সর্বাধিক করুন",
@@ -3309,6 +3319,7 @@
"seats_doesnt_support_recurring": "সিট ফিচার পুনরাবৃত্ত ইভেন্ট সমর্থন করে না। এটিকে পুনরাবৃত্ত করতে সিট ফিচার নিষ্ক্রিয় করুন।",
"recurring_event_seats_error": "পুনরাবৃত্ত ইভেন্ট সিট ফিচার সমর্থন করে না। সিট ফিচার নিষ্ক্রিয় করুন অথবা ইভেন্টটিকে অপুনরাবৃত্ত করুন।",
"segment": "সেগমেন্ট",
"saved_filters": "সংরক্ষিত ফিল্টার",
"rename": "পুনঃনামকরণ",
"rename_segment": "সেগমেন্ট পুনঃনামকরণ",
"delete_segment": "সেগমেন্ট মুছুন",
@@ -3338,6 +3349,7 @@
"picklist": "পিকলিস্ট",
"most_cancelled_bookings": "সবচেয়ে বেশি বাতিল করা বুকিং",
"interface_language": "ইন্টারফেস ভাষা",
"interface_language_description": "বুকিং ইন্টারফেসের জন্য আপনার পছন্দের ভাষা সেট করুন",
"visitors_browser_language": "দর্শনার্থীর ব্রাউজার ভাষা",
"name_or_email": "নাম বা ইমেইল",
"salesforce_round_robin_skip_fallback_to_lead_owner": "যদি কোন কন্টাক্ট না পাওয়া যায়, লিড মালিকে ফিরে যান যদি তা বিদ্যমান থাকে",
@@ -3452,6 +3464,7 @@
"roles_and_permissions_description": "আপনার সংস্থার জন্য রোল এবং অনুমতি পরিচালনা করুন",
"learn_more_permissions": "অনুমতি কীভাবে কাজ করে সে সম্পর্কে আরও জানুন",
"role_name": "ভূমিকার নাম",
"role_name_required": "ভূমিকার নাম আবশ্যক",
"role_name_placeholder": "যেমন বিক্রয় দল, সহায়তা কর্মী",
"role_description": "ভূমিকার বিবরণ",
"role_description_placeholder": "এই ভূমিকাটি কীসের জন্য এবং কোন অনুমতি প্রয়োজন তা বর্ণনা করুন",
@@ -3490,7 +3503,7 @@
"role_created_successfully": "ভূমিকা সফলভাবে তৈরি করা হয়েছে",
"role_updated_successfully": "ভূমিকা সফলভাবে আপডেট করা হয়েছে",
"delete_role": "ভূমিকা মুছুন",
"confirm_delete_role": "আপনি কি নিশ্চিত যে আপনি এই ভূমিকা মুছতে চান? এই কাজটি পূর্বাবস্থায় ফেরানো যাবে না।",
"confirm_delete_role": "আপনি কি নিশ্চিত যে আপনি ভূমিকা মুছতে চান: {{roleName}}। এই কাজটি পূর্বাবস্থায় ফেরানো যাবে না।",
"role_deleted_successfully": "ভূমিকা সফলভাবে মুছে ফেলা হয়েছে",
"error_creating_role": "ভূমিকা তৈরি করতে ত্রুটি",
"error_updating_role": "ভূমিকা আপডেট করতে ত্রুটি",
@@ -3509,10 +3522,20 @@
"pbac_desc_update_workflows": "কর্মপ্রবাহ সেটিংস সম্পাদনা ও পরিবর্তন করুন",
"pbac_desc_delete_workflows": "সিস্টেম থেকে কর্মপ্রবাহ সরান",
"pbac_resource_webhook": "ওয়েবহুক",
"pbac_resource_availability": "প্রাপ্যতা",
"pbac_resource_out_of_office": "অফিসের বাইরে",
"pbac_desc_create_webhooks": "ওয়েবহুক তৈরি করুন",
"pbac_desc_view_webhooks": "ওয়েবহুক দেখুন",
"pbac_desc_update_webhooks": "ওয়েবহুক আপডেট করুন",
"pbac_desc_delete_webhooks": "ওয়েবহুক মুছুন",
"pbac_desc_create_availability": "প্রাপ্যতা তৈরি করুন",
"pbac_desc_view_availability": "প্রাপ্যতা দেখুন",
"pbac_desc_update_availability": "প্রাপ্যতা আপডেট করুন",
"pbac_desc_delete_availability": "প্রাপ্যতা মুছুন",
"pbac_desc_create_out_of_office": "অফিসের বাইরে তৈরি করুন",
"pbac_desc_view_out_of_office": "অফিসের বাইরে দেখুন",
"pbac_desc_update_out_of_office": "অফিসের বাইরে আপডেট করুন",
"pbac_desc_delete_out_of_office": "অফিসের বাইরে মুছুন",
"pbac_desc_manage_workflows": "সমস্ত কর্মপ্রবাহের জন্য সম্পূর্ণ ব্যবস্থাপনা অ্যাক্সেস",
"pbac_desc_create_event_types": "ইভেন্টের ধরণ তৈরি করুন",
"pbac_desc_view_event_types": "ইভেন্টের ধরণ দেখুন",
@@ -3654,6 +3677,8 @@
"webhook_organizer_email": "আয়োজকের ইমেইল",
"webhook_organizer_timezone": "আয়োজকের টাইমজোন (যেমন, 'America/New_York', 'Asia/Kolkata')",
"webhook_organizer_locale": "আয়োজকের লোকেল (যেমন, 'en', 'fr')",
"webhook_organizer_username": "আয়োজকের ইউজারনেম (যেমন, 'john.doe')",
"webhook_organizer_username_in_org": "তাদের সংগঠনে আয়োজকের ইউজারনেম (যেমন, 'john.doe')",
"webhook_attendee_name": "প্রথম অংশগ্রহণকারীর নাম",
"webhook_attendee_email": "প্রথম অংশগ্রহণকারীর ইমেইল",
"webhook_attendee_timezone": "প্রথম অংশগ্রহণকারীর টাইমজোন",
@@ -3732,4 +3757,4 @@
"use_voice": "ভয়েস ব্যবহার করুন",
"current_voice": "বর্তমান ভয়েস",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑ুষান্ত"
}
}
+31 -6
View File
@@ -111,6 +111,7 @@
"no_available_users_found_error": "No s'han trobat usuaris disponibles. Podries provar una altra franja horària?",
"timeslot_unavailable_book_a_new_time": "La franja horària seleccionada ja no està disponible. <0>Si us plau, selecciona una nova hora</0>",
"timeslot_unavailable_short": "Ocupat",
"just_connected_description": "Acabes de connectar-te. Si us plau, reserva des dels espais de temps disponibles o torna-ho a provar més tard.",
"unavailable_timeslot_title": "Franja horària no disponible",
"booking_time_out_of_bounds_error": "El tipus d'esdeveniment no es pot reservar en aquest moment. Podries provar una altra franja horària?",
"request_body_end_time_internal_error": "Error intern. El cos de la sol·licitud no conté l'hora de finalització",
@@ -321,6 +322,7 @@
"secret_meeting": "Reunió secreta",
"login_instead": "Inicia sessió en lloc seu",
"create_account": "Crea un compte",
"get_started": "Comença",
"confirm_password": "Confirma la contrasenya",
"reset_your_password": "Estableix la teva nova contrasenya amb les instruccions enviades al teu correu electrònic.",
"org_banner_instructions": "Si us plau, puja una imatge de {{width}} d'amplada i {{height}} d'alçada.",
@@ -958,7 +960,7 @@
"light_event_type_color": "Color del tipus d'esdeveniment (tema clar)",
"dark_event_type_color": "Color del tipus d'esdeveniment (tema fosc)",
"file_not_named": "L'arxiu no té el nom [idOrSlug]/[user]",
"create_team": "Crea un equip",
"create_team": "Crea equip",
"name": "Nom",
"nameless_team": "Equip sense nom",
"oauth_clients": "Clients OAuth",
@@ -1019,6 +1021,7 @@
"attendees": "Assistents",
"add_attendees": "Afegir assistents",
"show_advanced_settings": "Mostrar configuració avançada",
"hide_advanced_settings": "Amaga la configuració avançada",
"event_name": "Nom de l'esdeveniment",
"event_name_in_calendar": "Amb nom d'esdeveniment",
"event_name_tooltip": "El nom que apareixerà als calendaris",
@@ -1231,7 +1234,7 @@
"contact_sales": "Contacta amb vendes",
"error_404": "Error 404",
"default": "Per defecte",
"set_to_default": "Estableix per defecte",
"set_to_default": "Estableix com a predeterminat",
"new_schedule_btn": "Nou horari",
"add_new_schedule": "Afegeix un nou horari",
"add_new_calendar": "Afegeix un nou calendari",
@@ -1329,7 +1332,7 @@
"make_team_private_description": "Els membres del teu equip no podran veure altres membres quan això estigui activat.",
"you_cannot_see_team_members": "No pots veure tots els membres d'un equip privat.",
"you_cannot_see_teams_of_org": "No pots veure els equips d'una organització privada.",
"allow_booker_to_select_duration": "Permetre que el reservant seleccioni la durada",
"allow_multiple_durations": "Permet múltiples duracions",
"impersonate_user_tip": "Tots els usos d'aquesta funcionalitat són auditats.",
"impersonating_user_warning": "Suplantant l'usuari \"{{user}}\".",
"impersonating_stop_instructions": "Fes clic aquí per aturar",
@@ -2150,6 +2153,9 @@
"delete_sso_configuration_confirmation_description": "Estàs segur que vols eliminar la configuració de {{connectionType}}? Els membres del teu equip que utilitzin l'inici de sessió {{connectionType}} ja no podran accedir a Cal.com.",
"organizer_timezone": "Zona horària de l'organitzador",
"email_user_cta": "Veure invitació",
"accept_invite": "Accepta la invitació",
"join_now": "Uneix-te ara",
"successfully_joined": "T'has unit correctament",
"email_no_user_invite_heading_subteam": "Has estat convidat a unir-te a un equip de l'organització {{parentTeamName}}",
"email_no_user_invite_heading_org": "Has estat convidat a unir-te a una organització de {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} t'ha convidat a unir-te al seu equip a {{appName}}. {{appName}} és el programador de reunions que us permet a tu i al teu equip programar trobades sense el ping-pong dels correus electrònics.",
@@ -2202,6 +2208,7 @@
"booking_confirmation_failed": "La confirmació de la reserva ha fallat",
"not_enough_seats": "No hi ha prou places",
"form_builder_field_already_exists": "Ja existeix un camp amb aquest nom",
"guests_field_must_be_multiemail": "El camp de convidats ha de ser del tipus 'Múltiples correus electrònics'",
"show_on_booking_page": "Mostra a la pàgina de reserva",
"visit_cancelled_booking": "Pots visitar la pàgina de reserva cancel·lada",
"get_started_zapier_templates": "Comença amb les plantilles de Zapier",
@@ -2379,6 +2386,7 @@
"invite_link_copied": "Enllaç d'invitació copiat",
"invite_link_deleted": "Enllaç d'invitació eliminat",
"api_key_deleted": "Clau API eliminada",
"delete_api_key_warning": "Això eliminarà permanentment la clau API. Qualsevol aplicació que utilitzi aquesta clau perdrà immediatament l'accés al teu compte. Aquesta acció no es pot desfer.",
"invite_link_updated": "Configuració de l'enllaç d'invitació desada",
"link_expires_after": "Els enllaços caduquen després de...",
"one_day": "1 dia",
@@ -2922,6 +2930,7 @@
"proceeding_agreement": "En continuar, acceptes les nostres",
"terms": "Condicions",
"always_show_x_days": "Sempre {{x}} dies disponibles",
"always_show_x_days_description": "Mou automàticament la finestra de reserva de {{x}} dies cap endavant cada dia, assegurant que el teu calendari sempre mostri els propers {{x}} dies d'espais disponibles.",
"team_subscription_cancelled_successfully": "Subscripció d'equip cancel·lada amb èxit",
"team_subscription_cancellation_error": "Error en cancel·lar la subscripció d'equip",
"unable_to_subscribe_to_the_platform": "S'ha produït un error en intentar subscriure's al pla de la plataforma, torna-ho a provar més tard",
@@ -3143,7 +3152,8 @@
"filter_operator_ends_with": "Acaba amb",
"filter_operator_is_empty": "És buit",
"filter_operator_not_empty": "No és buit",
"translate_description_button": "Tradueix el títol/descripció a l'idioma del navegador del visitant utilitzant IA",
"translate_description_button": "Tradueix automàticament el títol i la descripció",
"translate_description_button_description": "Tradueix automàticament els títols i les descripcions a l'idioma del navegador del visitant utilitzant IA.",
"rr_distribution_method": "Distribució",
"rr_distribution_method_description": "Permet optimitzar la distribució per maximitzar la disponibilitat o aconseguir una assignació més equilibrada",
"rr_distribution_method_availability_title": "Maximitzar disponibilitat",
@@ -3309,6 +3319,7 @@
"seats_doesnt_support_recurring": "La funció de seients no admet esdeveniments recurrents. Desactiveu la funció de seients per fer-lo recurrent.",
"recurring_event_seats_error": "Els esdeveniments recurrents no admeten la funció de seients. Desactiveu la funció de seients o feu que l'esdeveniment no sigui recurrent.",
"segment": "Segment",
"saved_filters": "Filtres desats",
"rename": "Canviar el nom",
"rename_segment": "Canviar el nom del segment",
"delete_segment": "Eliminar segment",
@@ -3338,6 +3349,7 @@
"picklist": "Llista de selecció",
"most_cancelled_bookings": "Més reserves cancel·lades",
"interface_language": "Idioma de la interfície",
"interface_language_description": "Estableix el teu idioma preferit per a la interfície de reserva",
"visitors_browser_language": "Idioma del navegador del visitant",
"name_or_email": "Nom o correu electrònic",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Si no es troba cap contacte, recorre al propietari del lead si existeix",
@@ -3452,6 +3464,7 @@
"roles_and_permissions_description": "Gestiona els rols i permisos per a la teva organització",
"learn_more_permissions": "Aprèn més sobre com funcionen els permisos",
"role_name": "Nom del rol",
"role_name_required": "El nom del rol és obligatori",
"role_name_placeholder": "p. ex. Equip de vendes, Personal de suport",
"role_description": "Descripció del rol",
"role_description_placeholder": "Descriu per a què serveix aquest rol i quins permisos necessita",
@@ -3490,7 +3503,7 @@
"role_created_successfully": "Rol creat correctament",
"role_updated_successfully": "Rol actualitzat correctament",
"delete_role": "Eliminar rol",
"confirm_delete_role": "Estàs segur que vols eliminar aquest rol? Aquesta acció no es pot desfer.",
"confirm_delete_role": "Esteu segur que voleu eliminar el rol: {{roleName}}? Aquesta acció no es pot desfer.",
"role_deleted_successfully": "Rol eliminat correctament",
"error_creating_role": "Error en crear el rol",
"error_updating_role": "Error en actualitzar el rol",
@@ -3509,10 +3522,20 @@
"pbac_desc_update_workflows": "Editar i modificar la configuració dels fluxos de treball",
"pbac_desc_delete_workflows": "Eliminar fluxos de treball del sistema",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Disponibilitat",
"pbac_resource_out_of_office": "Fora de l'oficina",
"pbac_desc_create_webhooks": "Crea webhooks",
"pbac_desc_view_webhooks": "Visualitza webhooks",
"pbac_desc_update_webhooks": "Actualitza els webhooks",
"pbac_desc_delete_webhooks": "Elimina els webhooks",
"pbac_desc_create_availability": "Crear disponibilitat",
"pbac_desc_view_availability": "Veure disponibilitat",
"pbac_desc_update_availability": "Actualitzar disponibilitat",
"pbac_desc_delete_availability": "Eliminar disponibilitat",
"pbac_desc_create_out_of_office": "Crear fora de l'oficina",
"pbac_desc_view_out_of_office": "Veure fora de l'oficina",
"pbac_desc_update_out_of_office": "Actualitzar fora de l'oficina",
"pbac_desc_delete_out_of_office": "Eliminar fora de l'oficina",
"pbac_desc_manage_workflows": "Accés complet de gestió a tots els fluxos de treball",
"pbac_desc_create_event_types": "Crear tipus d'esdeveniments",
"pbac_desc_view_event_types": "Veure tipus d'esdeveniments",
@@ -3654,6 +3677,8 @@
"webhook_organizer_email": "Correu electrònic de l'organitzador",
"webhook_organizer_timezone": "Zona horària de l'organitzador (p. ex., 'America/New_York', 'Asia/Kolkata')",
"webhook_organizer_locale": "Configuració regional de l'organitzador (p. ex., 'en', 'fr')",
"webhook_organizer_username": "Nom d'usuari de l'organitzador (p. ex., 'john.doe')",
"webhook_organizer_username_in_org": "Nom d'usuari de l'organitzador a la seva organització (p. ex., 'john.doe')",
"webhook_attendee_name": "Nom del primer assistent",
"webhook_attendee_email": "Correu electrònic del primer assistent",
"webhook_attendee_timezone": "Zona horària del primer assistent",
@@ -3732,4 +3757,4 @@
"use_voice": "Utilitza la veu",
"current_voice": "Veu actual",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Afegiu les vostres noves cadenes a dalt ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
+29 -4
View File
@@ -111,6 +111,7 @@
"no_available_users_found_error": "Nebyli nalezeni žádní dostupní uživatelé. Můžete zkusit jiný časový slot?",
"timeslot_unavailable_book_a_new_time": "Vybraný časový úsek již není k dispozici. <0>Prosím vyberte nový čas</0>",
"timeslot_unavailable_short": "Obsazeno",
"just_connected_description": "Právě jste se připojili. Prosím, rezervujte si termín z výše uvedených časových slotů nebo to zkuste později.",
"unavailable_timeslot_title": "Nedostupný časový slot",
"booking_time_out_of_bounds_error": "Tento typ události nelze v tomto čase rezervovat. Můžete zkusit jiný časový termín?",
"request_body_end_time_internal_error": "Interní chyba. Tělo požadavku neobsahuje čas ukončení",
@@ -321,6 +322,7 @@
"secret_meeting": "Tajná schůzka",
"login_instead": "Namísto toho se přihlásit",
"create_account": "Vytvořit účet",
"get_started": "Začít",
"confirm_password": "Potvrdit heslo",
"reset_your_password": "Nastavte si nové heslo podle pokynů odeslaných na vaši e-mailovou adresu.",
"org_banner_instructions": "Nahrajte prosím obrázek o šířce {{width}} a výšce {{height}}.",
@@ -1019,6 +1021,7 @@
"attendees": "Účastníci",
"add_attendees": "Přidat účastníky",
"show_advanced_settings": "Zobrazit pokročilá nastavení",
"hide_advanced_settings": "Skrýt pokročilá nastavení",
"event_name": "Název události",
"event_name_in_calendar": "Název události v kalendáři",
"event_name_tooltip": "Název, který se zobrazí v kalendářích",
@@ -1329,7 +1332,7 @@
"make_team_private_description": "Po zapnutí této funkce neuvidí členové vašeho týmu ostatní členy týmu.",
"you_cannot_see_team_members": "V soukromém týmu nelze vidět všechny členy týmu.",
"you_cannot_see_teams_of_org": "Nemůžete vidět týmy soukromé organizace.",
"allow_booker_to_select_duration": "Umožnit rezervující osobě vybrat dobu trvání",
"allow_multiple_durations": "Povolit více délek trvání",
"impersonate_user_tip": "Všechna použití této funkce jsou kontrolována.",
"impersonating_user_warning": "Zosobnění uživatelského jména „{{user}}“.",
"impersonating_stop_instructions": "Kliknutím sem zastavíte",
@@ -2150,6 +2153,9 @@
"delete_sso_configuration_confirmation_description": "Opravdu chcete odstranit konfiguraci {{connectionType}}? Členové vašeho týmu, kteří používají přihlášení {{connectionType}}, ztratí přístup k webu Cal.com.",
"organizer_timezone": "Časové pásmo organizátora",
"email_user_cta": "Zobrazit pozvánku",
"accept_invite": "Přijmout pozvánku",
"join_now": "Připojit se nyní",
"successfully_joined": "Úspěšně připojeno",
"email_no_user_invite_heading_subteam": "Byli jste pozváni k připojení do týmu organizace {{parentTeamName}}",
"email_no_user_invite_heading_org": "Byli jste pozváni k připojení do organizace {{appName}}",
"email_no_user_invite_subheading": "Uživatel {{invitedBy}} vás pozval, abyste se připojili k jeho týmu v aplikaci {{appName}}. {{appName}} je plánovač událostí, který vám a vašemu týmu umožňuje plánovat schůzky bez e-mailového ping-pongu.",
@@ -2202,6 +2208,7 @@
"booking_confirmation_failed": "Potvrzení rezervace se nezdařilo",
"not_enough_seats": "Nedostatek míst",
"form_builder_field_already_exists": "Pole s tímto názvem již existuje",
"guests_field_must_be_multiemail": "Pole pro hosty musí být typu 'Více e-mailů'",
"show_on_booking_page": "Zobrazit na stránce rezervace",
"visit_cancelled_booking": "Můžete navštívit stránku zrušené rezervace",
"get_started_zapier_templates": "Začněte používat šablony Zapier",
@@ -2379,6 +2386,7 @@
"invite_link_copied": "Odkaz pozvánky byl zkopírován",
"invite_link_deleted": "Odkaz pozvánky byl odstraněn",
"api_key_deleted": "Klíč API odstraněn",
"delete_api_key_warning": "Tímto trvale smažete API klíč. Všechny aplikace používající tento klíč okamžitě ztratí přístup k vašemu účtu. Tuto akci nelze vrátit zpět.",
"invite_link_updated": "Nastavení odkazu pozvánky bylo uloženo",
"link_expires_after": "Platnost odkazů vyprší za...",
"one_day": "1 den",
@@ -2922,6 +2930,7 @@
"proceeding_agreement": "Pokračováním souhlasíte s našimi",
"terms": "Podmínkami",
"always_show_x_days": "Vždy dostupné {{x}} dní",
"always_show_x_days_description": "Automaticky posouvá {{x}}-denní rezervační okno každý den vpřed, což zajišťuje, že váš kalendář vždy zobrazuje dostupné termíny na příštích {{x}} dní.",
"team_subscription_cancelled_successfully": "Předplatné týmu bylo úspěšně zrušeno",
"team_subscription_cancellation_error": "Chyba při rušení předplatného týmu",
"unable_to_subscribe_to_the_platform": "Při pokusu o přihlášení k plánu platformy došlo k chybě, zkuste to prosím znovu později",
@@ -3143,7 +3152,8 @@
"filter_operator_ends_with": "Končí na",
"filter_operator_is_empty": "Je prázdné",
"filter_operator_not_empty": "Není prázdné",
"translate_description_button": "Přeložit název/popis do jazyka prohlížeče návštěvníka pomocí AI",
"translate_description_button": "Automaticky přeložit název a popis",
"translate_description_button_description": "Automaticky překládá názvy a popisy do jazyka prohlížeče návštěvníka pomocí umělé inteligence.",
"rr_distribution_method": "Distribuce",
"rr_distribution_method_description": "Umožňuje optimalizovat distribuci pro maximální dostupnost nebo pro vyváženější přidělování.",
"rr_distribution_method_availability_title": "Maximalizovat dostupnost",
@@ -3309,6 +3319,7 @@
"seats_doesnt_support_recurring": "Funkce míst nepodporuje opakující se události. Zakažte funkci míst pro vytvoření opakující se události.",
"recurring_event_seats_error": "Opakující se událost nepodporuje funkci míst. Zakažte funkci míst nebo změňte událost na neopakující se.",
"segment": "Segment",
"saved_filters": "Uložené filtry",
"rename": "Přejmenovat",
"rename_segment": "Přejmenovat segment",
"delete_segment": "Smazat segment",
@@ -3338,6 +3349,7 @@
"picklist": "Výběrový seznam",
"most_cancelled_bookings": "Nejvíce zrušených rezervací",
"interface_language": "Jazyk rozhraní",
"interface_language_description": "Nastavte preferovaný jazyk pro rezervační rozhraní",
"visitors_browser_language": "Jazyk prohlížeče návštěvníka",
"name_or_email": "Jméno nebo e-mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Pokud není nalezen žádný kontakt, použít vlastníka leadu, pokud existuje",
@@ -3452,6 +3464,7 @@
"roles_and_permissions_description": "Správa rolí a oprávnění pro vaši organizaci",
"learn_more_permissions": "Zjistěte více o tom, jak fungují oprávnění",
"role_name": "Název role",
"role_name_required": "Název role je povinný",
"role_name_placeholder": "např. Prodejní tým, Podpora",
"role_description": "Popis role",
"role_description_placeholder": "Popište, k čemu tato role slouží a jaká oprávnění potřebuje",
@@ -3490,7 +3503,7 @@
"role_created_successfully": "Role byla úspěšně vytvořena",
"role_updated_successfully": "Role byla úspěšně aktualizována",
"delete_role": "Odstranit roli",
"confirm_delete_role": "Opravdu chcete tuto roli odstranit? Tuto akci nelze vrátit zpět.",
"confirm_delete_role": "Opravdu chcete smazat roli: {{roleName}}? Tuto akci nelze vrátit zpět.",
"role_deleted_successfully": "Role byla úspěšně odstraněna",
"error_creating_role": "Chyba při vytváření role",
"error_updating_role": "Chyba při aktualizaci role",
@@ -3509,10 +3522,20 @@
"pbac_desc_update_workflows": "Upravovat a měnit nastavení pracovních postupů",
"pbac_desc_delete_workflows": "Odstraňovat pracovní postupy ze systému",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Dostupnost",
"pbac_resource_out_of_office": "Mimo kancelář",
"pbac_desc_create_webhooks": "Vytvářet webhooky",
"pbac_desc_view_webhooks": "Zobrazit webhooky",
"pbac_desc_update_webhooks": "Aktualizovat webhooky",
"pbac_desc_delete_webhooks": "Smazat webhooky",
"pbac_desc_create_availability": "Vytvořit dostupnost",
"pbac_desc_view_availability": "Zobrazit dostupnost",
"pbac_desc_update_availability": "Aktualizovat dostupnost",
"pbac_desc_delete_availability": "Smazat dostupnost",
"pbac_desc_create_out_of_office": "Vytvořit mimo kancelář",
"pbac_desc_view_out_of_office": "Zobrazit mimo kancelář",
"pbac_desc_update_out_of_office": "Aktualizovat mimo kancelář",
"pbac_desc_delete_out_of_office": "Smazat mimo kancelář",
"pbac_desc_manage_workflows": "Plný přístup ke správě všech pracovních postupů",
"pbac_desc_create_event_types": "Vytvářet typy událostí",
"pbac_desc_view_event_types": "Zobrazit typy událostí",
@@ -3654,6 +3677,8 @@
"webhook_organizer_email": "E-mail organizátora",
"webhook_organizer_timezone": "Časové pásmo organizátora (např. 'America/New_York', 'Asia/Kolkata')",
"webhook_organizer_locale": "Lokalizace organizátora (např. 'en', 'fr')",
"webhook_organizer_username": "Uživatelské jméno organizátora (např. 'john.doe')",
"webhook_organizer_username_in_org": "Uživatelské jméno organizátora v jeho organizaci (např. 'john.doe')",
"webhook_attendee_name": "Jméno prvního účastníka",
"webhook_attendee_email": "E-mail prvního účastníka",
"webhook_attendee_timezone": "Časové pásmo prvního účastníka",
@@ -3732,4 +3757,4 @@
"use_voice": "Použít hlas",
"current_voice": "Aktuální hlas",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
+31 -6
View File
@@ -111,6 +111,7 @@
"no_available_users_found_error": "Ingen tilgængelige brugere fundet. Kan du prøve et andet tidsrum?",
"timeslot_unavailable_book_a_new_time": "Det valgte tidspunkt er ikke længere tilgængeligt. <0>Vælg venligst et nyt tidspunkt</0>",
"timeslot_unavailable_short": "Optaget",
"just_connected_description": "Du har lige forbundet. Book venligst fra de ovenstående tidsrum eller prøv igen senere.",
"unavailable_timeslot_title": "Utilgængeligt tidsrum",
"booking_time_out_of_bounds_error": "Begivenheden kan ikke bookes på dette tidspunkt. Kunne du prøve et andet tidspunkt?",
"request_body_end_time_internal_error": "Intern fejl. Anmodningskroppen indeholder ikke sluttidspunkt",
@@ -321,6 +322,7 @@
"secret_meeting": "Skjult Møde",
"login_instead": "Log ind i stedet",
"create_account": "Opret Konto",
"get_started": "Kom i gang",
"confirm_password": "Bekræft adgangskode",
"reset_your_password": "Indstil din nye adgangskode med instruktionerne sendt til din e-mailadresse.",
"org_banner_instructions": "Upload venligst et billede med {{width}} bredde og {{height}} højde.",
@@ -958,7 +960,7 @@
"light_event_type_color": "Begivenhedstype farve (Lyst tema)",
"dark_event_type_color": "Begivenhedstype farve (Mørkt tema)",
"file_not_named": "Filen er ikke navngivet [idOrSlug]/[user]",
"create_team": "Opret Team",
"create_team": "Opret team",
"name": "Navn",
"nameless_team": "Unavngivet team",
"oauth_clients": "OAuth-klienter",
@@ -1019,6 +1021,7 @@
"attendees": "Deltagere",
"add_attendees": "Tilføj deltagere",
"show_advanced_settings": "Vis avancerede indstillinger",
"hide_advanced_settings": "Skjul avancerede indstillinger",
"event_name": "Begivenhedsnavn",
"event_name_in_calendar": "Begivenhedsnavn i kalender",
"event_name_tooltip": "Navnet, der vises i kalendere",
@@ -1329,7 +1332,7 @@
"make_team_private_description": "Dine teammedlemmer vil ikke kunne se andre teammedlemmer, når dette er slået til.",
"you_cannot_see_team_members": "Du kan ikke se alle teammedlemmer i et privat team.",
"you_cannot_see_teams_of_org": "Du kan ikke se teams i en privat organisation.",
"allow_booker_to_select_duration": "Tillad bookeren at vælge varighed",
"allow_multiple_durations": "Tillad flere varigheder",
"impersonate_user_tip": "Al brug af denne funktion er revideret.",
"impersonating_user_warning": "Efterligner brugernavnet \"{{user}}\".",
"impersonating_stop_instructions": "Klik her for at stoppe",
@@ -2150,6 +2153,9 @@
"delete_sso_configuration_confirmation_description": "Er du sikker på, at du vil slette {{connectionType}} konfigurationen? Dine teammedlemmer, der bruger {{connectionType}} login, vil ikke længere kunne få adgang til Cal.com.",
"organizer_timezone": "Arrangørens tidszone",
"email_user_cta": "Vis Invitation",
"accept_invite": "Acceptér invitation",
"join_now": "Deltag nu",
"successfully_joined": "Deltog med succes",
"email_no_user_invite_heading_subteam": "Du er blevet inviteret til at blive medlem af et team i {{parentTeamName}}-organisationen.",
"email_no_user_invite_heading_org": "Du er blevet inviteret til at blive medlem af en {{appName}}-organisation.",
"email_no_user_invite_subheading": "{{invitedBy}} har inviteret dig til at deltage i deres team på {{appName}}. {{appName}} er den begivenheds-jonglerende planlægger, der gør det muligt for dig og dit team at planlægge møder uden e-mail tennis.",
@@ -2202,6 +2208,7 @@
"booking_confirmation_failed": "Reservationsbekræftelse mislykkedes",
"not_enough_seats": "Ikke nok pladser",
"form_builder_field_already_exists": "Et felt med dette navn findes allerede",
"guests_field_must_be_multiemail": "Gæstefeltet skal være af typen 'Flere e-mails'",
"show_on_booking_page": "Vis på bookingsiden",
"visit_cancelled_booking": "Du kan besøge siden for den aflyste booking",
"get_started_zapier_templates": "Kom i gang med Zapier skabeloner",
@@ -2379,6 +2386,7 @@
"invite_link_copied": "Invitationslink kopieret",
"invite_link_deleted": "Invitationslink slettet",
"api_key_deleted": "API-nøgle slettet",
"delete_api_key_warning": "Dette vil permanent slette API-nøglen. Alle applikationer, der bruger denne nøgle, vil straks miste adgang til din konto. Denne handling kan ikke fortrydes.",
"invite_link_updated": "Linkindstillinger gemt",
"link_expires_after": "Links indstillet til at udløbe efter...",
"one_day": "1 dag",
@@ -2688,7 +2696,7 @@
"enterprise": "Virksomhed",
"enterprise_description": "Opgrader til Enterprise for at oprette din organisation",
"create_your_org": "Opret din Organisation",
"create_org": "Opret Organisation",
"create_org": "Opret organisation",
"create_your_org_description": "Opgrader til Organisationer og få et subdomæne, samlet fakturering, Insights, omfattende whitelabeling og mere",
"create_your_enterprise_description": "Opgrader til Enterprise og få adgang til Active Directory Sync, SCIM Automatisk Brugerprovisionering, Cal.ai Voice Agents, Admin API'er og mere!",
"other_payment_app_enabled": "Du kan kun aktivere én betalingsapp pr. begivenhedstype",
@@ -2922,6 +2930,7 @@
"proceeding_agreement": "Ved at fortsætte accepterer du vores",
"terms": "Vilkår",
"always_show_x_days": "Vis altid {{x}} dage tilgængelige",
"always_show_x_days_description": "Flytter automatisk {{x}}-dages bookingvinduet frem hver dag, så din kalender altid viser de næste {{x}} dages tilgængelige tidsrum.",
"team_subscription_cancelled_successfully": "Teamabonnement annulleret med succes",
"team_subscription_cancellation_error": "Fejl under annullering af teamabonnement",
"unable_to_subscribe_to_the_platform": "Der opstod en fejl under forsøget på at abonnere på platformplanen, prøv venligst igen senere",
@@ -3143,7 +3152,8 @@
"filter_operator_ends_with": "Slutter med",
"filter_operator_is_empty": "Er tom",
"filter_operator_not_empty": "Er ikke tom",
"translate_description_button": "Oversæt titel/beskrivelse til besøgendes browsersprog ved hjælp af AI",
"translate_description_button": "Autooversæt titel og beskrivelse",
"translate_description_button_description": "Oversæt automatisk titler og beskrivelser til besøgendes browsersprog ved hjælp af AI.",
"rr_distribution_method": "Distribution",
"rr_distribution_method_description": "Gør det muligt at optimere distributionen for maksimal tilgængelighed eller for at opnå en mere balanceret tildeling.",
"rr_distribution_method_availability_title": "Maksimer tilgængelighed",
@@ -3309,6 +3319,7 @@
"seats_doesnt_support_recurring": "Sædefunktionen understøtter ikke tilbagevendende begivenheder. Deaktiver sædefunktionen for at gøre den tilbagevendende.",
"recurring_event_seats_error": "Tilbagevendende begivenheder understøtter ikke sædefunktionen. Deaktiver sædefunktionen eller gør begivenheden ikke-tilbagevendende.",
"segment": "Segment",
"saved_filters": "Gemte filtre",
"rename": "Omdøb",
"rename_segment": "Omdøb segment",
"delete_segment": "Slet segment",
@@ -3338,6 +3349,7 @@
"picklist": "Valgliste",
"most_cancelled_bookings": "Flest annullerede",
"interface_language": "Grænsefladesprog",
"interface_language_description": "Indstil dit foretrukne sprog for bookinggrænsefladen",
"visitors_browser_language": "Besøgerens browsersprog",
"name_or_email": "Navn eller e-mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Hvis ingen kontakt findes, fald tilbage til lead-ejeren, hvis denne eksisterer",
@@ -3452,6 +3464,7 @@
"roles_and_permissions_description": "Administrer roller og tilladelser for din organisation",
"learn_more_permissions": "Lær mere om, hvordan tilladelser fungerer",
"role_name": "Rollenavn",
"role_name_required": "Rollenavn er påkrævet",
"role_name_placeholder": "f.eks. Salgsteam, Supportpersonale",
"role_description": "Rollebekskrivelse",
"role_description_placeholder": "Beskriv, hvad denne rolle er til, og hvilke tilladelser den har brug for",
@@ -3490,7 +3503,7 @@
"role_created_successfully": "Rolle oprettet med succes",
"role_updated_successfully": "Rolle opdateret med succes",
"delete_role": "Slet rolle",
"confirm_delete_role": "Er du sikker på, at du vil slette denne rolle? Denne handling kan ikke fortrydes.",
"confirm_delete_role": "Er du sikker på, at du vil slette rollen: {{roleName}}? Denne handling kan ikke fortrydes.",
"role_deleted_successfully": "Rolle slettet med succes",
"error_creating_role": "Fejl ved oprettelse af rolle",
"error_updating_role": "Fejl ved opdatering af rolle",
@@ -3509,10 +3522,20 @@
"pbac_desc_update_workflows": "Rediger og tilpas workflowindstillinger",
"pbac_desc_delete_workflows": "Fjern workflows fra systemet",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Tilgængelighed",
"pbac_resource_out_of_office": "Ude af kontoret",
"pbac_desc_create_webhooks": "Opret webhooks",
"pbac_desc_view_webhooks": "Se webhooks",
"pbac_desc_update_webhooks": "Opdater webhooks",
"pbac_desc_delete_webhooks": "Slet webhooks",
"pbac_desc_create_availability": "Opret tilgængelighed",
"pbac_desc_view_availability": "Se tilgængelighed",
"pbac_desc_update_availability": "Opdater tilgængelighed",
"pbac_desc_delete_availability": "Slet tilgængelighed",
"pbac_desc_create_out_of_office": "Opret fravær",
"pbac_desc_view_out_of_office": "Se fravær",
"pbac_desc_update_out_of_office": "Opdater fravær",
"pbac_desc_delete_out_of_office": "Slet fravær",
"pbac_desc_manage_workflows": "Fuld administrationsadgang til alle workflows",
"pbac_desc_create_event_types": "Opret begivenhedstyper",
"pbac_desc_view_event_types": "Vis begivenhedstyper",
@@ -3654,6 +3677,8 @@
"webhook_organizer_email": "Arrangørens e-mail",
"webhook_organizer_timezone": "Arrangørens tidszone (f.eks. 'America/New_York', 'Asia/Kolkata')",
"webhook_organizer_locale": "Arrangørens lokalitet (f.eks. 'en', 'fr')",
"webhook_organizer_username": "Brugernavn for arrangøren (f.eks. 'john.doe')",
"webhook_organizer_username_in_org": "Brugernavn for arrangøren i deres organisation (f.eks. 'john.doe')",
"webhook_attendee_name": "Navn på den første deltager",
"webhook_attendee_email": "E-mail på den første deltager",
"webhook_attendee_timezone": "Tidszone for den første deltager",
@@ -3732,4 +3757,4 @@
"use_voice": "Brug stemme",
"current_voice": "Nuværende stemme",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Tilføj dine nye strenge ovenfor her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
+32 -7
View File
@@ -111,6 +111,7 @@
"no_available_users_found_error": "Keine verfügbaren Benutzer gefunden. Können Sie es mit einem anderen Zeitfenster versuchen?",
"timeslot_unavailable_book_a_new_time": "Der ausgewählte Zeitraum ist nicht mehr verfügbar. <0>Bitte wähle eine neue Zeit</0>",
"timeslot_unavailable_short": "Belegt",
"just_connected_description": "Sie haben gerade eine Verbindung hergestellt. Bitte buchen Sie aus den obigen Zeitfenstern oder versuchen Sie es später erneut.",
"unavailable_timeslot_title": "Nicht verfügbarer Zeitslot",
"booking_time_out_of_bounds_error": "Der Termin kann zu diesem Zeitpunkt nicht gebucht werden. Möchten Sie einen anderen Zeitslot versuchen?",
"request_body_end_time_internal_error": "Interner Fehler. Request-Body enthält keinen Endzeitpunkt",
@@ -321,6 +322,7 @@
"secret_meeting": "Geheimer Termin",
"login_instead": "Stattdessen einloggen",
"create_account": "Konto erstellen",
"get_started": "Loslegen",
"confirm_password": "Passwort bestätigen",
"reset_your_password": "Legen Sie Ihr neues Passwort mit den Anweisungen fest, die an Ihre E-Mail-Adresse gesendet wurden.",
"org_banner_instructions": "Bitte laden Sie Bild mit einer Breite von {{width}} und einer Höhe von {{height}} hoch.",
@@ -958,7 +960,7 @@
"light_event_type_color": "Ereignistyp-Farbe (Helles Design)",
"dark_event_type_color": "Ereignistyp-Farbe (Dunkles Design)",
"file_not_named": "Datei heißt nicht [idOrSlug]/[user]",
"create_team": "Team anlegen",
"create_team": "Team erstellen",
"name": "Name",
"nameless_team": "Namenloses Team",
"oauth_clients": "OAuth-Clients",
@@ -1019,6 +1021,7 @@
"attendees": "Teilnehmende",
"add_attendees": "Teilnehmende hinzufügen",
"show_advanced_settings": "Erweiterte Einstellungen anzeigen",
"hide_advanced_settings": "Erweiterte Einstellungen ausblenden",
"event_name": "Termin-Name",
"event_name_in_calendar": "Ereignisname im Kalender",
"event_name_tooltip": "Der Name, der in Kalendern angezeigt wird",
@@ -1231,7 +1234,7 @@
"contact_sales": "Vertrieb kontaktieren",
"error_404": "Fehler 404",
"default": "Standard",
"set_to_default": "Auf Standard setzen",
"set_to_default": "Als Standard festlegen",
"new_schedule_btn": "Neuer Verfügbarkeitsplan",
"add_new_schedule": "Neuen Verfügbarkeitsplan hinzufügen",
"add_new_calendar": "Neuen Kalender hinzufügen",
@@ -1329,7 +1332,7 @@
"make_team_private_description": "Wenn diese Einstellung aktiv ist, können deine Teammitglieder keine anderen Teammitglieder sehen.",
"you_cannot_see_team_members": "Sie können nicht alle Teammitglieder eines privaten Teams sehen.",
"you_cannot_see_teams_of_org": "Du kannst keine Teams einer privaten Organisation sehen.",
"allow_booker_to_select_duration": "Bucher erlauben, die Länge zu wählen",
"allow_multiple_durations": "Mehrere Dauern erlauben",
"impersonate_user_tip": "Alle Verwendungen dieser Funktion werden geprüft.",
"impersonating_user_warning": "Imitiere den Account {{user}}.",
"impersonating_stop_instructions": "Klicken Sie hier, um zu stoppen",
@@ -2150,6 +2153,9 @@
"delete_sso_configuration_confirmation_description": "Sind Sie sicher, dass Sie die {{connectionType}}-Konfiguration löschen möchten? Ihre Teammitglieder, die sich über {{connectionType}} anmelden, werden keinen Zugriff mehr auf Cal.com haben.",
"organizer_timezone": "Zeitzone des Veranstalters",
"email_user_cta": "Einladung anzeigen",
"accept_invite": "Einladung annehmen",
"join_now": "Jetzt beitreten",
"successfully_joined": "Erfolgreich beigetreten",
"email_no_user_invite_heading_subteam": "Du wurdest eingeladen, einem Team der Organisation {{parentTeamName}} beizutreten",
"email_no_user_invite_heading_org": "Du wurdest eingeladen, einer {{appName}}-Organisation beizutreten",
"email_no_user_invite_subheading": "{{invitedBy}} hat Sie eingeladen, dem Team in {{appName}} beizutreten. {{appName}} ist der Event-Planer, der es Ihnen und Ihrem Team ermöglicht, Meetings ohne ständiges hin und her zu planen.",
@@ -2202,6 +2208,7 @@
"booking_confirmation_failed": "Buchungsbestätigung fehlgeschlagen",
"not_enough_seats": "Nicht genügend Plätze",
"form_builder_field_already_exists": "Ein Feld mit diesem Namen ist bereits vorhanden",
"guests_field_must_be_multiemail": "Das Gästefeld muss vom Typ 'Mehrere E-Mails' sein",
"show_on_booking_page": "Auf der Buchungsseite anzeigen",
"visit_cancelled_booking": "Sie können die Seite der stornierten Buchung besuchen",
"get_started_zapier_templates": "Legen Sie mit Zapier-Vorlagen los",
@@ -2379,6 +2386,7 @@
"invite_link_copied": "Einladungslink kopiert",
"invite_link_deleted": "Einladungslink gelöscht",
"api_key_deleted": "API-Schlüssel gelöscht",
"delete_api_key_warning": "Dies wird den API-Schlüssel dauerhaft löschen. Alle Anwendungen, die diesen Schlüssel verwenden, verlieren sofort den Zugriff auf Ihr Konto. Diese Aktion kann nicht rückgängig gemacht werden.",
"invite_link_updated": "Einladungslink-Einstellungen gespeichert",
"link_expires_after": "Links verfallen nach...",
"one_day": "1 Tag",
@@ -2922,6 +2930,7 @@
"proceeding_agreement": "Indem Sie fortfahren, stimmen Sie unseren",
"terms": "Nutzungsbedingungen",
"always_show_x_days": "Immer für {{x}} Tage verfügbar",
"always_show_x_days_description": "Verschiebt das {{x}}-Tage-Buchungsfenster automatisch jeden Tag nach vorne und stellt sicher, dass Ihr Kalender immer die nächsten {{x}} Tage mit verfügbaren Zeitfenstern anzeigt.",
"team_subscription_cancelled_successfully": "Team-Abonnement erfolgreich gekündigt",
"team_subscription_cancellation_error": "Fehler bei der Kündigung des Team-Abonnements",
"unable_to_subscribe_to_the_platform": "Beim Abonnieren des Platform-Plans ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut",
@@ -3143,7 +3152,8 @@
"filter_operator_ends_with": "Endet mit",
"filter_operator_is_empty": "Ist leer",
"filter_operator_not_empty": "Ist nicht leer",
"translate_description_button": "Titel/Beschreibung mit KI in die Browsersprache des Besuchers übersetzen",
"translate_description_button": "Titel und Beschreibung automatisch übersetzen",
"translate_description_button_description": "Titel und Beschreibungen automatisch in die Browsersprache des Besuchers mittels KI übersetzen.",
"rr_distribution_method": "Verteilung",
"rr_distribution_method_description": "Ermöglicht die Optimierung der Verteilung für maximale Verfügbarkeit oder eine ausgewogenere Zuweisung.",
"rr_distribution_method_availability_title": "Verfügbarkeit maximieren",
@@ -3309,6 +3319,7 @@
"seats_doesnt_support_recurring": "Die Sitzplatzfunktion unterstützt keine wiederkehrenden Events. Deaktivieren Sie die Sitzplatzfunktion, um es wiederkehrend zu machen.",
"recurring_event_seats_error": "Wiederkehrende Events unterstützen die Sitzplatzfunktion nicht. Deaktivieren Sie die Sitzplatzfunktion oder machen Sie das Event nicht wiederkehrend.",
"segment": "Segment",
"saved_filters": "Gespeicherte Filter",
"rename": "Umbenennen",
"rename_segment": "Segment umbenennen",
"delete_segment": "Segment löschen",
@@ -3338,6 +3349,7 @@
"picklist": "Auswahlliste",
"most_cancelled_bookings": "Meiste stornierte Buchungen",
"interface_language": "Oberflächensprache",
"interface_language_description": "Legen Sie Ihre bevorzugte Sprache für die Buchungsoberfläche fest",
"visitors_browser_language": "Browsersprache des Besuchers",
"name_or_email": "Name oder E-Mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Wenn kein Kontakt gefunden wird, auf den Lead-Eigentümer zurückgreifen, falls vorhanden",
@@ -3452,6 +3464,7 @@
"roles_and_permissions_description": "Verwalte Rollen und Berechtigungen für deine Organisation",
"learn_more_permissions": "Erfahre mehr darüber, wie Berechtigungen funktionieren",
"role_name": "Rollenname",
"role_name_required": "Rollenname ist erforderlich",
"role_name_placeholder": "z.B. Vertriebsteam, Support-Mitarbeiter",
"role_description": "Rollenbeschreibung",
"role_description_placeholder": "Beschreibe, wofür diese Rolle gedacht ist und welche Berechtigungen sie benötigt",
@@ -3490,7 +3503,7 @@
"role_created_successfully": "Rolle erfolgreich erstellt",
"role_updated_successfully": "Rolle erfolgreich aktualisiert",
"delete_role": "Rolle löschen",
"confirm_delete_role": "Sind Sie sicher, dass Sie diese Rolle löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
"confirm_delete_role": "Sind Sie sicher, dass Sie die Rolle löschen möchten: {{roleName}}. Diese Aktion kann nicht rückgängig gemacht werden.",
"role_deleted_successfully": "Rolle erfolgreich gelöscht",
"error_creating_role": "Fehler beim Erstellen der Rolle",
"error_updating_role": "Fehler beim Aktualisieren der Rolle",
@@ -3509,10 +3522,20 @@
"pbac_desc_update_workflows": "Workflow-Einstellungen bearbeiten und ändern",
"pbac_desc_delete_workflows": "Workflows aus dem System entfernen",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Verfügbarkeit",
"pbac_resource_out_of_office": "Abwesenheit",
"pbac_desc_create_webhooks": "Webhooks erstellen",
"pbac_desc_view_webhooks": "Webhooks anzeigen",
"pbac_desc_update_webhooks": "Webhooks aktualisieren",
"pbac_desc_delete_webhooks": "Webhooks löschen",
"pbac_desc_create_availability": "Verfügbarkeit erstellen",
"pbac_desc_view_availability": "Verfügbarkeit anzeigen",
"pbac_desc_update_availability": "Verfügbarkeit aktualisieren",
"pbac_desc_delete_availability": "Verfügbarkeit löschen",
"pbac_desc_create_out_of_office": "Abwesenheit erstellen",
"pbac_desc_view_out_of_office": "Abwesenheit anzeigen",
"pbac_desc_update_out_of_office": "Abwesenheit aktualisieren",
"pbac_desc_delete_out_of_office": "Abwesenheit löschen",
"pbac_desc_manage_workflows": "Vollständiger Verwaltungszugriff auf alle Workflows",
"pbac_desc_create_event_types": "Ereignistypen erstellen",
"pbac_desc_view_event_types": "Ereignistypen anzeigen",
@@ -3560,8 +3583,8 @@
"rr_load_balancing_disabled_with_groups": "Lastausgleich ist mit Round-Robin-Gruppen nicht verfügbar",
"round_robin_settings_description": "Passen Sie die Standard-Round-Robin-Einstellungen für dieses Team an",
"round_robin_settings_updated_successfully": "Round-Robin-Einstellungen erfolgreich aktualisiert",
"booking_creation_time": "Zeitpunkt der Buchungserstellung",
"meeting_start_time": "Beginn des Meetings",
"booking_creation_time": "Erstellungszeit der Buchung",
"meeting_start_time": "Besprechungsstartzeit",
"members_affected_by_disabling_delegation_credential": "Betroffene Mitglieder",
"no_members_affected_by_disabling_delegation_credential": "Keine Mitglieder von der Deaktivierung der Delegationsanmeldedaten betroffen",
"download_expense_log": "Ausgabenprotokoll herunterladen",
@@ -3654,6 +3677,8 @@
"webhook_organizer_email": "E-Mail des Organisators",
"webhook_organizer_timezone": "Zeitzone des Organisators (z.B. 'America/New_York', 'Asia/Kolkata')",
"webhook_organizer_locale": "Sprache des Organisators (z.B. 'en', 'fr')",
"webhook_organizer_username": "Benutzername des Organisators (z.B. 'john.doe')",
"webhook_organizer_username_in_org": "Benutzername des Organisators in seiner Organisation (z.B. 'john.doe')",
"webhook_attendee_name": "Name des ersten Teilnehmers",
"webhook_attendee_email": "E-Mail des ersten Teilnehmers",
"webhook_attendee_timezone": "Zeitzone des ersten Teilnehmers",
+31 -6
View File
@@ -111,6 +111,7 @@
"no_available_users_found_error": "Δεν βρέθηκαν διαθέσιμοι χρήστες. Θα μπορούσατε να δοκιμάσετε άλλη χρονική θυρίδα;",
"timeslot_unavailable_book_a_new_time": "Η επιλεγμένη χρονοθυρίδα δεν είναι πλέον διαθέσιμη. <0>Παρακαλώ επιλέξτε νέο χρόνο</0>",
"timeslot_unavailable_short": "Κατειλημμένο",
"just_connected_description": "Μόλις συνδεθήκατε. Παρακαλώ κάντε κράτηση από τις παραπάνω διαθέσιμες ώρες ή δοκιμάστε ξανά αργότερα.",
"unavailable_timeslot_title": "Μη διαθέσιμη χρονοθυρίδα",
"booking_time_out_of_bounds_error": "Ο τύπος εκδήλωσης δεν μπορεί να κρατηθεί αυτή τη στιγμή. Θα μπορούσατε να δοκιμάσετε μια άλλη χρονική θυρίδα;",
"request_body_end_time_internal_error": "Εσωτερικό σφάλμα. Το σώμα του αιτήματος δεν περιέχει ώρα λήξης",
@@ -321,6 +322,7 @@
"secret_meeting": "Μυστική συνάντηση",
"login_instead": "Σύνδεση αντ' αυτού",
"create_account": "Δημιουργία Λογαριασμού",
"get_started": "Ξεκινήστε",
"confirm_password": "Επιβεβαίωση κωδικού πρόσβασης",
"reset_your_password": "Ορίστε τον νέο σας κωδικό πρόσβασης με τις οδηγίες που στάλθηκαν στη διεύθυνση email σας.",
"org_banner_instructions": "Παρακαλούμε ανεβάστε εικόνα με πλάτος {{width}} και ύψος {{height}}.",
@@ -1019,6 +1021,7 @@
"attendees": "Συμμετέχοντες",
"add_attendees": "Προσθήκη συμμετεχόντων",
"show_advanced_settings": "Εμφάνιση προχωρημένων ρυθμίσεων",
"hide_advanced_settings": "Απόκρυψη προχωρημένων ρυθμίσεων",
"event_name": "Όνομα εκδήλωσης",
"event_name_in_calendar": "Με όνομα εκδήλωσης",
"event_name_tooltip": "Το όνομα που θα εμφανίζεται στα ημερολόγια",
@@ -1231,7 +1234,7 @@
"contact_sales": "Επικοινωνία με Πωλήσεις",
"error_404": "Σφάλμα 404",
"default": "Προεπιλογή",
"set_to_default": "Ορισμός ως Προεπιλογή",
"set_to_default": "Ορισμός ως προεπιλογή",
"new_schedule_btn": "Νέο πρόγραμμα",
"add_new_schedule": "Προσθήκη νέου προγράμματος",
"add_new_calendar": "Προσθήκη νέου ημερολογίου",
@@ -1329,7 +1332,7 @@
"make_team_private_description": "Τα μέλη της ομάδας σας δεν θα μπορούν να βλέπουν άλλα μέλη της ομάδας όταν αυτό είναι ενεργοποιημένο.",
"you_cannot_see_team_members": "Δεν μπορείτε να δείτε όλα τα μέλη μιας ιδιωτικής ομάδας.",
"you_cannot_see_teams_of_org": "Δεν μπορείτε να δείτε τις ομάδες ενός ιδιωτικού οργανισμού.",
"allow_booker_to_select_duration": "Επιτρέψτε στον χρήστη να επιλέξει διάρκεια",
"allow_multiple_durations": "Επιτρέψτε πολλαπλές διάρκειες",
"impersonate_user_tip": "Όλες οι χρήσεις αυτής της λειτουργίας καταγράφονται.",
"impersonating_user_warning": "Μιμείστε τον χρήστη \"{{user}}\".",
"impersonating_stop_instructions": "Κάντε κλικ εδώ για διακοπή",
@@ -2150,6 +2153,9 @@
"delete_sso_configuration_confirmation_description": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τη διαμόρφωση {{connectionType}}; Τα μέλη της ομάδας σας που χρησιμοποιούν σύνδεση {{connectionType}} δεν θα μπορούν πλέον να έχουν πρόσβαση στο Cal.com.",
"organizer_timezone": "Ζώνη ώρας διοργανωτή",
"email_user_cta": "Προβολή πρόσκλησης",
"accept_invite": "Αποδοχή Πρόσκλησης",
"join_now": "Συμμετοχή Τώρα",
"successfully_joined": "Επιτυχής συμμετοχή",
"email_no_user_invite_heading_subteam": "Έχετε προσκληθεί να συμμετάσχετε σε μια ομάδα του οργανισμού {{parentTeamName}}",
"email_no_user_invite_heading_org": "Έχετε προσκληθεί να συμμετάσχετε σε έναν οργανισμό {{appName}}",
"email_no_user_invite_subheading": "Ο/Η {{invitedBy}} σας έχει προσκαλέσει να συμμετάσχετε στην ομάδα τους στο {{appName}}. Το {{appName}} είναι ο προγραμματιστής συναντήσεων που επιτρέπει σε εσάς και την ομάδα σας να κανονίζετε συναντήσεις χωρίς ατελείωτα email.",
@@ -2202,6 +2208,7 @@
"booking_confirmation_failed": "Η επιβεβαίωση της κράτησης απέτυχε",
"not_enough_seats": "Δεν υπάρχουν αρκετές θέσεις",
"form_builder_field_already_exists": "Υπάρχει ήδη ένα πεδίο με αυτό το όνομα",
"guests_field_must_be_multiemail": "Το πεδίο επισκεπτών πρέπει να είναι τύπου 'Πολλαπλά emails'",
"show_on_booking_page": "Εμφάνιση στη σελίδα κράτησης",
"visit_cancelled_booking": "Μπορείτε να επισκεφθείτε τη σελίδα ακυρωμένων κρατήσεων",
"get_started_zapier_templates": "Ξεκινήστε με τα πρότυπα του Zapier",
@@ -2379,6 +2386,7 @@
"invite_link_copied": "Ο σύνδεσμος πρόσκλησης αντιγράφηκε",
"invite_link_deleted": "Ο σύνδεσμος πρόσκλησης διαγράφηκε",
"api_key_deleted": "Το κλειδί API διαγράφηκε",
"delete_api_key_warning": "Αυτό θα διαγράψει μόνιμα το κλειδί API. Οποιεσδήποτε εφαρμογές χρησιμοποιούν αυτό το κλειδί θα χάσουν αμέσως την πρόσβαση στον λογαριασμό σας. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.",
"invite_link_updated": "Οι ρυθμίσεις του συνδέσμου πρόσκλησης αποθηκεύτηκαν",
"link_expires_after": "Οι σύνδεσμοι λήγουν μετά από...",
"one_day": "1 ημέρα",
@@ -2688,7 +2696,7 @@
"enterprise": "Enterprise",
"enterprise_description": "Αναβαθμίστε σε Enterprise για να δημιουργήσετε τον οργανισμό σας",
"create_your_org": "Δημιουργήστε τον Οργανισμό σας",
"create_org": "Δημιουργία Οργανισμού",
"create_org": "Δημιουργία οργανισμού",
"create_your_org_description": "Αναβαθμίστε σε Οργανισμούς και λάβετε υποτομέα, ενοποιημένη τιμολόγηση, Αναλύσεις, εκτεταμένο white-labeling και άλλα",
"create_your_enterprise_description": "Αναβαθμίστε σε Enterprise και αποκτήστε πρόσβαση σε Συγχρονισμό Active Directory, Αυτόματη παροχή χρηστών SCIM, Φωνητικούς πράκτορες Cal.ai, APIs διαχειριστή και άλλα!",
"other_payment_app_enabled": "Μπορείτε να ενεργοποιήσετε μόνο μία εφαρμογή πληρωμών ανά τύπο εκδήλωσης",
@@ -2922,6 +2930,7 @@
"proceeding_agreement": "Προχωρώντας, συμφωνείτε με τους",
"terms": "Όρους",
"always_show_x_days": "Πάντα {{x}} διαθέσιμες ημέρες",
"always_show_x_days_description": "Μετακινεί αυτόματα το παράθυρο κρατήσεων {{x}} ημερών κάθε μέρα, διασφαλίζοντας ότι το ημερολόγιό σας δείχνει πάντα τις επόμενες {{x}} ημέρες διαθέσιμων χρονοθυρίδων.",
"team_subscription_cancelled_successfully": "Η συνδρομή ομάδας ακυρώθηκε με επιτυχία",
"team_subscription_cancellation_error": "Σφάλμα κατά την ακύρωση της συνδρομής ομάδας",
"unable_to_subscribe_to_the_platform": "Παρουσιάστηκε σφάλμα κατά την εγγραφή στο πρόγραμμα της πλατφόρμας, παρακαλώ δοκιμάστε ξανά αργότερα",
@@ -3143,7 +3152,8 @@
"filter_operator_ends_with": "Τελειώνει με",
"filter_operator_is_empty": "Είναι κενό",
"filter_operator_not_empty": "Δεν είναι κενό",
"translate_description_button": "Μετάφραση τίτλου/περιγραφής στη γλώσσα του προγράμματος περιήγησης του επισκέπτη με χρήση AI",
"translate_description_button": "Αυτόματη μετάφραση τίτλου και περιγραφής",
"translate_description_button_description": "Αυτόματη μετάφραση τίτλων και περιγραφών στη γλώσσα του προγράμματος περιήγησης του επισκέπτη χρησιμοποιώντας AI.",
"rr_distribution_method": "Κατανομή",
"rr_distribution_method_description": "Επιτρέπει τη βελτιστοποίηση της κατανομής για μέγιστη διαθεσιμότητα ή για πιο ισορροπημένη ανάθεση.",
"rr_distribution_method_availability_title": "Μεγιστοποίηση διαθεσιμότητας",
@@ -3309,6 +3319,7 @@
"seats_doesnt_support_recurring": "Η λειτουργία θέσεων δεν υποστηρίζει επαναλαμβανόμενη εκδήλωση. Απενεργοποιήστε τη λειτουργία θέσεων για να την κάνετε επαναλαμβανόμενη.",
"recurring_event_seats_error": "Η επαναλαμβανόμενη εκδήλωση δεν υποστηρίζει τη λειτουργία θέσεων. Απενεργοποιήστε τη λειτουργία θέσεων ή κάντε την εκδήλωση μη επαναλαμβανόμενη.",
"segment": "Τμήμα",
"saved_filters": "Αποθηκευμένα φίλτρα",
"rename": "Μετονομασία",
"rename_segment": "Μετονομασία τμήματος",
"delete_segment": "Διαγραφή τμήματος",
@@ -3338,6 +3349,7 @@
"picklist": "Λίστα επιλογών",
"most_cancelled_bookings": "Περισσότερες ακυρωμένες κρατήσεις",
"interface_language": "Γλώσσα διεπαφής",
"interface_language_description": "Ορίστε την προτιμώμενη γλώσσα για το περιβάλλον κρατήσεων",
"visitors_browser_language": "Γλώσσα προγράμματος περιήγησης επισκέπτη",
"name_or_email": "Όνομα ή Email",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Αν δεν βρεθεί επαφή, επιστροφή στον ιδιοκτήτη του lead εάν υπάρχει",
@@ -3452,6 +3464,7 @@
"roles_and_permissions_description": "Διαχειριστείτε ρόλους και δικαιώματα για τον οργανισμό σας",
"learn_more_permissions": "Μάθετε περισσότερα για το πώς λειτουργούν τα Δικαιώματα",
"role_name": "Όνομα ρόλου",
"role_name_required": "Το όνομα ρόλου είναι υποχρεωτικό",
"role_name_placeholder": "π.χ. Ομάδα Πωλήσεων, Προσωπικό Υποστήριξης",
"role_description": "Περιγραφή ρόλου",
"role_description_placeholder": "Περιγράψτε για ποιο σκοπό προορίζεται αυτός ο ρόλος και ποια δικαιώματα χρειάζεται",
@@ -3490,7 +3503,7 @@
"role_created_successfully": "Ο ρόλος δημιουργήθηκε με επιτυχία",
"role_updated_successfully": "Ο ρόλος ενημερώθηκε με επιτυχία",
"delete_role": "Διαγραφή ρόλου",
"confirm_delete_role": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον ρόλο; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.",
"confirm_delete_role": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τον ρόλο: {{roleName}}; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.",
"role_deleted_successfully": "Ο ρόλος διαγράφηκε με επιτυχία",
"error_creating_role": "Σφάλμα κατά τη δημιουργία ρόλου",
"error_updating_role": "Σφάλμα κατά την ενημέρωση ρόλου",
@@ -3509,10 +3522,20 @@
"pbac_desc_update_workflows": "Επεξεργασία και τροποποίηση ρυθμίσεων ροών εργασίας",
"pbac_desc_delete_workflows": "Αφαίρεση ροών εργασίας από το σύστημα",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Διαθεσιμότητα",
"pbac_resource_out_of_office": "Εκτός Γραφείου",
"pbac_desc_create_webhooks": "Δημιουργία webhooks",
"pbac_desc_view_webhooks": "Προβολή webhooks",
"pbac_desc_update_webhooks": "Ενημέρωση webhooks",
"pbac_desc_delete_webhooks": "Διαγραφή webhooks",
"pbac_desc_create_availability": "Δημιουργία διαθεσιμότητας",
"pbac_desc_view_availability": "Προβολή διαθεσιμότητας",
"pbac_desc_update_availability": "Ενημέρωση διαθεσιμότητας",
"pbac_desc_delete_availability": "Διαγραφή διαθεσιμότητας",
"pbac_desc_create_out_of_office": "Δημιουργία απουσίας",
"pbac_desc_view_out_of_office": "Προβολή απουσίας",
"pbac_desc_update_out_of_office": "Ενημέρωση απουσίας",
"pbac_desc_delete_out_of_office": "Διαγραφή απουσίας",
"pbac_desc_manage_workflows": "Πλήρης πρόσβαση διαχείρισης σε όλες τις ροές εργασίας",
"pbac_desc_create_event_types": "Δημιουργία τύπων εκδηλώσεων",
"pbac_desc_view_event_types": "Προβολή τύπων εκδηλώσεων",
@@ -3654,6 +3677,8 @@
"webhook_organizer_email": "Email του διοργανωτή",
"webhook_organizer_timezone": "Ζώνη ώρας του διοργανωτή (π.χ., 'America/New_York', 'Asia/Kolkata')",
"webhook_organizer_locale": "Τοπικές ρυθμίσεις του διοργανωτή (π.χ., 'en', 'fr')",
"webhook_organizer_username": "Όνομα χρήστη του διοργανωτή (π.χ., 'john.doe')",
"webhook_organizer_username_in_org": "Όνομα χρήστη του διοργανωτή στον οργανισμό του (π.χ., 'john.doe')",
"webhook_attendee_name": "Όνομα του πρώτου συμμετέχοντα",
"webhook_attendee_email": "Email του πρώτου συμμετέχοντα",
"webhook_attendee_timezone": "Ζώνη ώρας του πρώτου συμμετέχοντα",
@@ -3732,4 +3757,4 @@
"use_voice": "Χρήση φωνής",
"current_voice": "Τρέχουσα φωνή",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Προσθέστε τις νέες συμβολοσειρές σας πάνω από εδώ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -111,6 +111,7 @@
"no_available_users_found_error": "No available users found. Could you try another time slot?",
"timeslot_unavailable_book_a_new_time": "The selected time slot is no longer available. <0>Please select a new time</0>",
"timeslot_unavailable_short": "Taken",
"just_connected_description": "You’ve just connected. Please book from above slots or retry later.",
"unavailable_timeslot_title": "Unavailable timeslot",
"booking_time_out_of_bounds_error": "The event type cannot be booked at this time. Could you try another time slot?",
"request_body_end_time_internal_error": "Internal Error. Request body does not contain end time",
@@ -2152,6 +2153,9 @@
"delete_sso_configuration_confirmation_description": "Are you sure you want to delete the {{connectionType}} configuration? Your team members who use {{connectionType}} login will no longer be able to access Cal.com.",
"organizer_timezone": "Organizer timezone",
"email_user_cta": "View Invitation",
"accept_invite": "Accept Invite",
"join_now": "Join Now",
"successfully_joined": "Successfully joined",
"email_no_user_invite_heading_subteam": "You've been invited to join a team of {{parentTeamName}} organization",
"email_no_user_invite_heading_org": "You've been invited to join a {{appName}} organization",
"email_no_user_invite_subheading": "{{invitedBy}} has invited you to join their team on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.",
@@ -2382,6 +2386,7 @@
"invite_link_copied": "Invite link copied",
"invite_link_deleted": "Invite link deleted",
"api_key_deleted": "API Key deleted",
"delete_api_key_warning": "This will permanently delete the API key. Any applications using this key will immediately lose access to your account. This action cannot be undone.",
"invite_link_updated": "Invite link settings saved",
"link_expires_after": "Links set to expire after...",
"one_day": "1 day",
@@ -3459,6 +3464,7 @@
"roles_and_permissions_description": "Manage roles and permissions for your organization",
"learn_more_permissions": "Learn more about how Permissions work",
"role_name": "Role name",
"role_name_required": "Role name is required",
"role_name_placeholder": "e.g. Sales Team, Support Staff",
"role_description": "Role description",
"role_description_placeholder": "Describe what this role is for and what permissions it needs",
@@ -3497,7 +3503,7 @@
"role_created_successfully": "Role created successfully",
"role_updated_successfully": "Role updated successfully",
"delete_role": "Delete role",
"confirm_delete_role": "Are you sure you want to delete this role? This action cannot be undone.",
"confirm_delete_role": "Are you sure you want to delete role: {{roleName}}. This action cannot be undone.",
"role_deleted_successfully": "Role deleted successfully",
"error_creating_role": "Error creating role",
"error_updating_role": "Error updating role",
@@ -3671,6 +3677,8 @@
"webhook_organizer_email": "Email of the organizer",
"webhook_organizer_timezone": "Timezone of the organizer (e.g., 'America/New_York', 'Asia/Kolkata')",
"webhook_organizer_locale": "Locale of the organizer (e.g., 'en', 'fr')",
"webhook_organizer_username": "Username of the organizer (e.g., 'john.doe')",
"webhook_organizer_username_in_org": "Username of the organizer in their organization (e.g., 'john.doe')",
"webhook_attendee_name": "Name of the first attendee",
"webhook_attendee_email": "Email of the first attendee",
"webhook_attendee_timezone": "Timezone of the first attendee",
@@ -111,6 +111,7 @@
"no_available_users_found_error": "No se encontraron usuarios disponibles. ¿Podrías intentar en otro horario?",
"timeslot_unavailable_book_a_new_time": "El horario seleccionado ya no está disponible. <0>Por favor selecciona un nuevo horario</0>",
"timeslot_unavailable_short": "Ocupado",
"just_connected_description": "Acabas de conectarte. Por favor, reserva desde los espacios disponibles arriba o inténtalo más tarde.",
"unavailable_timeslot_title": "Horario no disponible",
"booking_time_out_of_bounds_error": "El evento no se puede agendar en este momento. ¿Podrías intentar con otro horario?",
"request_body_end_time_internal_error": "Error interno. El cuerpo de la solicitud no contiene la hora de finalización",
@@ -321,6 +322,7 @@
"secret_meeting": "Reunión Secreta",
"login_instead": "Inicia sesión en su lugar",
"create_account": "Crear cuenta",
"get_started": "Comenzar",
"confirm_password": "Confirmar contraseña",
"reset_your_password": "Establece tu nueva contraseña con las instrucciones enviadas a tu dirección de correo electrónico.",
"org_banner_instructions": "Por favor, sube una imagen de {{width}} de ancho y {{height}} de alto.",
@@ -958,7 +960,7 @@
"light_event_type_color": "Color del Tipo de Evento (Tema Claro)",
"dark_event_type_color": "Color del Tipo de Evento (Tema Oscuro)",
"file_not_named": "El archivo no está nombrado [idOrSlug]/[user]",
"create_team": "Crear Equipo",
"create_team": "Crear equipo",
"name": "Nombre",
"nameless_team": "Equipo sin nombre",
"oauth_clients": "Clientes OAuth",
@@ -1019,6 +1021,7 @@
"attendees": "Asistentes",
"add_attendees": "Agregar asistentes",
"show_advanced_settings": "Mostrar configuraciones avanzadas",
"hide_advanced_settings": "Ocultar configuración avanzada",
"event_name": "Nombre del evento",
"event_name_in_calendar": "Con nombre del evento",
"event_name_tooltip": "El nombre que aparecerá en los calendarios",
@@ -1329,7 +1332,7 @@
"make_team_private_description": "Los miembros de tu equipo no podrán ver a otros miembros del equipo cuando esto esté activado.",
"you_cannot_see_team_members": "No puedes ver a todos los miembros de un equipo privado.",
"you_cannot_see_teams_of_org": "No puedes ver los equipos de una organización privada.",
"allow_booker_to_select_duration": "Permitir al reservador seleccionar la duración",
"allow_multiple_durations": "Permitir múltiples duraciones",
"impersonate_user_tip": "Todos los usos de esta función son auditados.",
"impersonating_user_warning": "Suplantando al usuario \"{{user}}\".",
"impersonating_stop_instructions": "Haz clic aquí para detener",
@@ -2150,6 +2153,9 @@
"delete_sso_configuration_confirmation_description": "¿Estás seguro de que deseas eliminar la configuración de {{connectionType}}? Los miembros de tu equipo que usan el inicio de sesión de {{connectionType}} ya no podrán acceder a Cal.com.",
"organizer_timezone": "Zona horaria del organizador",
"email_user_cta": "Ver invitación",
"accept_invite": "Aceptar invitación",
"join_now": "Unirse ahora",
"successfully_joined": "Te has unido exitosamente",
"email_no_user_invite_heading_subteam": "Has sido invitado a unirte a un equipo de la organización {{parentTeamName}}",
"email_no_user_invite_heading_org": "Has sido invitado a unirte a una organización de {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} te ha invitado a unirte a su equipo en {{appName}}. {{appName}} es el programador de eventos que te permite a ti y a tu equipo agendar reuniones sin el ir y venir de correos electrónicos.",
@@ -2202,6 +2208,7 @@
"booking_confirmation_failed": "La confirmación de la reserva falló",
"not_enough_seats": "No hay suficientes asientos",
"form_builder_field_already_exists": "Ya existe un campo con este nombre",
"guests_field_must_be_multiemail": "El campo de invitados debe ser de tipo 'Múltiples correos electrónicos'",
"show_on_booking_page": "Mostrar en la página de reserva",
"visit_cancelled_booking": "Puedes visitar la página de la reserva cancelada",
"get_started_zapier_templates": "Comienza con las plantillas de Zapier",
@@ -2379,6 +2386,7 @@
"invite_link_copied": "Enlace de invitación copiado",
"invite_link_deleted": "Enlace de invitación eliminado",
"api_key_deleted": "Clave API eliminada",
"delete_api_key_warning": "Esto eliminará permanentemente la clave API. Cualquier aplicación que use esta clave perderá inmediatamente el acceso a tu cuenta. Esta acción no se puede deshacer.",
"invite_link_updated": "Configuración del enlace de invitación guardada",
"link_expires_after": "Los enlaces están configurados para expirar después de...",
"one_day": "1 día",
@@ -2688,7 +2696,7 @@
"enterprise": "Empresa",
"enterprise_description": "Actualiza a Enterprise para crear tu Organización",
"create_your_org": "Crea tu Organización",
"create_org": "Crear Organización",
"create_org": "Crear organización",
"create_your_org_description": "Actualiza a Organizaciones y recibe un subdominio, facturación unificada, Insights, etiquetado blanco extensivo y más",
"create_your_enterprise_description": "Actualiza a Enterprise y obtén acceso a la sincronización de Active Directory, aprovisionamiento automático de usuarios SCIM, agentes de voz Cal.ai, APIs de administración y más",
"other_payment_app_enabled": "Solo puedes habilitar una aplicación de pago por tipo de evento",
@@ -2922,6 +2930,7 @@
"proceeding_agreement": "Al continuar, aceptas nuestros",
"terms": "Términos",
"always_show_x_days": "Siempre {{x}} días disponibles",
"always_show_x_days_description": "Mueve automáticamente la ventana de reservas de {{x}} días hacia adelante cada día, asegurando que tu calendario siempre muestre los próximos {{x}} días de espacios disponibles.",
"team_subscription_cancelled_successfully": "Suscripción del equipo cancelada con éxito",
"team_subscription_cancellation_error": "Error al cancelar la suscripción del equipo",
"unable_to_subscribe_to_the_platform": "Ocurrió un error al intentar suscribirse al plan de la plataforma, por favor intenta de nuevo más tarde",
@@ -3143,7 +3152,8 @@
"filter_operator_ends_with": "Termina con",
"filter_operator_is_empty": "Está vacío",
"filter_operator_not_empty": "No está vacío",
"translate_description_button": "Traducir título/descripción al idioma del navegador del visitante usando IA",
"translate_description_button": "Traducir automáticamente título y descripción",
"translate_description_button_description": "Traduce automáticamente títulos y descripciones al idioma del navegador del visitante usando IA.",
"rr_distribution_method": "Distribución",
"rr_distribution_method_description": "Permite optimizar la distribución para maximizar la disponibilidad o lograr una asignación más equilibrada.",
"rr_distribution_method_availability_title": "Maximizar disponibilidad",
@@ -3309,6 +3319,7 @@
"seats_doesnt_support_recurring": "La función de asientos no admite eventos recurrentes. Desactiva la función de asientos para hacerlo recurrente.",
"recurring_event_seats_error": "Los eventos recurrentes no admiten la función de asientos. Desactiva la función de asientos o haz que el evento no sea recurrente.",
"segment": "Segmento",
"saved_filters": "Filtros guardados",
"rename": "Renombrar",
"rename_segment": "Renombrar segmento",
"delete_segment": "Eliminar segmento",
@@ -3338,6 +3349,7 @@
"picklist": "Lista de selección",
"most_cancelled_bookings": "Más cancelaciones",
"interface_language": "Idioma de la interfaz",
"interface_language_description": "Establece tu idioma preferido para la interfaz de reservas",
"visitors_browser_language": "Idioma del navegador del visitante",
"name_or_email": "Nombre o correo electrónico",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Si no se encuentra ningún contacto, recurrir al propietario del lead si existe",
@@ -3452,6 +3464,7 @@
"roles_and_permissions_description": "Administra roles y permisos para tu organización",
"learn_more_permissions": "Aprende más sobre cómo funcionan los permisos",
"role_name": "Nombre del rol",
"role_name_required": "El nombre del rol es obligatorio",
"role_name_placeholder": "p. ej. Equipo de ventas, Personal de soporte",
"role_description": "Descripción del rol",
"role_description_placeholder": "Describe para qué es este rol y qué permisos necesita",
@@ -3490,7 +3503,7 @@
"role_created_successfully": "Rol creado exitosamente",
"role_updated_successfully": "Rol actualizado exitosamente",
"delete_role": "Eliminar rol",
"confirm_delete_role": "¿Estás seguro de que quieres eliminar este rol? Esta acción no se puede deshacer.",
"confirm_delete_role": "¿Estás seguro de que quieres eliminar el rol: {{roleName}}? Esta acción no se puede deshacer.",
"role_deleted_successfully": "Rol eliminado exitosamente",
"error_creating_role": "Error al crear el rol",
"error_updating_role": "Error al actualizar el rol",
@@ -3509,10 +3522,20 @@
"pbac_desc_update_workflows": "Editar y modificar configuraciones de flujos de trabajo",
"pbac_desc_delete_workflows": "Eliminar flujos de trabajo del sistema",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Disponibilidad",
"pbac_resource_out_of_office": "Fuera de la oficina",
"pbac_desc_create_webhooks": "Crear webhooks",
"pbac_desc_view_webhooks": "Ver webhooks",
"pbac_desc_update_webhooks": "Actualizar webhooks",
"pbac_desc_delete_webhooks": "Eliminar webhooks",
"pbac_desc_create_availability": "Crear disponibilidad",
"pbac_desc_view_availability": "Ver disponibilidad",
"pbac_desc_update_availability": "Actualizar disponibilidad",
"pbac_desc_delete_availability": "Eliminar disponibilidad",
"pbac_desc_create_out_of_office": "Crear ausencia",
"pbac_desc_view_out_of_office": "Ver ausencia",
"pbac_desc_update_out_of_office": "Actualizar ausencia",
"pbac_desc_delete_out_of_office": "Eliminar ausencia",
"pbac_desc_manage_workflows": "Acceso completo de gestión a todos los flujos de trabajo",
"pbac_desc_create_event_types": "Crear tipos de eventos",
"pbac_desc_view_event_types": "Ver tipos de eventos",
@@ -3654,6 +3677,8 @@
"webhook_organizer_email": "Correo electrónico del organizador",
"webhook_organizer_timezone": "Zona horaria del organizador (por ejemplo, 'America/New_York', 'Asia/Kolkata')",
"webhook_organizer_locale": "Configuración regional del organizador (por ejemplo, 'en', 'fr')",
"webhook_organizer_username": "Nombre de usuario del organizador (p. ej., 'john.doe')",
"webhook_organizer_username_in_org": "Nombre de usuario del organizador en su organización (p. ej., 'john.doe')",
"webhook_attendee_name": "Nombre del primer asistente",
"webhook_attendee_email": "Correo electrónico del primer asistente",
"webhook_attendee_timezone": "Zona horaria del primer asistente",
@@ -3732,4 +3757,4 @@
"use_voice": "Usar voz",
"current_voice": "Voz actual",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agrega tus nuevas cadenas arriba de esta línea ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
+31 -6
View File
@@ -111,6 +111,7 @@
"no_available_users_found_error": "No se encontraron usuarios disponibles. ¿Podrías intentar en otro horario?",
"timeslot_unavailable_book_a_new_time": "El horario seleccionado ya no está disponible. <0>Por favor selecciona un nuevo horario</0>",
"timeslot_unavailable_short": "Ocupado",
"just_connected_description": "Acabas de conectarte. Por favor, reserva desde los espacios de arriba o inténtalo más tarde.",
"unavailable_timeslot_title": "Horario no disponible",
"booking_time_out_of_bounds_error": "Este tipo de evento no se puede reservar en este momento. ¿Podrías intentar con otro horario?",
"request_body_end_time_internal_error": "Error interno. El cuerpo de la solicitud no contiene la hora de finalización",
@@ -321,6 +322,7 @@
"secret_meeting": "Reunión Secreta",
"login_instead": "Iniciar Sesión en su lugar",
"create_account": "Crear Cuenta",
"get_started": "Comenzar",
"confirm_password": "Confirmar Contraseña",
"reset_your_password": "Establezca su nueva contraseña con las instrucciones enviadas a su dirección de correo electrónico.",
"org_banner_instructions": "Por favor, sube una imagen de {{width}} de ancho y {{height}} de alto.",
@@ -958,7 +960,7 @@
"light_event_type_color": "Color del tipo de evento (Tema claro)",
"dark_event_type_color": "Color del tipo de evento (Tema oscuro)",
"file_not_named": "El archivo no se llama [idOrSlug] / [user]",
"create_team": "Crear Equipo",
"create_team": "Crear equipo",
"name": "Nombre",
"nameless_team": "Equipo sin nombre",
"oauth_clients": "Clientes OAuth",
@@ -1019,6 +1021,7 @@
"attendees": "Asistentes",
"add_attendees": "Añadir Asistentes",
"show_advanced_settings": "Mostrar Opciones Avanzadas",
"hide_advanced_settings": "Ocultar ajustes avanzados",
"event_name": "Nombre del Evento",
"event_name_in_calendar": "Nombre del evento en el calendario",
"event_name_tooltip": "Nombre que aparecerá en los calendarios",
@@ -1329,7 +1332,7 @@
"make_team_private_description": "Los miembros de su equipo no podrán ver a otros miembros del equipo cuando esto esté activado.",
"you_cannot_see_team_members": "No puede ver a todos los miembros de un equipo privado.",
"you_cannot_see_teams_of_org": "No puedes ver los equipos de una organización privada.",
"allow_booker_to_select_duration": "Permitir que el encargado de las reservas seleccione la duración",
"allow_multiple_durations": "Permitir múltiples duraciones",
"impersonate_user_tip": "Todos los usos de esta función son revisados.",
"impersonating_user_warning": "Suplantando nombre de usuario \"{{user}}\".",
"impersonating_stop_instructions": "Haga clic aquí para detener.",
@@ -2150,6 +2153,9 @@
"delete_sso_configuration_confirmation_description": "¿Está seguro de que desea eliminar la configuración de {{connectionType}}? Los miembros de su equipo que utilicen el inicio de sesión {{connectionType}} ya no podrán acceder a Cal.com.",
"organizer_timezone": "Zona horaria del organizador",
"email_user_cta": "Ver la invitación",
"accept_invite": "Aceptar invitación",
"join_now": "Unirse ahora",
"successfully_joined": "Unido con éxito",
"email_no_user_invite_heading_subteam": "Has sido invitado a unirte a un equipo de la organización {{parentTeamName}}",
"email_no_user_invite_heading_org": "Has sido invitado a unirte a una organización de {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} lo invitó a unirse a su equipo en {{appName}}. {{appName}} es el programador de eventos que les permite a usted y a su equipo programar reuniones sin necesidad de enviar correos electrónicos.",
@@ -2202,6 +2208,7 @@
"booking_confirmation_failed": "Error al confirmar la reserva",
"not_enough_seats": "No hay suficientes plazas",
"form_builder_field_already_exists": "Ya existe un campo con este nombre",
"guests_field_must_be_multiemail": "El campo de invitados debe ser de tipo 'Múltiples correos electrónicos'",
"show_on_booking_page": "Mostrar en la página de reserva",
"visit_cancelled_booking": "Puedes visitar la página de la reserva cancelada",
"get_started_zapier_templates": "Comience con las plantillas de Zapier",
@@ -2379,6 +2386,7 @@
"invite_link_copied": "Enlace de invitación copiado",
"invite_link_deleted": "Enlace de invitación eliminado",
"api_key_deleted": "Clave API eliminada",
"delete_api_key_warning": "Esto eliminará permanentemente la clave API. Cualquier aplicación que use esta clave perderá inmediatamente el acceso a tu cuenta. Esta acción no se puede deshacer.",
"invite_link_updated": "Configuración de enlace de invitación guardada",
"link_expires_after": "Enlaces establecidos para expirar después de...",
"one_day": "1 día",
@@ -2688,7 +2696,7 @@
"enterprise": "Empresa",
"enterprise_description": "Actualiza a Enterprise para crear tu Organización",
"create_your_org": "Crea tu Organización",
"create_org": "Crear Organización",
"create_org": "Crear organización",
"create_your_org_description": "Actualiza a Organizaciones y recibe un subdominio, facturación unificada, Insights, personalización extensa y más",
"create_your_enterprise_description": "Actualiza a Enterprise y obtén acceso a la sincronización de Active Directory, aprovisionamiento automático de usuarios SCIM, agentes de voz de Cal.ai, APIs de administración y más",
"other_payment_app_enabled": "Solo puedes habilitar una aplicación de pago por tipo de evento",
@@ -2922,6 +2930,7 @@
"proceeding_agreement": "Al continuar, aceptas nuestros",
"terms": "Términos",
"always_show_x_days": "Siempre {{x}} días disponibles",
"always_show_x_days_description": "Mueve automáticamente la ventana de reservas de {{x}} días hacia adelante cada día, asegurando que tu calendario siempre muestre los próximos {{x}} días de espacios disponibles.",
"team_subscription_cancelled_successfully": "Suscripción del equipo cancelada con éxito",
"team_subscription_cancellation_error": "Error al cancelar la suscripción del equipo",
"unable_to_subscribe_to_the_platform": "Ocurrió un error al intentar suscribirse al plan de la plataforma, por favor intente de nuevo más tarde",
@@ -3143,7 +3152,8 @@
"filter_operator_ends_with": "Termina con",
"filter_operator_is_empty": "Está vacío",
"filter_operator_not_empty": "No está vacío",
"translate_description_button": "Traducir título/descripción al idioma del navegador del visitante usando IA",
"translate_description_button": "Auto traducir título y descripción",
"translate_description_button_description": "Traduce automáticamente títulos y descripciones al idioma del navegador del visitante usando IA.",
"rr_distribution_method": "Distribución",
"rr_distribution_method_description": "Permite optimizar la distribución para maximizar la disponibilidad o lograr una asignación más equilibrada.",
"rr_distribution_method_availability_title": "Maximizar disponibilidad",
@@ -3309,6 +3319,7 @@
"seats_doesnt_support_recurring": "La función de asientos no admite eventos recurrentes. Desactiva la función de asientos para hacerlo recurrente.",
"recurring_event_seats_error": "Los eventos recurrentes no admiten la función de asientos. Desactiva la función de asientos o haz que el evento no sea recurrente.",
"segment": "Segmento",
"saved_filters": "Filtros guardados",
"rename": "Renombrar",
"rename_segment": "Renombrar segmento",
"delete_segment": "Eliminar segmento",
@@ -3338,6 +3349,7 @@
"picklist": "Lista de selección",
"most_cancelled_bookings": "Más cancelaciones",
"interface_language": "Idioma de la interfaz",
"interface_language_description": "Establece tu idioma preferido para la interfaz de reservas",
"visitors_browser_language": "Idioma del navegador del visitante",
"name_or_email": "Nombre o correo electrónico",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Si no se encuentra ningún contacto, recurrir al propietario del lead si existe",
@@ -3452,6 +3464,7 @@
"roles_and_permissions_description": "Gestiona roles y permisos para tu organización",
"learn_more_permissions": "Aprende más sobre cómo funcionan los permisos",
"role_name": "Nombre del rol",
"role_name_required": "El nombre del rol es obligatorio",
"role_name_placeholder": "p. ej. Equipo de ventas, Personal de soporte",
"role_description": "Descripción del rol",
"role_description_placeholder": "Describe para qué sirve este rol y qué permisos necesita",
@@ -3490,7 +3503,7 @@
"role_created_successfully": "Rol creado con éxito",
"role_updated_successfully": "Rol actualizado con éxito",
"delete_role": "Eliminar rol",
"confirm_delete_role": "¿Está seguro de que desea eliminar este rol? Esta acción no se puede deshacer.",
"confirm_delete_role": "¿Estás seguro de que quieres eliminar el rol: {{roleName}}? Esta acción no se puede deshacer.",
"role_deleted_successfully": "Rol eliminado con éxito",
"error_creating_role": "Error al crear el rol",
"error_updating_role": "Error al actualizar el rol",
@@ -3509,10 +3522,20 @@
"pbac_desc_update_workflows": "Editar y modificar ajustes de flujos de trabajo",
"pbac_desc_delete_workflows": "Eliminar flujos de trabajo del sistema",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Disponibilidad",
"pbac_resource_out_of_office": "Fuera de la oficina",
"pbac_desc_create_webhooks": "Crear webhooks",
"pbac_desc_view_webhooks": "Ver webhooks",
"pbac_desc_update_webhooks": "Actualizar webhooks",
"pbac_desc_delete_webhooks": "Eliminar webhooks",
"pbac_desc_create_availability": "Crear disponibilidad",
"pbac_desc_view_availability": "Ver disponibilidad",
"pbac_desc_update_availability": "Actualizar disponibilidad",
"pbac_desc_delete_availability": "Eliminar disponibilidad",
"pbac_desc_create_out_of_office": "Crear fuera de la oficina",
"pbac_desc_view_out_of_office": "Ver fuera de la oficina",
"pbac_desc_update_out_of_office": "Actualizar fuera de la oficina",
"pbac_desc_delete_out_of_office": "Eliminar fuera de la oficina",
"pbac_desc_manage_workflows": "Acceso completo de gestión a todos los flujos de trabajo",
"pbac_desc_create_event_types": "Crear tipos de eventos",
"pbac_desc_view_event_types": "Ver tipos de eventos",
@@ -3654,6 +3677,8 @@
"webhook_organizer_email": "Correo electrónico del organizador",
"webhook_organizer_timezone": "Zona horaria del organizador (p. ej., 'America/New_York', 'Asia/Kolkata')",
"webhook_organizer_locale": "Configuración regional del organizador (p. ej., 'en', 'fr')",
"webhook_organizer_username": "Nombre de usuario del organizador (p. ej., 'john.doe')",
"webhook_organizer_username_in_org": "Nombre de usuario del organizador en su organización (p. ej., 'john.doe')",
"webhook_attendee_name": "Nombre del primer asistente",
"webhook_attendee_email": "Correo electrónico del primer asistente",
"webhook_attendee_timezone": "Zona horaria del primer asistente",
@@ -3732,4 +3757,4 @@
"use_voice": "Usar voz",
"current_voice": "Voz actual",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
+31 -6
View File
@@ -111,6 +111,7 @@
"no_available_users_found_error": "Saadaolevaid kasutajaid ei leitud. Kas saaksite proovida teist ajavahemikku?",
"timeslot_unavailable_book_a_new_time": "Valitud ajavahemik pole enam saadaval. <0>Palun vali uus aeg</0>",
"timeslot_unavailable_short": "Võetud",
"just_connected_description": "Olete just ühenduse loonud. Palun broneerige ülaltoodud ajavahemikest või proovige hiljem uuesti.",
"unavailable_timeslot_title": "Mittesaadaval ajaperiood",
"booking_time_out_of_bounds_error": "Sündmust ei saa sellel ajal broneerida. Kas proovid mõnda teist aega?",
"request_body_end_time_internal_error": "Sisemine viga. Taotluse sisu ei sisalda lõpuaega",
@@ -321,6 +322,7 @@
"secret_meeting": "Salajane koosolek",
"login_instead": "Selle asemel logi sisse",
"create_account": "Loo konto",
"get_started": "Alusta",
"confirm_password": "Kinnita salasõna",
"reset_your_password": "Seadke oma uus parool teie e-posti aadressile saadetud juhiste järgi.",
"org_banner_instructions": "Palun laadige üles pilt laiusega {{width}} ja kõrgusega {{height}}.",
@@ -1019,6 +1021,7 @@
"attendees": "Osalejad",
"add_attendees": "Lisa osalejaid",
"show_advanced_settings": "Kuva täpsemad seaded",
"hide_advanced_settings": "Peida täpsemad seaded",
"event_name": "Sündmuse nimi",
"event_name_in_calendar": "Sündmuse nimega",
"event_name_tooltip": "Kalendrites kuvatav nimi",
@@ -1231,7 +1234,7 @@
"contact_sales": "Võtke ühendust müügiga",
"error_404": "Viga 404",
"default": "Vaikimisi",
"set_to_default": "Set to Default",
"set_to_default": "Määra vaikimisi",
"new_schedule_btn": "Uus ajakava",
"add_new_schedule": "Lisa uus ajakava",
"add_new_calendar": "Lisa uus kalender",
@@ -1329,7 +1332,7 @@
"make_team_private_description": "Teie meeskonnaliikmed ei näe teisi meeskonnaliikmeid, kui see on sisse lülitatud.",
"you_cannot_see_team_members": "Te ei saa näha erameeskonna kõiki meeskonnaliikmeid.",
"you_cannot_see_teams_of_org": "Te ei saa näha eraorganisatsiooni meeskondi.",
"allow_booker_to_select_duration": "Luba broneerijal valida kestus",
"allow_multiple_durations": "Luba mitu kestust",
"impersonate_user_tip": "Kõik selle funktsiooni kasutused on auditeeritud.",
"impersonating_user_warning": "Esitan end kasutajanimena \"{{user}}\".",
"impersonating_stop_instructions": "Peatamiseks klõpsake siin",
@@ -2150,6 +2153,9 @@
"delete_sso_configuration_confirmation_description": "Kas soovite kindlasti konfiguratsiooni {{connectionType}} kustutada? Teie meeskonnaliikmed, kes kasutavad {{connectionType}} sisselogimist, ei pääse enam saidile Cal.com juurde.",
"organizer_timezone": "Korraldaja ajavöönd",
"email_user_cta": "Vaata kutset",
"accept_invite": "Võta kutse vastu",
"join_now": "Liitu kohe",
"successfully_joined": "Liitumine õnnestus",
"email_no_user_invite_heading_subteam": "Teid on kutsutud liituma {{parentTeamName}} organisatsiooni meeskonnaga.",
"email_no_user_invite_heading_org": "Teid on kutsutud liituma {{appName}} organisatsiooniga.",
"email_no_user_invite_subheading": "{{invitedBy}} kutsus teid liituma oma meeskonnaga rakenduses {{appName}}. {{appName}} on sündmustega žongleerimise ajakava, mis võimaldab teil ja teie meeskonnal koosolekuid ajastada ilma meilitenniseta.",
@@ -2202,6 +2208,7 @@
"booking_confirmation_failed": "Broneeringu kinnitamine ebaõnnestus",
"not_enough_seats": "Pole piisavalt kohti",
"form_builder_field_already_exists": "Selle nimega väli on juba olemas",
"guests_field_must_be_multiemail": "Külaliste väli peab olema tüüpi 'Mitu e-posti'",
"show_on_booking_page": "Näita broneerimislehel",
"visit_cancelled_booking": "Saate külastada tühistatud broneeringu lehte",
"get_started_zapier_templates": "Alustage Zapieri mallidega",
@@ -2379,6 +2386,7 @@
"invite_link_copied": "Kutse link kopeeritud",
"invite_link_deleted": "Kutse link kustutatud",
"api_key_deleted": "API võti kustutatud",
"delete_api_key_warning": "See kustutab API-võtme jäädavalt. Kõik rakendused, mis seda võtit kasutavad, kaotavad kohe juurdepääsu teie kontole. Seda toimingut ei saa tagasi võtta.",
"invite_link_updated": "Kutse lingi seaded on salvestatud",
"link_expires_after": "Lingid aeguma pärast...",
"one_day": "1 päev",
@@ -2922,6 +2930,7 @@
"proceeding_agreement": "Jätkates nõustute meie",
"terms": "Tingimustega",
"always_show_x_days": "Alati {{x}} päeva saadaval",
"always_show_x_days_description": "Liigutab automaatselt {{x}}-päevase broneerimisakna iga päev edasi, tagades, et teie kalender näitab alati järgmise {{x}} päeva saadavalolevaid aegu.",
"team_subscription_cancelled_successfully": "Meeskonna tellimus tühistati edukalt",
"team_subscription_cancellation_error": "Viga meeskonna tellimuse tühistamisel",
"unable_to_subscribe_to_the_platform": "Platvormipaketi tellimisel ilmnes viga, proovige hiljem uuesti",
@@ -3143,7 +3152,8 @@
"filter_operator_ends_with": "Lõpeb",
"filter_operator_is_empty": "On tühi",
"filter_operator_not_empty": "Ei ole tühi",
"translate_description_button": "Tõlgi pealkiri/kirjeldus tehisintellekti abil külastaja brauseri keelde",
"translate_description_button": "Tõlgi pealkiri ja kirjeldus automaatselt",
"translate_description_button_description": "Tõlgib pealkirjad ja kirjeldused automaatselt külastaja brauseri keelde, kasutades tehisintellekti.",
"rr_distribution_method": "Jaotamine",
"rr_distribution_method_description": "Võimaldab optimeerida jaotust maksimaalseks kättesaadavuseks või tasakaalustatumaks määramiseks.",
"rr_distribution_method_availability_title": "Maksimeeri kättesaadavus",
@@ -3309,6 +3319,7 @@
"seats_doesnt_support_recurring": "Kohtade funktsioon ei toeta korduvat sündmust. Keelake kohtade funktsioon, et muuta see korduvaks.",
"recurring_event_seats_error": "Korduv sündmus ei toeta kohtade funktsiooni. Keelake kohtade funktsioon või muutke sündmus mittekorduvaks.",
"segment": "Segment",
"saved_filters": "Salvestatud filtrid",
"rename": "Nimeta ümber",
"rename_segment": "Nimeta segment ümber",
"delete_segment": "Kustuta segment",
@@ -3338,6 +3349,7 @@
"picklist": "Valikloend",
"most_cancelled_bookings": "Kõige rohkem tühistatud broneeringuid",
"interface_language": "Liidese keel",
"interface_language_description": "Määra broneerimisliidese eelistatud keel",
"visitors_browser_language": "Külastaja brauseri keel",
"name_or_email": "Nimi või e-post",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Kui kontakti ei leita, kasuta võimalusel liidi omanikku",
@@ -3452,6 +3464,7 @@
"roles_and_permissions_description": "Halda oma organisatsiooni rolle ja õigusi",
"learn_more_permissions": "Lisateave õiguste toimimise kohta",
"role_name": "Rolli nimi",
"role_name_required": "Rolli nimi on kohustuslik",
"role_name_placeholder": "nt Müügimeeskond, Tugipersonal",
"role_description": "Rolli kirjeldus",
"role_description_placeholder": "Kirjeldage, milleks see roll on ja milliseid õigusi see vajab",
@@ -3490,7 +3503,7 @@
"role_created_successfully": "Roll loodud edukalt",
"role_updated_successfully": "Roll uuendatud edukalt",
"delete_role": "Kustuta roll",
"confirm_delete_role": "Kas olete kindel, et soovite selle rolli kustutada? Seda toimingut ei saa tagasi võtta.",
"confirm_delete_role": "Kas olete kindel, et soovite rolli kustutada: {{roleName}}? Seda toimingut ei saa tagasi võtta.",
"role_deleted_successfully": "Roll kustutatud edukalt",
"error_creating_role": "Viga rolli loomisel",
"error_updating_role": "Viga rolli uuendamisel",
@@ -3509,10 +3522,20 @@
"pbac_desc_update_workflows": "Muuda ja redigeeri töövoo seadeid",
"pbac_desc_delete_workflows": "Eemalda töövood süsteemist",
"pbac_resource_webhook": "Veebihaak",
"pbac_resource_availability": "Saadavus",
"pbac_resource_out_of_office": "Kontorist väljas",
"pbac_desc_create_webhooks": "Loo veebihaake",
"pbac_desc_view_webhooks": "Vaata veebihaake",
"pbac_desc_update_webhooks": "Uuenda veebihaake",
"pbac_desc_delete_webhooks": "Kustuta veebihaake",
"pbac_desc_create_availability": "Loo saadavus",
"pbac_desc_view_availability": "Vaata saadavust",
"pbac_desc_update_availability": "Uuenda saadavust",
"pbac_desc_delete_availability": "Kustuta saadavus",
"pbac_desc_create_out_of_office": "Loo kontorist väljas olek",
"pbac_desc_view_out_of_office": "Vaata kontorist väljas olekut",
"pbac_desc_update_out_of_office": "Uuenda kontorist väljas olekut",
"pbac_desc_delete_out_of_office": "Kustuta kontorist väljas olek",
"pbac_desc_manage_workflows": "Täielik haldusõigus kõikidele töövoogudele",
"pbac_desc_create_event_types": "Loo sündmuse tüüpe",
"pbac_desc_view_event_types": "Vaata sündmuse tüüpe",
@@ -3560,7 +3583,7 @@
"rr_load_balancing_disabled_with_groups": "Koormuse tasakaalustamine ei ole saadaval Round Robin gruppidega",
"round_robin_settings_description": "Kohanda selle meeskonna vaikimisi round robin'i seadeid",
"round_robin_settings_updated_successfully": "Round robin'i seaded on edukalt uuendatud",
"booking_creation_time": "Broneeringu loomise aeg",
"booking_creation_time": "Broneerimise loomise aeg",
"meeting_start_time": "Kohtumise algusaeg",
"members_affected_by_disabling_delegation_credential": "Mõjutatud liikmed",
"no_members_affected_by_disabling_delegation_credential": "Delegatsiooni mandaadi keelamine ei mõjuta ühtegi liiget",
@@ -3654,6 +3677,8 @@
"webhook_organizer_email": "Korraldaja e-post",
"webhook_organizer_timezone": "Korraldaja ajavöönd (nt 'America/New_York', 'Asia/Kolkata')",
"webhook_organizer_locale": "Korraldaja lokaad (nt 'en', 'fr')",
"webhook_organizer_username": "Korraldaja kasutajanimi (nt 'john.doe')",
"webhook_organizer_username_in_org": "Korraldaja kasutajanimi nende organisatsioonis (nt 'john.doe')",
"webhook_attendee_name": "Esimese osaleja nimi",
"webhook_attendee_email": "Esimese osaleja e-post",
"webhook_attendee_timezone": "Esimese osaleja ajavöönd",
@@ -3732,4 +3757,4 @@
"use_voice": "Kasuta häält",
"current_voice": "Praegune hääl",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
+32 -7
View File
@@ -111,6 +111,7 @@
"no_available_users_found_error": "Ez da erabiltzaile eskuragarririk aurkitu. Beste denbora tarte bat probatu nahi duzu?",
"timeslot_unavailable_book_a_new_time": "Hautatutako denbora-tartea ez dago jada eskuragarri. <0>Mesedez, hautatu beste ordu bat</0>",
"timeslot_unavailable_short": "Hartuta",
"just_connected_description": "Konektatu berri zara. Mesedez, erreserbatu goiko ordutegietatik edo saiatu berriro geroago.",
"unavailable_timeslot_title": "Denbora-tarte ez erabilgarria",
"booking_time_out_of_bounds_error": "Ezin da gertaera mota hau momentu honetan erreserbatu. Beste denbora-tarte bat probatuko zenuke?",
"request_body_end_time_internal_error": "Barne errorea. Eskaeraren gorputzak ez du amaiera ordua",
@@ -321,6 +322,7 @@
"secret_meeting": "Bilera sekretua",
"login_instead": "Saioa hasi ordez",
"create_account": "Sortu kontua",
"get_started": "Hasi",
"confirm_password": "Baieztatu pasahitza",
"reset_your_password": "Ezarri zure pasahitz berria zure email helbidera bidalitako argibideak jarraituz.",
"org_banner_instructions": "Mesedez, igo {{width}} zabalera eta {{height}} altuera duen irudia.",
@@ -1019,6 +1021,7 @@
"attendees": "Partaideak",
"add_attendees": "Gehitu partaideak",
"show_advanced_settings": "Erakutsi ezarpen aurreratuak",
"hide_advanced_settings": "Ezkutatu ezarpen aurreratuak",
"event_name": "Gertaeraren izena",
"event_name_in_calendar": "Gertaeraren izenarekin",
"event_name_tooltip": "Egutegian agertuko den izena",
@@ -1231,7 +1234,7 @@
"contact_sales": "Jarri harremanetan salmenta taldearekin",
"error_404": "404 errorea",
"default": "Lehenetsitakoa",
"set_to_default": "Ezarri lehenetsitako gisa",
"set_to_default": "Ezarri lehenetsi gisa",
"new_schedule_btn": "Programazio berria",
"add_new_schedule": "Gehitu programazio berria",
"add_new_calendar": "Gehitu egutegi berria",
@@ -1329,7 +1332,7 @@
"make_team_private_description": "Hau aktibatuta dagoenean, zure taldeko kideek ezin izango dituzte beste taldekideak ikusi.",
"you_cannot_see_team_members": "Ezin dituzu talde pribatu bateko kide guztiak ikusi.",
"you_cannot_see_teams_of_org": "Ezin dituzu erakunde pribatu bateko taldeak ikusi.",
"allow_booker_to_select_duration": "Erreserbatzaileari iraupena aukeratzen utzi",
"allow_multiple_durations": "Baimendu iraupen anitzak",
"impersonate_user_tip": "Ezaugarri honen erabilera guztiak auditatzen dira.",
"impersonating_user_warning": "\"{{user}}\" erabiltzailea ordezkatzen.",
"impersonating_stop_instructions": "Klikatu hemen gelditzeko",
@@ -2150,6 +2153,9 @@
"delete_sso_configuration_confirmation_description": "Ziur zaude {{connectionType}} konfigurazioa ezabatu nahi duzula? {{connectionType}} saioa hasten duten zure taldekideek ezingo dute Cal.com erabili.",
"organizer_timezone": "Antolatzailearen ordu-zona",
"email_user_cta": "Ikusi gonbidapena",
"accept_invite": "Onartu gonbidapena",
"join_now": "Batu orain",
"successfully_joined": "Arrakastaz batu zara",
"email_no_user_invite_heading_subteam": "{{parentTeamName}} erakundeko talde batera batzeko gonbidapena jaso duzu",
"email_no_user_invite_heading_org": "{{appName}} erakunde batera batzeko gonbidapena jaso duzu",
"email_no_user_invite_subheading": "{{invitedBy}}-k bere taldera gonbidatu zaitu {{appName}}-en. {{appName}} bileren antolaketa errazten duen tresna da, zure taldeak bilerak antolatzeko aukera ematen duena posta elektroniko truke etengabea ekidinez.",
@@ -2202,6 +2208,7 @@
"booking_confirmation_failed": "Erreserba berrespena huts egin du",
"not_enough_seats": "Ez dago nahikoa eserleku",
"form_builder_field_already_exists": "Izen hori duen eremu bat existitzen da dagoeneko",
"guests_field_must_be_multiemail": "Gonbidatuen eremuak 'Helbide elektroniko anitzak' motakoa izan behar du",
"show_on_booking_page": "Erakutsi erreserba orrian",
"visit_cancelled_booking": "Bertan behera utzitako erreserba orria bisitatu dezakezu",
"get_started_zapier_templates": "Hasi Zapier txantiloiekin",
@@ -2379,6 +2386,7 @@
"invite_link_copied": "Gonbidapen-esteka kopiatuta",
"invite_link_deleted": "Gonbidapen-esteka ezabatuta",
"api_key_deleted": "API gakoa ezabatuta",
"delete_api_key_warning": "Honek API gakoa betiko ezabatuko du. Gako hau erabiltzen duten aplikazio guztiek zure konturako sarbidea galduko dute berehala. Ekintza hau ezin da desegin.",
"invite_link_updated": "Gonbidapen-estekaren ezarpenak gordeta",
"link_expires_after": "Estekak iraungitzeko denbora...",
"one_day": "Egun 1",
@@ -2922,6 +2930,7 @@
"proceeding_agreement": "Jarraituz gero, gure honekin ados zaudela adierazten duzu",
"terms": "Baldintzak",
"always_show_x_days": "Beti {{x}} egun erabilgarri",
"always_show_x_days_description": "Automatikoki mugitzen du {{x}} eguneko erreserba-leihoa egunero, zure egutegiak beti hurrengo {{x}} egunetako txanda erabilgarriak erakusten dituela ziurtatuz.",
"team_subscription_cancelled_successfully": "Taldearen harpidetza behar bezala ezeztatu da",
"team_subscription_cancellation_error": "Errorea gertatu da taldearen harpidetza ezeztatzean",
"unable_to_subscribe_to_the_platform": "Errore bat gertatu da plataformaren planera harpidetzen saiatzean, saiatu berriro geroago",
@@ -3143,7 +3152,8 @@
"filter_operator_ends_with": "Honekin amaitzen da",
"filter_operator_is_empty": "Hutsik dago",
"filter_operator_not_empty": "Ez dago hutsik",
"translate_description_button": "Bisitariaren nabigatzailearen hizkuntzara itzuli titulua/deskribapena AI erabiliz",
"translate_description_button": "Auto itzuli izenburua eta deskribapena",
"translate_description_button_description": "Automatikoki itzuli izenburuak eta deskribapenak bisitariaren nabigatzailearen hizkuntzara adimen artifizialaren bidez.",
"rr_distribution_method": "Banaketa",
"rr_distribution_method_description": "Banaketa optimizatzeko aukera ematen du, erabilgarritasun maximoa edo esleipen orekatuagoa lortzeko",
"rr_distribution_method_availability_title": "Erabilgarritasuna maximizatu",
@@ -3309,6 +3319,7 @@
"seats_doesnt_support_recurring": "Eserlekuen ezaugarriak ez du ekitaldi errepikakorrik onartzen. Desgaitu eserlekuen ezaugarria errepikakor egiteko.",
"recurring_event_seats_error": "Ekitaldi errepikakorrak ez du eserlekuen ezaugarria onartzen. Desgaitu eserlekuen ezaugarria edo egin ekitaldia ez-errepikakor.",
"segment": "Segmentua",
"saved_filters": "Gordetako iragazkiak",
"rename": "Berrizendatu",
"rename_segment": "Berrizendatu segmentua",
"delete_segment": "Ezabatu segmentua",
@@ -3338,6 +3349,7 @@
"picklist": "Hautaketa-zerrenda",
"most_cancelled_bookings": "Bertan behera utzitako erreserba gehien",
"interface_language": "Interfazearen hizkuntza",
"interface_language_description": "Ezarri zure hizkuntza hobetsia erreserba interfazerako",
"visitors_browser_language": "Bisitariaren nabigatzailearen hizkuntza",
"name_or_email": "Izena edo helbide elektronikoa",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Kontakturik aurkitzen ez bada, itzuli bezeroen arduradunera existitzen bada",
@@ -3452,6 +3464,7 @@
"roles_and_permissions_description": "Kudeatu zure erakundearen rolak eta baimenak",
"learn_more_permissions": "Ikasi gehiago baimenek nola funtzionatzen duten",
"role_name": "Rolaren izena",
"role_name_required": "Rolaren izena beharrezkoa da",
"role_name_placeholder": "adib. Salmenta taldea, Laguntza langileak",
"role_description": "Rolaren deskribapena",
"role_description_placeholder": "Deskribatu rol hau zertarako den eta zer baimen behar dituen",
@@ -3490,7 +3503,7 @@
"role_created_successfully": "Rola arrakastaz sortu da",
"role_updated_successfully": "Rola arrakastaz eguneratu da",
"delete_role": "Ezabatu rola",
"confirm_delete_role": "Ziur zaude rol hau ezabatu nahi duzula? Ekintza hau ezin da desegin.",
"confirm_delete_role": "Ziur zaude {{roleName}} rola ezabatu nahi duzula? Ekintza hau ezin da desegin.",
"role_deleted_successfully": "Rola arrakastaz ezabatu da",
"error_creating_role": "Errorea rola sortzerakoan",
"error_updating_role": "Errorea rola eguneratzerakoan",
@@ -3509,10 +3522,20 @@
"pbac_desc_update_workflows": "Editatu eta aldatu lan-fluxuen ezarpenak",
"pbac_desc_delete_workflows": "Kendu lan-fluxuak sistematik",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Erabilgarritasuna",
"pbac_resource_out_of_office": "Bulegotik kanpo",
"pbac_desc_create_webhooks": "Sortu webhook-ak",
"pbac_desc_view_webhooks": "Ikusi webhook-ak",
"pbac_desc_update_webhooks": "Webhookak eguneratu",
"pbac_desc_delete_webhooks": "Webhookak ezabatu",
"pbac_desc_create_availability": "Sortu erabilgarritasuna",
"pbac_desc_view_availability": "Ikusi erabilgarritasuna",
"pbac_desc_update_availability": "Eguneratu erabilgarritasuna",
"pbac_desc_delete_availability": "Ezabatu erabilgarritasuna",
"pbac_desc_create_out_of_office": "Sortu bulegotik kanpo",
"pbac_desc_view_out_of_office": "Ikusi bulegotik kanpo",
"pbac_desc_update_out_of_office": "Eguneratu bulegotik kanpo",
"pbac_desc_delete_out_of_office": "Ezabatu bulegotik kanpo",
"pbac_desc_manage_workflows": "Lan-fluxu guztien kudeaketa-sarbide osoa",
"pbac_desc_create_event_types": "Gertaera motak sortu",
"pbac_desc_view_event_types": "Gertaera motak ikusi",
@@ -3560,8 +3583,8 @@
"rr_load_balancing_disabled_with_groups": "Karga-orekatzea ez dago eskuragarri Round Robin taldeekin",
"round_robin_settings_description": "Pertsonalizatu talde honentzako txandakako ezarpen lehenetsiak",
"round_robin_settings_updated_successfully": "Txandakako ezarpenak ondo eguneratu dira",
"booking_creation_time": "Erreserba sortze-ordua",
"meeting_start_time": "Bilera hasiera-ordua",
"booking_creation_time": "Erreserba sortzeko ordua",
"meeting_start_time": "Bilera hasteko ordua",
"members_affected_by_disabling_delegation_credential": "Eragindako kideak",
"no_members_affected_by_disabling_delegation_credential": "Ez dago kiderik delegazio kredentzialak desgaitzeak eraginda",
"download_expense_log": "Deskargatu gastuen erregistroa",
@@ -3654,6 +3677,8 @@
"webhook_organizer_email": "Antolatzailearen helbide elektronikoa",
"webhook_organizer_timezone": "Antolatzailearen ordu-zona (adib., 'America/New_York', 'Asia/Kolkata')",
"webhook_organizer_locale": "Antolatzailearen lokala (adib., 'en', 'fr')",
"webhook_organizer_username": "Antolatzailearen erabiltzaile-izena (adib., 'john.doe')",
"webhook_organizer_username_in_org": "Antolatzailearen erabiltzaile-izena bere erakundean (adib., 'john.doe')",
"webhook_attendee_name": "Lehen parte-hartzailearen izena",
"webhook_attendee_email": "Lehen parte-hartzailearen helbide elektronikoa",
"webhook_attendee_timezone": "Lehen parte-hartzailearen ordu-zona",
@@ -3732,4 +3757,4 @@
"use_voice": "Erabili ahotsa",
"current_voice": "Uneko ahotsa",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Gehitu zure kate berriak honen gainean ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}

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