Compare commits
50
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
630eee4fab | ||
|
|
1ebf553181 | ||
|
|
e8a315ac4e | ||
|
|
c196ff1095 | ||
|
|
bf0fbe4888 | ||
|
|
eb6418c0d5 | ||
|
|
2df2868b20 | ||
|
|
7f48c7fdad | ||
|
|
2101051623 | ||
|
|
a241cb0bfc | ||
|
|
5417012cec | ||
|
|
8e818e0567 | ||
|
|
1fdd04215d | ||
|
|
0bf0749b29 | ||
|
|
4006143e3a | ||
|
|
c6ad767565 | ||
|
|
896f8dbd3d | ||
|
|
f1db27c0ad | ||
|
|
3a1e04bb42 | ||
|
|
ab37847ea1 | ||
|
|
0a2ddf05ca | ||
|
|
04fc0f1d52 | ||
|
|
b76c4c9dcc | ||
|
|
68b71bfb0a | ||
|
|
763c568797 | ||
|
|
4659d08c07 | ||
|
|
b9c350cc45 | ||
|
|
f63d705523 | ||
|
|
2907c5e696 | ||
|
|
dcb47e31ed | ||
|
|
cd5b75fe2c | ||
|
|
97bdf837a9 | ||
|
|
5663b2ae59 | ||
|
|
02b57eb82e | ||
|
|
b23dfa194e | ||
|
|
2484b6fad2 | ||
|
|
c154bb31b7 | ||
|
|
0cf4ebd544 | ||
|
|
805c4d4c09 | ||
|
|
99330b3117 | ||
|
|
cc90a62090 | ||
|
|
79bcd6dc30 | ||
|
|
c6daa61e5e | ||
|
|
b693b6898a | ||
|
|
fb55bc2dcd | ||
|
|
71bd140fe4 | ||
|
|
4c5608147c | ||
|
|
7d16da5c98 | ||
|
|
0ae6e92558 | ||
|
|
052a38e39b |
No files matched your search
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@calcom/atoms": minor
|
||||
---
|
||||
|
||||
Fix Booker atom verify email dialog infinite re-render when entering verification code
|
||||
Vendored
Whitespace-only changes.
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
Vendored
Whitespace-only changes.
@@ -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()
|
||||
|
||||
+1
-1
@@ -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);
|
||||
@@ -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()) });
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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'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);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }),
|
||||
}));
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
export function MailIcon() {
|
||||
return (
|
||||
<svg width="124" height="110" viewBox="0 0 124 110" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<style>
|
||||
{`:root {
|
||||
--mail-envelope-start: white;
|
||||
--mail-envelope-end: #F7F7F7;
|
||||
--mail-border-start: #E5E5E6;
|
||||
--mail-border-end: #B2B2B2;
|
||||
--mail-badge-start: #F7F7F7;
|
||||
--mail-badge-end: #D3D3D3;
|
||||
--mail-badge-stroke: #E8E8E8;
|
||||
--mail-badge-text: #6B7280;
|
||||
--mail-circle-start: white;
|
||||
--mail-circle-end: #EEEEEE;
|
||||
--mail-circle-border-start: #E8E8E8;
|
||||
--mail-circle-border-end: #B2B2B2;
|
||||
--mail-at-start: #6B7280;
|
||||
--mail-at-end: #3C3F43;
|
||||
}
|
||||
.dark {
|
||||
--mail-envelope-start: #1c1917;
|
||||
--mail-envelope-end: #292524;
|
||||
--mail-border-start: #44403c;
|
||||
--mail-border-end: #57534e;
|
||||
--mail-badge-start: #292524;
|
||||
--mail-badge-end: #1c1917;
|
||||
--mail-badge-stroke: #44403c;
|
||||
--mail-badge-text: #a8a29e;
|
||||
--mail-circle-start: #292524;
|
||||
--mail-circle-end: #1c1917;
|
||||
--mail-circle-border-start: #44403c;
|
||||
--mail-circle-border-end: #57534e;
|
||||
--mail-at-start: #a8a29e;
|
||||
--mail-at-end: #78716c;
|
||||
}`}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
d="M17.3 18H91.7C96.815 18 101 22.1625 101 27.25V82.75C101 87.8375 96.815 92 91.7 92H17.3C12.185 92 8 87.8375 8 82.75V27.25C8 22.1625 12.185 18 17.3 18Z"
|
||||
fill="url(#paint0_linear_3039_74057)"
|
||||
/>
|
||||
<path
|
||||
d="M101 27.25L59.0711 56.4424C56.3238 58.3552 52.6762 58.3552 49.9289 56.4424L8 27.25"
|
||||
fill="url(#paint0_linear_3039_74057)"
|
||||
/>
|
||||
<path
|
||||
d="M101 27.25C101 22.1625 96.815 18 91.7 18H17.3C12.185 18 8 22.1625 8 27.25M101 27.25V82.75C101 87.8375 96.815 92 91.7 92H17.3C12.185 92 8 87.8375 8 82.75V27.25M101 27.25L59.0711 56.4424C56.3238 58.3552 52.6762 58.3552 49.9289 56.4424L8 27.25"
|
||||
stroke="url(#paint2_linear_3039_74057)"
|
||||
strokeWidth="1.63366"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<g opacity="0.6">
|
||||
<path
|
||||
d="M11 83C11 79.6863 13.6863 77 17 77H29C32.3137 77 35 79.6863 35 83C35 86.3137 32.3137 89 29 89H17C13.6863 89 11 86.3137 11 83Z"
|
||||
fill="url(#paint3_linear_3039_74057)"
|
||||
/>
|
||||
<path
|
||||
d="M17 77.25H29C32.1756 77.25 34.75 79.8244 34.75 83C34.75 86.1756 32.1756 88.75 29 88.75H17C13.8244 88.75 11.25 86.1756 11.25 83C11.25 79.8244 13.8244 77.25 17 77.25Z"
|
||||
stroke="var(--mail-badge-stroke)"
|
||||
strokeWidth="0.5"
|
||||
/>
|
||||
<path
|
||||
d="M16.3807 85.0724C16.1733 85.0724 15.9858 85.0341 15.8182 84.9574C15.6506 84.8793 15.5178 84.7663 15.4197 84.6186C15.3232 84.4709 15.2749 84.2898 15.2749 84.0753C15.2749 83.8906 15.3104 83.7386 15.3814 83.6193C15.4524 83.5 15.5483 83.4055 15.669 83.3359C15.7898 83.2663 15.9247 83.2138 16.0739 83.1783C16.223 83.1428 16.375 83.1158 16.5298 83.0973C16.7259 83.0746 16.8849 83.0561 17.0071 83.0419C17.1293 83.0263 17.218 83.0014 17.2734 82.9673C17.3288 82.9332 17.3565 82.8778 17.3565 82.8011V82.7862C17.3565 82.6001 17.304 82.456 17.1989 82.3537C17.0952 82.2514 16.9403 82.2003 16.7344 82.2003C16.5199 82.2003 16.3509 82.2479 16.2273 82.343C16.1051 82.4368 16.0206 82.5412 15.9737 82.6562L15.375 82.5199C15.446 82.321 15.5497 82.1605 15.6861 82.0384C15.8239 81.9148 15.9822 81.8253 16.1612 81.7699C16.3402 81.7131 16.5284 81.6847 16.7259 81.6847C16.8565 81.6847 16.995 81.7003 17.1413 81.7315C17.2891 81.7614 17.4268 81.8168 17.5547 81.8977C17.6839 81.9787 17.7898 82.0945 17.8722 82.245C17.9545 82.3942 17.9957 82.5881 17.9957 82.8267V85H17.3736V84.5526H17.348C17.3068 84.6349 17.245 84.7159 17.1626 84.7955C17.0803 84.875 16.9744 84.9411 16.8452 84.9936C16.7159 85.0462 16.5611 85.0724 16.3807 85.0724ZM16.5192 84.5611C16.6953 84.5611 16.8459 84.5263 16.9709 84.4567C17.0973 84.3871 17.1932 84.2962 17.2585 84.1839C17.3253 84.0703 17.3587 83.9489 17.3587 83.8196V83.3977C17.3359 83.4205 17.2919 83.4418 17.2266 83.4616C17.1626 83.4801 17.0895 83.4964 17.0071 83.5107C16.9247 83.5234 16.8445 83.5355 16.7663 83.5469C16.6882 83.5568 16.6229 83.5653 16.5703 83.5724C16.4467 83.5881 16.3338 83.6143 16.2315 83.6513C16.1307 83.6882 16.0497 83.7415 15.9886 83.8111C15.929 83.8793 15.8991 83.9702 15.8991 84.0838C15.8991 84.2415 15.9574 84.3608 16.0739 84.4418C16.1903 84.5213 16.3388 84.5611 16.5192 84.5611ZM20.2228 85.0661C19.9061 85.0661 19.6333 84.9943 19.4047 84.8509C19.1774 84.706 19.0027 84.5064 18.8805 84.2521C18.7583 83.9979 18.6973 83.7067 18.6973 83.3786C18.6973 83.0462 18.7598 82.7528 18.8848 82.4986C19.0098 82.2429 19.1859 82.0433 19.4132 81.8999C19.6404 81.7564 19.9082 81.6847 20.2164 81.6847C20.465 81.6847 20.6866 81.7308 20.8812 81.8232C21.0758 81.9141 21.2328 82.0419 21.3521 82.2067C21.4728 82.3714 21.5446 82.5639 21.5673 82.7841H20.9473C20.9132 82.6307 20.835 82.4986 20.7129 82.3878C20.5922 82.277 20.4302 82.2216 20.2271 82.2216C20.0495 82.2216 19.894 82.2685 19.7605 82.3622C19.6284 82.4545 19.5254 82.5866 19.4515 82.7585C19.3777 82.929 19.3407 83.1307 19.3407 83.3636C19.3407 83.6023 19.377 83.8082 19.4494 83.9815C19.5218 84.1548 19.6241 84.2891 19.7562 84.3842C19.8897 84.4794 20.0467 84.527 20.2271 84.527C20.3478 84.527 20.4572 84.505 20.5552 84.4609C20.6547 84.4155 20.7377 84.3509 20.8045 84.267C20.8727 84.1832 20.9203 84.0824 20.9473 83.9645H21.5673C21.5446 84.1761 21.4757 84.3651 21.3606 84.5312C21.2456 84.6974 21.0914 84.8281 20.8983 84.9233C20.7065 85.0185 20.4814 85.0661 20.2228 85.0661ZM22.2486 85V81.7273H22.8601V82.2599H22.9006C22.9688 82.0795 23.0803 81.9389 23.2351 81.8381C23.3899 81.7358 23.5753 81.6847 23.7912 81.6847C24.0099 81.6847 24.1932 81.7358 24.3409 81.8381C24.4901 81.9403 24.6001 82.081 24.6712 82.2599H24.7053C24.7834 82.0852 24.9077 81.946 25.0781 81.8423C25.2486 81.7372 25.4517 81.6847 25.6875 81.6847C25.9844 81.6847 26.2266 81.7777 26.4141 81.9638C26.603 82.1499 26.6974 82.4304 26.6974 82.8054V85H26.0604V82.8651C26.0604 82.6435 26 82.483 25.8793 82.3835C25.7585 82.2841 25.6143 82.2344 25.4467 82.2344C25.2393 82.2344 25.0781 82.2983 24.9631 82.4261C24.848 82.5526 24.7905 82.7152 24.7905 82.9141V85H24.1555V82.8246C24.1555 82.647 24.1001 82.5043 23.9893 82.3963C23.8786 82.2884 23.7344 82.2344 23.5568 82.2344C23.4361 82.2344 23.3246 82.2663 23.2223 82.3303C23.1214 82.3928 23.0398 82.4801 22.9773 82.5923C22.9162 82.7045 22.8857 82.8345 22.8857 82.9822V85H22.2486ZM28.9618 85.0661C28.6394 85.0661 28.3617 84.9972 28.1287 84.8594C27.8972 84.7202 27.7182 84.5249 27.5918 84.2734C27.4668 84.0206 27.4043 83.7244 27.4043 83.3849C27.4043 83.0497 27.4668 82.7543 27.5918 82.4986C27.7182 82.2429 27.8944 82.0433 28.1202 81.8999C28.3475 81.7564 28.6131 81.6847 28.9171 81.6847C29.1017 81.6847 29.2807 81.7152 29.454 81.7763C29.6273 81.8374 29.7828 81.9332 29.9206 82.0639C30.0584 82.1946 30.1671 82.3643 30.2466 82.5732C30.3262 82.7805 30.3659 83.0327 30.3659 83.3295V83.5554H27.7644V83.0781H29.7417C29.7417 82.9105 29.7076 82.7621 29.6394 82.6328C29.5712 82.5021 29.4753 82.3991 29.3517 82.3239C29.2296 82.2486 29.0861 82.2109 28.9213 82.2109C28.7424 82.2109 28.5861 82.255 28.4526 82.343C28.3205 82.4297 28.2182 82.5433 28.1458 82.6839C28.0748 82.8232 28.0392 82.9744 28.0392 83.1378V83.5107C28.0392 83.7294 28.0776 83.9155 28.1543 84.0689C28.2324 84.2223 28.3411 84.3395 28.4803 84.4205C28.6195 84.5 28.7821 84.5398 28.9682 84.5398C29.089 84.5398 29.199 84.5227 29.2985 84.4886C29.3979 84.4531 29.4838 84.4006 29.5563 84.331C29.6287 84.2614 29.6841 84.1754 29.7225 84.0732L30.3255 84.1818C30.2772 84.3594 30.1905 84.5149Line truncated
|
||||
fill="var(--mail-badge-text)"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
d="M84 23C84 13.0589 92.0589 5 102 5C111.941 5 120 13.0589 120 23C120 32.9411 111.941 41 102 41C92.0589 41 84 32.9411 84 23Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M84 23C84 13.0589 92.0589 5 102 5C111.941 5 120 13.0589 120 23C120 32.9411 111.941 41 102 41C92.0589 41 84 32.9411 84 23Z"
|
||||
fill="url(#paint4_linear_3039_74057)"
|
||||
/>
|
||||
<path
|
||||
d="M102 5.81641C111.49 5.81641 119.184 13.51 119.184 23C119.184 32.49 111.49 40.1836 102 40.1836C92.51 40.1836 84.8164 32.49 84.8164 23C84.8164 13.51 92.51 5.81641 102 5.81641Z"
|
||||
stroke="url(#paint5_linear_3039_74057)"
|
||||
strokeWidth="1.63366"
|
||||
/>
|
||||
<g opacity="0.6">
|
||||
<path
|
||||
d="M105.63 19.3697V23.9076C105.63 24.6297 105.917 25.3223 106.428 25.8329C106.938 26.3435 107.631 26.6304 108.353 26.6304C109.075 26.6304 109.768 26.3435 110.278 25.8329C110.789 25.3223 111.076 24.6297 111.076 23.9076V23C111.076 20.9516 110.383 18.9635 109.109 17.3589C107.836 15.7544 106.057 14.6277 104.063 14.1622C102.068 13.6966 99.9742 13.9196 98.1222 14.7948C96.2702 15.67 94.7687 17.146 93.8619 18.9828C92.955 20.8195 92.6962 22.909 93.1275 24.9115C93.5588 26.914 94.6548 28.7117 96.2374 30.0123C97.8199 31.3129 99.7958 32.0399 101.844 32.0752C103.892 32.1104 105.892 31.4517 107.518 30.2063M105.63 23C105.63 25.005 104.005 26.6304 102 26.6304C99.995 26.6304 98.3696 25.005 98.3696 23C98.3696 20.995 99.995 19.3697 102 19.3697C104.005 19.3697 105.63 20.995 105.63 23Z"
|
||||
stroke="url(#paint6_linear_3039_74057)"
|
||||
strokeWidth="1.63366"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_3039_74057"
|
||||
x1="54.5"
|
||||
y1="18"
|
||||
x2="54.5"
|
||||
y2="92"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="var(--mail-envelope-start)" />
|
||||
<stop offset="1" stopColor="var(--mail-envelope-end)" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_3039_74057"
|
||||
x1="54.5"
|
||||
y1="18"
|
||||
x2="54.5"
|
||||
y2="92"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.195" stopColor="var(--mail-border-start)" />
|
||||
<stop offset="1" stopColor="var(--mail-border-end)" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_3039_74057"
|
||||
x1="23"
|
||||
y1="77"
|
||||
x2="23"
|
||||
y2="89"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="var(--mail-badge-start)" />
|
||||
<stop offset="1" stopColor="var(--mail-badge-end)" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint4_linear_3039_74057"
|
||||
x1="102"
|
||||
y1="5"
|
||||
x2="102"
|
||||
y2="41"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.592418" stopColor="var(--mail-circle-start)" />
|
||||
<stop offset="1" stopColor="var(--mail-circle-end)" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint5_linear_3039_74057"
|
||||
x1="102"
|
||||
y1="5"
|
||||
x2="102"
|
||||
y2="41"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="var(--mail-circle-border-start)" />
|
||||
<stop offset="1" stopColor="var(--mail-circle-border-end)" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint6_linear_3039_74057"
|
||||
x1="102"
|
||||
y1="13.9247"
|
||||
x2="102"
|
||||
y2="32.0765"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="var(--mail-at-start)" />
|
||||
<stop offset="1" stopColor="var(--mail-at-end)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -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,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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="345" height="138" fill="none"><g clip-path="url(#a)"><path fill="#262543" d="M190.792 16.5h76.416c5.922 0 10.334 0 13.831.286 3.491.285 6.026.852 8.202 1.961a20.623 20.623 0 0 1 9.012 9.012c1.109 2.176 1.676 4.711 1.961 8.202.286 3.497.286 7.909.286 13.831v94.416c0 5.922 0 10.334-.286 13.831-.285 3.491-.852 6.026-1.961 8.202a20.623 20.623 0 0 1-9.012 9.012c-2.176 1.109-4.711 1.676-8.202 1.961-3.497.286-7.909.286-13.831.286h-76.416c-5.922 0-10.334 0-13.831-.286-3.491-.285-6.026-.852-8.202-1.961a20.623 20.623 0 0 1-9.012-9.012c-1.109-2.176-1.676-4.711-1.961-8.202-.286-3.497-.286-7.909-.286-13.831V49.792c0-5.922 0-10.334.286-13.831.285-3.49.852-6.026 1.961-8.202a20.623 20.623 0 0 1 9.012-9.012c2.176-1.109 4.711-1.676 8.202-1.96 3.497-.287 7.909-.287 13.831-.287Z"/><path stroke="url(#b)" d="M190.792 16.5h76.416c5.922 0 10.334 0 13.831.286 3.491.285 6.026.852 8.202 1.961a20.623 20.623 0 0 1 9.012 9.012c1.109 2.176 1.676 4.711 1.961 8.202.286 3.497.286 7.909.286 13.831v94.416c0 5.922 0 10.334-.286 13.831-.285 3.491-.852 6.026-1.961 8.202a20.623 20.623 0 0 1-9.012 9.012c-2.176 1.109-4.711 1.676-8.202 1.961-3.497.286-7.909.286-13.831.286h-76.416c-5.922 0-10.334 0-13.831-.286-3.491-.285-6.026-.852-8.202-1.961a20.623 20.623 0 0 1-9.012-9.012c-1.109-2.176-1.676-4.711-1.961-8.202-.286-3.497-.286-7.909-.286-13.831V49.792c0-5.922 0-10.334.286-13.831.285-3.49.852-6.026 1.961-8.202a20.623 20.623 0 0 1 9.012-9.012c2.176-1.109 4.711-1.676 8.202-1.96 3.497-.287 7.909-.287 13.831-.287Z"/><path fill="#F7F7F7" d="M188.74 30.627v-3.433h-.052l-1.037.73v-.699l1.089-.765h.742v4.167h-.742ZM185.743 30.627v-.8h-2.039v-.61l.329-.562a54.437 54.437 0 0 1 .682-1.12c.115-.185.23-.367.343-.546.116-.181.231-.358.347-.529h1.048v2.752h.563v.615h-.563v.8h-.71Zm-1.346-1.397h1.357v-2.175h-.043l-.263.404-.269.425c-.09.144-.18.29-.268.436a32.092 32.092 0 0 0-.514.866v.044ZM182.41 30.067a.444.444 0 0 1-.459-.453.446.446 0 0 1 .459-.456.44.44 0 0 1 .329.132c.087.087.13.195.13.323a.444.444 0 0 1-.459.454Zm0-2.143a.444.444 0 0 1-.459-.453.446.446 0 0 1 .459-.456.44.44 0 0 1 .329.132c.087.087.13.195.13.324a.444.444 0 0 1-.459.453ZM179.433 26.356c.223 0 .435.042.635.124.2.081.378.208.532.382.155.17.278.391.366.66.089.268.133.59.133.968v.006c0 .468-.067.868-.202 1.201a1.69 1.69 0 0 1-.58.768c-.251.177-.55.266-.899.266-.256 0-.487-.046-.693-.139a1.42 1.42 0 0 1-.508-.386 1.346 1.346 0 0 1-.268-.566l-.006-.032h.73l.012.029a.722.722 0 0 0 .404.424.894.894 0 0 0 .329.058c.22 0 .399-.065.538-.194.14-.13.245-.305.314-.522.072-.22.112-.462.122-.728.002-.029.002-.057.002-.084v-.084l-.129-.695a.859.859 0 0 0-.11-.43.82.82 0 0 0-1.43-.01.804.804 0 0 0-.112.422v.006c0 .162.035.305.107.43.071.123.168.22.291.292a.828.828 0 0 0 .422.107.859.859 0 0 0 .424-.104.774.774 0 0 0 .298-.289.806.806 0 0 0 .11-.419v-.005h.173v.768h-.101a.99.99 0 0 1-.223.306c-.098.094-.219.17-.363.228-.143.058-.31.087-.503.087-.266 0-.5-.06-.705-.18a1.302 1.302 0 0 1-.482-.49 1.432 1.432 0 0 1-.173-.705v-.006c0-.283.065-.534.196-.753a1.4 1.4 0 0 1 .549-.52c.235-.127.501-.19.8-.19ZM287.143 27.912v1.417c.279-.12.462-.4.462-.709a.771.771 0 0 0-.462-.709ZM278.793 28.336c0-.487 0-.731.095-.917a.865.865 0 0 1 .38-.38c.186-.095.43-.095.917-.095h4.522c.487 0 .731 0 .917.095a.865.865 0 0 1 .38.38c.095.186.095.43.095.917v.348c0 .487 0 .73-.095.916a.866.866 0 0 1-.38.38c-.186.095-.43.095-.917.095h-4.522c-.487 0-.731 0-.917-.095a.866.866 0 0 1-.38-.38c-.095-.186-.095-.43-.095-.917v-.347Z"/><path fill="#F7F7F7" fill-rule="evenodd" d="M272.625 27.328c.866 0 1.698.32 2.325.896a.123.123 0 0 0 .169-.001l.451-.44a.119.119 0 0 0 .037-.086.118.118 0 0 0-.038-.085c-1.645-1.522-4.242-1.522-5.888 0a.117.117 0 0 0-.037.085.118.118 0 0 0 .036.086l.452.44a.123.123 0 0 0 .169.001 3.44 3.44 0 0 1 2.324-.896Zm-.001 1.468c.472 0 .928.178 1.278.5a.12.12 0 0 0 .168-.002l.448-.46a.131.131 0 0 0 .036-.09.126.126 0 0 0-.038-.09 2.748 2.748 0 0 0-3.782 0 .127.127 0 0 0-.002.18l.448.46a.12.12 0 0 0 .168.002c.349-.322.804-.5 1.276-.5Zm.878.972a.141.141 0 0 1-.035.098l-.757.854a.114.114 0 0 1-.084.039.111.111 0 0 1-.084-.04l-.758-.853a.141.141 0 0 1-.035-.098c0-.037.015-.071.039-.096a1.194 1.194 0 0 1 1.675 0c.025.025.039.06.039.096ZM267.035 26.833c0-.22-.166-.398-.371-.398h-.371c-.205 0-.371.178-.371.398v3.456c0 .22.166.399.371.399h.371c.205 0 .371-.178.371-.399v-3.456Zm-2.586.452h.371c.205 0 .371.183.371.409v2.586c0 .225-.166.408-.371.408h-.371c-.205 0-.372-.183-.372-.408v-2.586c0-.226.167-.409.372-.409Zm-1.507.922h-.371c-.205 0-.372.185-.372.413v1.654c0 .229.167.414.372.414h.371c.205 0 .371-.185.371-.413V28.62c0-.228-.166-.413-.371-.413Zm-1.844.85h-.372c-.204 0-.371.183-.371.408v.815c0 .226.167.408.371.408h.372c.204 0 .371-.182.371-.408v-.815c0-.225-.167-.407-.371-.407Z" clip-rule="evenodd"/><path fill="#B2B2B2" d="M270.752 57.498c-.322 0-.642-.061-.957-.185a2.34 2.34 0 0 1-.855-.6c-.254-.277-.457-.642-.61-1.094-.153-.456-.23-1.016-.23-1.68v-.01c0-.8.108-1.484.323-2.05.215-.57.524-1.Line truncated
|
||||
|
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": "Τοποθεσία συμβάντος",
|
||||
|
||||
@@ -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": "活動地點",
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
File renamed without changes.
@@ -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
|
||||
|
||||
Loaded 100 of 226 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user