Compare commits

...
Author SHA1 Message Date
emrysal 630eee4fab chore: release v5.8.10
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-05 13:06:46 +00:00
Morgan 1ebf553181 refactor: checkBookingRequiresAuthentication with rescheduleUid (#24943) 2025-11-05 13:02:01 +00:00
Hariom Balhara e8a315ac4e Remove cal.cache query parameter to prevent stale calendar data (#24939)
- Remove cal.cache query parameter parsing from booking and slots flows
- Set shouldServeCache to false for all booking availability checks
- Remove _shouldServeCache from schemas and type definitions
- Clean up all references in platform atoms and API endpoints

This ensures fresh Google Calendar data is always fetched for conflict checking,
preventing bookings when there are actual calendar conflicts.
2025-11-05 13:01:22 +00:00
Udit TakkarandVolnei Munhoz c196ff1095 feat: link email to participant (requireEmailForGuests) (#24661)
* feat: link email to participatn

* fix: bugs

* refactor: improve code

* refactor: prevent repload

* chore: remove unued

* fix: type

* refactor

* fix: type

* feat: restrict host

* feat: type

* feat: tests

* fix: don't allow guest

* fix: merk guest

* fix: bugs

* fix: test

* fix: test

* refactor: feedback

* fix: tests

---------

Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
2025-11-05 11:15:41 +00:00
bf0fbe4888 feat: skip authentication check for reschedule bookings with validation (#24903)
* feat: skip authentication check for reschedule bookings with validation

- Skip checkBookingRequiresAuthentication when rescheduleUid is present
- Add validation to ensure rescheduleUid points to a real booking
- Verify booking status is ACCEPTED (upcoming)
- Verify booking uses the same event-type
- Throw appropriate errors for invalid reschedule attempts

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

* refactor: move reschedule validation logic into checkBookingRequiresAuthentication

- Refactor checkBookingRequiresAuthentication to accept optional rescheduleUid parameter
- Move reschedule booking validation logic inside the method
- Simplify createBooking method by removing duplicate validation code
- Maintain same validation logic: check booking exists, is ACCEPTED, and uses same event-type

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

* fix: allow PENDING bookings to be rescheduled

- Update status validation to allow both ACCEPTED and PENDING bookings
- Change error message to reflect both allowed statuses
- PENDING bookings can now be rescheduled without authentication check

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

* refactor: separate reschedule validation from auth check

- Extract validateRescheduleBooking method to handle reschedule-specific validation
- Keep checkBookingRequiresAuthentication strictly for auth checks
- Use conditional logic in createBooking: validate reschedule OR check auth
- Improves code clarity by separating concerns per lauris's feedback

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-05 11:07:43 +00:00
Syed Ali Shahbaz eb6418c0d5 Add enableOverflow prop to DialogContent (#24935) 2025-11-05 10:46:44 +00:00
Udit TakkarandCarinaWolli 2df2868b20 fix: cal ai webhook (#24368)
* fix: cal ai email

* fix: remove

* fix: org

* replace Cal AI with Cal.ai

* fix: use

* fix: feedback

* fix: types

* fix: types

* fix: types

* fix: tests

* Merge branch 'main' into fix/cal-ai-credits

* refactor: feedback

* refactor: imporvement

* fix: type

* refactor: feedback

* fix: tests

* fix: use pbac

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-11-05 13:31:55 +04:00
Anik Dhabal Babu 7f48c7fdad fix: Ignore userIds form filter segment if no permission (#24194)
* 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
2025-11-05 13:09:13 +05:30
Udit Takkar 2101051623 style: fix height of report button (#24918)
* style: fix height

* style: fix height
2025-11-05 07:35:58 +00:00
Alex van Andel a241cb0bfc hotfix: Bring back English translation for cal.ai banner (#24922) 2025-11-04 19:30:29 +00:00
Anik Dhabal Babu 5417012cec Revert "fix: sanitize username input to prevent hyperlinks (#24210)" (#24916)
This reverts commit 7d16da5c98.
2025-11-04 17:01:05 +00:00
Dhairyashil Shinde 8e818e0567 refactor: remove-zapier-setup-screen (#24652)
* refactor: remove-zapier-setup-screen

* remove setup page files, related config and update readme

* make zapier a redirecting app

* update readme

* add check for missing website

* add noopener,noreferrer for _blank links
2025-11-04 15:55:09 +00:00
Morgan 1fdd04215d fix: missing await in updateEventType old endpoint (#24911) 2025-11-04 15:15:19 +00:00
Dhairyashil Shinde 0bf0749b29 only custom template needs upgrade and free users cannot edit email title and body of existing templates (#24907) 2025-11-04 13:43:40 +00:00
Dhairyashil Shinde 4006143e3a refactor: remove cal ai tip (#24904) 2025-11-04 11:56:21 +00:00
c6ad767565 refactor: decouple @calcom/prisma from @calcom/features, @calcom/ee, and @calcom/lib (#24802)
* refactor: decouple @calcom/prisma from @calcom/features, @calcom/ee, and @calcom/lib

- Inline helper functions in zod-utils.ts (emailSchema, slugify, getValidRhfFieldName, isPasswordValid, intervalLimitsType, zodAttributesQueryValue)
- Update schema.prisma @zod.import comments to reference zod-utils instead of @calcom/lib
- Inline idempotency key generation in booking-idempotency-key extension using uuid v5
- Move usage-tracking extension to @calcom/ee/prisma-extensions/
- Remove usage-tracking extension from packages/prisma/index.ts
- Move Prisma DI module from @calcom/prisma to @calcom/features/di/modules/Prisma.ts
- Update 20 import paths in @calcom/features to use new Prisma DI module
- Remove @calcom/lib dependency from @calcom/prisma package.json
- Add uuid dependency to @calcom/prisma package.json

This reduces the dependency footprint of @calcom/prisma and breaks circular dependencies between packages.

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

* feat: add EE-specific Prisma DI module with usage tracking

Create packages/ee/di/modules/PrismaEE.ts to apply the usage-tracking extension in EE contexts. This module:
- Imports the base prisma client from @calcom/prisma
- Applies the usageTrackingExtention from @calcom/ee/prisma-extensions/usage-tracking
- Exports the same DI tokens as the base Prisma module for override in EE containers

This ensures usage tracking functionality is preserved in EE builds while keeping the base @calcom/prisma package free of EE dependencies.

Note: EE containers should load this module after the base Prisma module to override the bindings.
Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-04 06:51:13 -03:00
896f8dbd3d fix: align booking limit timezone between availability and validation (#24846)
* fix: align booking limit timezone between availability and validation

- Use eventType.schedule?.timeZone for booking limits in availability calculation
- Previously used user's timezone causing day boundary mismatch
- Add unit tests to verify timezone alignment
- Fixes issue where slots remain available after reaching booking limit

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

* Rename variable limitsTz to eventTimeZone

* update

* fix: correct timezone fallback to include user timezone without forcing UTC

- Changed fallback chain to: schedule → event → user → undefined
- Treats empty strings as missing timezones
- Does not force UTC when all timezones are missing
- Aligns with validation behavior when timezone is undefined
- Fixes getSchedule.test.ts failures

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

* Revert "fix: correct timezone fallback to include user timezone without forcing UTC"

This reverts commit 721e0bdfd7b08004e68df6bdf5b97b9bd6cf3d5c.

* test: remove complex unit tests with 'as any' casts

- Removed Booking Limits Timezone Alignment tests that required heavy mocking
- These tests relied on spying private methods and had many 'as any' casts
- Timezone alignment is already covered by integration tests in getSchedule.test.ts
- Kept BookingDateInPastError test which has no type issues

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

* address review

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-04 12:13:42 +05:30
emrysal f1db27c0ad chore: release v5.8.9
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-04 00:30:52 +00:00
Eunjae LeeandDevin AI 3a1e04bb42 refactor: extract booking actions dropdown to separate component (#24872)
* refactor: extract booking actions dropdown to separate component

- Created BookingActionsDropdown component with all dropdown actions and dialogs
- Component receives booking as prop and handles all action logic internally
- Removed duplicate dialog states and mutations from BookingListItem
- Fixed img tag to use Next.js Image component
- Maintains same functionality and UI while improving code reusability

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

* fix: restore cardCharged variable in BookingListItem

The cardCharged variable was accidentally removed during the refactoring
but is still needed for the actionContext in BookingListItem.tsx

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

* refactor: move BookingItemProps type to separate types.ts file

- Created types.ts to hold shared booking types
- Updated BookingListItem.tsx to import from types.ts
- Updated BookingActionsDropdown.tsx to import from types.ts
- Updated bookingActions.ts to import from types.ts
- Removed unused RouterInputs and RouterOutputs imports from BookingListItem.tsx
- Removes unwanted dependency chain where BookingActionsDropdown imported from BookingListItem

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

* remove unnecessary description

* extract states as a zustand store

* fix dialog issue

* fix type errors

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-04 00:25:48 +00:00
Alex van Andel ab37847ea1 chore: User->UUID as a required field, ready for use in foreign keys (#24881) 2025-11-04 00:20:52 +00:00
Dhairyashil Shinde 0a2ddf05ca refactor: remove cal ai banners (#24885) 2025-11-04 01:57:43 +05:30
Lauris Skraucis 04fc0f1d52 docs: event calVideoSettings not available for platform (#24879) 2025-11-03 18:53:40 +00:00
Amit Sharma b76c4c9dcc fix: alby booking page breaking (#24859)
* fix: alby booking page breaking

* remove logs
2025-11-03 14:35:20 -03:00
Vibhor PhalkeandKartik Saini 68b71bfb0a address linting errors (#24171)
Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
2025-11-03 17:29:24 +00:00
Eunjae Lee 763c568797 fix: existence check of searchParams in NavigationItem (#24877) 2025-11-03 16:42:31 +00:00
Rajiv Sahal 4659d08c07 chore: return back timezone of event owner (#24876) 2025-11-03 16:29:29 +00:00
Sachin Yadav b9c350cc45 fix: Recurring events set to "yearly" don't recur (#24361)
* #fix: Recurring events set to “yearly” don’t recur

* fix: handle yearly recurring events with explicit null check

* fix: use explicit null check for recurring event frequency
2025-11-03 16:20:15 +00:00
an_ifrah24 f63d705523 fix: url truncate properly in the input field on diff screen sizes (#24700) 2025-11-03 16:14:49 +00:00
Dhairyashil Shinde 2907c5e696 fix(ui): interface language toggle (#24371) 2025-11-03 21:20:34 +05:30
sean-brydon dcb47e31ed fix: address onboarding v3 teams redirectfrom onboarding (#24875)
* Address redirect in subdomain for teams v3

* Revert constants
2025-11-03 15:43:55 +00:00
Dhairyashil ShindeandAnik Dhabal Babu cd5b75fe2c fix: hide organizer email on cancellation page (#24616)
* fix: hide organizer email on cancellation page

* if cancelled by any host - hide the cancelled by

* Refactor organizer email logic and clean up code

Removed the isOrganizerEmail function and updated conditions for displaying the cancelledBy information.

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-11-03 14:18:38 +00:00
Steven Adger 97bdf837a9 Include setup_intent events in webhook (#24805)
The setup_intent webhook is needed for held payments / no-show fees to work. 

If it is not included, a booking gets stuck saying "Pending payment" and "Unconfirmed" despite successfully submitting the card info. After including it, the booking status will successfully change to "Card held".

See:
https://github.com/calcom/cal.com/blob/817e20f11e3a75ec0bc62535a77c618bd27eaaf4/packages/features/ee/payments/api/webhook.ts#L323

https://cal.com/blog/cal-com-s-held-payments-a-useful-tool-for-organizations-offering-free-consultatio
2025-11-03 14:16:06 +00:00
Dhairyashil Shinde 5663b2ae59 feat: disable form workflows for free plan, add upgrade badge, make reminder template free (#24839) 2025-11-03 14:08:38 +00:00
Chad RobinsonandPallav 02b57eb82e fix: Add refresh token to refresh-token request response (#24831)
* Add refresh token to refresh-token request response

This change addresses an issue reported in https://github.com/calcom/cal.com/issues/23939#issuecomment-3443480099 in which a new refresh token is not included in the OAuth2 refresh-token endpoint response.

* Address failing type checks

---------

Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-11-03 19:10:36 +05:30
sean-brydon b23dfa194e fix: video install step v3 (#24865)
* Add teams i18n

* Fix UI nits in invite flow

* chore: update redirect

* Update personal view settings

* Fix video redirect
2025-11-03 13:25:54 +00:00
Eunjae LeeandDevin AI 2484b6fad2 fix: filter deleted fields from routing form insights headers (#24830)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-03 12:57:37 +00:00
Hariom Balhara c154bb31b7 fix: Arrows visible when embed has hidden entire page (#24868) 2025-11-03 12:28:54 +00:00
emrysal 0cf4ebd544 chore: release v5.8.8
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-03 12:03:36 +00:00
Dhairyashil Shinde 805c4d4c09 fix: dont allow email exclusion bypass with capital letters (#24741)
* fix: dont allow email exclusion bypass with capital letters

* trim before lowercasing to filter out empty tokens

* update console message

* use only regex change to implement this

* use only regex change to implement this
2025-11-03 11:59:14 +00:00
sean-brydon 99330b3117 chore: fixes UI nits in onboarding v3 flow (#24863)
* Add teams i18n

* Fix UI nits in invite flow

* chore: update redirect

* Update personal view settings
2025-11-03 11:45:27 +00:00
sean-brydon cc90a62090 feat: add banner for upgrade when using company email (#24829)
* Add banner plus mail icon

* Update MailIcon.tsx

* Update profile-view.tsx

* Fix weird line
2025-11-03 11:16:34 +00:00
sean-brydon 79bcd6dc30 feat: welcome to organizations modal after creation (#24823)
## What does this PR do?

Adds a welcome modal for new organizations that appears after organization creation. The modal showcases key features of the Organizations plan and provides a better onboarding experience.

## Visual Demo (For contributors especially)

#### Image Demo:

![CleanShot 2025-10-31 at 12.19.17.gif](https://app.graphite.dev/user-attachments/assets/4f8c3286-9400-40e6-aeb4-8a012f604c64.gif)

## Mandatory Tasks (DO NOT REMOVE)

- [x] I have self-reviewed the code.
- [x] I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. N/A
- [x] I confirm automated tests are in place that prove my fix is effective or that my feature works.

## How should this be tested?

1. Create a new organization through either:
   - The onboarding flow
   - The settings/organizations/new page
   - The organization creation form

2. After successful creation and redirect, verify the welcome modal appears showing organization features.

3. Verify the modal can be closed by:
   - Clicking the "Continue" button
   - Clicking outside the modal
   - Pressing ESC key

4. Verify the modal doesn't reappear after being closed (query param and session storage should be cleared).

## Checklist

- I have read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- My code follows the style guidelines of this project
- I have commented my code, particularly in hard-to-understand areas
- I have checked if my changes generate no new warnings
2025-11-03 11:11:16 +00:00
chauhan_s c6daa61e5e fix: Maximum update depth exceeded error when entering the 6th digit of the email verification code. (#24857) 2025-11-03 11:07:20 +00:00
Udit TakkarandVolnei Munhoz b693b6898a feat: booking report table backend (#24794)
* feat: report table backend

* fix: types

* fix: types

* fix: bugs

* fix: type error

---------

Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
2025-11-03 10:44:09 +00:00
Abir Roy fb55bc2dcd fix(docker): correct build context for calcom-api (#24849) 2025-11-03 10:38:33 +00:00
Udit TakkarandVolnei Munhoz 71bd140fe4 fix: add event type validation (#24429)
* fix: add event type validation

* fix: use isinteger

* fix: use isinteger

* fix: use isNumber

* fix: use Error

---------

Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
2025-11-03 10:26:40 +00:00
Dhairyashil Shinde 4c5608147c feat: Add Preview buttons for every private link (#24623) 2025-11-03 08:17:31 +00:00
7d16da5c98 fix: sanitize username input to prevent hyperlinks (#24210)
* fix(username): sanitize input to prevent hyperlinks

* fix(username): sanitize input to prevent hyperlinks

* Implemented CodeRabbits comments

* clean username before saving and keep input consistent

* sanitize initial username on load to avoid mismatch with input

* remove comments

* remove comments

* Used existing slugify lib instead of creating new one

---------

Co-authored-by: Dhairyashil <dhairyashilshinde@xoxo-2.local>
Co-authored-by: Devanshu Sharma <devanshusharma658@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-03 13:16:51 +05:30
spandev 0ae6e92558 fix: round robin settings update only on change (#24853) 2025-11-03 11:54:00 +05:30
Volnei Munhoz 052a38e39b cleanup unused env vars (#24819) 2025-10-31 12:14:54 -03:00
226 changed files with 5019 additions and 1462 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@calcom/atoms": minor
---
Fix Booker atom verify email dialog infinite re-render when entering verification code
View File
+2
View File
@@ -0,0 +1,2 @@
undecided:
- calcom-monorepo
View File
+1 -1
View File
@@ -57,7 +57,7 @@ export const eventTypeSelect = {
},
},
owner: {
select: { username: true, id: true },
select: { username: true, id: true, timeZone: true },
},
children: {
select: {
@@ -205,7 +205,8 @@ export class BookingsController_2024_04_15 {
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId));
const { orgSlug, locationUrl } = body;
try {
await this.checkBookingRequiresAuthentication(req, body.eventTypeId);
await this.checkBookingRequiresAuthentication(req, body.eventTypeId, body.rescheduleUid);
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl, isEmbed);
const booking = await this.regularBookingService.createBooking({
bookingData: bookingRequest.body,
@@ -459,7 +460,25 @@ export class BookingsController_2024_04_15 {
return oAuthClientParams.platformClientId;
}
private async checkBookingRequiresAuthentication(req: Request, eventTypeId: number): Promise<void> {
private async isValidRescheduleBooking(rescheduleUid: string, eventTypeId: number): Promise<boolean> {
const { bookingInfo } = await getBookingInfo(rescheduleUid);
if (!bookingInfo) {
return false;
}
if (bookingInfo.status !== "ACCEPTED" && bookingInfo.status !== "PENDING") {
return false;
}
if (bookingInfo.eventTypeId !== eventTypeId) {
return false;
}
return true;
}
private async checkBookingRequiresAuthentication(
req: Request,
eventTypeId: number,
rescheduleUid?: string
): Promise<void> {
const eventType = await this.eventTypeRepository.findByIdIncludeHostsAndTeamMembers({
id: eventTypeId,
});
@@ -468,6 +487,17 @@ export class BookingsController_2024_04_15 {
return;
}
if (rescheduleUid) {
const isValidRescheduleBooking = await this.isValidRescheduleBooking(rescheduleUid, eventTypeId);
if (isValidRescheduleBooking) {
return;
} else {
throw new BadRequestException(
"Trying to reschedule an event-type which requires authentication but provided invalid rescheduleUid."
);
}
}
const userId = await this.getOwnerId(req);
if (!userId) {
@@ -478,8 +508,7 @@ export class BookingsController_2024_04_15 {
const isEventTypeOwner = eventType.userId === userId;
const isHost = eventType.hosts.some((host) => host.userId === userId);
const isTeamAdminOrOwner =
eventType.team?.members.some((member) => member.userId === userId) ?? false;
const isTeamAdminOrOwner = eventType.team?.members.some((member) => member.userId === userId) ?? false;
let isOrgAdminOrOwner = false;
if (eventType.team?.parentId) {
@@ -194,11 +194,6 @@ export class CreateBookingInput_2024_04_15 {
@ApiHideProperty()
skipContactOwner?: boolean;
@IsBoolean()
@IsOptional()
@ApiHideProperty()
_shouldServeCache?: boolean;
@IsBoolean()
@IsOptional()
@ApiHideProperty()
@@ -129,7 +129,7 @@ export class EventTypesService_2024_04_15 {
}
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput_2024_04_15, user: UserWithProfile) {
this.checkCanUpdateEventType(user.id, eventTypeId);
await this.checkCanUpdateEventType(user.id, eventTypeId);
const eventTypeUser = await this.getUserToUpdateEvent(user);
const bookingFields = [...(body.bookingFields || [])];
@@ -130,7 +130,7 @@ const Page = async ({ searchParams }: PageProps) => {
throw error;
}
console.error("Failed to create Cal AI workflow:", error);
console.error("Failed to create Cal.ai workflow:", error);
redirect("/workflows?error=failed-to-create-workflow");
}
};
@@ -50,7 +50,7 @@ async function handler(req: NextRequest) {
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
}
const payload: OAuthTokenPayload = {
const payloadAuthToken: OAuthTokenPayload = {
userId: decodedRefreshToken.userId,
teamId: decodedRefreshToken.teamId,
scope: decodedRefreshToken.scope,
@@ -58,11 +58,23 @@ async function handler(req: NextRequest) {
clientId: client_id,
};
const access_token = jwt.sign(payload, secretKey, {
const payloadRefreshToken: OAuthTokenPayload = {
userId: decodedRefreshToken.userId,
teamId: decodedRefreshToken.teamId,
scope: decodedRefreshToken.scope,
token_type: "Refresh Token",
clientId: client_id,
};
const access_token = jwt.sign(payloadAuthToken, secretKey, {
expiresIn: 1800, // 30 min
});
return NextResponse.json({ access_token }, { status: 200 });
const refresh_token = jwt.sign(payloadRefreshToken, secretKey, {
expiresIn: 30 * 24 * 60 * 60, // 30 days
});
return NextResponse.json({ access_token, refresh_token }, { status: 200 });
}
export const POST = defaultResponderForAppDir(handler);
+4 -5
View File
@@ -6,6 +6,7 @@ import type { NextRequest } from "next/server";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
import { bookingCancelWithCsrfSchema } from "@calcom/prisma/zod-utils";
import { validateCsrfToken } from "@calcom/web/lib/validateCsrfToken";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -17,13 +18,11 @@ async function handler(req: NextRequest) {
return NextResponse.json({ success: false, message: "Invalid JSON" }, { status: 400 });
}
const bookingData = bookingCancelWithCsrfSchema.parse(appDirRequestBody);
const cookieStore = await cookies();
const cookieToken = cookieStore.get("calcom.csrf_token")?.value;
if (!cookieToken || cookieToken !== bookingData.csrfToken) {
return NextResponse.json({ success: false, message: "Invalid CSRF token" }, { status: 403 });
const csrfError = await validateCsrfToken(bookingData.csrfToken);
if (csrfError) {
return csrfError;
}
cookieStore.delete("calcom.csrf_token");
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
+13
View File
@@ -8,6 +8,7 @@ import { Plan, SubscriptionStatus } from "@calcom/features/ee/billing/repository
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
import { InternalTeamBilling } from "@calcom/features/ee/billing/teams/internal-team-billing";
import stripe from "@calcom/features/ee/payments/server/stripe";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
@@ -20,6 +21,7 @@ const checkoutSessionMetadataSchema = z.object({
teamName: z.string(),
teamSlug: z.string(),
userId: z.string().transform(Number),
isOnboarding: z.string().optional(),
});
const generateRandomString = () => {
@@ -66,6 +68,7 @@ async function getHandler(req: NextRequest) {
teamName: checkoutSession?.metadata?.teamName ?? generateRandomString(),
teamSlug: checkoutSession?.metadata?.teamSlug ?? generateRandomString(),
userId: checkoutSession.metadata.userId,
isOnboarding: checkoutSession.metadata.isOnboarding,
};
const team = await prisma.team.create({
@@ -102,6 +105,16 @@ async function getHandler(req: NextRequest) {
});
}
// Check if this is from onboarding flow and redirect accordingly
const isOnboarding = checkoutSessionMetadata.isOnboarding === "true";
if (isOnboarding) {
// Redirect to event-types for onboarding flow
return NextResponse.redirect(new URL("/onboarding/personal/settings", WEBAPP_URL), {
status: 302,
});
}
// redirect to team screen
return NextResponse.redirect(
new URL(`/settings/teams/${team.id}/onboard-members?event=team_created`, req.nextUrl.origin),
@@ -0,0 +1,94 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { z } from "zod";
import {
generateGuestMeetingTokenFromOwnerMeetingToken,
updateMeetingTokenIfExpired,
} from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { getHostsAndGuests } from "@calcom/features/bookings/lib/getHostsAndGuests";
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
import { getCalVideoReference } from "@calcom/features/get-cal-video-reference";
import { VideoCallGuestRepository } from "@calcom/features/video-call-guest/repositories/VideoCallGuestRepository";
import prisma from "@calcom/prisma";
import { validateCsrfToken } from "@calcom/web/lib/validateCsrfToken";
const videoCallGuestWithCsrfSchema = z.object({
bookingUid: z.string(),
email: z.string().email(),
name: z.string().min(1),
csrfToken: z.string().length(64, "Invalid CSRF token"),
});
async function handler(req: NextRequest) {
let appDirRequestBody;
try {
appDirRequestBody = await req.json();
} catch {
return NextResponse.json({ success: false, message: "Invalid JSON" }, { status: 400 });
}
const guestData = videoCallGuestWithCsrfSchema.parse(appDirRequestBody);
const csrfError = await validateCsrfToken(guestData.csrfToken);
if (csrfError) {
return csrfError;
}
const bookingRepo = new BookingRepository(prisma);
const booking = await bookingRepo.findBookingIncludeCalVideoSettingsAndReferences({
bookingUid: guestData.bookingUid,
});
if (!booking) {
return NextResponse.json({ error: "Booking not found" }, { status: 404 });
}
const { hosts, guests } = getHostsAndGuests(booking);
const isHost = hosts.some((host) => host.email.toLowerCase() === guestData.email.trim().toLowerCase());
const isGuest = guests.some((guest) => guest.email.toLowerCase() === guestData.email.trim().toLowerCase());
if (isHost) {
return NextResponse.json({ error: "hosts_must_use_login" }, { status: 403 });
} else if (!isGuest) {
return NextResponse.json({ error: "invalid_guest_email" }, { status: 403 });
}
const videoCallGuestRepo = new VideoCallGuestRepository(prisma);
const guestSession = await videoCallGuestRepo.upsertVideoCallGuest({
bookingUid: guestData.bookingUid,
email: guestData.email,
name: guestData.name,
});
const videoReference = getCalVideoReference(booking.references);
if (!videoReference) {
return NextResponse.json({ error: "Video reference not found" }, { status: 404 });
}
const endTime = new Date(booking.endTime);
const fourteenDaysAfter = new Date(endTime.getTime() + 14 * 24 * 60 * 60 * 1000);
const epochTimeFourteenDaysAfter = Math.floor(fourteenDaysAfter.getTime() / 1000);
const videoReferencePassword = await updateMeetingTokenIfExpired({
bookingReferenceId: videoReference.id,
roomName: videoReference.uid,
meetingToken: videoReference.meetingPassword,
exp: epochTimeFourteenDaysAfter,
});
const guestMeetingPassword = await generateGuestMeetingTokenFromOwnerMeetingToken({
meetingToken: videoReferencePassword,
userId: guestSession.id,
});
return NextResponse.json({
guestSessionId: guestSession.id,
meetingPassword: guestMeetingPassword,
meetingUrl: videoReference.meetingUrl,
});
}
export const POST = defaultResponderForAppDir(handler);
@@ -3,6 +3,7 @@ import { Retell } from "retell-sdk";
import { describe, it, expect, vi, beforeEach } from "vitest";
import type { CalAiPhoneNumber, User, Team, Agent } from "@calcom/prisma/client";
import { CreditUsageType } from "@calcom/prisma/enums";
import { POST } from "../route";
@@ -71,6 +72,8 @@ vi.mock("retell-sdk", () => ({
const mockHasAvailableCredits = vi.fn();
const mockChargeCredits = vi.fn();
const mockSendCreditBalanceLimitReachedEmails = vi.fn();
const mockSendCreditBalanceLowWarningEmails = vi.fn();
vi.mock("@calcom/features/ee/billing/credit-service", () => ({
CreditService: vi.fn().mockImplementation(() => ({
@@ -79,6 +82,12 @@ vi.mock("@calcom/features/ee/billing/credit-service", () => ({
})),
}));
vi.mock("@calcom/emails/email-manager", () => ({
sendCreditBalanceLimitReachedEmails: (...args: unknown[]) =>
mockSendCreditBalanceLimitReachedEmails(...args),
sendCreditBalanceLowWarningEmails: (...args: unknown[]) => mockSendCreditBalanceLowWarningEmails(...args),
}));
const mockFindByPhoneNumber = vi.fn();
const mockFindByProviderAgentId = vi.fn();
@@ -231,6 +240,7 @@ describe("Retell AI Webhook Handler", () => {
credits: 58, // 120 seconds = 2 minutes * $0.29 = $0.58 = 58 credits
callDuration: 120,
externalRef: "retell:test-call-id",
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
})
);
});
@@ -288,6 +298,7 @@ describe("Retell AI Webhook Handler", () => {
credits: 87, // 180 seconds = 3 minutes * $0.29 = $0.87 = 87 credits
callDuration: 180,
externalRef: "retell:test-call-id",
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
})
);
});
@@ -471,6 +482,7 @@ describe("Retell AI Webhook Handler", () => {
credits: expectedCredits,
callDuration: durationSeconds,
externalRef: expect.stringMatching(/^retell:test-call-/),
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
})
);
}
@@ -517,7 +529,12 @@ describe("Retell AI Webhook Handler", () => {
const response = await callPOST(createMockRequest(body, "valid-signature"));
expect(response.status).toBe(200);
expect(mockChargeCredits).toHaveBeenCalledWith(
expect.objectContaining({ userId: 42, credits: 61, callDuration: 125 }) // 125s = 2.083 minutes * $0.29 = $0.604 = 61 credits (rounded up)
expect.objectContaining({
userId: 42,
credits: 61,
callDuration: 125,
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
}) // 125s = 2.083 minutes * $0.29 = $0.604 = 61 credits (rounded up)
);
});
@@ -574,6 +591,7 @@ describe("Retell AI Webhook Handler", () => {
credits: 29, // 60 seconds = 1 minute * $0.29 = $0.29 = 29 credits
callDuration: 60,
externalRef: "retell:test-idempotency-call",
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
})
);
});
@@ -760,6 +778,7 @@ describe("Retell AI Webhook Handler", () => {
credits: 4, // 7 seconds = 0.117 minutes * $0.29 = $0.034 = 4 credits (rounded up)
callDuration: 7,
externalRef: "retell:call_bcd94f5a50832873a5fd68cb1aa",
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
})
);
});
@@ -794,6 +813,7 @@ describe("Retell AI Webhook Handler", () => {
teamId: 10,
credits: 29, // 60 seconds = 1 minute * $0.29 = 29 credits
callDuration: 60,
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
})
);
});
+5 -3
View File
@@ -63,6 +63,8 @@ const RetellWebhookSchema = z.object({
.passthrough(),
});
type RetellCallData = z.infer<typeof RetellWebhookSchema>["call"];
async function chargeCreditsForCall({
userId,
teamId,
@@ -120,7 +122,7 @@ async function chargeCreditsForCall({
}
}
async function handleCallAnalyzed(callData: any) {
async function handleCallAnalyzed(callData: RetellCallData) {
const { from_number, call_id, call_cost, call_type, agent_id } = callData;
if (
@@ -165,7 +167,7 @@ async function handleCallAnalyzed(callData: any) {
}
userId = agent.userId ?? undefined;
teamId = agent.teamId ?? undefined;
teamId = agent.team?.parentId ?? agent.teamId ?? undefined;
log.info(`Processing web call ${call_id} for agent ${agent_id}, user ${userId}, team ${teamId}`);
} else {
@@ -181,7 +183,7 @@ async function handleCallAnalyzed(callData: any) {
}
userId = phoneNumber.userId ?? undefined;
teamId = phoneNumber.teamId ?? undefined;
teamId = phoneNumber.team?.parentId ?? phoneNumber.teamId ?? undefined;
log.info(`Processing phone call ${call_id} from ${from_number}, user ${userId}, team ${teamId}`);
}
+12 -9
View File
@@ -125,16 +125,19 @@ export default async function RootLayout({ children }: { children: React.ReactNo
style={
isEmbed
? {
background: "transparent",
// Keep the embed hidden till parent initializes and
// - gives it the appropriate styles if UI instruction is there.
// - gives iframe the appropriate height(equal to document height) which can only be known after loading the page once in browser.
// - Tells iframe which mode it should be in (dark/light) - if there is a a UI instruction for that
visibility: "hidden",
}
background: "transparent",
// Keep the embed hidden till parent initializes and
// - gives it the appropriate styles if UI instruction is there.
// - gives iframe the appropriate height(equal to document height) which can only be known after loading the page once in browser.
// - Tells iframe which mode it should be in (dark/light) - if there is a a UI instruction for that
visibility: "hidden",
// This in addition to visibility: hidden is to ensure that elements with specific opacity set are not visible
opacity: 0,
}
: {
visibility: "visible",
}
visibility: "visible",
opacity: 1,
}
}>
<IconSprites />
<SpeculationRules
+2 -2
View File
@@ -115,8 +115,8 @@ export const AppPage = ({
const handleAppInstall = () => {
if (isRedirectApp(slug)) {
setIsLoading(true);
mutation.mutate({ type, variant, slug });
// For redirect apps, open the external URL directly
if (website) window.open(website, "_blank", "noopener,noreferrer");
return;
}
setIsLoading(true);
@@ -10,7 +10,6 @@ export const AppSetupMap = {
"exchange2016-calendar": dynamic(() => import("@calcom/web/components/apps/exchange2016calendar/Setup")),
"caldav-calendar": dynamic(() => import("@calcom/web/components/apps/caldavcalendar/Setup")),
"ics-feed": dynamic(() => import("@calcom/web/components/apps/ics-feedcalendar/Setup")),
zapier: dynamic(() => import("@calcom/web/components/apps/zapier/Setup")),
make: dynamic(() => import("@calcom/web/components/apps/make/Setup")),
sendgrid: dynamic(() => import("@calcom/web/components/apps/sendgrid/Setup")),
stripe: dynamic(() => import("@calcom/web/components/apps/stripepayment/Setup")),
@@ -90,7 +90,7 @@ export const AlbyPaymentComponent = (props: IAlbyPaymentComponentProps) => {
<p className="text-sm">Click or scan the invoice below to pay</p>
<Link
href={`lightning:${paymentRequest}`}
className="inline-flex items-center justify-center rounded-2xl rounded-md border border-transparent bg-white p-2 font-medium text-black shadow-sm hover:brightness-95 focus:outline-none focus:ring-offset-2">
className="inline-flex items-center justify-center rounded-2xl border border-transparent bg-white p-2 font-medium text-black shadow-sm hover:brightness-95 focus:outline-none focus:ring-offset-2">
<QRCode size={192} value={paymentRequest} />
</Link>
-179
View File
@@ -1,179 +0,0 @@
import Link from "next/link";
import { useState } from "react";
import { Toaster } from "sonner";
import AppNotInstalledMessage from "@calcom/app-store/_components/AppNotInstalledMessage";
import ServerTrans from "@calcom/lib/components/ServerTrans";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
import { Tooltip } from "@calcom/ui/components/tooltip";
export interface IZapierSetupProps {
inviteLink?: string;
}
const ZAPIER = "zapier";
export default function ZapierSetup(props: IZapierSetupProps) {
const [newApiKeys, setNewApiKeys] = useState<Record<string, string>>({});
const { t } = useLocale();
const utils = trpc.useUtils();
const integrations = trpc.viewer.apps.integrations.useQuery({ variant: "automation" });
const oldApiKey = trpc.viewer.apiKeys.findKeyOfType.useQuery({ appId: ZAPIER });
const teamsList = trpc.viewer.teams.listOwnedTeams.useQuery(undefined, {
refetchOnWindowFocus: false,
});
const teams = teamsList.data?.map((team) => ({ id: team.id, name: team.name }));
const deleteApiKey = trpc.viewer.apiKeys.delete.useMutation({
onSuccess: () => {
utils.viewer.apiKeys.findKeyOfType.invalidate();
},
});
const zapierCredentials: { userCredentialIds: number[] } | undefined = integrations.data?.items.find(
(item: { type: string }) => item.type === "zapier_automation"
);
const [credentialId] = zapierCredentials?.userCredentialIds || [false];
const showContent = integrations.data && integrations.isSuccess && credentialId;
const isCalDev = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.dev";
async function createApiKey(teamId?: number) {
const event = { note: "Zapier", expiresAt: null, appId: ZAPIER, teamId };
const apiKey = await utils.client.viewer.apiKeys.create.mutate(event);
if (oldApiKey.data) {
const oldKey = teamId
? oldApiKey.data.find((key) => key.teamId === teamId)
: oldApiKey.data.find((key) => !key.teamId);
if (oldKey) {
deleteApiKey.mutate({
id: oldKey.id,
});
}
}
return apiKey;
}
async function generateApiKey(teamId?: number) {
const apiKey = await createApiKey(teamId);
setNewApiKeys({ ...newApiKeys, [teamId || ""]: apiKey });
}
if (integrations.isPending) {
return <div className="bg-emphasis absolute z-50 flex h-screen w-full items-center" />;
}
return (
<div className="bg-emphasis flex h-screen">
{showContent ? (
<div className="bg-default m-auto max-w-[43em] overflow-auto rounded pb-10 md:p-10">
<div className="md:flex md:flex-row">
<div className="invisible md:visible">
<img className="h-11" src="/api/app-store/zapier/icon.svg" alt="Zapier Logo" />
</div>
<div className="ml-2 ltr:mr-2 rtl:ml-2 md:ml-5">
<div className="text-default">{t("setting_up_zapier")}</div>
<>
<div className="mt-1 text-xl">{t("generate_api_key")}:</div>
{!teams ? (
<Button color="secondary" onClick={() => createApiKey()} className="mb-4 mt-2">
{t("generate_api_key")}
</Button>
) : (
<>
<div className="mt-8 text-sm font-semibold">Your event types:</div>
{!newApiKeys[""] ? (
<Button color="secondary" onClick={() => generateApiKey()} className="mb-4 mt-2">
{t("generate_api_key")}
</Button>
) : (
<CopyApiKey apiKey={newApiKeys[""]} />
)}
{teams.map((team) => {
return (
<div key={team.name}>
<div className="mt-2 text-sm font-semibold">{team.name}:</div>
{!newApiKeys[team.id] ? (
<Button
color="secondary"
onClick={() => generateApiKey(team.id)}
className="mb-4 mt-2">
{t("generate_api_key")}
</Button>
) : (
<CopyApiKey apiKey={newApiKeys[team.id]} />
)}
</div>
);
})}
</>
)}
</>
<ol className="mb-5 ml-5 mt-5 list-decimal ltr:mr-5 rtl:ml-5">
{isCalDev && (
<li>
{t("go_to")}
<a href={props.inviteLink} className="text-orange-600 underline">
{t("zapier_invite_link")}
</a>
</li>
)}
<ServerTrans
t={t}
i18nKey="zapier_setup_instructions"
components={[
<li key="instruction_1">Log into your Zapier account and create a new Zap.</li>,
<li key="instruction_2">
Select Cal.com as your Trigger app. Also choose a Trigger event.
</li>,
<li key="instruction_3">Choose your account and then enter your Unique API Key.</li>,
<li key="instruction_4">Test your Trigger.</li>,
<li key="instruction_5">You&apos;re set!</li>,
]}
/>
</ol>
<Link href="/apps/installed/automation?hl=zapier" passHref={true} legacyBehavior>
<Button color="secondary">{t("done")}</Button>
</Link>
</div>
</div>
</div>
) : (
<AppNotInstalledMessage appName="zapier" />
)}
<Toaster position="bottom-right" />
</div>
);
}
const CopyApiKey = ({ apiKey }: { apiKey: string }) => {
const { t } = useLocale();
return (
<div>
<div className="my-2 mt-3 flex-wrap sm:flex sm:flex-nowrap">
<code className="bg-subtle h-full w-full whitespace-pre-wrap rounded-md py-[6px] pl-2 pr-2 sm:rounded-r-none sm:pr-5">
{apiKey}
</code>
<Tooltip side="top" content={t("copy_to_clipboard")}>
<Button
onClick={() => {
navigator.clipboard.writeText(apiKey);
showToast(t("api_key_copied"), "success");
}}
type="button"
className="mt-4 text-base sm:mt-0 sm:rounded-l-none">
<Icon name="clipboard" className="h-4 w-4 ltr:mr-2 rtl:ml-2" />
{t("copy")}
</Button>
</Tooltip>
</div>
<div className="text-subtle mb-5 mt-2 text-sm">{t("copy_somewhere_safe")}</div>
</div>
);
};
@@ -0,0 +1,593 @@
import { useState } from "react";
import type { z } from "zod";
import { Dialog } from "@calcom/features/components/controlled-dialog";
import { MeetingSessionDetailsDialog } from "@calcom/features/ee/video/MeetingSessionDetailsDialog";
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { DialogContent, DialogFooter, DialogClose } from "@calcom/ui/components/dialog";
import {
Dropdown,
DropdownItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuPortal,
} from "@calcom/ui/components/dropdown";
import { TextAreaField } from "@calcom/ui/components/form";
import type { ActionType } from "@calcom/ui/components/table";
import { showToast } from "@calcom/ui/components/toast";
import { AddGuestsDialog } from "@components/dialog/AddGuestsDialog";
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { ReassignDialog } from "@components/dialog/ReassignDialog";
import { ReportBookingDialog } from "@components/dialog/ReportBookingDialog";
import { RerouteDialog } from "@components/dialog/RerouteDialog";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
import { useBookingActionsStoreContext } from "./BookingActionsStoreProvider";
import {
getCancelEventAction,
getEditEventActions,
getAfterEventActions,
getReportAction,
shouldShowEditActions,
type BookingActionContext,
} from "./bookingActions";
import type { BookingItemProps } from "./types";
interface BookingActionsDropdownProps {
booking: BookingItemProps;
}
export function BookingActionsDropdown({ booking }: BookingActionsDropdownProps) {
const { t } = useLocale();
const utils = trpc.useUtils();
// Use store for all dialog states
const rejectionReason = useBookingActionsStoreContext((state) => state.rejectionReason);
const setRejectionReason = useBookingActionsStoreContext((state) => state.setRejectionReason);
const rejectionDialogIsOpen = useBookingActionsStoreContext((state) => state.rejectionDialogIsOpen);
const setRejectionDialogIsOpen = useBookingActionsStoreContext((state) => state.setRejectionDialogIsOpen);
const chargeCardDialogIsOpen = useBookingActionsStoreContext((state) => state.chargeCardDialogIsOpen);
const setChargeCardDialogIsOpen = useBookingActionsStoreContext((state) => state.setChargeCardDialogIsOpen);
const viewRecordingsDialogIsOpen = useBookingActionsStoreContext(
(state) => state.viewRecordingsDialogIsOpen
);
const setViewRecordingsDialogIsOpen = useBookingActionsStoreContext(
(state) => state.setViewRecordingsDialogIsOpen
);
const meetingSessionDetailsDialogIsOpen = useBookingActionsStoreContext(
(state) => state.meetingSessionDetailsDialogIsOpen
);
const setMeetingSessionDetailsDialogIsOpen = useBookingActionsStoreContext(
(state) => state.setMeetingSessionDetailsDialogIsOpen
);
const isNoShowDialogOpen = useBookingActionsStoreContext((state) => state.isNoShowDialogOpen);
const setIsNoShowDialogOpen = useBookingActionsStoreContext((state) => state.setIsNoShowDialogOpen);
const isOpenRescheduleDialog = useBookingActionsStoreContext((state) => state.isOpenRescheduleDialog);
const setIsOpenRescheduleDialog = useBookingActionsStoreContext((state) => state.setIsOpenRescheduleDialog);
const isOpenReassignDialog = useBookingActionsStoreContext((state) => state.isOpenReassignDialog);
const setIsOpenReassignDialog = useBookingActionsStoreContext((state) => state.setIsOpenReassignDialog);
const isOpenSetLocationDialog = useBookingActionsStoreContext((state) => state.isOpenSetLocationDialog);
const setIsOpenLocationDialog = useBookingActionsStoreContext((state) => state.setIsOpenLocationDialog);
const isOpenAddGuestsDialog = useBookingActionsStoreContext((state) => state.isOpenAddGuestsDialog);
const setIsOpenAddGuestsDialog = useBookingActionsStoreContext((state) => state.setIsOpenAddGuestsDialog);
const isOpenReportDialog = useBookingActionsStoreContext((state) => state.isOpenReportDialog);
const setIsOpenReportDialog = useBookingActionsStoreContext((state) => state.setIsOpenReportDialog);
const rerouteDialogIsOpen = useBookingActionsStoreContext((state) => state.rerouteDialogIsOpen);
const setRerouteDialogIsOpen = useBookingActionsStoreContext((state) => state.setRerouteDialogIsOpen);
const cardCharged = booking?.payment[0]?.success;
const attendeeList = booking.attendees.map((attendee) => {
return {
name: attendee.name,
email: attendee.email,
id: attendee.id,
noShow: attendee.noShow || false,
phoneNumber: attendee.phoneNumber,
};
});
const noShowMutation = trpc.viewer.loggedInViewerRouter.markNoShow.useMutation({
onSuccess: async (data) => {
showToast(data.message, "success");
await utils.viewer.bookings.invalidate();
},
onError: (err) => {
showToast(err.message, "error");
},
});
const mutation = trpc.viewer.bookings.confirm.useMutation({
onSuccess: (data) => {
if (data?.status === "REJECTED") {
setRejectionDialogIsOpen(false);
showToast(t("booking_rejection_success"), "success");
} else {
showToast(t("booking_confirmation_success"), "success");
}
utils.viewer.bookings.invalidate();
utils.viewer.me.bookingUnconfirmedCount.invalidate();
},
onError: () => {
showToast(t("booking_confirmation_failed"), "error");
utils.viewer.bookings.invalidate();
},
});
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
onSuccess: () => {
showToast(t("location_updated"), "success");
setIsOpenLocationDialog(false);
utils.viewer.bookings.invalidate();
},
onError: (e) => {
const errorMessages: Record<string, string> = {
UNAUTHORIZED: t("you_are_unauthorized_to_make_this_change_to_the_booking"),
BAD_REQUEST: e.message,
};
const message = errorMessages[e.data?.code as string] || t("location_update_failed");
showToast(message, "error");
},
});
const isUpcoming = new Date(booking.endTime) >= new Date();
const isOngoing = isUpcoming && new Date() >= new Date(booking.startTime);
const isBookingInPast = new Date(booking.endTime) < new Date();
const isCancelled = booking.status === "CANCELLED";
const isConfirmed = booking.status === "ACCEPTED";
const isRejected = booking.status === "REJECTED";
const isPending = booking.status === "PENDING";
const isRescheduled = booking.fromReschedule !== null;
const isRecurring = booking.recurringEventId !== null;
const getBookingStatus = (): "upcoming" | "past" | "cancelled" | "rejected" => {
if (isCancelled) return "cancelled";
if (isRejected) return "rejected";
if (isBookingInPast) return "past";
return "upcoming";
};
const isTabRecurring = booking.listingStatus === "recurring";
const isTabUnconfirmed = booking.listingStatus === "unconfirmed";
const isBookingFromRoutingForm = !!booking.routedFromRoutingFormReponse && !!booking.eventType?.team;
const userEmail = booking.loggedInUser.userEmail;
const userSeat = booking.seatsReferences.find((seat) => !!userEmail && seat.attendee?.email === userEmail);
const isAttendee = !!userSeat;
const isCalVideoLocation =
!booking.location ||
booking.location === "integrations:daily" ||
(typeof booking.location === "string" && booking.location.trim() === "");
const isDisabledCancelling = booking.eventType.disableCancelling;
const isDisabledRescheduling = booking.eventType.disableRescheduling;
const getSeatReferenceUid = () => {
return userSeat?.referenceUid;
};
const bookingConfirm = async (confirm: boolean) => {
let body = {
bookingId: booking.id,
confirmed: confirm,
reason: rejectionReason,
};
if ((isTabRecurring || isTabUnconfirmed) && isRecurring) {
body = Object.assign({}, body, { recurringEventId: booking.recurringEventId });
}
mutation.mutate(body);
};
const saveLocation = async ({
newLocation,
credentialId,
}: {
newLocation: string;
credentialId: number | null;
}) => {
try {
await setLocationMutation.mutateAsync({
bookingId: booking.id,
newLocation,
credentialId,
});
} catch {
// Errors are shown through the mutation onError handler
}
};
const actionContext: BookingActionContext = {
booking,
isUpcoming,
isOngoing,
isBookingInPast,
isCancelled,
isConfirmed,
isRejected,
isPending,
isRescheduled,
isRecurring,
isTabRecurring,
isTabUnconfirmed,
isBookingFromRoutingForm,
isDisabledCancelling,
isDisabledRescheduling,
isCalVideoLocation,
showPendingPayment: false, // This will be calculated below
isAttendee,
cardCharged,
attendeeList,
getSeatReferenceUid,
t,
} as BookingActionContext;
const cancelEventAction = getCancelEventAction(actionContext);
const baseEditEventActions = getEditEventActions(actionContext);
const editEventActions: ActionType[] = baseEditEventActions.map((action) => ({
...action,
onClick:
action.id === "reschedule_request"
? () => setIsOpenRescheduleDialog(true)
: action.id === "reroute"
? () => setRerouteDialogIsOpen(true)
: action.id === "change_location"
? () => setIsOpenLocationDialog(true)
: action.id === "add_members"
? () => setIsOpenAddGuestsDialog(true)
: action.id === "reassign"
? () => setIsOpenReassignDialog(true)
: undefined,
})) as ActionType[];
const baseAfterEventActions = getAfterEventActions(actionContext);
const afterEventActions: ActionType[] = baseAfterEventActions.map((action) => ({
...action,
onClick:
action.id === "view_recordings"
? () => setViewRecordingsDialogIsOpen(true)
: action.id === "meeting_session_details"
? () => setMeetingSessionDetailsDialogIsOpen(true)
: action.id === "charge_card"
? () => setChargeCardDialogIsOpen(true)
: action.id === "no_show"
? () => {
if (attendeeList.length === 1) {
const attendee = attendeeList[0];
noShowMutation.mutate({
bookingUid: booking.uid,
attendees: [{ email: attendee.email, noShow: !attendee.noShow }],
});
return;
}
setIsNoShowDialogOpen(true);
}
: undefined,
disabled:
action.disabled ||
(action.id === "no_show" && !(isBookingInPast || isOngoing)) ||
(action.id === "view_recordings" && !booking.isRecorded),
})) as ActionType[];
const reportAction = getReportAction(actionContext);
const reportActionWithHandler = {
...reportAction,
onClick: () => setIsOpenReportDialog(true),
};
const NoShowAttendeesDialog = ({
bookingUid,
attendees,
setIsOpen,
isOpen,
}: {
bookingUid: string;
attendees: Array<{
name: string;
email: string;
id: number;
noShow: boolean;
phoneNumber: string | null;
}>;
setIsOpen: (open: boolean) => void;
isOpen: boolean;
}) => {
const [noShowAttendees, setNoShowAttendees] = useState<
Array<{
email: string;
noShow: boolean;
}>
>(attendees.map((attendee) => ({ email: attendee.email, noShow: attendee.noShow })));
const noShowMutation = trpc.viewer.loggedInViewerRouter.markNoShow.useMutation({
onSuccess: async (data) => {
showToast(data.message, "success");
setIsOpen(false);
await utils.viewer.bookings.invalidate();
},
onError: (err) => {
showToast(err.message, "error");
},
});
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent title={t("mark_as_no_show")} enableOverflow>
<div className="space-y-2">
{attendees.map((attendee, index) => (
<label key={attendee.email} className="flex items-center space-x-2">
<input
type="checkbox"
checked={noShowAttendees[index]?.noShow || false}
onChange={(e) => {
const newNoShowAttendees = [...noShowAttendees];
newNoShowAttendees[index] = {
email: attendee.email,
noShow: e.target.checked,
};
setNoShowAttendees(newNoShowAttendees);
}}
/>
<span>
{attendee.name} ({attendee.email})
</span>
</label>
))}
</div>
<DialogFooter>
<DialogClose />
<Button
onClick={() => {
noShowMutation.mutate({
bookingUid,
attendees: noShowAttendees,
});
}}>
{t("confirm")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
const parsedBooking = {
...booking,
eventType: booking.eventType.id ? booking.eventType : null,
};
// Render dialogs that might be triggered from BookingListItem even if dropdown is not shown
const dialogs = (
<>
<RescheduleDialog
isOpenDialog={isOpenRescheduleDialog}
setIsOpenDialog={setIsOpenRescheduleDialog}
bookingUId={booking.uid}
/>
{isOpenReassignDialog && (
<ReassignDialog
isOpenDialog={isOpenReassignDialog}
setIsOpenDialog={setIsOpenReassignDialog}
bookingId={booking.id}
teamId={booking.eventType?.team?.id || 0}
bookingFromRoutingForm={isBookingFromRoutingForm}
/>
)}
<EditLocationDialog
booking={booking}
saveLocation={saveLocation}
isOpenDialog={isOpenSetLocationDialog}
setShowLocationModal={setIsOpenLocationDialog}
teamId={booking.eventType?.team?.id}
/>
<AddGuestsDialog
isOpenDialog={isOpenAddGuestsDialog}
setIsOpenDialog={setIsOpenAddGuestsDialog}
bookingId={booking.id}
/>
<ReportBookingDialog
isOpenDialog={isOpenReportDialog}
setIsOpenDialog={setIsOpenReportDialog}
bookingUid={booking.uid}
isRecurring={isRecurring}
status={getBookingStatus()}
/>
{booking.paid && booking.payment[0] && (
<ChargeCardDialog
isOpenDialog={chargeCardDialogIsOpen}
setIsOpenDialog={setChargeCardDialogIsOpen}
bookingId={booking.id}
paymentAmount={booking.payment[0].amount}
paymentCurrency={booking.payment[0].currency}
/>
)}
{isCalVideoLocation && (
<ViewRecordingsDialog
booking={booking}
isOpenDialog={viewRecordingsDialogIsOpen}
setIsOpenDialog={setViewRecordingsDialogIsOpen}
timeFormat={booking.loggedInUser.userTimeFormat ?? null}
/>
)}
{isCalVideoLocation && meetingSessionDetailsDialogIsOpen && (
<MeetingSessionDetailsDialog
booking={booking}
isOpenDialog={meetingSessionDetailsDialogIsOpen}
setIsOpenDialog={setMeetingSessionDetailsDialogIsOpen}
timeFormat={booking.loggedInUser.userTimeFormat ?? null}
/>
)}
{isNoShowDialogOpen && (
<NoShowAttendeesDialog
bookingUid={booking.uid}
attendees={attendeeList}
setIsOpen={setIsNoShowDialogOpen}
isOpen={isNoShowDialogOpen}
/>
)}
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
<DialogContent title={t("rejection_reason_title")} description={t("rejection_reason_description")}>
<div>
<TextAreaField
name="rejectionReason"
label={
<>
{t("rejection_reason")}
<span className="text-subtle font-normal"> (Optional)</span>
</>
}
value={rejectionReason}
onChange={(e) => setRejectionReason(e.target.value)}
/>
</div>
<DialogFooter>
<DialogClose />
<Button
disabled={mutation.isPending}
data-testid="rejection-confirm"
onClick={() => {
bookingConfirm(false);
}}>
{t("rejection_confirmation")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{isBookingFromRoutingForm &&
parsedBooking.eventType &&
parsedBooking.eventType.id !== undefined &&
parsedBooking.eventType.slug !== undefined &&
parsedBooking.eventType.title !== undefined &&
parsedBooking.routedFromRoutingFormReponse && (
<RerouteDialog
isOpenDialog={rerouteDialogIsOpen}
setIsOpenDialog={setRerouteDialogIsOpen}
booking={{
...parsedBooking,
metadata: parsedBooking.metadata as z.infer<typeof bookingMetadataSchema>,
routedFromRoutingFormReponse: parsedBooking.routedFromRoutingFormReponse,
eventType: {
length: parsedBooking.eventType.length ?? 0,
schedulingType: parsedBooking.eventType.schedulingType ?? null,
title: parsedBooking.eventType.title,
id: parsedBooking.eventType.id,
slug: parsedBooking.eventType.slug,
team: parsedBooking.eventType.team ?? null,
},
}}
/>
)}
</>
);
// Don't render dropdown if edit actions shouldn't be shown
if (!shouldShowEditActions(actionContext)) {
return dialogs;
}
return (
<>
{dialogs}
<Dropdown>
<DropdownMenuTrigger asChild>
<Button
type="button"
color="secondary"
variant="icon"
StartIcon="ellipsis"
data-testid="booking-actions-dropdown"
/>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent>
<DropdownMenuLabel className="px-2 pb-1 pt-1.5">{t("edit_event")}</DropdownMenuLabel>
{editEventActions.map((action) => (
<DropdownMenuItem className="rounded-lg" key={action.id} disabled={action.disabled}>
<DropdownItem
type="button"
color={action.color}
StartIcon={action.icon}
href={action.href}
disabled={action.disabled}
onClick={action.onClick}
data-bookingid={action.bookingId}
data-testid={action.id}
className={action.disabled ? "text-muted" : undefined}>
{action.label}
</DropdownItem>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuLabel className="px-2 pb-1 pt-1.5">{t("after_event")}</DropdownMenuLabel>
{afterEventActions.map((action) => (
<DropdownMenuItem className="rounded-lg" key={action.id} disabled={action.disabled}>
<DropdownItem
type="button"
color={action.color}
StartIcon={action.icon}
href={action.href}
onClick={action.onClick}
disabled={action.disabled}
data-bookingid={action.bookingId}
data-testid={action.id}
className={action.disabled ? "text-muted" : undefined}>
{action.label}
</DropdownItem>
</DropdownMenuItem>
))}
<>
<DropdownMenuSeparator />
<DropdownMenuItem
className="rounded-lg"
key={reportActionWithHandler.id}
disabled={reportActionWithHandler.disabled}>
<DropdownItem
type="button"
color={reportActionWithHandler.color}
StartIcon={reportActionWithHandler.icon}
onClick={reportActionWithHandler.onClick}
disabled={reportActionWithHandler.disabled}
data-testid={reportActionWithHandler.id}
className={reportActionWithHandler.disabled ? "text-muted" : undefined}>
{reportActionWithHandler.label}
</DropdownItem>
</DropdownMenuItem>
</>
<DropdownMenuSeparator />
<DropdownMenuItem
className="rounded-lg"
key={cancelEventAction.id}
disabled={cancelEventAction.disabled}>
<DropdownItem
type="button"
color={cancelEventAction.color}
StartIcon={cancelEventAction.icon}
href={cancelEventAction.disabled ? undefined : cancelEventAction.href}
onClick={cancelEventAction.onClick}
disabled={cancelEventAction.disabled}
data-bookingid={cancelEventAction.bookingId}
data-testid={cancelEventAction.id}
className={cancelEventAction.disabled ? "text-muted" : undefined}>
{cancelEventAction.label}
</DropdownItem>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenuPortal>
</Dropdown>
</>
);
}
@@ -0,0 +1,39 @@
"use client";
import { createContext, useContext, useRef, type ReactNode } from "react";
import { useStore } from "zustand";
import type { StoreApi } from "zustand";
import { createBookingActionsStore, type BookingActionsStore } from "./store";
export const BookingActionsStoreContext = createContext<StoreApi<BookingActionsStore> | null>(null);
export interface BookingActionsStoreProviderProps {
children: ReactNode;
}
export const BookingActionsStoreProvider = ({ children }: BookingActionsStoreProviderProps) => {
const storeRef = useRef<StoreApi<BookingActionsStore>>();
if (!storeRef.current) {
storeRef.current = createBookingActionsStore();
}
return (
<BookingActionsStoreContext.Provider value={storeRef.current}>
{children}
</BookingActionsStoreContext.Provider>
);
};
export const useBookingActionsStoreContext = <T,>(
selector: (store: BookingActionsStore) => T,
equalityFn?: (a: T, b: T) => boolean
): T => {
const bookingActionsStoreContext = useContext(BookingActionsStoreContext);
if (!bookingActionsStoreContext) {
throw new Error("useBookingActionsStoreContext must be used within BookingActionsStoreProvider");
}
return useStore(bookingActionsStoreContext, selector, equalityFn);
};
+23 -385
View File
@@ -1,3 +1,4 @@
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
@@ -9,8 +10,6 @@ import dayjs from "@calcom/dayjs";
// TODO: Use browser locale, implement Intl in Dayjs maybe?
import "@calcom/dayjs/locales";
import { Dialog } from "@calcom/features/components/controlled-dialog";
import { MeetingSessionDetailsDialog } from "@calcom/features/ee/video/MeetingSessionDetailsDialog";
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
import { formatTime } from "@calcom/lib/dayjs";
import { useCopy } from "@calcom/lib/hooks/useCopy";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -20,7 +19,6 @@ import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import type { AssignmentReason } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { Ensure } from "@calcom/types/utils";
import classNames from "@calcom/ui/classNames";
@@ -48,42 +46,18 @@ import { Tooltip } from "@calcom/ui/components/tooltip";
import assignmentReasonBadgeTitleMap from "@lib/booking/assignmentReasonBadgeTitleMap";
import { AddGuestsDialog } from "@components/dialog/AddGuestsDialog";
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { ReassignDialog } from "@components/dialog/ReassignDialog";
import { ReportBookingDialog } from "@components/dialog/ReportBookingDialog";
import { RerouteDialog } from "@components/dialog/RerouteDialog";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
import { BookingActionsDropdown } from "./BookingActionsDropdown";
import { useBookingActionsStoreContext, BookingActionsStoreProvider } from "./BookingActionsStoreProvider";
import {
getPendingActions,
getCancelEventAction,
getEditEventActions,
getAfterEventActions,
getReportAction,
shouldShowPendingActions,
shouldShowEditActions,
shouldShowRecurringCancelAction,
shouldShowIndividualReportButton,
type BookingActionContext,
getReportAction,
} from "./bookingActions";
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"];
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
export type BookingItemProps = BookingItem & {
listingStatus: BookingListingStatus;
recurringInfo: RouterOutputs["viewer"]["bookings"]["get"]["recurringInfo"][number] | undefined;
loggedInUser: {
userId: number | undefined;
userTimeZone: string | undefined;
userTimeFormat: number | null | undefined;
userEmail: string | undefined;
};
isToday: boolean;
};
import type { BookingItemProps } from "./types";
type ParsedBooking = ReturnType<typeof buildParsedBooking>;
type TeamEvent = Ensure<NonNullable<ParsedBooking["eventType"]>, "team">;
@@ -131,11 +105,6 @@ function BookingListItem(booking: BookingItemProps) {
const utils = trpc.useUtils();
const [rejectionReason, setRejectionReason] = useState<string>("");
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false);
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
const [meetingSessionDetailsDialogIsOpen, setMeetingSessionDetailsDialogIsOpen] = useState<boolean>(false);
const [isNoShowDialogOpen, setIsNoShowDialogOpen] = useState<boolean>(false);
const cardCharged = booking?.payment[0]?.success;
const attendeeList = booking.attendees.map((attendee) => {
return {
@@ -147,17 +116,6 @@ function BookingListItem(booking: BookingItemProps) {
};
});
const noShowMutation = trpc.viewer.loggedInViewerRouter.markNoShow.useMutation({
onSuccess: async (data) => {
showToast(data.message, "success");
// Invalidate and refetch the bookings query to update the UI
await utils.viewer.bookings.invalidate();
},
onError: (err) => {
showToast(err.message, "error");
},
});
const mutation = trpc.viewer.bookings.confirm.useMutation({
onSuccess: (data) => {
if (data?.status === BookingStatus.REJECTED) {
@@ -185,12 +143,6 @@ function BookingListItem(booking: BookingItemProps) {
const isRescheduled = booking.fromReschedule !== null;
const isRecurring = booking.recurringEventId !== null;
const getBookingStatus = (): "upcoming" | "past" | "cancelled" | "rejected" => {
if (isCancelled) return "cancelled";
if (isRejected) return "rejected";
if (isBookingInPast) return "past";
return "upcoming";
};
const isTabRecurring = booking.listingStatus === "recurring";
const isTabUnconfirmed = booking.listingStatus === "unconfirmed";
const isBookingFromRoutingForm = isBookingReroutable(parsedBooking);
@@ -219,6 +171,7 @@ function BookingListItem(booking: BookingItemProps) {
const isDisabledCancelling = booking.eventType.disableCancelling;
const isDisabledRescheduling = booking.eventType.disableRescheduling;
const cardCharged = booking?.payment[0]?.success;
const bookingConfirm = async (confirm: boolean) => {
let body = {
@@ -298,49 +251,6 @@ function BookingListItem(booking: BookingItemProps) {
.tz(userTimeZone)
.locale(language)
.format(isUpcoming ? (isDifferentYear ? "ddd, D MMM YYYY" : "ddd, D MMM") : "D MMMM YYYY");
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
const [isOpenReassignDialog, setIsOpenReassignDialog] = useState(false);
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
const [isOpenAddGuestsDialog, setIsOpenAddGuestsDialog] = useState(false);
const [isOpenReportDialog, setIsOpenReportDialog] = useState(false);
const [rerouteDialogIsOpen, setRerouteDialogIsOpen] = useState(false);
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
onSuccess: () => {
showToast(t("location_updated"), "success");
setIsOpenLocationDialog(false);
utils.viewer.bookings.invalidate();
},
onError: (e) => {
const errorMessages: Record<string, string> = {
UNAUTHORIZED: t("you_are_unauthorized_to_make_this_change_to_the_booking"),
BAD_REQUEST: e.message,
};
const message = errorMessages[e.data?.code as string] || t("location_update_failed");
showToast(message, "error");
},
});
const saveLocation = async ({
newLocation,
credentialId,
}: {
newLocation: string;
/**
* It could be set for conferencing locations that support team level installations.
*/
credentialId: number | null;
}) => {
try {
await setLocationMutation.mutateAsync({
bookingId: booking.id,
newLocation,
credentialId,
});
} catch {
// Errors are shown through the mutation onError handler
}
};
// Getting accepted recurring dates to show
const recurringDates = booking.recurringInfo?.bookings[BookingStatus.ACCEPTED]
@@ -360,58 +270,9 @@ function BookingListItem(booking: BookingItemProps) {
const title = booking.title;
const isCalVideoLocation =
!booking.location ||
booking.location === "integrations:daily" ||
(typeof booking.location === "string" && booking.location.trim() === "");
const showPendingPayment = paymentAppData.enabled && booking.payment.length && !booking.paid;
const baseEditEventActions = getEditEventActions(actionContext);
const editEventActions: ActionType[] = baseEditEventActions.map((action) => ({
...action,
onClick:
action.id === "reschedule_request"
? () => setIsOpenRescheduleDialog(true)
: action.id === "reroute"
? () => setRerouteDialogIsOpen(true)
: action.id === "change_location"
? () => setIsOpenLocationDialog(true)
: action.id === "add_members"
? () => setIsOpenAddGuestsDialog(true)
: action.id === "reassign"
? () => setIsOpenReassignDialog(true)
: undefined,
})) as ActionType[];
const baseAfterEventActions = getAfterEventActions(actionContext);
const afterEventActions: ActionType[] = baseAfterEventActions.map((action) => ({
...action,
onClick:
action.id === "view_recordings"
? () => setViewRecordingsDialogIsOpen(true)
: action.id === "meeting_session_details"
? () => setMeetingSessionDetailsDialogIsOpen(true)
: action.id === "charge_card"
? () => setChargeCardDialogIsOpen(true)
: action.id === "no_show"
? () => {
if (attendeeList.length === 1) {
const attendee = attendeeList[0];
noShowMutation.mutate({
bookingUid: booking.uid,
attendees: [{ email: attendee.email, noShow: !attendee.noShow }],
});
return;
}
setIsNoShowDialogOpen(true);
}
: undefined,
disabled:
action.disabled ||
(action.id === "no_show" && !(isBookingInPast || isOngoing)) ||
(action.id === "view_recordings" && !booking.isRecorded),
})) as ActionType[];
const setIsOpenReportDialog = useBookingActionsStoreContext((state) => state.setIsOpenReportDialog);
const reportAction = getReportAction(actionContext);
const reportActionWithHandler = {
@@ -421,72 +282,6 @@ function BookingListItem(booking: BookingItemProps) {
return (
<>
<RescheduleDialog
isOpenDialog={isOpenRescheduleDialog}
setIsOpenDialog={setIsOpenRescheduleDialog}
bookingUId={booking.uid}
/>
{isOpenReassignDialog && (
<ReassignDialog
isOpenDialog={isOpenReassignDialog}
setIsOpenDialog={setIsOpenReassignDialog}
bookingId={booking.id}
teamId={booking.eventType?.team?.id || 0}
bookingFromRoutingForm={isBookingFromRoutingForm}
/>
)}
<EditLocationDialog
booking={booking}
saveLocation={saveLocation}
isOpenDialog={isOpenSetLocationDialog}
setShowLocationModal={setIsOpenLocationDialog}
teamId={booking.eventType?.team?.id}
/>
<AddGuestsDialog
isOpenDialog={isOpenAddGuestsDialog}
setIsOpenDialog={setIsOpenAddGuestsDialog}
bookingId={booking.id}
/>
<ReportBookingDialog
isOpenDialog={isOpenReportDialog}
setIsOpenDialog={setIsOpenReportDialog}
bookingUid={booking.uid}
isRecurring={isRecurring}
status={getBookingStatus()}
/>
{booking.paid && booking.payment[0] && (
<ChargeCardDialog
isOpenDialog={chargeCardDialogIsOpen}
setIsOpenDialog={setChargeCardDialogIsOpen}
bookingId={booking.id}
paymentAmount={booking.payment[0].amount}
paymentCurrency={booking.payment[0].currency}
/>
)}
{isCalVideoLocation && (
<ViewRecordingsDialog
booking={booking}
isOpenDialog={viewRecordingsDialogIsOpen}
setIsOpenDialog={setViewRecordingsDialogIsOpen}
timeFormat={userTimeFormat ?? null}
/>
)}
{isCalVideoLocation && meetingSessionDetailsDialogIsOpen && (
<MeetingSessionDetailsDialog
booking={booking}
isOpenDialog={meetingSessionDetailsDialogIsOpen}
setIsOpenDialog={setMeetingSessionDetailsDialogIsOpen}
timeFormat={userTimeFormat ?? null}
/>
)}
{isNoShowDialogOpen && (
<NoShowAttendeesDialog
bookingUid={booking.uid}
attendees={attendeeList}
setIsOpen={setIsNoShowDialogOpen}
isOpen={isNoShowDialogOpen}
/>
)}
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
<DialogContent title={t("rejection_reason_title")} description={t("rejection_reason_description")}>
<div>
@@ -555,8 +350,10 @@ function BookingListItem(booking: BookingItemProps) {
className="text-sm leading-6 text-blue-600 hover:underline dark:text-blue-400">
<div className="flex items-center gap-2">
{provider?.iconUrl && (
<img
<Image
src={provider.iconUrl}
width={16}
height={16}
className="h-4 w-4 rounded-sm"
alt={`${provider?.label} logo`}
/>
@@ -661,94 +458,7 @@ function BookingListItem(booking: BookingItemProps) {
</div>
<div className="flex w-full flex-col flex-wrap items-end justify-end space-x-2 space-y-2 py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:flex-row sm:flex-nowrap sm:items-start sm:space-y-0 sm:pl-0">
{shouldShowPendingActions(actionContext) && <TableActions actions={pendingActions} />}
{shouldShowEditActions(actionContext) && (
<Dropdown>
<DropdownMenuTrigger asChild>
<Button
type="button"
color="secondary"
variant="icon"
StartIcon="ellipsis"
data-testid="booking-actions-dropdown"
/>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent>
<DropdownMenuLabel className="px-2 pb-1 pt-1.5">{t("edit_event")}</DropdownMenuLabel>
{editEventActions.map((action) => (
<DropdownMenuItem className="rounded-lg" key={action.id} disabled={action.disabled}>
<DropdownItem
type="button"
color={action.color}
StartIcon={action.icon}
href={action.href}
disabled={action.disabled}
onClick={action.onClick}
data-bookingid={action.bookingId}
data-testid={action.id}
className={action.disabled ? "text-muted" : undefined}>
{action.label}
</DropdownItem>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuLabel className="px-2 pb-1 pt-1.5">{t("after_event")}</DropdownMenuLabel>
{afterEventActions.map((action) => (
<DropdownMenuItem className="rounded-lg" key={action.id} disabled={action.disabled}>
<DropdownItem
type="button"
color={action.color}
StartIcon={action.icon}
href={action.href}
onClick={action.onClick}
disabled={action.disabled}
data-bookingid={action.bookingId}
data-testid={action.id}
className={action.disabled ? "text-muted" : undefined}>
{action.label}
</DropdownItem>
</DropdownMenuItem>
))}
<>
<DropdownMenuSeparator />
<DropdownMenuItem
className="rounded-lg"
key={reportActionWithHandler.id}
disabled={reportActionWithHandler.disabled}>
<DropdownItem
type="button"
color={reportActionWithHandler.color}
StartIcon={reportActionWithHandler.icon}
onClick={reportActionWithHandler.onClick}
disabled={reportActionWithHandler.disabled}
data-testid={reportActionWithHandler.id}
className={reportActionWithHandler.disabled ? "text-muted" : undefined}>
{reportActionWithHandler.label}
</DropdownItem>
</DropdownMenuItem>
</>
<DropdownMenuSeparator />
<DropdownMenuItem
className="rounded-lg"
key={cancelEventAction.id}
disabled={cancelEventAction.disabled}>
<DropdownItem
type="button"
color={cancelEventAction.color}
StartIcon={cancelEventAction.icon}
href={cancelEventAction.disabled ? undefined : cancelEventAction.href}
onClick={cancelEventAction.onClick}
disabled={cancelEventAction.disabled}
data-bookingid={cancelEventAction.bookingId}
data-testid={cancelEventAction.id}
className={cancelEventAction.disabled ? "text-muted" : undefined}>
{cancelEventAction.label}
</DropdownItem>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenuPortal>
</Dropdown>
)}
<BookingActionsDropdown booking={booking} />
{shouldShowRecurringCancelAction(actionContext) && <TableActions actions={[cancelEventAction]} />}
{shouldShowIndividualReportButton(actionContext) && (
<div className="flex items-center space-x-2">
@@ -760,7 +470,7 @@ function BookingListItem(booking: BookingItemProps) {
onClick={reportActionWithHandler.onClick}
disabled={reportActionWithHandler.disabled}
data-testid={reportActionWithHandler.id}
className="h-8 w-8"
className="min-h-[34px] min-w-[34px]"
tooltip={reportActionWithHandler.label}
/>
</div>
@@ -782,14 +492,6 @@ function BookingListItem(booking: BookingItemProps) {
isRescheduled={isRescheduled}
/>
</div>
{isBookingFromRoutingForm && (
<RerouteDialog
isOpenDialog={rerouteDialogIsOpen}
setIsOpenDialog={setRerouteDialogIsOpen}
booking={{ ...parsedBooking, eventType: parsedBooking.eventType }}
/>
)}
</>
);
}
@@ -903,7 +605,7 @@ const RecurringBookingsTooltip = ({
return (
(booking.recurringInfo &&
booking.eventType?.recurringEvent?.freq &&
booking.eventType?.recurringEvent?.freq != null &&
(booking.listingStatus === "recurring" ||
booking.listingStatus === "unconfirmed" ||
booking.listingStatus === "cancelled") && (
@@ -1174,79 +876,6 @@ const GroupedAttendees = (groupedAttendeeProps: GroupedAttendeeProps) => {
);
};
const NoShowAttendeesDialog = ({
attendees,
isOpen,
setIsOpen,
bookingUid,
}: {
attendees: AttendeeProps[];
isOpen: boolean;
setIsOpen: (value: boolean) => void;
bookingUid: string;
}) => {
const { t } = useLocale();
const [noShowAttendees, setNoShowAttendees] = useState(
attendees.map((attendee) => ({
id: attendee.id,
email: attendee.email,
name: attendee.name,
noShow: attendee.noShow || false,
}))
);
const utils = trpc.useUtils();
const noShowMutation = trpc.viewer.loggedInViewerRouter.markNoShow.useMutation({
onSuccess: async (data) => {
const newValue = data.attendees[0];
setNoShowAttendees((old) =>
old.map((attendee) =>
attendee.email === newValue.email ? { ...attendee, noShow: newValue.noShow } : attendee
)
);
showToast(t(data.message), "success");
await utils.viewer.bookings.invalidate();
},
onError: (err) => {
showToast(err.message, "error");
},
});
return (
<Dialog open={isOpen} onOpenChange={() => setIsOpen(false)}>
<DialogContent title={t("mark_as_no_show_title")} description={t("no_show_description")} enableOverflow>
{noShowAttendees.map((attendee, index) => (
<form
key={attendee.id}
onSubmit={(e) => {
e.preventDefault();
noShowMutation.mutate({
bookingUid,
attendees: [{ email: attendee.email, noShow: !attendee.noShow }],
});
}}>
<div
className={`bg-muted flex items-center justify-between px-4 py-2 ${
index === 0 ? "rounded-t-md" : ""
} ${index === noShowAttendees.length - 1 ? "rounded-b-md" : "border-subtle border-b"}`}>
<span className="text-emphasis flex flex-col text-sm">
{attendee.name}
{attendee.email && <span className="text-muted">({attendee.email})</span>}
</span>
<Button color="minimal" type="submit" StartIcon={attendee.noShow ? "eye-off" : "eye"}>
{attendee.noShow ? t("unmark_as_no_show") : t("mark_as_no_show")}
</Button>
</div>
</form>
))}
<DialogFooter noSticky>
<DialogClose>{t("done")}</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
const GroupedGuests = ({ guests }: { guests: AttendeeProps[] }) => {
const [openDropdown, setOpenDropdown] = useState(false);
const { t } = useLocale();
@@ -1369,4 +998,13 @@ const AssignmentReasonTooltip = ({ assignmentReason }: { assignmentReason: Assig
);
};
export default BookingListItem;
// Wrap BookingListItem with BookingActionsStoreProvider to provide isolated store for each booking
const BookingListItemWithProvider = (props: BookingItemProps) => {
return (
<BookingActionsStoreProvider>
<BookingListItem {...props} />
</BookingActionsStoreProvider>
);
};
export default BookingListItemWithProvider;
@@ -1,7 +1,7 @@
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
import type { ActionType } from "@calcom/ui/components/table";
import type { BookingItemProps } from "./BookingListItem";
import type { BookingItemProps } from "./types";
export interface BookingActionContext {
booking: BookingItemProps;
+106
View File
@@ -0,0 +1,106 @@
"use client";
import type React from "react";
import { createStore } from "zustand";
export type BookingActionsStore = {
// Dialog states
rejectionDialogIsOpen: boolean;
chargeCardDialogIsOpen: boolean;
viewRecordingsDialogIsOpen: boolean;
meetingSessionDetailsDialogIsOpen: boolean;
isNoShowDialogOpen: boolean;
isOpenRescheduleDialog: boolean;
isOpenReassignDialog: boolean;
isOpenSetLocationDialog: boolean;
isOpenAddGuestsDialog: boolean;
isOpenReportDialog: boolean;
rerouteDialogIsOpen: boolean;
// Dialog setters
setRejectionDialogIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
setChargeCardDialogIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
setViewRecordingsDialogIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
setMeetingSessionDetailsDialogIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
setIsNoShowDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
setIsOpenRescheduleDialog: React.Dispatch<React.SetStateAction<boolean>>;
setIsOpenReassignDialog: React.Dispatch<React.SetStateAction<boolean>>;
setIsOpenLocationDialog: React.Dispatch<React.SetStateAction<boolean>>;
setIsOpenAddGuestsDialog: React.Dispatch<React.SetStateAction<boolean>>;
setIsOpenReportDialog: React.Dispatch<React.SetStateAction<boolean>>;
setRerouteDialogIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
// Rejection reason state
rejectionReason: string;
setRejectionReason: (reason: string) => void;
};
export const createBookingActionsStore = () => {
return createStore<BookingActionsStore>((set) => ({
// Initial dialog states
rejectionDialogIsOpen: false,
chargeCardDialogIsOpen: false,
viewRecordingsDialogIsOpen: false,
meetingSessionDetailsDialogIsOpen: false,
isNoShowDialogOpen: false,
isOpenRescheduleDialog: false,
isOpenReassignDialog: false,
isOpenSetLocationDialog: false,
isOpenAddGuestsDialog: false,
isOpenReportDialog: false,
rerouteDialogIsOpen: false,
// Dialog setters
setRejectionDialogIsOpen: (isOpen) =>
set((state) => ({
rejectionDialogIsOpen: typeof isOpen === "function" ? isOpen(state.rejectionDialogIsOpen) : isOpen,
})),
setChargeCardDialogIsOpen: (isOpen) =>
set((state) => ({
chargeCardDialogIsOpen: typeof isOpen === "function" ? isOpen(state.chargeCardDialogIsOpen) : isOpen,
})),
setViewRecordingsDialogIsOpen: (isOpen) =>
set((state) => ({
viewRecordingsDialogIsOpen:
typeof isOpen === "function" ? isOpen(state.viewRecordingsDialogIsOpen) : isOpen,
})),
setMeetingSessionDetailsDialogIsOpen: (isOpen) =>
set((state) => ({
meetingSessionDetailsDialogIsOpen:
typeof isOpen === "function" ? isOpen(state.meetingSessionDetailsDialogIsOpen) : isOpen,
})),
setIsNoShowDialogOpen: (isOpen) =>
set((state) => ({
isNoShowDialogOpen: typeof isOpen === "function" ? isOpen(state.isNoShowDialogOpen) : isOpen,
})),
setIsOpenRescheduleDialog: (isOpen) =>
set((state) => ({
isOpenRescheduleDialog: typeof isOpen === "function" ? isOpen(state.isOpenRescheduleDialog) : isOpen,
})),
setIsOpenReassignDialog: (isOpen) =>
set((state) => ({
isOpenReassignDialog: typeof isOpen === "function" ? isOpen(state.isOpenReassignDialog) : isOpen,
})),
setIsOpenLocationDialog: (isOpen) =>
set((state) => ({
isOpenSetLocationDialog:
typeof isOpen === "function" ? isOpen(state.isOpenSetLocationDialog) : isOpen,
})),
setIsOpenAddGuestsDialog: (isOpen) =>
set((state) => ({
isOpenAddGuestsDialog: typeof isOpen === "function" ? isOpen(state.isOpenAddGuestsDialog) : isOpen,
})),
setIsOpenReportDialog: (isOpen) =>
set((state) => ({
isOpenReportDialog: typeof isOpen === "function" ? isOpen(state.isOpenReportDialog) : isOpen,
})),
setRerouteDialogIsOpen: (isOpen) =>
set((state) => ({
rerouteDialogIsOpen: typeof isOpen === "function" ? isOpen(state.rerouteDialogIsOpen) : isOpen,
})),
// Rejection reason state
rejectionReason: "",
setRejectionReason: (reason: string) => set({ rejectionReason: reason }),
}));
};
+17
View File
@@ -0,0 +1,17 @@
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"];
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
export type BookingItemProps = BookingItem & {
listingStatus: BookingListingStatus;
recurringInfo: RouterOutputs["viewer"]["bookings"]["get"]["recurringInfo"][number] | undefined;
loggedInUser: {
userId: number | undefined;
userTimeZone: string | undefined;
userTimeFormat: number | null | undefined;
userEmail: string | undefined;
};
isToday: boolean;
};
+13
View File
@@ -0,0 +1,13 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
export async function validateCsrfToken(csrfToken: string): Promise<NextResponse | null> {
const cookieStore = await cookies();
const cookieToken = cookieStore.get("calcom.csrf_token")?.value;
if (!cookieToken || cookieToken !== csrfToken) {
return NextResponse.json({ success: false, message: "Invalid CSRF token" }, { status: 403 });
}
cookieStore.delete("calcom.csrf_token");
return null;
}
+14 -10
View File
@@ -10,7 +10,6 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { getCalVideoReference } from "@calcom/features/get-cal-video-reference";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { CAL_VIDEO_MEETING_LINK_FOR_TESTING } from "@calcom/lib/constants";
@@ -26,6 +25,7 @@ type CalVideoSettings = {
enableAutomaticRecordingForOrganizer: boolean;
disableTranscriptionForGuests: boolean;
disableTranscriptionForOrganizer: boolean;
requireEmailForGuests: boolean;
};
const shouldEnableRecordButton = ({
@@ -124,7 +124,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const { req } = context;
const bookingRepo = new BookingRepository(prisma);
const booking = await bookingRepo.findBookingForMeetingPage({
const booking = await bookingRepo.findBookingIncludeCalVideoSettingsAndReferences({
bookingUid: context.query.uid as string,
});
@@ -211,6 +211,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
});
const sessionUserId = session?.user?.impersonatedBy ? session.user.impersonatedBy.id : session?.user.id;
const sessionUserEmail = session?.user?.email;
const isOrganizer = await checkIfUserIsHost({
booking: {
eventTypeId: bookingObj.eventType?.id,
@@ -219,18 +220,25 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
sessionUserId,
});
const isAttendee = sessionUserEmail
? bookingObj.attendees?.some(
(attendee) => attendee.email.toLowerCase() === sessionUserEmail.toLowerCase()
) ?? false
: false;
// set meetingPassword for guests
if (!isOrganizer) {
const userIdForToken = sessionUserId;
const guestMeetingPassword = await generateGuestMeetingTokenFromOwnerMeetingToken({
meetingToken: videoReferencePassword,
userId: sessionUserId,
userId: userIdForToken,
});
bookingObj.references.forEach((bookRef) => {
bookRef.meetingPassword = guestMeetingPassword;
});
}
// Only for backward compatibility and setting user id in participants for organizer
else {
const meetingPassword = await setEnableRecordingUIAndUserIdForOrganizer(
@@ -247,11 +255,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const videoReference = getCalVideoReference(bookingObj.references);
const featureRepo = new FeaturesRepository(prisma);
const displayLogInOverlay = profile?.organizationId
? await featureRepo.checkIfTeamHasFeature(profile.organizationId, "cal-video-log-in-overlay")
: false;
const showRecordingButton = shouldEnableRecordButton({
hasTeamPlan: !!hasTeamPlan,
calVideoSettings: bookingObj.eventType?.calVideoSettings,
@@ -293,7 +296,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
},
hasTeamPlan: !!hasTeamPlan,
calVideoLogo,
displayLogInOverlay,
loggedInUserName: sessionUserId ? session?.user?.name : undefined,
showRecordingButton,
enableAutomaticTranscription,
@@ -303,6 +305,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
? undefined
: bookingObj.eventType?.calVideoSettings?.redirectUrlOnExit,
overrideName: Array.isArray(context.query.name) ? context.query.name[0] : context.query.name,
requireEmailForGuests: bookingObj.eventType?.calVideoSettings?.requireEmailForGuests ?? false,
isLoggedInUserPartOfMeeting: isAttendee || isOrganizer,
},
};
}
@@ -580,14 +580,16 @@ export default function Success(props: PageProps) {
</div>
</>
)}
{isCancelled && bookingInfo?.cancelledBy && (
<>
<div className="font-medium">{t("cancelled_by")}</div>
<div className="col-span-2 mb-6 last:mb-0">
<p className="break-words">{bookingInfo?.cancelledBy}</p>
</div>
</>
)}
{isCancelled &&
bookingInfo?.cancelledBy &&
!(bookingInfo.eventType?.hideOrganizerEmail && !isHost) && (
<>
<div className="font-medium">{t("cancelled_by")}</div>
<div className="col-span-2 mb-6 last:mb-0">
<p className="break-words">{bookingInfo?.cancelledBy}</p>
</div>
</>
)}
{previousBooking && (
<>
<div className="font-medium">{t("rescheduled_by")}</div>
@@ -743,7 +745,7 @@ export default function Success(props: PageProps) {
{showUtmParams && (
<div className="col-span-2 mb-2 mt-2">
{Object.entries(utmParams).filter(([_, value]) => Boolean(value)).length >
0 ? (
0 ? (
<ul className="list-disc space-y-1 p-1 pl-5 sm:w-80">
{Object.entries(utmParams)
.filter(([_, value]) => Boolean(value))
@@ -851,10 +853,11 @@ export default function Success(props: PageProps) {
<span className="text-default inline">
<span className="underline" data-testid="reschedule-link">
<Link
href={`/reschedule/${seatReferenceUid || bookingInfo?.uid}${currentUserEmail
? `?rescheduledBy=${encodeURIComponent(currentUserEmail)}`
: ""
}`}
href={`/reschedule/${seatReferenceUid || bookingInfo?.uid}${
currentUserEmail
? `?rescheduledBy=${encodeURIComponent(currentUserEmail)}`
: ""
}`}
legacyBehavior>
{t("reschedule")}
</Link>
@@ -8,6 +8,8 @@ import { useMemo } from "react";
import dayjs from "@calcom/dayjs";
import {
DataTableProvider,
DataTableFilters,
DataTableSegment,
type SystemFilterSegment,
useDataTable,
ColumnFilterType,
@@ -403,10 +405,25 @@ function BookingsContent({ status, permissions, isCalendarViewEnabled }: Booking
</div>
<main className="w-full">
<div className="flex w-full flex-col">
{query.status === "error" && (
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
)}
{query.status !== "error" && (
{query.status === "error" ? (
<>
<div className="grid w-full items-center gap-2 pb-4">
<div className="flex w-full flex-col gap-2">
<div className="flex w-full flex-wrap justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<DataTableFilters.FilterBar table={table} />
</div>
<div className="flex flex-wrap items-center gap-2">
<DataTableFilters.ClearFiltersButton />
<DataTableSegment.SaveButton />
<DataTableSegment.Select />
</div>
</div>
</div>
</div>
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
</>
) : (
<>
{!!bookingsToday.length && status === "upcoming" && (
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
@@ -24,6 +24,7 @@ export function useCreateTeam() {
name: teamDetails.name,
slug: teamDetails.slug,
logo: teamBrand.logo,
isOnboarding: true,
});
// If there's a checkout URL, redirect to Stripe payment
@@ -35,7 +36,7 @@ export function useCreateTeam() {
if (result.team) {
// Not sure we need this flag check - keeping it here for safe keeping as this is called only from v3 onboarding flow
const gettingStartedPath = flags["onboarding-v3"]
? "/onboarding/getting-started"
? "/onboarding/personal/settings"
: "/getting-started";
router.push(gettingStartedPath);
}
@@ -1,6 +1,7 @@
import { useRouter } from "next/navigation";
import { useState } from "react";
import { setShowNewOrgModalFlag } from "@calcom/features/ee/organizations/hooks/useWelcomeModal";
import { useFlagMap } from "@calcom/features/flags/context/provider";
import { CreationSource } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
@@ -25,14 +26,8 @@ export const useSubmitOnboarding = () => {
setError(null);
try {
const {
selectedPlan,
organizationDetails,
organizationBrand,
teams,
inviteRole,
resetOnboarding,
} = store;
const { selectedPlan, organizationDetails, organizationBrand, teams, inviteRole, resetOnboarding } =
store;
if (selectedPlan !== "organization") {
throw new Error("Only organization plan is currently supported");
@@ -83,7 +78,8 @@ export const useSubmitOnboarding = () => {
// No checkout URL means billing is disabled (self-hosted flow)
// Organization has already been created by the backend
showToast("Organization created successfully!", "success");
// TODO: after this redirect we need to hard refresh the page to see org
// Set flag to show welcome modal after personal onboarding redirect
setShowNewOrgModalFlag();
skipToPersonal(resetOnboarding);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Failed to create organization";
@@ -188,14 +188,22 @@ export const OrganizationInviteView = ({ userEmail }: OrganizationInviteViewProp
<Form form={form} handleSubmit={handleContinue} className="w-full">
<div className="flex w-full flex-col gap-4">
{/* Email and Team inputs */}
<div className="flex flex-col">
<div className="grid grid-cols-2 gap-2">
<Label className="text-emphasis text-sm font-medium">{t("email")}</Label>
<Label className="text-emphasis text-sm font-medium">{t("team")}</Label>
<div className="flex flex-col gap-2">
<div className="grid grid-cols-2">
<Label
className="text-emphasis mb-0 text-sm font-medium"
htmlFor="invites.0.email">
{t("email")}
</Label>
<Label
className="text-emphasis mb-0 text-sm font-medium"
htmlFor="invites.0.team">
{t("team")}
</Label>
</div>
{fields.map((field, index) => (
<div key={field.id} className="flex items-start gap-0.5 pt-2 md:pt-0">
<div key={field.id} className="flex items-start gap-0.5">
<div className="grid flex-1 items-start gap-2 md:grid-cols-2 ">
<TextField
labelSrOnly
@@ -113,7 +113,6 @@ export const PersonalSettingsView = ({ userEmail, userName }: PersonalSettingsVi
form.setValue("name", e.target.value);
}}
placeholder="John Doe"
className="border-default h-7 rounded-[10px] border px-2 py-1.5 text-sm"
/>
{form.formState.errors.name && (
<p className="text-error text-sm">{form.formState.errors.name.message}</p>
@@ -128,7 +127,6 @@ export const PersonalSettingsView = ({ userEmail, userName }: PersonalSettingsVi
<TimezoneSelect
value={selectedTimeZone}
onChange={({ value }) => setSelectedTimeZone(value)}
size="sm"
/>
<p className="text-subtle text-xs font-normal leading-3">
{t("current_time")}{" "}
@@ -0,0 +1,84 @@
"use client";
import { useRouter } from "next/navigation";
import { useFlagMap } from "@calcom/features/flags/context/provider";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui/components/button";
import { MailIcon } from "./MailIcon";
type CompanyEmailOrganizationBannerProps = {
onDismissAction: () => void;
};
export const CompanyEmailOrganizationBanner = ({ onDismissAction }: CompanyEmailOrganizationBannerProps) => {
const { t } = useLocale();
const router = useRouter();
const flags = useFlagMap();
const handleLearnMore = () => {
const redirectPath = flags["onboarding-v3"]
? "/onboarding/organization/details"
: "/settings/organizations/new";
router.push(redirectPath);
};
if (!flags["onboarding-v3"]) {
return null;
}
return (
<div className="border-subtle from-muted relative mb-6 overflow-hidden rounded-lg border bg-gradient-to-r to-transparent p-4">
{/* Dot grid background with fade */}
<svg className="pointer-events-none absolute inset-0 h-full w-full" xmlns="http://www.w3.org/2000/svg">
<defs>
{/* Dot pattern - 12px spacing */}
<pattern id="banner-dot-pattern" x="0" y="0" width="12" height="12" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.8" fill="currentColor" className="text-subtle" opacity="0.3" />
</pattern>
{/* Fade mask from left to right */}
<linearGradient id="banner-fade-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="white" stopOpacity="1" />
<stop offset="10%" stopColor="white" stopOpacity="0.4" />
<stop offset="100%" stopColor="white" stopOpacity="0" />
</linearGradient>
<mask id="banner-fade-mask">
<rect x="0" y="0" width="100%" height="100%" fill="url(#banner-fade-gradient)" />
</mask>
</defs>
{/* Apply pattern with mask */}
<rect
x="0"
y="0"
width="100%"
height="100%"
fill="url(#banner-dot-pattern)"
mask="url(#banner-fade-mask)"
/>
</svg>
<div className="relative z-10 flex gap-4">
<div>
<MailIcon />
</div>
<div className="flex flex-1 flex-col gap-2">
<div className="flex flex-col">
<h3 className="text-default text-sm font-semibold">
{t("it_appears_you_are_using_company_email")}
</h3>
<p className="text-default text-sm">{t("explore_organizational_plan_description")}</p>
</div>
<div className="mt-2 flex gap-2">
<Button color="secondary" onClick={onDismissAction}>
{t("dismiss")}
</Button>
<Button color="primary" EndIcon="external-link" onClick={handleLearnMore}>
{t("upgrade")}
</Button>
</div>
</div>
</div>
</div>
);
};
File diff suppressed because one or more lines are too long
@@ -12,6 +12,7 @@ import { z } from "zod";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { Dialog } from "@calcom/features/components/controlled-dialog";
import { isCompanyEmail } from "@calcom/features/ee/organizations/lib/utils";
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { DisplayInfo } from "@calcom/features/users/components/UserTable/EditSheet/DisplayInfo";
@@ -46,6 +47,8 @@ import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
import type { TRPCClientErrorLike } from "@trpc/client";
import { CompanyEmailOrganizationBanner } from "./components/CompanyEmailOrganizationBanner";
interface DeleteAccountValues {
totpCode: string;
}
@@ -72,7 +75,8 @@ type Props = {
const ProfileView = ({ user }: Props) => {
const { t } = useLocale();
const utils = trpc.useUtils();
const { update } = useSession();
const session = useSession();
const { update } = session;
const updateProfileMutation = trpc.viewer.me.updateProfile.useMutation({
onSuccess: async (res) => {
await update(res);
@@ -138,6 +142,7 @@ const ProfileView = ({ user }: Props) => {
const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
const [hasDeleteErrors, setHasDeleteErrors] = useState(false);
const [deleteErrorMessage, setDeleteErrorMessage] = useState("");
const [isCompanyEmailAlertDismissed, setIsCompanyEmailAlertDismissed] = useState(false);
const form = useForm<DeleteAccountValues>();
const onDeleteMeSuccessMutation = async () => {
@@ -250,6 +255,14 @@ const ProfileView = ({ user }: Props) => {
],
};
// Check if user should see company email alert
const shouldShowCompanyEmailAlert =
!isCompanyEmailAlertDismissed &&
!session.data?.user?.org?.id &&
!user.organization?.id &&
userEmail &&
isCompanyEmail(userEmail);
return (
<SettingsHeader
title={t("profile")}
@@ -301,6 +314,12 @@ const ProfileView = ({ user }: Props) => {
isCALIdentityProvider={isCALIdentityProvider}
/>
{shouldShowCompanyEmailAlert && (
<div className="mt-6">
<CompanyEmailOrganizationBanner onDismissAction={() => setIsCompanyEmailAlertDismissed(true)} />
</div>
)}
<div className="border-subtle mt-6 rounded-lg rounded-b-none border border-b-0 p-6">
<Label className="mb-0 text-base font-semibold text-red-700">{t("danger_zone")}</Label>
<p className="text-subtle text-sm">{t("account_deletion_cannot_be_undone")}</p>
@@ -5,6 +5,7 @@ import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { setShowNewOrgModalFlag } from "@calcom/features/ee/organizations/hooks/useWelcomeModal";
import { useOnboarding } from "@calcom/features/ee/organizations/lib/onboardingStore";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc";
@@ -52,7 +53,9 @@ const useOrgCreation = () => {
await utils.viewer.organizations.listCurrent.invalidate();
await session.update();
reset();
window.location.href = `${window.location.origin}/settings/organizations/profile`;
// Set flag to show welcome modal (using both query param and sessionStorage for reliability)
setShowNewOrgModalFlag();
window.location.href = `${window.location.origin}/settings/organizations/profile?newOrganizationModal=true`;
} else {
// Unexpected state
setServerErrorMessage("Unexpected response from server");
@@ -28,7 +28,7 @@ const PaymentStatusView = () => {
if (organization) {
setOrganizationCreated(true);
// Organization is created, redirect to next step
router.push(`/settings/organizations`);
router.push(`/settings/organizations?newOrganizationModal=true`);
}
}, [organization, router, useOnboardingStore]);
@@ -11,6 +11,7 @@ import { WEBSITE_URL } from "@calcom/lib/constants";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { TRANSCRIPTION_STOPPED_ICON, RECORDING_DEFAULT_ICON } from "@calcom/lib/constants";
import { formatToLocalizedDate, formatToLocalizedTime } from "@calcom/lib/dayjs";
import { emailRegex } from "@calcom/lib/emailSchema";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
@@ -34,7 +35,6 @@ export default function JoinCall(props: PageProps) {
booking,
hasTeamPlan,
calVideoLogo,
displayLogInOverlay,
loggedInUserName,
overrideName,
showRecordingButton,
@@ -42,16 +42,27 @@ export default function JoinCall(props: PageProps) {
enableAutomaticRecordingForOrganizer,
showTranscriptionButton,
rediectAttendeeToOnExit,
requireEmailForGuests,
isLoggedInUserPartOfMeeting,
} = props;
const [daily, setDaily] = useState<DailyCall | null>(null);
const [userNameForCall, setUserNameForCall] = useState<string | undefined>(
overrideName ?? loggedInUserName ?? undefined
);
const [isUserNameConfirmed, setIsUserNameConfirmed] = useState<boolean>(!displayLogInOverlay);
const [guestCredentials, setGuestCredentials] = useState<{
meetingPassword: string;
meetingUrl: string;
userName: string;
} | null>(null);
const userNameForCall = overrideName ?? loggedInUserName ?? undefined;
const hideLoginModal =
!!userNameForCall && (requireEmailForGuests ? !!loggedInUserName && isLoggedInUserPartOfMeeting : true);
const [isCallFrameReady, setIsCallFrameReady] = useState<boolean>(false);
const activeMeetingPassword = guestCredentials?.meetingPassword ?? meetingPassword;
const activeMeetingUrl = guestCredentials?.meetingUrl ?? meetingUrl;
const activeUserName = guestCredentials?.userName ?? userNameForCall;
const createCallFrame = useCallback(
(userName?: string) => {
(userName?: string, password?: string, url?: string) => {
let callFrame: DailyCall | undefined;
try {
@@ -76,9 +87,9 @@ export default function JoinCall(props: PageProps) {
width: "100%",
height: "100%",
},
url: meetingUrl,
url: url ?? meetingUrl,
userName: userName,
...(typeof meetingPassword === "string" && { token: meetingPassword }),
...(typeof (password ?? meetingPassword) === "string" && { token: password ?? meetingPassword }),
...(hasTeamPlan && {
customTrayButtons: {
...(showRecordingButton
@@ -116,16 +127,15 @@ export default function JoinCall(props: PageProps) {
},
[meetingUrl, meetingPassword, hasTeamPlan, showRecordingButton, showTranscriptionButton, t]
);
useEffect(() => {
if (displayLogInOverlay && !loggedInUserName && !overrideName && !isUserNameConfirmed) {
if (!hideLoginModal && !guestCredentials) {
return;
}
let callFrame: DailyCall | null = null;
try {
callFrame = createCallFrame(userNameForCall) ?? null;
callFrame = createCallFrame(activeUserName, activeMeetingPassword, activeMeetingUrl) ?? null;
setDaily(callFrame);
setIsCallFrameReady(true);
@@ -146,28 +156,16 @@ export default function JoinCall(props: PageProps) {
setIsCallFrameReady(false);
};
}, [
displayLogInOverlay,
isUserNameConfirmed,
userNameForCall,
hideLoginModal,
activeUserName,
activeMeetingPassword,
activeMeetingUrl,
createCallFrame,
loggedInUserName,
overrideName,
guestCredentials,
]);
const handleJoinAsGuest = useCallback((guestName: string) => {
const trimmedName = guestName.trim();
if (!trimmedName) {
return;
}
setUserNameForCall(trimmedName);
setIsUserNameConfirmed(true);
}, []);
const handleUserNameConfirmed = useCallback(() => {
setIsUserNameConfirmed(true);
}, []);
return (
<DailyProvider callObject={daily}>
{isCallFrameReady && (
@@ -205,14 +203,16 @@ export default function JoinCall(props: PageProps) {
/>
)}
</div>
{displayLogInOverlay && !isUserNameConfirmed && (
{!hideLoginModal && (
<LogInOverlay
isLoggedIn={!!loggedInUserName}
isOpen={!hideLoginModal}
bookingUid={booking.uid}
loggedInUserName={loggedInUserName ?? undefined}
overrideName={overrideName}
onJoinAsGuest={handleJoinAsGuest}
onUserNameConfirmed={handleUserNameConfirmed}
requireEmailForGuests={requireEmailForGuests}
onGuestCredentialsReceived={setGuestCredentials}
meetingPassword={meetingPassword}
meetingUrl={meetingUrl}
/>
)}
@@ -291,46 +291,125 @@ function ProgressBar(props: ProgressBarProps) {
}
interface LogInOverlayProps {
isLoggedIn: boolean;
isOpen: boolean;
bookingUid: string;
loggedInUserName?: string;
overrideName?: string;
onJoinAsGuest: (guestName: string) => void;
onUserNameConfirmed?: () => void;
requireEmailForGuests?: boolean;
onGuestCredentialsReceived: (credentials: {
meetingPassword: string;
meetingUrl: string;
userName: string;
}) => void;
meetingPassword?: string;
meetingUrl: string;
}
export function LogInOverlay(props: LogInOverlayProps) {
const { t } = useLocale();
const { bookingUid, isLoggedIn, loggedInUserName, overrideName, onJoinAsGuest, onUserNameConfirmed } =
props;
const {
bookingUid,
isOpen: _open,
loggedInUserName,
overrideName,
requireEmailForGuests = false,
onGuestCredentialsReceived,
meetingPassword,
meetingUrl,
} = props;
const [isOpen, setIsOpen] = useState(!isLoggedIn);
const [isOpen, setIsOpen] = useState(_open);
const [userName, setUserName] = useState(overrideName ?? loggedInUserName ?? "");
const [userEmail, setUserEmail] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleContinueAsGuest = useCallback(async () => {
const trimmedName = userName.trim();
const trimmedEmail = userEmail.trim();
if (!trimmedName) {
setError(t("please_enter_name"));
return;
}
if (requireEmailForGuests) {
if (!trimmedEmail) {
setError(t("please_enter_name_and_email"));
return;
}
if (!emailRegex.test(trimmedEmail)) {
setError(t("invalid_email_address"));
return;
}
}
setIsLoading(true);
setError(null);
try {
onJoinAsGuest(trimmedName);
onUserNameConfirmed?.();
setIsOpen(false);
} catch (error) {
console.error("Error joining as guest:", error);
const errorMessage = error instanceof Error ? error.message : t("failed_to_join_call");
// Only create guest session if email is required and provided
if (requireEmailForGuests && trimmedEmail) {
const csrfResponse = await fetch("/api/csrf", { cache: "no-store" });
const { csrfToken } = await csrfResponse.json();
const response = await fetch("/api/video/guest-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
bookingUid,
email: trimmedEmail,
name: trimmedName,
csrfToken,
}),
});
if (!response.ok) {
const errorData = await response.json();
const errorKey = errorData.error;
throw new Error(errorKey || "Failed to create guest session");
}
const { meetingPassword, meetingUrl } = await response.json();
onGuestCredentialsReceived({
meetingPassword,
meetingUrl,
userName: trimmedName,
});
setIsOpen(false);
} else {
// If email not required, use existing credentials from SSR props
if (!meetingPassword) {
throw new Error("Meeting password not available");
}
onGuestCredentialsReceived({
meetingPassword,
meetingUrl,
userName: trimmedName,
});
setIsOpen(false);
}
} catch (error) {
const errorKey = error instanceof Error ? error.message : "failed_to_join_call";
const errorMessage = t(errorKey) || errorKey;
setError(errorMessage);
} finally {
setIsLoading(false);
}
}, [userName, onJoinAsGuest, onUserNameConfirmed, t]);
}, [
userName,
userEmail,
bookingUid,
requireEmailForGuests,
t,
onGuestCredentialsReceived,
meetingPassword,
meetingUrl,
]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
@@ -351,38 +430,81 @@ export function LogInOverlay(props: LogInOverlayProps) {
[error]
);
const handleEmailChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setUserEmail(e.target.value);
if (error) {
setError(null);
}
},
[error]
);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent
title={t("join_video_call")}
description={t("choose_how_you_d_like_to_appear_on_the_call")}
className="bg-white text-black dark:bg-black dark:text-white sm:max-w-[480px]">
<div className="pb-4">
className="bg-black/80 p-6 sm:max-w-lg">
<div className="mt-2 pb-4">
<div className="space-y-4">
<div>
<div className="font-semibold">{t("join_as_guest")}</div>
<p className="text-subtle text-sm">{t("ideal_for_one_time_calls")}</p>
</div>
<div className="flex gap-4">
<Input
type="text"
placeholder={t("your_name")}
className="w-full flex-1"
value={userName}
onChange={handleUserNameChange}
onKeyDown={handleKeyDown}
disabled={isLoading}
autoFocus
/>
<Button color="secondary" onClick={handleContinueAsGuest} loading={isLoading}>
{t("continue")}
</Button>
</div>
{requireEmailForGuests ? (
<div className="flex flex-col gap-4">
<Input
type="text"
placeholder={t("your_name")}
className="w-full flex-1"
value={userName}
onChange={handleUserNameChange}
onKeyDown={handleKeyDown}
disabled={isLoading}
autoFocus
/>
<Input
type="email"
placeholder={t("email_address")}
className="w-full flex-1"
value={userEmail}
onChange={handleEmailChange}
onKeyDown={handleKeyDown}
disabled={isLoading}
/>
<Button
color="secondary"
className="w-fit self-end"
onClick={handleContinueAsGuest}
loading={isLoading}>
{t("continue")}
</Button>
</div>
) : (
<div className="flex gap-4">
<Input
type="text"
placeholder={t("your_name")}
className="w-full flex-1"
value={userName}
onChange={handleUserNameChange}
onKeyDown={handleKeyDown}
disabled={isLoading}
autoFocus
/>
<Button color="secondary" onClick={handleContinueAsGuest} loading={isLoading}>
{t("continue")}
</Button>
</div>
)}
</div>
{error && (
<div className="rounded-md bg-red-50 p-3 dark:bg-red-900/20">
<div className="mt-4 rounded-md bg-red-50 p-3 dark:bg-red-900/20">
<div className="flex">
<div className="ml-3">
<p className="text-sm font-medium text-red-800 dark:text-red-200">{error}</p>
@@ -392,7 +514,7 @@ export function LogInOverlay(props: LogInOverlayProps) {
)}
{/* Divider */}
<hr className="my-5 h-0.5 border-t-0 bg-neutral-100 dark:bg-white/10" />
<hr className="my-6 h-0.5 border-t-0 bg-neutral-100 dark:bg-white/10" />
<div className="mt-5 space-y-4">
<div>
@@ -487,7 +609,6 @@ export function VideoMeetingInfo(props: VideoMeetingInfo) {
<div
className="prose-sm prose prose-invert"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: markdownToSafeHTML(booking.description) }}
/>
</>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "5.8.7",
"version": "5.8.10",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -2,6 +2,7 @@ import { expect } from "@playwright/test";
import { applySelectFilter } from "./filter-helpers";
import { test } from "./lib/fixtures";
import { MembershipRole } from "@calcom/prisma/enums";
test.describe.configure({ mode: "parallel" });
@@ -228,4 +229,80 @@ test.describe("Booking Filters", () => {
const eventTypeSearchParams = new URL(page.url()).searchParams;
expect(eventTypeSearchParams.get("activeFilters")).toBeFalsy();
});
test("Filter segments with removed team members should not cause stuck UI", async ({
page,
users,
prisma,
}) => {
const teamOwner = await users.create(undefined, {
hasTeam: true,
teamRole: MembershipRole.OWNER,
});
const secondUser = await users.create({ name: "Second" });
const thirdUser = await users.create({ name: "Third" });
const { team } = await teamOwner.getFirstTeamMembership();
// Add teammates to the team
await prisma.membership.createMany({
data: [
{
teamId: team.id,
userId: secondUser.id,
role: MembershipRole.MEMBER,
accepted: true,
},
{
teamId: team.id,
userId: thirdUser.id,
role: MembershipRole.MEMBER,
accepted: true,
},
],
});
await teamOwner.apiLogin();
const bookingsGetResponse = page.waitForResponse((response) =>
/\/api\/trpc\/bookings\/get.*/.test(response.url())
);
await page.goto(`/bookings/upcoming`);
await bookingsGetResponse;
await test.step("Create filter segment with team member userIds", async () => {
await page.getByTestId("add-filter-button").click();
await page.getByTestId("add-filter-item-userId").click();
await page.getByTestId(`select-filter-options-userId`).getByRole("option", { name: "Second" }).click();
await page.getByTestId(`select-filter-options-userId`).getByRole("option", { name: "Third" }).click();
await page.keyboard.press("Escape");
const segmentName = "Team Members Filter";
await page.getByTestId("save-filter-segment-button").click();
await page.getByTestId("save-filter-segment-name").fill(segmentName);
await page.getByTestId("save-filter-segment-dialog").getByRole("button", { name: "Save" }).click();
await expect(page.getByText("Filter segment saved")).toBeVisible();
});
await test.step("Remove one team member from team", async () => {
await prisma.membership.delete({
where: {
userId_teamId: {
userId: secondUser.id,
teamId: team.id,
},
},
});
});
await test.step("Verify filter segment still works and UI is not stuck", async () => {
await page.reload();
await page.waitForLoadState("domcontentloaded");
await expect(page.getByTestId("add-filter-button")).toBeVisible();
await expect(page.getByText("You do not have permissions")).toBeHidden();
});
});
});
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 87 KiB

@@ -1366,13 +1366,11 @@
"code_copied": "تم نسخ الرمز!",
"how_you_want_add_cal_site": "كيف تريد إضافة {{appName}} إلى موقعك الإلكتروني؟",
"choose_ways_put_cal_site": "اختر واحدة من الطرق التالية لوضع {{appName}} على موقعك الإلكتروني.",
"setting_up_zapier": "إعداد دمج Zapier الخاص بك",
"setting_up_make": "إعداد دمج Make لديك",
"generate_api_key": "إنشاء مفتاح API",
"generate_api_key_description": "إنشاء مفتاح API لاستخدامه مع {{appName}} على",
"your_unique_api_key": "مفتاح API الفريد الخاص بك",
"copy_safe_api_key": "انسخ مفتاح API هذا واحفظه في مكان آمن. إذا فقدت هذا المفتاح، يتوجب عليك إنشاء مفتاح جديد.",
"zapier_setup_instructions": "<0>سجل الدخول إلى حساب Zapier الخاص بك وأنشئ Zap جديد.</0><1>حدد Cal.com كتطبيق Trigger لديك. اختر أيضًا حدث Trigger.</1><2>اختر حسابك ثم أدخل مفتاح API الفريد الخاص بك.</2><3>اختبر Trigger الخاص بك.</3><4>أنت جاهز!</4>",
"make_setup_instructions_1": "انتقل إلى <0>رابط دعوة Make</0> وقم بتثبيت تطبيق Cal.com.",
"make_setup_instructions_2": "قم بتسجيل الدخول إلى حساب Make الخاص بك وإنشاء سيناريو جديد.",
"make_setup_instructions_3": "اختر Cal.com كتطبيق المشغل. اختر أيضًا حدث المشغل.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "أو",
"nevermind": "غير مهتم",
"go_to": "انتقل إلى: ",
"zapier_invite_link": "رابط دعوة Zapier",
"meeting_url_provided_after_confirmed": "سيتم إنشاء رابط الاجتماع بمجرد تأكيد الحدث.",
"dynamically_display_attendee_or_organizer": "عرض اسم حضورك بشكل ديناميكي، أو اسمك عندما يراه حضورك",
"event_location": "موقع الحدث",
@@ -1366,13 +1366,11 @@
"code_copied": "Kod kopyalandı!",
"how_you_want_add_cal_site": "{{appName}} saytınıza necə əlavə etmək istəyirsiniz?",
"choose_ways_put_cal_site": "{{appName}} saytınıza yerləşdirmək üçün aşağıdakı üsullardan birini seçin.",
"setting_up_zapier": "Zapier inteqrasiyanızı qurmaq",
"setting_up_make": "Make inteqrasiyasını qurmaq",
"generate_api_key": "API açarını yarat",
"generate_api_key_description": "{{appName}} ilə istifadə etmək üçün API açarını yarat",
"your_unique_api_key": "Sizin unikal API açarınız",
"copy_safe_api_key": "Bu API açarını kopyalayın və təhlükəsiz bir yerdə saxlayın. Bu açarı itirsəniz, yenisini yaratmalısınız.",
"zapier_setup_instructions": "<0>Zapier hesabınıza daxil olun və yeni bir Zap yaradın.</0><1>Cal.com-u Trigger tətbiqi olaraq seçin. Həmçinin bir Trigger hadisəsi seçin.</1><2>Hesabınızı seçin və sonra Unikal API Açarınızı daxil edin.</2><3>Trigger-i test edin.</3><4>Hazırsınız!</4>",
"make_setup_instructions_1": "<0>Make Dəvət Linkinə</0> gedin və Cal.com tətbiqini quraşdırın.",
"make_setup_instructions_2": "Make hesabınıza daxil olun və yeni Ssenari yaradın.",
"make_setup_instructions_3": "Cal.com-u Trigger tətbiqi kimi seçin. Həmçinin Trigger hadisəsini seçin.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "və ya",
"nevermind": "Boş ver",
"go_to": "Get: ",
"zapier_invite_link": "Zapier Dəvət Linki",
"meeting_url_provided_after_confirmed": "Tədbir təsdiqləndikdən sonra görüş URL-i yaradılacaq.",
"dynamically_display_attendee_or_organizer": "İştirakçının adını sizin üçün və ya iştirakçınızın baxdığı zaman sizin adınızı dinamik olaraq göstərin",
"event_location": "Tədbirin yeri",
@@ -1366,13 +1366,11 @@
"code_copied": "Кодът е копиран!",
"how_you_want_add_cal_site": "Как искате да добавите {{appName}} към вашия сайт?",
"choose_ways_put_cal_site": "Изберете един от следните начини за добавяне на {{appName}} към вашия сайт.",
"setting_up_zapier": "Настройване на вашата Zapier интеграция",
"setting_up_make": "Настройване на вашата Make интеграция",
"generate_api_key": "Генериране на API ключ",
"generate_api_key_description": "Генерирайте API ключ за използване с {{appName}} на",
"your_unique_api_key": "Вашият уникален API ключ",
"copy_safe_api_key": "Копирайте този API ключ и го запазете на сигурно място. Ако изгубите този ключ, ще трябва да генерирате нов.",
"zapier_setup_instructions": "<0>Влезте във вашия Zapier акаунт и създайте нов Zap.</0><1>Изберете Cal.com като ваше Trigger приложение. Изберете и Trigger събитие.</1><2>Изберете вашия акаунт и въведете вашия уникален API ключ.</2><3>Тествайте вашия Trigger.</3><4>Готово!</4>",
"make_setup_instructions_1": "Отидете на <0>Make Invite Link</0> и инсталирайте приложението Cal.com.",
"make_setup_instructions_2": "Влезте във вашия Make акаунт и създайте нов сценарий.",
"make_setup_instructions_3": "Изберете Cal.com като ваше приложение за тригер. Също така изберете събитие за тригер.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "или",
"nevermind": "Няма значение",
"go_to": "Отиди на: ",
"zapier_invite_link": "Линк за покана в Zapier",
"meeting_url_provided_after_confirmed": "URL адресът на срещата ще бъде създаден след потвърждение на събитието.",
"dynamically_display_attendee_or_organizer": "Динамично показва името на участника за вас или вашето име, ако се преглежда от участника",
"event_location": "Локация на събитието",
@@ -1366,13 +1366,11 @@
"code_copied": "কোড অনুলিপি!",
"how_you_want_add_cal_site": "আপনি কিভাবে যোগ করতে চান {{appName}} আপনার সাইটে?",
"choose_ways_put_cal_site": "রাখার জন্য নিম্নলিখিত একটি উপায় চয়ন করুন {{appName}} আপনার সাইটে",
"setting_up_zapier": "আপনার জ্যাপিয়ার ইন্টিগ্রেশন সেট আপ করা",
"setting_up_make": "আপনার মেক ইন্টিগ্রেশন সেট আপ করা",
"generate_api_key": "এপিআই কী উত্পন্ন করুন",
"generate_api_key_description": "ব্যবহার করার জন্য একটি এপিআই কী তৈরি করুন {{appName}} এ",
"your_unique_api_key": "আপনার অনন্য এপিআই কী",
"copy_safe_api_key": "এই এপিআই কীটি অনুলিপি করুন এবং এটিকে নিরাপদ কোথাও সংরক্ষণ করুন। ",
"zapier_setup_instructions": "<0>আপনার জ্যাপিয়ার অ্যাকাউন্টে লগইন করুন এবং একটি নতুন জ্যাপ তৈরি করুন।</0><1>আপনার ট্রিগার অ্যাপ্লিকেশন হিসাবে Cal.com নির্বাচন করুন। </1><2>আপনার অ্যাকাউন্টটি চয়ন করুন এবং তারপরে আপনার অনন্য এপিআই কী লিখুন।</2><3>আপনার ট্রিগার পরীক্ষা করুন।</3><4>আপনি সেট!</4>",
"make_setup_instructions_1": "<0>Make Invite Link</0>-এ যান এবং Cal.com অ্যাপ ইনস্টল করুন।",
"make_setup_instructions_2": "আপনার Make অ্যাকাউন্টে লগ ইন করুন এবং একটি নতুন সিনারিও তৈরি করুন।",
"make_setup_instructions_3": "Cal.com কে আপনার ট্রিগার অ্যাপ হিসাবে নির্বাচন করুন। একটি ট্রিগার ইভেন্টও বেছে নিন।",
@@ -1417,7 +1415,6 @@
"or_lowercase": "বা",
"nevermind": "কিছু না",
"go_to": "যেতে: ",
"zapier_invite_link": "জ্যাপিয়ার আমন্ত্রণ লিঙ্ক",
"meeting_url_provided_after_confirmed": "ইভেন্টটি নিশ্চিত হয়ে গেলে একটি সভা ইউআরএল তৈরি করা হবে।",
"dynamically_display_attendee_or_organizer": "আপনার জন্য আপনার অংশগ্রহণকারীদের নামটি গতিশীলভাবে প্রদর্শন করুন বা আপনার নাম যদি এটি আপনার অংশগ্রহণকারী দ্বারা দেখা হয়",
"event_location": "ইভেন্টের অবস্থান",
@@ -1366,13 +1366,11 @@
"code_copied": "Codi copiat!",
"how_you_want_add_cal_site": "Com vols afegir {{appName}} al teu lloc web?",
"choose_ways_put_cal_site": "Tria una de les següents maneres per posar {{appName}} al teu lloc web",
"setting_up_zapier": "Configurant la integració amb Zapier",
"setting_up_make": "Configurant la integració amb Make",
"generate_api_key": "Genera una clau API",
"generate_api_key_description": "Genera una clau API per utilitzar amb {{appName}} a",
"your_unique_api_key": "La teva clau API única",
"copy_safe_api_key": "Copia aquesta clau API i guarda-la en un lloc segur. Si la perds, hauràs de generar-ne una de nova",
"zapier_setup_instructions": "<0>Inicia sessió al teu compte de Zapier i crea un nou Zap.</0><1>Selecciona Cal.com com a aplicació d'activació. També tria un esdeveniment d'activació.</1><2>Tria el teu compte i introdueix la teva clau API única.</2><3>Prova el teu activador.</3><4>Ja està configurat!</4>",
"make_setup_instructions_1": "Ves a <0>Make Invite Link</0> i instal·la l'aplicació Cal.com.",
"make_setup_instructions_2": "Inicia sessió al teu compte de Make i crea un nou escenari.",
"make_setup_instructions_3": "Selecciona Cal.com com la teva aplicació de disparador. També tria un esdeveniment de disparador.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "o",
"nevermind": "No importa",
"go_to": "Anar a: ",
"zapier_invite_link": "Enllaç d'invitació de Zapier",
"meeting_url_provided_after_confirmed": "Es crearà un URL de reunió un cop es confirmi l'esdeveniment.",
"dynamically_display_attendee_or_organizer": "Mostra dinàmicament el nom del teu assistent per a tu, o el teu nom si ho veu el teu assistent",
"event_location": "Ubicació de l'esdeveniment",
@@ -1366,13 +1366,11 @@
"code_copied": "Kód byl zkopírován!",
"how_you_want_add_cal_site": "Jak chcete přidat {{appName}} na vaše stránky?",
"choose_ways_put_cal_site": "Vyberte si některý z následujících způsobů, jak umístit {{appName}} na své stránky.",
"setting_up_zapier": "Nastavení integrace Zapier",
"setting_up_make": "Nastavení integrace s Make",
"generate_api_key": "Vygenerovat klíč API",
"generate_api_key_description": "Vygenerujte klíč API pro použití s {{appName}} na adrese",
"your_unique_api_key": "Váš unikátní klíč API",
"copy_safe_api_key": "Zkopírujte tento klíč API a uschovejte si ho na bezpečné místo. Pokud tento klíč ztratíte, musíte vygenerovat nový.",
"zapier_setup_instructions": "<0>Přihlaste se ke svému účtu Zapier a vytvořte nový Zap.</0><1>Vyberte Cal.com jako svoji aplikaci Trigger. Také vyberte událost Trigger.</1><2>Vyberte svůj účet a pak zadejte svůj unikátní klíč API.</2><3>Otestujte svůj Trigger.</3><4>Všechno máte nastaveno!</4>",
"make_setup_instructions_1": "Přejděte na <0>Make Invite Link</0> a nainstalujte aplikaci Cal.com.",
"make_setup_instructions_2": "Přihlaste se ke svému účtu Make a vytvořte nový scénář.",
"make_setup_instructions_3": "Vyberte Cal.com jako svou spouštěcí aplikaci. Také vyberte spouštěcí událost.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "nebo",
"nevermind": "Je mi to jedno",
"go_to": "Přejít na: ",
"zapier_invite_link": "Odkaz s pozvánkou do služby Zapier",
"meeting_url_provided_after_confirmed": "Po potvrzení události bude vytvořena adresa URL schůzky.",
"dynamically_display_attendee_or_organizer": "Dynamické zobrazení jména účastníka pro vás nebo vašeho jména pro účastníka",
"event_location": "Místo události",
@@ -1366,13 +1366,11 @@
"code_copied": "Kode kopieret!",
"how_you_want_add_cal_site": "Hvordan vil du tilføje {{appName}} til dit websted?",
"choose_ways_put_cal_site": "Vælg en af følgende måder at sætte {{appName}} på dit websted.",
"setting_up_zapier": "Opsætning af din Zapier integration",
"setting_up_make": "Opsætning af din Make-integration",
"generate_api_key": "Generér API-nøgle",
"generate_api_key_description": "Generér en API-nøgle til brug med {{appName}} på",
"your_unique_api_key": "Din unikke API-nøgle",
"copy_safe_api_key": "Kopiér denne API-nøgle og gem den et sikkert sted. Hvis du mister denne nøgle, skal du generere en ny.",
"zapier_setup_instructions": "<0>Log ind på din Zapier-konto og opret en ny Zap.</0><1>Vælg Cal.com som din Trigger app. Vælg også en Trigger event. </1><2>Vælg din konto og indtast derefter din unikke API-nøgle.</2><3>Test din Trigger.</3><4>Du er klar!</4>",
"make_setup_instructions_1": "Gå til <0>Make invitationslink</0> og installer Cal.com-appen.",
"make_setup_instructions_2": "Log ind på din Make-konto og opret et nyt scenarie.",
"make_setup_instructions_3": "Vælg Cal.com som din trigger-app. Vælg også en trigger-begivenhed.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "eller",
"nevermind": "Fortryd",
"go_to": "Gå til: ",
"zapier_invite_link": "Zapier Invitationslink",
"meeting_url_provided_after_confirmed": "Et møde URL vil blive oprettet, når begivenheden er bekræftet.",
"dynamically_display_attendee_or_organizer": "Vis dynamisk navnet på din deltager for dig, eller dit navn hvis det ses af din deltager",
"event_location": "Begivenhedens placering",
@@ -1366,13 +1366,11 @@
"code_copied": "Code kopiert!",
"how_you_want_add_cal_site": "Wie möchten Sie {{appName}} zu Ihrer Website hinzufügen?",
"choose_ways_put_cal_site": "Wählen Sie eine der folgenden Möglichkeiten, um {{appName}} in Ihre Website zu integrieren.",
"setting_up_zapier": "Einrichtung Ihrer Zapier-Integration",
"setting_up_make": "Einrichtung Ihrer Make-Integration",
"generate_api_key": "Api Key generieren",
"generate_api_key_description": "Generieren Sie einen API-Schlüssel für {{appName}} unter",
"your_unique_api_key": "Ihr eindeutiger API-Key",
"copy_safe_api_key": "Kopieren Sie diesen API-Schlüssel und speichern Sie ihn an einem sicheren Ort. Wenn Sie diesen Schlüssel verlieren, müssen Sie einen neuen generieren.",
"zapier_setup_instructions": "<0>Loggen Sie sich in Ihr Zapier-Konto ein und erstelle einen neuen Zap.</0><1>Wählen Sie Cal.com als ihre Trigger-App aus. Wählen Sie auch ein Auslöserereignis.</0></1>Wählen Sie ihr Konto aus und geben dann ihren eindeutigen API-Key ein.</2><3>Testen Sie den Trigger.</3><4>Alles fertig!</4>",
"make_setup_instructions_1": "Gehe zu <0>Make Einladungslink</0> und installiere die Cal.com-App.",
"make_setup_instructions_2": "Melde dich bei deinem Make-Konto an und erstelle ein neues Szenario.",
"make_setup_instructions_3": "Wähle Cal.com als deine Trigger-App. Wähle auch ein Trigger-Ereignis.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "oder",
"nevermind": "Abbrechen",
"go_to": "Gehe zu: ",
"zapier_invite_link": "Zapier Einladungs-Link",
"meeting_url_provided_after_confirmed": "Eine Termin-URL wird angelegt, sobald der Termin bestätigt wurde.",
"dynamically_display_attendee_or_organizer": "Zeigt dynamisch entweder Ihnen den Namen Ihres Teilnehmers bzw. Ihrer Teilnehmerin an oder zeigt Ihrem/Ihrer Teilnehmerin Ihren Namen an",
"event_location": "Ort des Termins",
@@ -1366,13 +1366,11 @@
"code_copied": "Ο κώδικας αντιγράφηκε!",
"how_you_want_add_cal_site": "Πώς θέλετε να προσθέσετε το {{appName}} στον ιστότοπό σας;",
"choose_ways_put_cal_site": "Επιλέξτε έναν από τους παρακάτω τρόπους για να προσθέσετε το {{appName}} στον ιστότοπό σας.",
"setting_up_zapier": "Ρύθμιση της ενσωμάτωσης Zapier",
"setting_up_make": "Ρύθμιση της ενσωμάτωσης Make",
"generate_api_key": "Δημιουργία κλειδιού API",
"generate_api_key_description": "Δημιουργήστε ένα κλειδί API για χρήση με το {{appName}} στο",
"your_unique_api_key": "Το μοναδικό σας κλειδί API",
"copy_safe_api_key": "Αντιγράψτε αυτό το κλειδί API και αποθηκεύστε το σε ασφαλές μέρος. Αν χάσετε αυτό το κλειδί, θα πρέπει να δημιουργήσετε νέο.",
"zapier_setup_instructions": "<0>Συνδεθείτε στον λογαριασμό σας στο Zapier και δημιουργήστε ένα νέο Zap.</0><1>Επιλέξτε το Cal.com ως την εφαρμογή Trigger. Επίσης επιλέξτε ένα συμβάν Trigger.</1><2>Επιλέξτε τον λογαριασμό σας και εισάγετε το Μοναδικό Κλειδί API σας.</2><3>Δοκιμάστε το Trigger σας.</3><4>Είστε έτοιμοι!</4>",
"make_setup_instructions_1": "Πηγαίνετε στο <0>Make Invite Link</0> και εγκαταστήστε την εφαρμογή Cal.com.",
"make_setup_instructions_2": "Συνδεθείτε στον λογαριασμό σας στο Make και δημιουργήστε ένα νέο Σενάριο.",
"make_setup_instructions_3": "Επιλέξτε το Cal.com ως την εφαρμογή Ενεργοποίησης. Επίσης επιλέξτε ένα συμβάν Ενεργοποίησης.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "ή",
"nevermind": "Άκυρο",
"go_to": "Μετάβαση σε: ",
"zapier_invite_link": "Σύνδεσμος πρόσκλησης Zapier",
"meeting_url_provided_after_confirmed": "Ο σύνδεσμος της συνάντησης θα δημιουργηθεί μόλις επιβεβαιωθεί το συμβάν.",
"dynamically_display_attendee_or_organizer": "Εμφανίζει δυναμικά το όνομα του συμμετέχοντα για εσάς, ή το δικό σας όνομα αν προβάλλεται από τον συμμετέχοντα",
"event_location": "Τοποθεσία συμβάντος",
+27 -4
View File
@@ -67,6 +67,8 @@
"disable_recording_for_organizer": "Disable recording for organizer",
"disable_transcription_for_guests": "Disable transcription for guests",
"disable_transcription_for_organizer": "Disable transcription for organizer",
"require_email_for_guests": "Require email for guests",
"require_email_for_guests_description": "When enabled, guests must provide their email address to join the call",
"event_request_declined_recurring": "Your recurring event request has been declined",
"event_request_cancelled": "Your scheduled event was canceled",
"event_request_reassigned": "Your scheduled event was reassigned",
@@ -680,6 +682,8 @@
"continue_as_guest": "Continue as Guest",
"join_as_guest": "Join as Guest",
"ideal_for_one_time_calls": "Ideal for one-time calls",
"hosts_must_use_login": "Hosts must login to cal.com and then join this meeting",
"invalid_guest_email": "We don't recognize this email address for this meeting. Double-check you're using the same email that received the invitation.",
"any_location": "Any location",
"yes": "Yes",
"no": "No",
@@ -1383,13 +1387,11 @@
"code_copied": "Code copied!",
"how_you_want_add_cal_site": "How do you want to add {{appName}} to your site?",
"choose_ways_put_cal_site": "Choose one of the following ways to put {{appName}} on your site.",
"setting_up_zapier": "Setting up your Zapier integration",
"setting_up_make": "Setting up your Make integration",
"generate_api_key": "Generate API key",
"generate_api_key_description": "Generate an API key to use with {{appName}} at",
"your_unique_api_key": "Your unique API key",
"copy_safe_api_key": "Copy this API key and save it somewhere safe. If you lose this key you have to generate a new one.",
"zapier_setup_instructions": "<0>Log into your Zapier account and create a new Zap.</0><1>Select Cal.com as your Trigger app. Also choose a Trigger event.</1><2>Choose your account and then enter your Unique API Key.</2><3>Test your Trigger.</3><4>You're set!</4>",
"make_setup_instructions_1": "Go to <0>Make Invite Link</0> and install the Cal.com app.",
"make_setup_instructions_2": "Log into your Make account and create a new Scenario.",
"make_setup_instructions_3": "Select Cal.com as your Trigger app. Also choose a Trigger event.",
@@ -1434,7 +1436,6 @@
"or_lowercase": "or",
"nevermind": "Nevermind",
"go_to": "Go to: ",
"zapier_invite_link": "Zapier Invite Link",
"meeting_url_provided_after_confirmed": "A Meeting URL will be created once the event is confirmed.",
"dynamically_display_attendee_or_organizer": "Dynamically display the name of your attendee for you, or your name if it's viewed by your attendee",
"event_location": "Event's location",
@@ -1456,6 +1457,8 @@
"track_your_meetings": "Track your meetings, get notifications, and manage your schedule more effectively.",
"track_meetings_and_manage_schedule": "Track your meetings and manage your schedule more effectively",
"failed_to_join_call": "Failed to join the call. Please try again.",
"please_enter_name": "Please enter your name",
"please_enter_name_and_email": "Please enter your name and email address",
"choose_how_you_d_like_to_join_call": "Choose how you'd like to join this call",
"choose_how_you_d_like_to_appear_on_the_call": "Choose how you'd like to appear on the call",
"new_event_trigger": "when new event is booked",
@@ -1692,6 +1695,8 @@
"profile_picture": "Profile Picture",
"upload": "Upload",
"add_profile_photo": "Add profile photo",
"it_appears_you_are_using_company_email": "It appears that you are utilizing your company email.",
"explore_organizational_plan_description": "Explore our new organizational plan! It boosts collaboration across event types and directs lost customers to the right team member.",
"token_address": "Token Address",
"blockchain": "Blockchain",
"old_password": "Old password",
@@ -1800,7 +1805,6 @@
"team_url": "Team URL",
"team_members": "Team members",
"more": "More",
"cal_ai_workflows": "Cal.ai Workflows",
"and_count_more": "and {{count}} more",
"more_page_footer": "We view the mobile application as an extension of the web application. If you are performing any complicated actions, please refer back to the web application.",
"workflow_example_1": "Send SMS reminder 24 hours before event starts to attendee",
@@ -2494,6 +2498,7 @@
"additional_url_parameters": "Additional URL parameters",
"about_your_organization": "About your organization",
"about_your_organization_description": "Organizations are shared environments where you can create multiple teams with shared members, event types, apps, workflows and more.",
"create_your_team": "Create your team",
"create_your_teams": "Create your teams",
"create_your_teams_description": "Start scheduling together by adding your team members to your organization",
"invite_organization_admins": "Invite organization members",
@@ -2527,6 +2532,15 @@
"resolve": "Resolve",
"no_organization_slug": "There was an error creating teams for this organization. Missing URL slug.",
"copy_link_org": "Copy link to organization",
"welcome_to_organizations": "Welcome to Organizations!",
"organizations_welcome_description": "We're excited to connect your bookers with your team members. With your Teams's plan you get:",
"1_parent_team_unlimited_subteams": "1 parent team and unlimited sub-teams",
"organization_workflows": "Organization workflows",
"custom_subdomain": "Yourcompany.cal.com subdomain",
"instant_meetings": "Cal.com instant meetings",
"collective_round_robin_events": "Collective & Round-robin events",
"routing_forms": "Routing forms",
"team_workflows": "Team workflows",
"404_the_org": "The organization",
"404_the_team": "The team",
"404_claim_entity_org": "Claim your subdomain for your organization",
@@ -3482,6 +3496,10 @@
"low_credits_warning_message_user": "Your Cal.com account is running low on credits. To avoid any disruption in service, please purchase additional credits. If your balance runs out, SMS messages will stop sending and will be sent as emails instead.",
"credit_limit_reached_message": "Your Cal.com team {{teamName}} has run out of credits. As a result, SMS messages are now being sent via email instead. To resume sending SMS, please purchase additional credits.",
"credit_limit_reached_message_user": "Your Cal.com account has run out of credits. As a result, SMS messages are now being sent via email instead. To resume sending SMS, please purchase additional credits.",
"cal_ai_low_credits_warning_message": "Your Cal.com team {{teamName}} is running low on credits. To avoid any disruption in Cal.ai phone service, please purchase additional credits. If your balance runs out, Cal.ai phone calls will be disabled.",
"cal_ai_low_credits_warning_message_user": "Your Cal.com account is running low on credits. To avoid any disruption in Cal.ai phone service, please purchase additional credits. If your balance runs out, Cal.ai phone calls will be disabled.",
"cal_ai_credit_limit_reached_message": "Your Cal.com team {{teamName}} has run out of credits. As a result, Cal.ai phone calls are now disabled. To resume using Cal.ai phone calls, please purchase additional credits.",
"cal_ai_credit_limit_reached_message_user": "Your Cal.com account has run out of credits. As a result, Cal.ai phone calls are now disabled. To resume using Cal.ai phone calls, please purchase additional credits.",
"current_credit_balance": "Current balance: {{balance}} credits",
"current_balance": "Current balance:",
"notification_about_your_booking": "Notification about your booking",
@@ -3906,13 +3924,18 @@
"view_previous_week": "View previous week",
"view_next_week": "View next week",
"invite_teammates": "Invite Teammates",
"invite_team_members": "Invite team members",
"organization_brand": "Organization Brand",
"organization_details": "Organization Details",
"organization_teams": "Organization Teams",
"personal_profile": "Personal Profile",
"personal_settings": "Personal Settings",
"customize_team_brand": "Customize your team brand",
"team_brand": "Team Brand",
"team_brand_subtitle": "Customize your team's branding with a logo and color",
"team_details": "Team Details",
"team_invite": "Team Invite",
"team_invite_subtitle": "Invite team members to collaborate and schedule together",
"team_onboarding_details_subtitle": "Add your team's name and create a unique URL for your team",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1366,13 +1366,11 @@
"code_copied": "¡Código copiado!",
"how_you_want_add_cal_site": "¿Cómo quieres añadir {{appName}} a tu sitio?",
"choose_ways_put_cal_site": "Elige una de las siguientes formas de poner {{appName}} en tu sitio.",
"setting_up_zapier": "Configurando tu integración con Zapier",
"setting_up_make": "Configurando tu integración con Make",
"generate_api_key": "Generar clave API",
"generate_api_key_description": "Genera una clave API para usar con {{appName}} en",
"your_unique_api_key": "Tu clave API única",
"copy_safe_api_key": "Copia esta clave API y guárdala en un lugar seguro. Si pierdes esta clave, tendrás que generar una nueva.",
"zapier_setup_instructions": "<0>Inicia sesión en tu cuenta de Zapier y crea un nuevo Zap.</0><1>Selecciona Cal.com como tu aplicación de disparador. También elige un evento de disparador.</1><2>Elige tu cuenta e ingresa tu Clave API Única.</2><3>Prueba tu disparador.</3><4>¡Listo!</4>",
"make_setup_instructions_1": "Ve a <0>Enlace de invitación de Make</0> e instala la aplicación de Cal.com.",
"make_setup_instructions_2": "Inicia sesión en tu cuenta de Make y crea un nuevo escenario.",
"make_setup_instructions_3": "Selecciona Cal.com como tu aplicación de activación. También elige un evento de activación.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "o",
"nevermind": "No importa",
"go_to": "Ir a: ",
"zapier_invite_link": "Enlace de invitación de Zapier",
"meeting_url_provided_after_confirmed": "Se creará una URL de la reunión una vez que el evento esté confirmado.",
"dynamically_display_attendee_or_organizer": "Muestra dinámicamente el nombre de tu asistente para ti, o tu nombre si lo ve tu asistente",
"event_location": "Ubicación del evento",
@@ -1366,13 +1366,11 @@
"code_copied": "¡Código copiado!",
"how_you_want_add_cal_site": "¿Cómo quiere añadir {{appName}} a su sitio?",
"choose_ways_put_cal_site": "Elija una de las siguientes maneras de poner {{appName}} en su sitio.",
"setting_up_zapier": "Configurando su integración con Zapier",
"setting_up_make": "Configurando su integración Make",
"generate_api_key": "Generar clave API",
"generate_api_key_description": "Generar una clave API para usar con {{appName}} en",
"your_unique_api_key": "Su clave API única",
"copy_safe_api_key": "Copie esta clave API y guárdela en algún lugar seguro. Si pierde esta clave tendrá que generar una nueva.",
"zapier_setup_instructions": "<0>Inicie sesión en su cuenta de Zapier y cree un nuevo Zap.</0><1>Seleccione Cal.com cómo su aplicación Trigger. También elija un evento Trigger.</1><2>Seleccione su cuenta e ingrese su Clave API única.</2><3>Pruebe su Trigger.</3><4>¡Listo!</4>",
"make_setup_instructions_1": "Ve a <0>Enlace de invitación de Make</0> e instala la aplicación de Cal.com.",
"make_setup_instructions_2": "Inicia sesión en tu cuenta de Make y crea un nuevo escenario.",
"make_setup_instructions_3": "Selecciona Cal.com como tu aplicación de activación. También elige un evento de activación.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "o",
"nevermind": "No tiene importancia",
"go_to": "Ir a: ",
"zapier_invite_link": "Zapier Invite Link",
"meeting_url_provided_after_confirmed": "Se creará una URL de la reunión una vez que se confirme el evento.",
"dynamically_display_attendee_or_organizer": "Mostrar dinámicamente el nombre de su asistente para usted, o su nombre si es visto por su asistente",
"event_location": "Ubicación del evento",
@@ -1366,13 +1366,11 @@
"code_copied": "Kood kopeeritud!",
"how_you_want_add_cal_site": "Kuidas soovite rakenduse {{appName}} oma saidile lisada?",
"choose_ways_put_cal_site": "Valige üks järgmistest viisidest, et lisada {{appName}} oma saidile.",
"setting_up_zapier": "Zapieri integratsiooni seadistamine",
"setting_up_make": "Make integratsiooni seadistamine",
"generate_api_key": "Generate API võti",
"generate_api_key_description": "Generate API võti, mida kasutada rakendusega {{appName}} aadressil",
"your_unique_api_key": "Teie ainulaadne API võti",
"copy_safe_api_key": "Kopeerige see API võti ja salvestage see kuskile turvalisse kohta. Kui kaotate selle võtme, peate looma uue.",
"zapier_setup_instructions": "<0>Logige sisse oma Zapieri kontole ja looge uus Zap.</0><1>Valige oma Triggeri rakenduseks Cal.com. Valige ka Triggeri sündmus.</1><2>Valige oma konto ja seejärel sisestage teie unikaalne API võti.</2><3>Testige oma päästikut.</3><4>Olete valmis!</4>",
"make_setup_instructions_1": "Mine <0>Make kutselingile</0> ja paigalda Cal.com rakendus.",
"make_setup_instructions_2": "Logi oma Make kontosse sisse ja loo uus stsenaarium.",
"make_setup_instructions_3": "Vali Cal.com oma päästikrakenduseks. Vali ka päästikusündmus.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "või",
"nevermind": "Ära pane tähele",
"go_to": "Minema: ",
"zapier_invite_link": "Zapieri kutselink",
"meeting_url_provided_after_confirmed": "Koosoleku URL luuakse pärast sündmuse kinnitamist.",
"dynamically_display_attendee_or_organizer": "Kuva dünaamiliselt teie jaoks osaleja nimi või teie nimi, kui teie osaleja seda vaatab",
"event_location": "Sündmuse asukoht",
@@ -1366,13 +1366,11 @@
"code_copied": "Kodea kopiatuta!",
"how_you_want_add_cal_site": "Nola gehitu nahi duzu {{appName}} zure webgunean?",
"choose_ways_put_cal_site": "Aukeratu ondorengo bideetako bat {{appName}} zure webgunean jartzeko.",
"setting_up_zapier": "Zapier integrazioa konfiguratzen",
"setting_up_make": "Make integrazioa konfiguratzen",
"generate_api_key": "Sortu API gakoa",
"generate_api_key_description": "Sortu API gako bat {{appName}}-rekin erabiltzeko hemen:",
"your_unique_api_key": "Zure API gako bakarra",
"copy_safe_api_key": "Kopiatu API gako hau eta gorde leku seguru batean. Gako hau galtzen baduzu, berri bat sortu beharko duzu.",
"zapier_setup_instructions": "<0>Hasi saioa zure Zapier kontuan eta sortu Zap berri bat.</0><1>Aukeratu Cal.com zure Trigger aplikazio gisa. Aukeratu ere Trigger gertaera bat.</1><2>Aukeratu zure kontua eta sartu zure API Gako Bakarra.</2><3>Probatu zure Trigger-a.</3><4>Prest zaude!</4>",
"make_setup_instructions_1": "Joan <0>Make Gonbidapen Esteka</0>-ra eta instalatu Cal.com aplikazioa.",
"make_setup_instructions_2": "Hasi saioa zure Make kontuan eta sortu Agertoki berri bat.",
"make_setup_instructions_3": "Hautatu Cal.com zure Trigger aplikazio gisa. Hautatu ere Trigger gertaera bat.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "edo",
"nevermind": "Ez du axola",
"go_to": "Joan hona: ",
"zapier_invite_link": "Zapier gonbidapen esteka",
"meeting_url_provided_after_confirmed": "Bilera URL bat sortuko da gertaera baieztatzen denean.",
"dynamically_display_attendee_or_organizer": "Parte-hartzailearen izena dinamikoki erakutsiko dizu, edo zure izena parte-hartzaileak ikusten badu",
"event_location": "Gertaeraren kokapena",
@@ -1366,13 +1366,11 @@
"code_copied": "Koodi kopioitu!",
"how_you_want_add_cal_site": "Miten haluat lisätä {{appName}}:n sivustollesi?",
"choose_ways_put_cal_site": "Valitse yksi seuraavista tavoista lisätä {{appName}} sivustollesi",
"setting_up_zapier": "Zapier-integraation määrittäminen",
"setting_up_make": "Make-integraation määrittäminen",
"generate_api_key": "Luo API-avain",
"generate_api_key_description": "Luo API-avain käytettäväksi {{appName}}:n kanssa osoitteessa",
"your_unique_api_key": "Yksilöllinen API-avaimesi",
"copy_safe_api_key": "Kopioi tämä API-avain ja tallenna se turvalliseen paikkaan. Jos kadotat avaimen, sinun täytyy luoda uusi",
"zapier_setup_instructions": "<0>Kirjaudu Zapier-tilillesi ja luo uusi Zap.</0><1>Valitse Cal.com käynnistyssovellukseksi. Valitse myös käynnistystapahtuma.</1><2>Valitse tilisi ja syötä yksilöllinen API-avaimesi.</2><3>Testaa käynnistintä.</3><4>Valmista!</4>",
"make_setup_instructions_1": "Mene kohtaan <0>Make-kutsulinkki</0> ja asenna Cal.com-sovellus.",
"make_setup_instructions_2": "Kirjaudu Make-tilillesi ja luo uusi skenaario.",
"make_setup_instructions_3": "Valitse Cal.com käynnistyssovellukseksi. Valitse myös käynnistystapahtuma.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "tai",
"nevermind": "Unohda",
"go_to": "Siirry: ",
"zapier_invite_link": "Zapier-kutsulinkki",
"meeting_url_provided_after_confirmed": "Kokouslinkki luodaan, kun tapahtuma on vahvistettu.",
"dynamically_display_attendee_or_organizer": "Näyttää dynaamisesti osallistujan nimen sinulle tai sinun nimesi osallistujalle",
"event_location": "Tapahtuman sijainti",
@@ -1366,13 +1366,11 @@
"code_copied": "Code copié !",
"how_you_want_add_cal_site": "Comment souhaitez-vous ajouter {{appName}} à votre site ?",
"choose_ways_put_cal_site": "Choisissez l'une des méthodes suivantes pour mettre {{appName}} sur votre site.",
"setting_up_zapier": "Configuration de votre intégration Zapier",
"setting_up_make": "Configuration de votre intégration Make",
"generate_api_key": "Générer une clé API",
"generate_api_key_description": "Générer une clé API à utiliser avec {{appName}} sur",
"your_unique_api_key": "Votre clé API unique",
"copy_safe_api_key": "Copiez cette clé API et conservez-la dans un endroit sûr. Si vous perdez cette clé, vous devrez en générer une nouvelle.",
"zapier_setup_instructions": "<0>Connectez-vous à votre compte Zapier et créez un nouveau Zap.</0><1>Sélectionnez Cal.com comme application déclencheur. Choisissez également un événement déclencheur.</1><2>Choisissez votre compte, puis saisissez votre clé API unique.</2><3>Testez votre déclencheur.</3><4>Vous êtes prêt !</4>",
"make_setup_instructions_1": "Accédez à <0>Make Invite Link</0> et installez l'application Cal.com.",
"make_setup_instructions_2": "Connectez-vous à votre compte Make et créez un nouveau scénario.",
"make_setup_instructions_3": "Sélectionnez Cal.com comme application déclencheur. Choisissez également un événement déclencheur.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "ou",
"nevermind": "Annuler",
"go_to": "Aller à : ",
"zapier_invite_link": "Lien d'invitation Zapier",
"meeting_url_provided_after_confirmed": "Un lien de rendez-vous sera créé une fois l'événement confirmé.",
"dynamically_display_attendee_or_organizer": "Affiche dynamiquement le nom du participant pour vous ou votre nom s'il est vu par le participant",
"event_location": "Lieu de l'événement",
@@ -1366,13 +1366,11 @@
"code_copied": "הקוד הועתק!",
"how_you_want_add_cal_site": "איך ברצונך להוסיף את {{appName}} לאתר שלך?",
"choose_ways_put_cal_site": "בחר/י אחת מהדרכים הבאות כדי לשלב את {{appName}} באתר שלך.",
"setting_up_zapier": "המערכת מגדירה את השילוב שלך עם Zapier",
"setting_up_make": "המערכת מגדירה את השילוב שלך עם Make",
"generate_api_key": "יצירת מפתח ממשק תכנות יישומים (API)",
"generate_api_key_description": "צור מפתח ממשק מפתחים, API, לשימוש עם {{appName}} ב-",
"your_unique_api_key": "מפתח ה-API הייחודי שלך",
"copy_safe_api_key": "העתק/י את מפתח ה-API הזה ושמור/י אותו במקום בטוח. אם תאבד/י אותו, יהיה עליך ליצור מפתח חדש.",
"zapier_setup_instructions": "<0>התחבר/י לחשבון Zapier שלך וצור/י Zap חדש.</0><1>בחר/י את Cal.com כאפליקציית ה-Trigger. בנוסף, בחר/י אירוע Trigger.</1><2>בחר/י את החשבון שלך ולאחר מכן הזן/י את מפתח ה-API הייחודי שלך.</2><3>בדוק/י את ה-Trigger.</3><4>וזהו, הכל מוכן!</4>",
"make_setup_instructions_1": "עבור אל <0>קישור ההזמנה של Make</0> והתקן את האפליקציה של Cal.com.",
"make_setup_instructions_2": "התחבר לחשבון Make שלך וצור תרחיש חדש.",
"make_setup_instructions_3": "בחר ב-Cal.com כאפליקציית הטריגר שלך. בחר גם אירוע טריגר.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "או",
"nevermind": "לא משנה",
"go_to": "מעבר אל: ",
"zapier_invite_link": "קישור הזמנה אל Zapier",
"meeting_url_provided_after_confirmed": "ברגע שהאירוע יאושר, תיווצר כתובת URL של הפגישה.",
"dynamically_display_attendee_or_organizer": "הצגה דינאמית של השם של המשתתף/ת בפניך או של השם שלך אם הצופה הוא/היא המשתתף/ת",
"event_location": "מיקום האירוע",
@@ -1366,13 +1366,11 @@
"code_copied": "Kód kimásolva!",
"how_you_want_add_cal_site": "Hogyan szeretné hozzáadni a(z) {{appName}} alkalmazást webhelyéhez?",
"choose_ways_put_cal_site": "Válasszon egyet a következő módok közül, hogy elhelyezze az {{appName}} alkalmazást webhelyén.",
"setting_up_zapier": "A Zapier integráció beállítása",
"setting_up_make": "A Make integráció beállítása",
"generate_api_key": "API kulcs generálása",
"generate_api_key_description": "Hozzon létre egy API-kulcsot a(z) {{appName}} alkalmazáshoz a következő helyen:",
"your_unique_api_key": "Az egyedi API kulcsod",
"copy_safe_api_key": "Másolja ki ezt az API-kulcsot, és mentse el biztonságos helyre. Ha elveszíti ezt a kulcsot, újat kell létrehoznia.",
"zapier_setup_instructions": "<0>Jelentkezzen be Zapier-fiókjába, és hozzon létre egy új Zap-et.</0><1>Válassza ki a Cal.com-ot Trigger alkalmazásként. Válasszon egy Trigger eseményt is.</1><2>Válassza ki fiókját, majd adja meg egyedi API-kulcsát.</2><3>Tesztelje le a triggert.</3><4>Kész!</4> >",
"make_setup_instructions_1": "Menj a <0>Make meghívó linkre</0> és telepítsd a Cal.com alkalmazást.",
"make_setup_instructions_2": "Jelentkezz be a Make fiókodba és hozz létre egy új forgatókönyvet.",
"make_setup_instructions_3": "Válaszd a Cal.com-ot mint kiváltó alkalmazást. Válassz egy kiváltó eseményt is.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "vagy",
"nevermind": "Mégsem",
"go_to": "Menj: ",
"zapier_invite_link": "Zapier meghívó link",
"meeting_url_provided_after_confirmed": "A megbeszélés URL-címe az esemény megerősítése után jön létre.",
"dynamically_display_attendee_or_organizer": "Dinamikusan jelenítse meg a résztvevő nevét, vagy ha a résztvevő megtekinti a nevét",
"event_location": "Az esemény helyszíne",
@@ -1366,13 +1366,11 @@
"code_copied": "Codice copiato!",
"how_you_want_add_cal_site": "Come desideri aggiungere {{appName}} al tuo sito?",
"choose_ways_put_cal_site": "Scegli uno dei seguenti modi per inserire {{appName}} nel tuo sito.",
"setting_up_zapier": "Configurare l'integrazione con Zapier",
"setting_up_make": "Configurare l'integrazione con Make",
"generate_api_key": "Genera una chiave API",
"generate_api_key_description": "Genera una chiave API da usare con {{appName}} su",
"your_unique_api_key": "La tua chiave API univoca",
"copy_safe_api_key": "Copia questa chiave API e conservala in un posto sicuro. Se perdi la chiave, dovrai generarne una nuova.",
"zapier_setup_instructions": "<0>Accedi al tuo account Zapier e crea un nuovo Zap.</0><1>Seleziona Cal.com come app trigger. Scegli l'evento trigger.</1><2>Seleziona il tuo account e immetti la tua chiave API univoca.</2><3>Fai una prova del tuo trigger.</3><4>Fatto!</4>",
"make_setup_instructions_1": "Vai su <0>Make Invite Link</0> e installa l'app Cal.com.",
"make_setup_instructions_2": "Accedi al tuo account Make e crea un nuovo Scenario.",
"make_setup_instructions_3": "Seleziona Cal.com come app Trigger. Scegli anche un evento Trigger.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "o",
"nevermind": "Non importa",
"go_to": "Vai a: ",
"zapier_invite_link": "Link di invito Zapier",
"meeting_url_provided_after_confirmed": "Una volta confermato l'evento, verrà creato un URL di riunione.",
"dynamically_display_attendee_or_organizer": "Visualizza dinamicamente il nome del partecipante oppure il tuo nome, se visualizzato dal partecipante",
"event_location": "Luogo dell'evento",
@@ -1366,13 +1366,11 @@
"code_copied": "コードをコピーしました!",
"how_you_want_add_cal_site": "{{appName}} をどうやってあなたのサイトに追加しますか?",
"choose_ways_put_cal_site": "サイトに {{appName}} を設置する方法を、次の中から 1 つ選んでください。",
"setting_up_zapier": "Zapier 連携の設定",
"setting_up_make": "Make 連携の設定",
"generate_api_key": "API キーを生成",
"generate_api_key_description": "以下より {{appName}} で使用する API キーを生成する: ",
"your_unique_api_key": "あなた専用の API キー",
"copy_safe_api_key": "この API キーをコピーして安全な場所に保存してください。このキーを紛失した場合は、新しいキーを生成する必要があります。",
"zapier_setup_instructions": "<0>Zapier アカウントにログインし、新しい Zap を作成します。</0><1>Cal.com をトリガーアプリとして選択します。また、トリガーイベントも選択します。</1><2>アカウントを選択し、あなた専用の API キーを入力します。</2><3>トリガーをテストします。</3><4>これで設定は完了です!</4>",
"make_setup_instructions_1": "<0>Make招待リンク</0>にアクセスし、Cal.comアプリをインストールします。",
"make_setup_instructions_2": "Makeアカウントにログインし、新しいシナリオを作成します。",
"make_setup_instructions_3": "Cal.comをトリガーアプリとして選択します。また、トリガーイベントも選択してください。",
@@ -1417,7 +1415,6 @@
"or_lowercase": "または",
"nevermind": "無視する",
"go_to": "移動先: ",
"zapier_invite_link": "Zapier 招待リンク",
"meeting_url_provided_after_confirmed": "イベントが確定されるとミーティング URL が作成されます。",
"dynamically_display_attendee_or_organizer": "あなたに対しては出席者の名前を、出席者に対してはあなたの名前を動的に表示します",
"event_location": "イベントの場所",
@@ -1366,13 +1366,11 @@
"code_copied": "កូដត្រូវបានចម្លង!",
"how_you_want_add_cal_site": "តើអ្នកចង់បន្ថែម {{appName}} ទៅតំបន់បណ្តាញរបស់អ្នកយ៉ាងដូចម្តេច?",
"choose_ways_put_cal_site": "ជ្រើសរើសមួយក្នុងចំណោមវិធីដូចខាងក្រោមដើម្បីដាក់ {{appName}} នៅលើតំបន់បណ្តាញរបស់អ្នក។",
"setting_up_zapier": "កំពុងតំឡើងការរួមបញ្ចូល Zapier របស់អ្នក",
"setting_up_make": "កំពុងតំឡើងការរួមបញ្ចូល Make របស់អ្នក",
"generate_api_key": "បង្កើត API key",
"generate_api_key_description": "បង្កើត API key ដើម្បីប្រើជាមួយ {{appName}} នៅ",
"your_unique_api_key": "API key តែមួយគត់របស់អ្នក",
"copy_safe_api_key": "ចម្លង API key នេះ និងរក្សាទុកវាកន្លែងដែលមានសុវត្ថិភាព។ ប្រសិនបើអ្នកបាត់កូនសោនេះ អ្នកត្រូវបង្កើតថ្មីមួយ។",
"zapier_setup_instructions": "<0>ចូលទៅក្នុងគណនី Zapier របស់អ្នក និងបង្កើត Zap ថ្មី។</0><1>ជ្រើសរើស Cal.com ជាកម្មវិធី Trigger របស់អ្នក។ ក៏ជ្រើសរើសព្រឹត្តិការណ៍ Trigger មួយផងដែរ។</1><2>ជ្រើសរើសគណនីរបស់អ្នក ហើយបញ្ចូល API Key តែមួយគត់របស់អ្នក។</2><3>សាកល្បង Trigger របស់អ្នក។</3><4>អ្នកបានតំឡើងរួចរាល់!</4>",
"make_setup_instructions_1": "ចូលទៅកាន់ <0>Make Invite Link</0> និងដំឡើងកម្មវិធី Cal.com។",
"make_setup_instructions_2": "ចូលទៅក្នុងគណនី Make របស់អ្នក និងបង្កើតសេណារីយ៉ូថ្មី។",
"make_setup_instructions_3": "ជ្រើសរើស Cal.com ជាកម្មវិធីជំរុញរបស់អ្នក។ ជ្រើសរើសព្រឹត្តិការណ៍ជំរុញផងដែរ។",
@@ -1417,7 +1415,6 @@
"or_lowercase": "ឬ",
"nevermind": "មិនអីទេ",
"go_to": "ទៅកាន់: ",
"zapier_invite_link": "តំណភ្ជាប់អញ្ជើញ Zapier",
"meeting_url_provided_after_confirmed": "តំណ URL នៃកិច្ចប្រជុំនឹងត្រូវបានបង្កើតបន្ទាប់ពីព្រឹត្តិការណ៍ត្រូវបានបញ្ជាក់។",
"dynamically_display_attendee_or_organizer": "បង្ហាញឈ្មោះអ្នកចូលរួមឬអ្នករៀបចំដោយស្វ័យប្រវត្តិ",
"event_location": "ទីតាំងនៃព្រឹត្តិការណ៍",
@@ -1366,13 +1366,11 @@
"code_copied": "코드가 복사되었습니다!",
"how_you_want_add_cal_site": "귀하의 사이트에 {{appName}}을 어떻게 추가하시겠습니까?",
"choose_ways_put_cal_site": "귀하의 사이트에 {{appName}}을 추가하는 다음 방법 중 하나를 선택하십시오.",
"setting_up_zapier": "Zapier 통합을 설정하는 중",
"setting_up_make": "Make 통합을 설정하는 중",
"generate_api_key": "API 키 생성",
"generate_api_key_description": "{{appName}}에서 사용할 API 키 생성 위치",
"your_unique_api_key": "귀하의 고유 API 키",
"copy_safe_api_key": "이 API 키를 복사하여 안전한 곳에 보관하십시오. 이 키를 분실한 경우 새 키를 생성해야 합니다.",
"zapier_setup_instructions": "<0>Zapier 계정에 로그인하고 새 Zap을 만듭니다.</0><1>Cal.com을 트리거 앱으로 선택합니다. 또한 트리거 이벤트를 선택합니다.</1><2>계정을 선택한 후 고유 API 키를 입력합니다.</2><3>트리거를 테스트합니다.</3><4>설정이 완료되었습니다!</4 >",
"make_setup_instructions_1": "<0>Make 초대 링크</0>로 이동하여 Cal.com 앱을 설치하세요.",
"make_setup_instructions_2": "Make 계정에 로그인하고 새 시나리오를 만드세요.",
"make_setup_instructions_3": "Cal.com을 트리거 앱으로 선택하세요. 트리거 이벤트도 선택하세요.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "또는",
"nevermind": "괜찮습니다",
"go_to": "이동: ",
"zapier_invite_link": "Zapier 초대 링크",
"meeting_url_provided_after_confirmed": "회의 URL은 이벤트가 확정되면 생성됩니다.",
"dynamically_display_attendee_or_organizer": "참석자의 이름을 동적으로 표시하거나, 참석자가 보는 경우 귀하의 이름을 표시합니다",
"event_location": "이벤트 위치",
@@ -1366,13 +1366,11 @@
"code_copied": "Code gekopieerd!",
"how_you_want_add_cal_site": "Hoe wilt u {{appName}} aan uw website toevoegen?",
"choose_ways_put_cal_site": "Kies een van de volgende manieren om {{appName}} op uw website te plaatsen.",
"setting_up_zapier": "Uw Zapier-integratie configureren",
"setting_up_make": "Uw Make-integratie configureren",
"generate_api_key": "API-sleutel genereren",
"generate_api_key_description": "Genereer een API-sleutel die gebruikt moet worden met {{appName}} op",
"your_unique_api_key": "Uw unieke API-sleutel",
"copy_safe_api_key": "Kopieer deze API-sleutel en sla deze ergens veilig op. Als u deze sleutel verliest, moet u een nieuwe genereren.",
"zapier_setup_instructions": "<0>Meld u aan op uw Zapier-account en maak een nieuwe Zap.</0><1>Selecteer {{appName}} als uw activatie-app. Kies ook een activatiegebeurtenis.</1><2>Kies uw account en voer vervolgens uw unieke API-sleutel in.</2><3>Test uw activatie.</3><4>U bent er klaar voor!</4>",
"make_setup_instructions_1": "Ga naar <0>Make Uitnodigingslink</0> en installeer de Cal.com app.",
"make_setup_instructions_2": "Log in op je Make account en maak een nieuw scenario.",
"make_setup_instructions_3": "Selecteer Cal.com als je trigger-app. Kies ook een trigger-gebeurtenis.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "of",
"nevermind": "Laat maar",
"go_to": "Ga naar: ",
"zapier_invite_link": "Zapier-uitnodigingslink",
"meeting_url_provided_after_confirmed": "Er wordt een afspraak-URL gemaakt zodra de gebeurtenis bevestigd is.",
"dynamically_display_attendee_or_organizer": "Geef dynamisch de naam van uw deelnemer weer aan u of uw naam als het door uw deelnemer bekeken wordt",
"event_location": "Locatie gebeurtenis",
@@ -1366,13 +1366,11 @@
"code_copied": "Kode kopiert!",
"how_you_want_add_cal_site": "Hvordan vil du legge til {{appName}} på nettstedet ditt?",
"choose_ways_put_cal_site": "Velg en av følgende måter å plassere {{appName}} på nettstedet ditt.",
"setting_up_zapier": "Sette opp din Zapier-integrasjon",
"setting_up_make": "Setter opp Make-integrasjon",
"generate_api_key": "Generer API nøkkel",
"generate_api_key_description": "Generer en API nøkkel til bruk med {{appName}} på",
"your_unique_api_key": "Din unike API nøkkel",
"copy_safe_api_key": "Kopier denne API nøkkelen og lagre den et trygt sted. Hvis du mister denne nøkkelen, må du generere en ny.",
"zapier_setup_instructions": "<0>Logg på Zapier-kontoen din og opprett en ny Zap.</0><1>Velg {{appName}} som din Trigger-app. Velg også en utløserhendelse.</1><2>Velg kontoen din og skriv deretter inn din unike API nøkkel.</2><3>Test utløseren.</3><4>Du er klar!</4 >",
"make_setup_instructions_1": "Gå til <0>Make invitasjonslenke</0> og installer Cal.com-appen.",
"make_setup_instructions_2": "Logg inn på Make-kontoen din og opprett et nytt scenario.",
"make_setup_instructions_3": "Velg Cal.com som din utløserapp. Velg også en utløserhendelse.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "eller",
"nevermind": "Glem det",
"go_to": "Gå til: ",
"zapier_invite_link": "Zapier Invitasjons-lenke",
"meeting_url_provided_after_confirmed": "En møte-URL opprettes når hendelsen er bekreftet.",
"dynamically_display_attendee_or_organizer": "Vis navnet på deltakeren din dynamisk for deg, eller navnet ditt hvis det er sett av deltakeren din",
"event_location": "Hendelsens sted",
@@ -1366,13 +1366,11 @@
"code_copied": "Kod skopiowany!",
"how_you_want_add_cal_site": "Jak chcesz dodać aplikację {{appName}} do swojej witryny?",
"choose_ways_put_cal_site": "Wybierz jeden z następujących sposobów, aby umieścić aplikację {{appName}} w swojej witrynie.",
"setting_up_zapier": "Konfiguracja integracji z Zapier",
"setting_up_make": "Konfiguracja integracji z Make",
"generate_api_key": "Wygeneruj klucz interfejsu API",
"generate_api_key_description": "Wygeneruj klucz interfejsu API do użycia z aplikacją {{appName}} na",
"your_unique_api_key": "Twój niepowtarzalny klucz API",
"copy_safe_api_key": "Skopiuj ten klucz API i zapisz go w bezpiecznym miejscu. Jeśli stracisz ten klucz, trzeba będzie wygenerować nowy.",
"zapier_setup_instructions": "<0>Zaloguj się do swojego konta Zapier i utwórz nowy Zap.</0><1>Wybierz Cal.com jako aplikację wyzwalającą. Wybierz również wydarzenie wyzwalające. </1><2>Wybierz konto, a następnie wprowadź niepowtarzalny klucz interfejsu API.</2><3>Przetestuj wyzwalanie.</3><4>Wszystko gotowe!</4>",
"make_setup_instructions_1": "Przejdź do <0>Make Invite Link</0> i zainstaluj aplikację Cal.com.",
"make_setup_instructions_2": "Zaloguj się na swoje konto Make i utwórz nowy scenariusz.",
"make_setup_instructions_3": "Wybierz Cal.com jako aplikację wyzwalającą. Wybierz również zdarzenie wyzwalające.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "lub",
"nevermind": "Nieważne",
"go_to": "Przejdź do: ",
"zapier_invite_link": "Link z zaproszeniem Zapier",
"meeting_url_provided_after_confirmed": "Adres URL spotkania zostanie utworzony po potwierdzeniu wydarzenia.",
"dynamically_display_attendee_or_organizer": "Dynamicznie wyświetlaj nazwę uczestnika lub Twoją nazwę, jeśli zostanie ona wyświetlone przez Twojego uczestnika",
"event_location": "Lokalizacja wydarzenia",
@@ -1366,13 +1366,11 @@
"code_copied": "Código copiado!",
"how_you_want_add_cal_site": "Como deseja adicionar o {{appName}} ao seu site?",
"choose_ways_put_cal_site": "Escolha uma das maneiras a seguir para colocar o {{appName}} no seu site.",
"setting_up_zapier": "Configurando a sua integração com o Zapier",
"setting_up_make": "Configurando a sua integração com o Make",
"generate_api_key": "Gerar chave de API",
"generate_api_key_description": "Gerar uma chave de API para usar com o {{appName}} em",
"your_unique_api_key": "Sua chave de API única",
"copy_safe_api_key": "Copie esta chave de API e salve em um local seguro. Se perder a chave, será preciso gerar uma nova.",
"zapier_setup_instructions": "<0>Entre na conta do Zapier e crie um novo Zap.</0><1>Selecione {{appName}} como seu app de gatilho. Escolha também um evento de gatilho.</1><2>Escolha sua conta e insira sua chave de API única.</2><3>Teste seu gatilho.</3><4>Tudo pronto!</4>",
"make_setup_instructions_1": "Vá para <0>Link de Convite do Make</0> e instale o aplicativo Cal.com.",
"make_setup_instructions_2": "Faça login na sua conta Make e crie um novo Cenário.",
"make_setup_instructions_3": "Selecione Cal.com como seu aplicativo de Gatilho. Escolha também um evento de Gatilho.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "ou",
"nevermind": "Não importa",
"go_to": "Ir para: ",
"zapier_invite_link": "Link de convite do Zapier",
"meeting_url_provided_after_confirmed": "Um URL de Reunião será criado assim que o evento for confirmado.",
"dynamically_display_attendee_or_organizer": "Exibe dinamicamente o nome do participante para você, ou seu nome se for visto pelo seu participante",
"event_location": "Local do evento",
@@ -1366,13 +1366,11 @@
"code_copied": "Código copiado!",
"how_you_want_add_cal_site": "Como deseja adicionar o {{appName}} ao seu site?",
"choose_ways_put_cal_site": "Escolha uma das seguintes formas de colocar o {{appName}} no seu site.",
"setting_up_zapier": "A configurar a sua integração com Zapier",
"setting_up_make": "A configurar a sua integração com Make",
"generate_api_key": "Gerar chave de API",
"generate_api_key_description": "Gerar uma chave da API para utilizar com {{appName}} em",
"your_unique_api_key": "A sua chave de API única",
"copy_safe_api_key": "Copie esta chave da API e guarde-a num lugar seguro. Se perder esta chave, terá de gerar uma nova.",
"zapier_setup_instructions": "<0>Inicie sessão na sua conta Zapier e crie um novo Zap.</0><1>Seleccione {{appName}} como aplicação acionadora. Selecione também um evento acionador.</1><2>Seleccione a sua conta e insira a sua chave de API única.</2><3>Teste o seu accionador.</3><4>Está configurado!</4>",
"make_setup_instructions_1": "Vá para <0>Link de Convite do Make</0> e instale o aplicativo Cal.com.",
"make_setup_instructions_2": "Faça login na sua conta Make e crie um novo Cenário.",
"make_setup_instructions_3": "Selecione Cal.com como seu aplicativo de Gatilho. Escolha também um evento de Gatilho.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "ou",
"nevermind": "Deixar como está",
"go_to": "Vá a: ",
"zapier_invite_link": "Ligação de convite do Zapier",
"meeting_url_provided_after_confirmed": "Será criado um URL da reunião assim que o evento for confirmado.",
"dynamically_display_attendee_or_organizer": "Mostrar dinamicamente o nome do seu participante, ou o seu nome, se visto pelo seu participante",
"event_location": "Localização do evento",
@@ -1366,13 +1366,11 @@
"code_copied": "Cod copiat!",
"how_you_want_add_cal_site": "Cum doriți să adăugați {{appName}} pe site-ul dvs.?",
"choose_ways_put_cal_site": "Alegeți una dintre următoarele modalități de a include {{appName}} pe site-ul dvs.",
"setting_up_zapier": "Configurarea integrării dvs. Zapier",
"setting_up_make": "Configurarea integrării dvs. Make",
"generate_api_key": "Generare cheie API",
"generate_api_key_description": "Generați o cheie API pentru utilizare cu {{appName}} pe",
"your_unique_api_key": "Cheia dvs. API unică",
"copy_safe_api_key": "Copiați această cheie API și salvați-o într-un loc sigur. Dacă pierdeți această cheie, va trebui să generați una nouă.",
"zapier_setup_instructions": "<0>Conectează-te la contul Zapier și creează un Zap nou.</0><1>Selectează Cal.com ca aplicație Trigger. Alege și un eveniment Trigger.</1><2>Alege contul, apoi introdu cheia API unică.</2><3>Testează Trigger-ul.</3><4>Ai terminat!</4>",
"make_setup_instructions_1": "Mergi la <0>Make Invite Link</0> și instalează aplicația Cal.com.",
"make_setup_instructions_2": "Conectează-te la contul tău Make și creează un nou scenariu.",
"make_setup_instructions_3": "Selectează Cal.com ca aplicație de declanșare. De asemenea, alege un eveniment de declanșare.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "sau",
"nevermind": "Anulare",
"go_to": "Accesați: ",
"zapier_invite_link": "Link invitație Zapier",
"meeting_url_provided_after_confirmed": "Un URL pentru întâlnire va fi creat după confirmarea evenimentului.",
"dynamically_display_attendee_or_organizer": "Afișați dinamic numele participantului pentru dvs. sau numele dvs. dacă este vizualizat de participantul dvs.",
"event_location": "Locul evenimentului",
@@ -1366,13 +1366,11 @@
"code_copied": "Код скопирован!",
"how_you_want_add_cal_site": "Как вы хотите добавить {{appName}} на сайт?",
"choose_ways_put_cal_site": "Выберите способ размещения {{appName}} на сайте.",
"setting_up_zapier": "Настройка интеграции с Zapier",
"setting_up_make": "Настройка интеграции с Make",
"generate_api_key": "Сгенерировать ключ API",
"generate_api_key_description": "Сгенерируйте ключ API, чтобы использовать его в {{appName}}, по адресу",
"your_unique_api_key": "Ваш уникальный ключ API",
"copy_safe_api_key": "Скопируйте этот ключ API и сохраните его в надежном месте. Если ключ потеряется, вам придется создать новый.",
"zapier_setup_instructions": "<0>Войдите в свой аккаунт Zapier и создайте новый Zap.</0><1>Выберите в качестве приложения-триггера Cal.com. Также выберите событие-триггер.</1><2>Выберите свою учетную запись и введите уникальный ключ API.</2><3>Проверьте триггер.</3><4>Готово!</4>",
"make_setup_instructions_1": "Перейдите в <0>Make Invite Link</0> и установите приложение Cal.com.",
"make_setup_instructions_2": "Войдите в свою учетную запись Make и создайте новый сценарий.",
"make_setup_instructions_3": "Выберите Cal.com в качестве приложения-триггера. Также выберите событие триггера.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "или",
"nevermind": "Нет, спасибо",
"go_to": "Перейти: ",
"zapier_invite_link": "Ссылка с приглашением в Zapier",
"meeting_url_provided_after_confirmed": "После подтверждения события будет создан URL-адрес встречи.",
"dynamically_display_attendee_or_organizer": "Динамически отображать имя участника, если событие просматриваете вы, или ваше имя, если его просматривает участник",
"event_location": "Местоположение события",
@@ -1366,13 +1366,11 @@
"code_copied": "Kód skopírovaný!",
"how_you_want_add_cal_site": "Ako chcete pridať {{appName}} na vašu stránku?",
"choose_ways_put_cal_site": "Vyberte si jeden z nasledujúcich spôsobov, ako umiestniť {{appName}} na vašu stránku.",
"setting_up_zapier": "Nastavenie integrácie Zapier",
"setting_up_make": "Nastavenie integrácie Make",
"generate_api_key": "Generovať API kľúč",
"generate_api_key_description": "Generujte API kľúč na použitie s {{appName}} na",
"your_unique_api_key": "Váš jedinečný API kľúč",
"copy_safe_api_key": "Skopírujte tento API kľúč a uložte ho na bezpečné miesto. Ak tento kľúč stratíte, budete musieť vygenerovať nový.",
"zapier_setup_instructions": "<0>Prihláste sa do svojho účtu Zapier a vytvorte nový Zap.</0><1>Vyberte Cal.com ako svoju spúšťaciu aplikáciu. Tiež vyberte spúšťaciu udalosť.</1><2>Vyberte svoj účet a zadajte svoj jedinečný API kľúč.</2><3>Otestujte svoju spúšť.</3><4>Hotovo!</4>",
"make_setup_instructions_1": "Prejdite na <0>Make Invite Link</0> a nainštalujte aplikáciu Cal.com.",
"make_setup_instructions_2": "Prihláste sa do svojho účtu Make a vytvorte nový scenár.",
"make_setup_instructions_3": "Vyberte Cal.com ako vašu spúšťaciu aplikáciu. Tiež vyberte spúšťaciu udalosť.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "alebo",
"nevermind": "Nevadí",
"go_to": "Prejsť na: ",
"zapier_invite_link": "Zapier pozvánkový odkaz",
"meeting_url_provided_after_confirmed": "URL stretnutia bude vytvorené po potvrdení udalosti.",
"dynamically_display_attendee_or_organizer": "Dynamicky zobrazí meno účastníka pre vás, alebo vaše meno, ak to vidí účastník",
"event_location": "Miesto udalosti",
@@ -1366,13 +1366,11 @@
"code_copied": "Kôd kopiran!",
"how_you_want_add_cal_site": "Kako želite da dodate {{appName}} na vaš sajt?",
"choose_ways_put_cal_site": "Izaberite jedan od sledećih načina za postavljanje {{appName}}-a na vaš sajt.",
"setting_up_zapier": "Podešavanje vaše Zapier integracije",
"setting_up_make": "Podešavanje vaše Make integracije",
"generate_api_key": "Generiši API ključ",
"generate_api_key_description": "Generišite API ključ za upotrebu sa {{appName}} na",
"your_unique_api_key": "Vaš jedinstveni API ključ",
"copy_safe_api_key": "Kopirajte ovaj API ključ i sačuvajte ga na bezbednom mestu. Ako izgubite ovaj ključ, moraćete da generište novi.",
"zapier_setup_instructions": "<0>Prijavite se na vaš Zapier nalog i kreirajte novi Zap.</0><1>Izaberite Cal.com kao vašu Trigger aplikaciju. Takođe izaberite i Trigger događaj.</1><2>Izaberite svoj nalog i unesite vaš jedinstveni API ključ.</2><3>Testirajte svoj Trigger.</3><4>Završili ste!</4>",
"make_setup_instructions_1": "Idite na <0>Make Invite Link</0> i instalirajte Cal.com aplikaciju.",
"make_setup_instructions_2": "Prijavite se na svoj Make nalog i kreirajte novi scenario.",
"make_setup_instructions_3": "Izaberite Cal.com kao vašu aplikaciju za okidač. Takođe izaberite događaj okidača.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "ili",
"nevermind": "Svejedno mi je",
"go_to": "Idi na: ",
"zapier_invite_link": "Link za pozivnicu za Zapier",
"meeting_url_provided_after_confirmed": "URL za sastanak će biti kreiran kada se događaj potvrdi.",
"dynamically_display_attendee_or_organizer": "Dinamičko prikazivanje imena učesnika vama, ili prikazivanje vašeg imena učesniku",
"event_location": "Mesto događaja",
@@ -1366,13 +1366,11 @@
"code_copied": "Kod kopierad!",
"how_you_want_add_cal_site": "Hur vill du lägga till {{appName}} till din webbplats?",
"choose_ways_put_cal_site": "Välj ett av följande sätt att lägga {{appName}} på din webbplats.",
"setting_up_zapier": "Ställa in din Zapier-integration",
"setting_up_make": "Konfigurera din Make-integration",
"generate_api_key": "Generera API-nyckel",
"generate_api_key_description": "Skapa en API-nyckel att använda med {{appName}} på",
"your_unique_api_key": "Din unika API-nyckel",
"copy_safe_api_key": "Kopiera denna API-nyckel och spara den någonstans säkert. Om du förlorar denna nyckel måste du skapa en ny.",
"zapier_setup_instructions": "<0>Logga in på ditt Zapier-konto och skapa en ny Zap.</0><1>Välj {{appName}} som din Trigger-app. Välj också en Trigger-händelse. /1><2>Välj ditt konto och ange sedan din unika API-nyckel.</2><3>Testa din Trigger.</3><4>Du är klar!</4>",
"make_setup_instructions_1": "Gå till <0>Make inbjudningslänk</0> och installera Cal.com-appen.",
"make_setup_instructions_2": "Logga in på ditt Make-konto och skapa ett nytt scenario.",
"make_setup_instructions_3": "Välj Cal.com som din utlösarapp. Välj också en utlösarhändelse.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "eller",
"nevermind": "Glöm",
"go_to": "Gå till:",
"zapier_invite_link": "Zapier inbjudningslänk",
"meeting_url_provided_after_confirmed": "En mötes-URL kommer att skapas när evenemanget har bekräftats.",
"dynamically_display_attendee_or_organizer": "Visa dynamiskt deltagarens namn för dig, eller ditt namn om det visas av deltagaren",
"event_location": "Evenemangsplats",
@@ -1366,13 +1366,11 @@
"code_copied": "Kod kopyalandı!",
"how_you_want_add_cal_site": "{{appName}}'ı sitenize nasıl eklemek istersiniz?",
"choose_ways_put_cal_site": "{{appName}}'ı sitenize eklemek için aşağıdaki seçeneklerden birini belirleyin.",
"setting_up_zapier": "Zapier entegrasyonunuzu ayarlama",
"setting_up_make": "Make entegrasyonunuzu ayarlayın",
"generate_api_key": "API anahtarı oluştur",
"generate_api_key_description": "{{appName}} ile kullanmak için bir API anahtarı oluşturun:",
"your_unique_api_key": "Benzersiz API anahtarınız",
"copy_safe_api_key": "Bu API anahtarını kopyalayın ve güvenli bir yere kaydedin. Bu anahtarı kaybederseniz yeni bir anahtar oluşturmanız gerekir.",
"zapier_setup_instructions": "<0>Zapier hesabınıza giriş yapın ve yeni bir Zap oluşturun.</0><1>Trigger uygulamanız olarak Cal.com'u seçin. Ayrıca bir Trigger etkinliği seçin.</1><2>Hesabınızı seçin ve ardından Benzersiz API Anahtarınızı girin.</2><3>Trigger'ınızı test edin.</3><4>Hazırsınız!</4 >",
"make_setup_instructions_1": "<0>Make Davet Bağlantısına</0> gidin ve Cal.com uygulamasını yükleyin.",
"make_setup_instructions_2": "Make hesabınıza giriş yapın ve yeni bir Senaryo oluşturun.",
"make_setup_instructions_3": "Cal.com'u Tetikleyici uygulamanız olarak seçin. Ayrıca bir Tetikleyici etkinliği seçin.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "veya",
"nevermind": "Boş ver",
"go_to": "Git: ",
"zapier_invite_link": "Zapier Davet Bağlantısı",
"meeting_url_provided_after_confirmed": "Etkinlik onaylandıktan sonra bir Toplantı URL'si oluşturulacak.",
"dynamically_display_attendee_or_organizer": "Kendiniz için katılımcınızın adını veya katılımcınız tarafından görüntüleniyorsa adınızı dinamik olarak gösterin",
"event_location": "Etkinliğin konumu",
@@ -1366,13 +1366,11 @@
"code_copied": "Код скопійовано!",
"how_you_want_add_cal_site": "Як саме ви хочете додати {{appName}} на свій сайт?",
"choose_ways_put_cal_site": "Виберіть один із наведених нижче способів розмістити {{appName}} на сайті.",
"setting_up_zapier": "Налаштування інтеграції із Zapier",
"setting_up_make": "Налаштування інтеграції з Make",
"generate_api_key": "Створити ключ API",
"generate_api_key_description": "Створіть ключ API для використання з {{appName}} на сайті",
"your_unique_api_key": "Ваш унікальний ключ API",
"copy_safe_api_key": "Скопіюйте цей ключ API і збережіть його в надійному місці. Якщо ви втратите цей ключ, потрібно буде створити новий.",
"zapier_setup_instructions": "<0>Увійдіть в обліковий запис Zapier та створіть Zap.</0><1>Виберіть Cal.com як додаток Trigger, а також укажіть подію Trigger.</1><2>Виберіть свій обліковий запис і введіть унікальний ключ API.</2><3>Перевірте тригер.</3><4>Усе готово!</4>",
"make_setup_instructions_1": "Перейдіть до <0>Make Invite Link</0> та встановіть додаток Cal.com.",
"make_setup_instructions_2": "Увійдіть у свій обліковий запис Make та створіть новий сценарій.",
"make_setup_instructions_3": "Виберіть Cal.com як додаток-тригер. Також виберіть подію-тригер.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "або",
"nevermind": "Закрити",
"go_to": "Перейди до: ",
"zapier_invite_link": "Zapier Invite Link",
"meeting_url_provided_after_confirmed": "URL-адресу наради буде створено після підтвердження заходу.",
"dynamically_display_attendee_or_organizer": "Динамічно показувати ім’я учасника вам або ваше ім’я учаснику, який його переглядає",
"event_location": "Розташування заходу",
@@ -1366,13 +1366,11 @@
"code_copied": "Mã đã sao chép!",
"how_you_want_add_cal_site": "Bạn muốn thêm {{appName}} vào website của mình bằng cách nào?",
"choose_ways_put_cal_site": "Chọn một trong những cách sau để đặt {{appName}} trên website.",
"setting_up_zapier": "Thiết lập tích hợp Zapier của bạn",
"setting_up_make": "Thiết lập tích hợp Make của bạn",
"generate_api_key": "Tạo khoá API",
"generate_api_key_description": "Tạo một khoá API để sử dụng với {{appName}} tại",
"your_unique_api_key": "Khoá API đặc trưng của bạn",
"copy_safe_api_key": "Sao chép khoá API này và lưu nó ở nơi an toàn. Nếu bạn làm mất khoá này, bạn phải tạo khoá mới.",
"zapier_setup_instructions": "<0>Đăng nhập vào tài khoản Zapier của bạn và tạo một Zap mới.</0><1>Chọn {{appName}} làm ứng dụng Trigger của bạn. Đồng thời chọn một sự kiện Trigger.</1><2>Chọn tài khoản của bạn và sau đó điền vào Khoá API đặc trưng.</2><3>Thử nghiệm Trigger của bạn.</3><4>Bạn đã xong!</4>",
"make_setup_instructions_1": "Truy cập <0>Make Invite Link</0> và cài đặt ứng dụng Cal.com.",
"make_setup_instructions_2": "Đăng nhập vào tài khoản Make của bạn và tạo một Kịch bản mới.",
"make_setup_instructions_3": "Chọn Cal.com làm ứng dụng Kích hoạt. Đồng thời chọn một sự kiện Kích hoạt.",
@@ -1417,7 +1415,6 @@
"or_lowercase": "hoặc",
"nevermind": "Không sao",
"go_to": "Đến: ",
"zapier_invite_link": "Liên kết mời Zapier",
"meeting_url_provided_after_confirmed": "Một URL Họp sẽ được tạo sau khi sự kiện này được xác nhận.",
"dynamically_display_attendee_or_organizer": "Hiển thị linh động tên người tham dự cho bạn thấy, hoặc hiển thị tên bạn nếu tên đó được thấy bởi người tham dự",
"event_location": "Địa điểm sự kiện",
@@ -1366,13 +1366,11 @@
"code_copied": "代码已复制!",
"how_you_want_add_cal_site": "您希望如何将 {{appName}} 添加到您的网站?",
"choose_ways_put_cal_site": "选择下列方式之一将 {{appName}} 放到您的网站上。",
"setting_up_zapier": "设置您的 Zapier 集成",
"setting_up_make": "设置您的 Make 集成",
"generate_api_key": "生成 API 密钥",
"generate_api_key_description": "生成 API 密钥以将 {{appName}} 配合用于",
"your_unique_api_key": "您的唯一 API 密钥",
"copy_safe_api_key": "复制此 API 密钥并将其保存在安全的位置。如果丢失此密钥,则必须生成新的密钥。",
"zapier_setup_instructions": "<0>登录您的 Zapier 帐户并创建新的 Zap。</0><1>将 Cal.com 选择为 Trigger 应用。同时选择一个 Trigger 事件。</1><2>选择您的帐户,然后输入您的唯一 API 密钥。</2><3>测试您的 Trigger。</3><4>设置完毕!</4>",
"make_setup_instructions_1": "前往<0>Make邀请链接</0>并安装Cal.com应用。",
"make_setup_instructions_2": "登录您的Make账户并创建新场景。",
"make_setup_instructions_3": "选择Cal.com作为您的触发应用。同时选择一个触发事件。",
@@ -1417,7 +1415,6 @@
"or_lowercase": "或",
"nevermind": "没关系",
"go_to": "转到:",
"zapier_invite_link": "Zapier 邀请链接",
"meeting_url_provided_after_confirmed": "一旦确认活动,即会创建会议链接。",
"dynamically_display_attendee_or_organizer": "动态显示参与者的姓名,如果参与者查看,则显示您的姓名",
"event_location": "活动的位置",
@@ -1366,13 +1366,11 @@
"code_copied": "程式碼已複製!",
"how_you_want_add_cal_site": "您希望以何種方式將 {{appName}} 加入您的網站?",
"choose_ways_put_cal_site": "選擇下列其中一種方式將 {{appName}} 放到您的網站。",
"setting_up_zapier": "正在設定您的 Zapier 整合",
"setting_up_make": "正在設定您的 Make 整合",
"generate_api_key": "產生 API 金鑰",
"generate_api_key_description": "產生 API 金鑰以在下列位置使用 {{appName}}:",
"your_unique_api_key": "您的唯一 API 金鑰",
"copy_safe_api_key": "複製此 API 金鑰並存在安全的地方。如果您遺失此金鑰,就必須產生新的金鑰。",
"zapier_setup_instructions": "<0>登入您的 Zapier 帳號並建立新的 Zap。</0><1>選取 {{appName}} 作為您的 Trigger 應用程式,也請一併選擇 Trigger 事件。</1><2>選擇您的帳號,然後輸入您的唯一 API 金鑰。</2><3>測試您的 Trigger。</3><4>一切就大功告成了!</4>",
"make_setup_instructions_1": "前往<0>Make邀請連結</0>並安裝Cal.com應用程式。",
"make_setup_instructions_2": "登入您的Make帳戶並創建新的情境。",
"make_setup_instructions_3": "選擇Cal.com作為您的觸發應用程式。同時選擇一個觸發事件。",
@@ -1417,7 +1415,6 @@
"or_lowercase": "或",
"nevermind": "沒關係",
"go_to": "前往: ",
"zapier_invite_link": "Zapier Invite Link",
"meeting_url_provided_after_confirmed": "活動一經確認後,即會建立會議網址。",
"dynamically_display_attendee_or_organizer": "為您動態顯示與會者的姓名,若與會者查看,則會顯示您的姓名",
"event_location": "活動地點",
+97
View File
@@ -2735,6 +2735,103 @@ describe("getSchedule", () => {
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit
});
test("booking limits use schedule timezone when set, falling back to user timezone", async () => {
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
const scenarioData = {
eventTypes: [
{
id: 1,
length: 60,
beforeEventBuffer: 0,
afterEventBuffer: 0,
bookingLimits: {
PER_DAY: 1,
},
schedule: {
id: 1,
name: "Schedule with timezone",
availability: [
{
userId: null,
eventTypeId: null,
days: [0, 1, 2, 3, 4, 5, 6],
startTime: new Date("1970-01-01T00:00:00.000Z"),
endTime: new Date("1970-01-01T23:59:59.999Z"),
date: null,
},
],
timeZone: Timezones["+5:30"],
},
users: [
{
id: 101,
},
],
},
],
users: [
{
...TestData.users.example,
id: 101,
schedules: [
{
id: 2,
name: "User schedule",
availability: [
{
userId: null,
eventTypeId: null,
days: [0, 1, 2, 3, 4, 5, 6],
startTime: new Date("1970-01-01T00:00:00.000Z"),
endTime: new Date("1970-01-01T23:59:59.999Z"),
date: null,
},
],
timeZone: Timezones["+6:00"],
},
],
},
],
bookings: [
{
userId: 101,
eventTypeId: 1,
startTime: `${plus2DateString}T08:00:00.000Z`,
endTime: `${plus2DateString}T09:00:00.000Z`,
status: "ACCEPTED" as BookingStatus,
},
],
};
await createBookingScenario(scenarioData);
const availability = await availableSlotsService.getAvailableSlots({
input: {
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T00:00:00.000Z`,
endTime: `${plus3DateString}T23:59:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
orgSlug: null,
},
});
const availableSlotsInScheduleTz: dayjs.Dayjs[] = [];
for (const date in availability.slots) {
availability.slots[date].forEach((timeObj) => {
availableSlotsInScheduleTz.push(dayjs(timeObj.time).tz(Timezones["+5:30"]));
});
}
expect(availableSlotsInScheduleTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(
0
);
});
test("a slot counts as being busy when the eventType is requiresConfirmation and requiresConfirmationWillBlockSlot", async () => {
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
+1 -1
View File
@@ -68,7 +68,7 @@ services:
calcom-api:
container_name: calcom-api
build:
context: ./calcom
context: .
dockerfile: apps/api/v2/Dockerfile
args:
DATABASE_URL: ${DATABASE_URL}
+2 -2
View File
@@ -16355,7 +16355,7 @@
"description": "Boolean to Hide organizer's email address from the booking screen, email notifications, and calendar events"
},
"calVideoSettings": {
"description": "Cal video settings for the event type",
"description": "Cal video settings for the event type. Platform customers can't manage this property because currently we have no way of determining if managed user is a host or an attendee.",
"allOf": [
{
"$ref": "#/components/schemas/CalVideoSettings"
@@ -19492,7 +19492,7 @@
"description": "Boolean to Hide organizer's email address from the booking screen, email notifications, and calendar events"
},
"calVideoSettings": {
"description": "Cal video settings for the event type",
"description": "Cal video settings for the event type. Platform customers can't manage this property because currently we have no way of determining if managed user is a host or an attendee.",
"allOf": [
{
"$ref": "#/components/schemas/CalVideoSettings"
@@ -30,7 +30,7 @@ title: "Stripe"
as the webhook for connected applications.
</Step>
<Step title="Select webhook events">
Select all `payment_intent` events for the webhook.
Select all `payment_intent` and `setup_intent` events for the webhook.
</Step>
<Step title="Save the webhook secret">
Copy the webhook secret (`whsec_...`) to `STRIPE_WEBHOOK_SECRET` in the `.env` file.
@@ -3,7 +3,6 @@ import type { GetServerSidePropsContext } from "next";
export const AppSetupPageMap = {
alby: import("../../alby/pages/setup/_getServerSideProps"),
make: import("../../make/pages/setup/_getServerSideProps"),
zapier: import("../../zapier/pages/setup/_getServerSideProps"),
stripe: import("../../stripepayment/pages/setup/_getServerSideProps"),
hitpay: import("../../hitpay/pages/setup/_getServerSideProps"),
btcpayserver: import("../../btcpayserver/pages/setup/_getServerSideProps"),
@@ -17,6 +17,7 @@ export const REDIRECT_APPS = [
"telli",
"vimcal",
"wordpress",
"zapier",
];
export const isRedirectApp = (slug: string): boolean => {
+1 -1
View File
@@ -9,7 +9,7 @@
"@getalby/lightning-tools": "^5.1.2",
"@getalby/sdk": "^4.0.0",
"@webbtc/webln-types": "^3.0.0",
"react-qr-code": "^2.0.12",
"react-qr-code": "^2.0.18",
"svix": "^0.85.1"
},
"devDependencies": {
@@ -105,7 +105,7 @@ import whatsapp_config_json from "./whatsapp/config.json";
import whereby_config_json from "./whereby/config.json";
import { metadata as wipemycalother__metadata_ts } from "./wipemycalother/_metadata";
import wordpress_config_json from "./wordpress/config.json";
import { metadata as zapier__metadata_ts } from "./zapier/_metadata";
import zapier_config_json from "./zapier/config.json";
import zoho_bigin_config_json from "./zoho-bigin/config.json";
import zohocalendar_config_json from "./zohocalendar/config.json";
import zohocrm_config_json from "./zohocrm/config.json";
@@ -215,7 +215,7 @@ export const appStoreMetadata = {
whereby: whereby_config_json,
wipemycalother: wipemycalother__metadata_ts,
wordpress: wordpress_config_json,
zapier: zapier__metadata_ts,
zapier: zapier_config_json,
"zoho-bigin": zoho_bigin_config_json,
zohocalendar: zohocalendar_config_json,
zohocrm: zohocrm_config_json,
@@ -202,10 +202,9 @@ export const generateGuestMeetingTokenFromOwnerMeetingToken = async ({
userId,
}: {
meetingToken: string | null;
userId?: number;
userId?: number | string;
}) => {
if (!meetingToken) return null;
const token = await fetcher(`/meeting-tokens/${meetingToken}`).then(ZGetMeetingTokenResponseSchema.parse);
const guestMeetingToken = await postToDailyAPI("/meeting-tokens", {
properties: {
+3 -1
View File
@@ -4,4 +4,6 @@ items:
- 2.jpg
---
Workflow automation for everyone. Use the [Cal.com](https://cal.com) Zapier app to automate your workflows when a booking is created, rescheduled, cancelled or when a meeting ended.<br /><br />**After Installation:** Have you lost your API key? You can always generate a new key on the <a href="/apps/zapier/setup">**<ins>Zapier Setup Page</ins>**</a>
Connect Cal.com with 8,000+ apps through Zapier to automate your scheduling workflows. Set up triggers for booking events (created, rescheduled, cancelled, meeting ended) and integrate with popular tools like Slack, Gmail, Google Sheets, and more.
**Get started:** Click "Visit" to access the Zapier integrations page and create your first automation workflow.
+17 -74
View File
@@ -5,87 +5,30 @@
</a>
</div>
# Setting up Zapier Integration
# Zapier Integration – Developer Guide
If you run it on localhost, check out the [additional information](https://github.com/CarinaWolli/cal.com/edit/feat/zapier-app/packages/app-store/zapier/README.md#localhost) below.
**Note:** This is a redirect app. End users should connect Cal.com to Zapier directly via the Zapier Integrations page: https://zapier.com/apps/calcom/integrations
1. Create [Zapier Account](https://zapier.com/sign-up?next=https%3A%2F%2Fdeveloper.zapier.com%2F)
2. If not redirected to developer account, go to: [Zapier Developer Account](https://developer.zapier.com)
3. Click **Start a Zapier Integration**
4. Create Integration
- Name: Cal.com
- Description: [Cal.com](https://cal.com) is a scheduling infrastructure for absolutely everyone.
- Intended Audience: Private
- Role: choose whatever is appropriate
- Category: Calendar
The Zapier app in Cal.com now functions as a redirect app that takes users directly to Zapier's integration page. No local setup or OAuth configuration is required.
## Authentication
## How It Works
1. Go to Authentication, choose Api key and click save
2. Click Add Fields
- Key: apiKey
- Check the box ‘is this field required?’
3. Configure a Test
- Test: GET `<baseUrl>`/api/integrations/zapier/listBookings
- URL Params
- apiKey: {{bundle.authData.apiKey}}
4. Test your authentication —> First you have to install Zapier in the [Cal.com](https://cal.com)App Store and generate an API key, use this API key to test your authentication (only zapier Api key works)
1. **User clicks "Visit"** → Opens Zapier integrations page
2. **User sets up automation** → Directly on Zapier's platform
3. **No Cal.com credentials needed** → Users authenticate directly with Zapier
## Triggers
## For Developers
Booking created, Booking rescheduled, Booking cancelled, Meeting ended, Out Of Office Created
This app uses the `link-as-an-app` template and requires no additional configuration beyond the redirect URL.
### Booking created
## Legacy API Endpoints
1. Settings
- Key: booking_created
- Name: Booking created
- Noun: Booking
- Description: Triggers when a new booking is created
2. API Configuration (apiKey is set automatically, leave it like it is):
- Trigger Type: REST Hook
- Subscribe: POST `<baseUrl>`/api/integrations/zapier/addSubscription
- Request Body
- subscriberUrl: {{bundle.targetUrl}}
- triggerEvent: BOOKING_CREATED
- Unsubscribe: DELETE `<baseUrl>`/api/integrations/zapier/deleteSubscription
- URL Params (in addition to apiKey)
- id: {{bundle.subscribeData.id}}
- PerformList: GET `<baseUrl>`/api/integrations/zapier/listBookings
3. Test your API request
The following API endpoints are maintained for backward compatibility with existing Zapier integrations:
Create the other triggers (booking rescheduled, booking cancelled and meeting ended) exactly like this one, just use the appropriate naming (e.g. booking_rescheduled instead of booking_created)
- `GET /api/integrations/zapier/listBookings` - List user bookings
- `GET /api/integrations/zapier/listOOOEntries` - List out-of-office entries
- `POST /api/integrations/zapier/addSubscription` - Subscribe to webhooks
- `DELETE /api/integrations/zapier/deleteSubscription` - Unsubscribe from webhooks
- `GET /api/integrations/zapier/me` - Get user/team information
### Out Of Office Created
1. Settings
- Key: ooo_created
- Name: Out Of Office Created
- Noun: OOO Entry
- Description: Triggers when a new Out Of Office entry is created.
2. API Configuration (apiKey is set automatically, leave it like it is):
- Trigger Type: REST Hook
- Subscribe: POST `<baseUrl>`/api/integrations/zapier/addSubscription
- Request Body
- subscriberUrl: {{bundle.targetUrl}}
- triggerEvent: OOO_CREATED
- Unsubscribe: DELETE `<baseUrl>`/api/integrations/zapier/deleteSubscription
- URL Params (in addition to apiKey)
- id: {{bundle.subscribeData.id}}
- PerformList: GET `<baseUrl>`/api/integrations/zapier/listOOOEntries
3. Test your API request
4. Note: When creating the ZAP you need to remember that data is stored in the { payload: { oooEntry: { ... } } }
### Set ZAPIER_INVITE_LINK
The invite link can be found under Manage → Sharing.
## Localhost
Localhost urls can not be used as the base URL for api endpoints
Possible solution: using [https://ngrok.com/](https://ngrok.com/)
1. Create Account
2. [Download](https://ngrok.com/download) ngrok and start a tunnel to your running localhost
- Use forwarding url as your baseUrl for the URL endpoints
**Note:** These endpoints support both API key and OAuth authentication for existing integrations. The endpoints are dynamically routed through `/api/integrations/[...args].ts`.
+1 -1
View File
@@ -13,7 +13,7 @@ export const metadata = {
slug: "zapier",
title: "Zapier",
type: "zapier_automation",
url: "https://cal.com/apps/zapier",
url: "https://zapier.com/apps/calcom/integrations",
variant: "automation",
email: "help@cal.com",
dirName: "zapier",
+19 -36
View File
@@ -1,39 +1,22 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import prisma from "@calcom/prisma";
import appConfig from "../config.json";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session?.user?.id) {
return res.status(401).json({ message: "You must be logged in to do this" });
}
const appType = "zapier_automation";
try {
const alreadyInstalled = await prisma.credential.findFirst({
where: {
type: appType,
userId: req.session.user.id,
},
});
if (alreadyInstalled) {
throw new Error("Already installed");
}
const installation = await prisma.credential.create({
data: {
type: appType,
key: {},
userId: req.session.user.id,
appId: "zapier",
},
});
if (!installation) {
throw new Error("Unable to create user credential for zapier");
}
} catch (error: unknown) {
if (error instanceof Error) {
return res.status(500).json({ message: error.message });
}
return res.status(500);
}
const handler: AppDeclarativeHandler = {
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
redirect: {
newTab: true,
url: "https://zapier.com/apps/calcom/integrations",
},
// Creates minimal credential record for tracking purposes only
// No actual credentials are stored since this is a redirect app
createCredential: ({ appType, user, slug, teamId }) =>
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
};
return res.status(200).json({ url: "/apps/zapier/setup" });
}
export default handler;
+16
View File
@@ -0,0 +1,16 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Zapier",
"slug": "zapier",
"type": "zapier_automation",
"logo": "icon.svg",
"url": "https://zapier.com/apps/calcom/integrations",
"variant": "automation",
"categories": ["automation"],
"publisher": "Cal.com",
"email": "help@cal.com",
"description": "Workflow automation for everyone. Use the Cal.com Zapier app to automate your workflows when a booking is created, rescheduled, cancelled or when a meeting ends.",
"isTemplate": false,
"__createdUsingCli": true,
"__template": "link-as-an-app"
}
@@ -1,22 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import getAppKeysFromSlug from "../../../_utils/getAppKeysFromSlug";
export interface IZapierSetupProps {
inviteLink: string;
}
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const notFound = { notFound: true } as const;
if (typeof ctx.params?.slug !== "string") return notFound;
let inviteLink = "";
const appKeys = await getAppKeysFromSlug("zapier");
if (typeof appKeys.invite_link === "string") inviteLink = appKeys.invite_link;
return {
props: {
inviteLink,
},
};
};
+23
View File
@@ -0,0 +1,23 @@
import { type Container, createModule } from "@calcom/features/di/di";
import { DI_TOKENS } from "@calcom/features/di/tokens";
import { prisma, readonlyPrisma } from "@calcom/prisma";
import { usageTrackingExtention } from "../../prisma-extensions/usage-tracking";
export const prismaEEModule = createModule();
const token = DI_TOKENS.PRISMA_CLIENT;
const readOnlyToken = DI_TOKENS.READ_ONLY_PRISMA_CLIENT;
const moduleToken = DI_TOKENS.PRISMA_MODULE;
const prismaWithUsageTracking = prisma.$extends(usageTrackingExtention(prisma));
prismaEEModule.bind(token).toFactory(() => prismaWithUsageTracking, "singleton");
prismaEEModule.bind(readOnlyToken).toFactory(() => readonlyPrisma, "singleton");
export const moduleLoader = {
token,
readOnlyToken,
loadModule: (container: Container) => {
container.load(moduleToken, prismaEEModule);
},
};
+13 -5
View File
@@ -10,6 +10,7 @@ import { formatCalEvent } from "@calcom/lib/formatCalendarEvent";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { withReporting } from "@calcom/lib/sentryWrapper";
import type { CreditUsageType } from "@calcom/prisma/enums";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
@@ -852,28 +853,32 @@ export const sendCreditBalanceLowWarningEmails = async (input: {
t: TFunction;
};
balance: number;
creditFor?: CreditUsageType;
}) => {
const { team, balance, user } = input;
const { team, balance, user, creditFor } = input;
if ((!team || !team.adminAndOwners.length) && !user) return;
if (team) {
const emailsToSend: Promise<unknown>[] = [];
for (const admin of team.adminAndOwners) {
emailsToSend.push(sendEmail(() => new CreditBalanceLowWarningEmail({ user: admin, balance, team })));
emailsToSend.push(
sendEmail(() => new CreditBalanceLowWarningEmail({ user: admin, balance, team, creditFor }))
);
}
await Promise.all(emailsToSend);
}
if (user) {
await sendEmail(() => new CreditBalanceLowWarningEmail({ user, balance }));
await sendEmail(() => new CreditBalanceLowWarningEmail({ user, balance, creditFor }));
}
};
export const sendCreditBalanceLimitReachedEmails = async ({
team,
user,
creditFor,
}: {
team?: {
name: string;
@@ -891,6 +896,7 @@ export const sendCreditBalanceLimitReachedEmails = async ({
email: string;
t: TFunction;
};
creditFor?: CreditUsageType;
}) => {
if ((!team || !team.adminAndOwners.length) && !user) return;
@@ -898,13 +904,15 @@ export const sendCreditBalanceLimitReachedEmails = async ({
const emailsToSend: Promise<unknown>[] = [];
for (const admin of team.adminAndOwners) {
emailsToSend.push(sendEmail(() => new CreditBalanceLimitReachedEmail({ user: admin, team })));
emailsToSend.push(
sendEmail(() => new CreditBalanceLimitReachedEmail({ user: admin, team, creditFor }))
);
}
await Promise.all(emailsToSend);
}
if (user) {
await sendEmail(() => new CreditBalanceLimitReachedEmail({ user }));
await sendEmail(() => new CreditBalanceLimitReachedEmail({ user, creditFor }));
}
};
@@ -1,6 +1,7 @@
import type { TFunction } from "i18next";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { CreditUsageType } from "@calcom/prisma/enums";
import { CallToAction, V2BaseEmailHtml } from "../components";
import type { BaseScheduledEmail } from "./BaseScheduledEmail";
@@ -17,9 +18,11 @@ export const CreditBalanceLimitReachedEmail = (
email: string;
t: TFunction;
};
creditFor?: CreditUsageType;
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>>
) => {
const { team, user } = props;
const { team, user, creditFor } = props;
const isCalAi = creditFor === CreditUsageType.CAL_AI_PHONE_CALL;
if (team) {
return (
@@ -28,7 +31,11 @@ export const CreditBalanceLimitReachedEmail = (
<> {user.t("hi_user_name", { name: user.name })},</>
</p>
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
<>{user.t("credit_limit_reached_message", { teamName: team.name })}</>
<>
{isCalAi
? user.t("cal_ai_credit_limit_reached_message", { teamName: team.name })
: user.t("credit_limit_reached_message", { teamName: team.name })}
</>
</p>
<div style={{ textAlign: "center", marginTop: "24px" }}>
<CallToAction
@@ -47,7 +54,11 @@ export const CreditBalanceLimitReachedEmail = (
<> {user.t("hi_user_name", { name: user.name })},</>
</p>
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
<>{user.t("credit_limit_reached_message_user")}</>
<>
{isCalAi
? user.t("cal_ai_credit_limit_reached_message_user")
: user.t("credit_limit_reached_message_user")}
</>
</p>
<div style={{ textAlign: "center", marginTop: "24px" }}>
<CallToAction
@@ -1,6 +1,7 @@
import type { TFunction } from "i18next";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { CreditUsageType } from "@calcom/prisma/enums";
import { CallToAction, V2BaseEmailHtml } from "../components";
import type { BaseScheduledEmail } from "./BaseScheduledEmail";
@@ -18,9 +19,11 @@ export const CreditBalanceLowWarningEmail = (
email: string;
t: TFunction;
};
creditFor?: CreditUsageType;
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>>
) => {
const { team, balance, user } = props;
const { team, balance, user, creditFor } = props;
const isCalAi = creditFor === CreditUsageType.CAL_AI_PHONE_CALL;
if (team) {
return (
@@ -29,7 +32,11 @@ export const CreditBalanceLowWarningEmail = (
<> {user.t("hi_user_name", { name: user.name })},</>
</p>
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
<>{user.t("low_credits_warning_message", { teamName: team.name })}</>
<>
{isCalAi
? user.t("cal_ai_low_credits_warning_message", { teamName: team.name })
: user.t("low_credits_warning_message", { teamName: team.name })}
</>
</p>
<p
style={{
@@ -56,7 +63,11 @@ export const CreditBalanceLowWarningEmail = (
<> {user.t("hi_user_name", { name: user.name })},</>
</p>
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
<>{user.t("low_credits_warning_message_user")}</>
<>
{isCalAi
? user.t("cal_ai_low_credits_warning_message_user")
: user.t("low_credits_warning_message_user")}
</>
</p>
<div style={{ textAlign: "center", marginTop: "24px" }}>
<CallToAction

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