Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01d764c6d3 | ||
|
|
49333addc2 | ||
|
|
6c40d29745 | ||
|
|
fc5ad53a6d | ||
|
|
4d38c6d0c7 | ||
|
|
cd15335eed | ||
|
|
aaa75100d4 | ||
|
|
ae9063804d | ||
|
|
30f9d9b980 | ||
|
|
42a264380a | ||
|
|
62aaba32cf | ||
|
|
aaf630f01f | ||
|
|
838e8b9e89 | ||
|
|
ce373c475c | ||
|
|
ed662b7956 | ||
|
|
2e49110ce6 | ||
|
|
fc35f9de85 | ||
|
|
9c67ee7375 | ||
|
|
a1477dc7e1 | ||
|
|
25d39b4273 | ||
|
|
4976e24e12 | ||
|
|
5ae6e1e983 | ||
|
|
6080ce0c85 | ||
|
|
79f0754bf7 | ||
|
|
540bd478dc | ||
|
|
9873e373dc | ||
|
|
52093862ce | ||
|
|
e101f35b4f | ||
|
|
54cc2dc094 | ||
|
|
88e57d48fe | ||
|
|
6619701c88 | ||
|
|
ebb8bb9c67 | ||
|
|
993bf23986 | ||
|
|
4b38d73120 | ||
|
|
8f92c9b884 | ||
|
|
edecc7e60e | ||
|
|
4eefb08a0d | ||
|
|
3f7e632d5c | ||
|
|
ef0fd67646 | ||
|
|
8e7567b6a3 | ||
|
|
193855d60f | ||
|
|
92fafa7344 | ||
|
|
21aaff01fa | ||
|
|
e07a28a2d1 | ||
|
|
9a1b5ad149 | ||
|
|
1c20a257d3 |
@@ -19,7 +19,7 @@
|
||||
"test:watch": "yarn dev:build && jest --watch",
|
||||
"test:cov": "yarn dev:build && jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "yarn dev:build && NODE_OPTIONS='--max_old_space_size=8192' jest --ci --forceExit --config ./jest-e2e.json",
|
||||
"test:e2e": "yarn dev:build && NODE_OPTIONS='--max_old_space_size=8192 --experimental-vm-modules' jest --ci --forceExit --config ./jest-e2e.json",
|
||||
"test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json --watch",
|
||||
"prisma": "yarn workspace @calcom/prisma prisma",
|
||||
"generate-schemas": "yarn prisma generate && yarn prisma format",
|
||||
@@ -28,7 +28,7 @@
|
||||
"dependencies": {
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.56",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.57",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
@@ -45,7 +45,9 @@
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@nestjs/throttler": "6.2.1",
|
||||
"@sentry/nestjs": "^8.37.1",
|
||||
"@sentry/node": "^8.8.0",
|
||||
"@sentry/profiling-node": "^8.37.1",
|
||||
"body-parser": "^1.20.2",
|
||||
"bull": "^4.12.4",
|
||||
"class-transformer": "^0.5.1",
|
||||
|
||||
@@ -17,13 +17,15 @@ import { ThrottlerStorageRedisService } from "@nest-lab/throttler-storage-redis"
|
||||
import { BullModule } from "@nestjs/bull";
|
||||
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core";
|
||||
import { APP_GUARD, APP_INTERCEPTOR, APP_FILTER } from "@nestjs/core";
|
||||
import { seconds, ThrottlerModule } from "@nestjs/throttler";
|
||||
import { SentryModule, SentryGlobalFilter } from "@sentry/nestjs/setup";
|
||||
|
||||
import { AppController } from "./app.controller";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
SentryModule.forRoot(),
|
||||
ConfigModule.forRoot({
|
||||
ignoreEnvFile: true,
|
||||
isGlobal: true,
|
||||
@@ -59,6 +61,10 @@ import { AppController } from "./app.controller";
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: SentryGlobalFilter,
|
||||
},
|
||||
{
|
||||
provide: ThrottlerStorageRedisService,
|
||||
useFactory: (redisService: RedisService) => {
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { ValidationError } from "@nestjs/common";
|
||||
import { BadRequestException, ValidationPipe, VersioningType } from "@nestjs/common";
|
||||
import { BaseExceptionFilter, HttpAdapterHost } from "@nestjs/core";
|
||||
import type { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import * as cookieParser from "cookie-parser";
|
||||
import { Request } from "express";
|
||||
import helmet from "helmet";
|
||||
@@ -71,9 +70,6 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
||||
|
||||
// Exception filters, new filters go at the bottom, keep the order
|
||||
const { httpAdapter } = app.get(HttpAdapterHost);
|
||||
if (process.env.SENTRY_DSN) {
|
||||
Sentry.setupNestErrorHandler(app, new BaseExceptionFilter(httpAdapter));
|
||||
}
|
||||
app.useGlobalFilters(new PrismaExceptionFilter());
|
||||
app.useGlobalFilters(new ZodExceptionFilter());
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
|
||||
@@ -56,6 +56,17 @@ export class BookingsRepository_2024_08_13 {
|
||||
});
|
||||
}
|
||||
|
||||
async getByUidWithUser(bookingUid: string) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getByIdWithAttendeesAndUserAndEvent(id: number) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.gua
|
||||
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
|
||||
import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||
import { VERSION_2024_08_13_VALUE } from "@/lib/api-versions";
|
||||
@@ -9,6 +10,7 @@ import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
@@ -46,8 +48,7 @@ import {
|
||||
CreateBookingInputPipe,
|
||||
CreateBookingInput,
|
||||
GetBookingsInput_2024_08_13,
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
CancelBookingInput_2024_08_13,
|
||||
ReassignToUserBookingInput_2024_08_13,
|
||||
MarkAbsentBookingInput_2024_08_13,
|
||||
CreateBookingInput_2024_08_13,
|
||||
CreateInstantBookingInput_2024_08_13,
|
||||
@@ -227,4 +228,57 @@ export class BookingsController_2024_08_13 {
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/:bookingUid/reassign")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Permissions([BOOKING_WRITE])
|
||||
@UseGuards(ApiAuthGuard, BookingUidGuard)
|
||||
@ApiHeader({
|
||||
name: "Authorization",
|
||||
description:
|
||||
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
|
||||
required: true,
|
||||
})
|
||||
@ApiOperation({ summary: "Automatically reassign booking to a new host" })
|
||||
async reassignBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<ReassignBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.reassignBooking(bookingUid, user);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/:bookingUid/reassign/:userId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Permissions([BOOKING_WRITE])
|
||||
@UseGuards(ApiAuthGuard, BookingUidGuard)
|
||||
@ApiHeader({
|
||||
name: "Authorization",
|
||||
description:
|
||||
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
|
||||
required: true,
|
||||
})
|
||||
@ApiOperation({ summary: "Reassign a booking to a specific user" })
|
||||
async reassignBookingToUser(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Param("userId") userId: number,
|
||||
@GetUser("id") reassignedById: number,
|
||||
@Body() body: ReassignToUserBookingInput_2024_08_13
|
||||
): Promise<ReassignBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.reassignBookingToUser(
|
||||
bookingUid,
|
||||
userId,
|
||||
reassignedById,
|
||||
body
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { Booking, User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13", () => {
|
||||
describe("Reassign bookings", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
let team: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let hostsRepositoryFixture: HostsRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
|
||||
const teamUserEmail = "orgUser1team1@api.com";
|
||||
const teamUserEmail2 = "orgUser2team1@api.com";
|
||||
let teamUser1: User;
|
||||
let teamUser2: User;
|
||||
|
||||
let teamRoundRobinEventTypeId: number;
|
||||
|
||||
let roundRobinBooking: Booking;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
teamUserEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await organizationsRepositoryFixture.create({ name: "organization team bookings" });
|
||||
team = await teamRepositoryFixture.create({
|
||||
name: "team 1",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: organization.id } },
|
||||
});
|
||||
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
teamUser1 = await userRepositoryFixture.create({
|
||||
email: teamUserEmail,
|
||||
locale: "it",
|
||||
name: "orgUser1team1",
|
||||
});
|
||||
|
||||
teamUser2 = await userRepositoryFixture.create({
|
||||
email: teamUserEmail2,
|
||||
locale: "it",
|
||||
name: "orgUser2team1",
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(teamUser1.id, userSchedule);
|
||||
await schedulesService.createUserSchedule(teamUser2.id, userSchedule);
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${teamUser1.id}`,
|
||||
username: teamUserEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: organization.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser1.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${teamUser2.id}`,
|
||||
username: teamUserEmail2,
|
||||
organization: {
|
||||
connect: {
|
||||
id: organization.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser2.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
team: { connect: { id: team.id } },
|
||||
user: { connect: { id: teamUser1.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
team: { connect: { id: team.id } },
|
||||
user: { connect: { id: teamUser2.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
const team1EventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
team: {
|
||||
connect: { id: team.id },
|
||||
},
|
||||
users: {
|
||||
connect: [{ id: teamUser1.id }, { id: teamUser2.id }],
|
||||
},
|
||||
title: "Round Robin Event Type",
|
||||
slug: "round-robin-event-type",
|
||||
length: 60,
|
||||
assignAllTeamMembers: false,
|
||||
bookingFields: [],
|
||||
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
|
||||
});
|
||||
|
||||
teamRoundRobinEventTypeId = team1EventType.id;
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: false,
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser1.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: team1EventType.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: false,
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser2.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: team1EventType.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
roundRobinBooking = await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser1.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2050, 0, 7, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2050, 0, 7, 14, 0, 0)),
|
||||
title: "round robin coding lets goo",
|
||||
uid: "round-robin-coding",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: teamRoundRobinEventTypeId,
|
||||
},
|
||||
},
|
||||
location: "via 10, rome, italy",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "Bob",
|
||||
email: "bob@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "bob@gmail.com",
|
||||
name: "Bob",
|
||||
locale: "it",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should reassign round robin booking", async () => {
|
||||
const booking = await bookingsRepositoryFixture.getByUid(roundRobinBooking.uid);
|
||||
expect(booking?.userId).toEqual(teamUser1.id);
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${roundRobinBooking.uid}/reassign`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ReassignBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data;
|
||||
expect(data.bookingUid).toEqual(roundRobinBooking.uid);
|
||||
expect(data.reassignedTo).toEqual({
|
||||
id: teamUser2.id,
|
||||
name: teamUser2.name,
|
||||
email: teamUser2.email,
|
||||
});
|
||||
|
||||
const reassigned = await bookingsRepositoryFixture.getByUid(roundRobinBooking.uid);
|
||||
expect(reassigned?.userId).toEqual(teamUser2.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should reassign round robin booking to a specific user", async () => {
|
||||
const booking = await bookingsRepositoryFixture.getByUid(roundRobinBooking.uid);
|
||||
expect(booking?.userId).toEqual(teamUser2.id);
|
||||
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: teamRoundRobinEventTypeId,
|
||||
attendee: {
|
||||
name: "alice",
|
||||
email: "alice@gmail.com",
|
||||
timeZone: "Europe/Madrid",
|
||||
language: "es",
|
||||
},
|
||||
meetingUrl: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${roundRobinBooking.uid}/reassign/${teamUser1.id}`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ReassignBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data;
|
||||
expect(data.bookingUid).toEqual(roundRobinBooking.uid);
|
||||
expect(data.reassignedTo).toEqual({
|
||||
id: teamUser1.id,
|
||||
name: teamUser1.name,
|
||||
email: teamUser1.email,
|
||||
});
|
||||
|
||||
const reassigned = await bookingsRepositoryFixture.getByUid(roundRobinBooking.uid);
|
||||
expect(reassigned?.userId).toEqual(teamUser1.id);
|
||||
});
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(teamUser1.email);
|
||||
await userRepositoryFixture.deleteByEmail(teamUserEmail2);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(teamUser1.id, teamUser1.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(teamUser2.id, teamUser2.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { ReassignBookingOutput_2024_08_13 as ReassignBookingOutputData_2024_08_13 } from "@calcom/platform-types";
|
||||
|
||||
export class ReassignBookingOutput_2024_08_13 {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
oneOf: [{ $ref: getSchemaPath(ReassignBookingOutputData_2024_08_13) }],
|
||||
description:
|
||||
"Booking data, which can be either a ReassignAutoBookingOutput object or a ReassignManualBookingOutput object",
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => Object)
|
||||
data!: ReassignBookingOutputData_2024_08_13;
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_20
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
@@ -15,6 +17,8 @@ import {
|
||||
getAllUserBookings,
|
||||
handleInstantMeeting,
|
||||
handleCancelBooking,
|
||||
roundRobinReassignment,
|
||||
roundRobinManualReassignment,
|
||||
handleMarkNoShow,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
@@ -23,8 +27,8 @@ import {
|
||||
CreateRecurringBookingInput_2024_08_13,
|
||||
GetBookingsInput_2024_08_13,
|
||||
CreateInstantBookingInput_2024_08_13,
|
||||
CancelBookingInput_2024_08_13,
|
||||
MarkAbsentBookingInput_2024_08_13,
|
||||
ReassignToUserBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
@@ -50,7 +54,9 @@ export class BookingsService_2024_08_13 {
|
||||
private readonly bookingSeatRepository: BookingSeatRepository,
|
||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||
private readonly prismaReadService: PrismaReadService,
|
||||
private readonly billingService: BillingService
|
||||
private readonly billingService: BillingService,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly usersRepository: UsersRepository
|
||||
) {}
|
||||
|
||||
async createBooking(request: Request, body: CreateBookingInput) {
|
||||
@@ -358,4 +364,54 @@ export class BookingsService_2024_08_13 {
|
||||
fromReschedule: oldBookingUid,
|
||||
});
|
||||
}
|
||||
|
||||
async reassignBooking(bookingUid: string, requestUser: UserWithProfile) {
|
||||
const booking = await this.bookingsRepository.getByUid(bookingUid);
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
const profile = this.usersService.getUserMainProfile(requestUser);
|
||||
|
||||
await roundRobinReassignment({
|
||||
bookingId: booking.id,
|
||||
orgId: profile?.organizationId || null,
|
||||
});
|
||||
|
||||
const reassigned = await this.bookingsRepository.getByUidWithUser(bookingUid);
|
||||
if (!reassigned) {
|
||||
throw new NotFoundException(`Reassigned booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
return this.outputService.getOutputReassignedBooking(reassigned);
|
||||
}
|
||||
|
||||
async reassignBookingToUser(
|
||||
bookingUid: string,
|
||||
newUserId: number,
|
||||
reassignedById: number,
|
||||
body: ReassignToUserBookingInput_2024_08_13
|
||||
) {
|
||||
const booking = await this.bookingsRepository.getByUid(bookingUid);
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
const user = await this.usersRepository.findByIdWithProfile(newUserId);
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with id=${newUserId} was not found in the database`);
|
||||
}
|
||||
|
||||
const profile = this.usersService.getUserMainProfile(user);
|
||||
|
||||
const reassigned = await roundRobinManualReassignment({
|
||||
bookingId: booking.id,
|
||||
newUserId,
|
||||
orgId: profile?.organizationId || null,
|
||||
reassignReason: body.reason,
|
||||
reassignedById,
|
||||
});
|
||||
|
||||
return this.outputService.getOutputReassignedBooking(reassigned);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CreateSeatedBookingOutput_2024_08_13,
|
||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
ReassignBookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
SeatedAttendee,
|
||||
} from "@calcom/platform-types";
|
||||
@@ -50,6 +51,8 @@ type DatabaseBooking = Booking & {
|
||||
}[];
|
||||
} & { user: { id: number; name: string | null; email: string } | null };
|
||||
|
||||
type BookingWithUser = Booking & { user: { id: number; name: string | null; email: string } | null };
|
||||
|
||||
@Injectable()
|
||||
export class OutputBookingsService_2024_08_13 {
|
||||
constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {}
|
||||
@@ -308,4 +311,17 @@ export class OutputBookingsService_2024_08_13 {
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
getOutputReassignedBooking(
|
||||
databaseBooking: Pick<BookingWithUser, "uid" | "user">
|
||||
): ReassignBookingOutput_2024_08_13 {
|
||||
return {
|
||||
bookingUid: databaseBooking.uid,
|
||||
reassignedTo: {
|
||||
id: databaseBooking?.user?.id || 0,
|
||||
name: databaseBooking?.user?.name || "unknown",
|
||||
email: databaseBooking?.user?.email || "unknown",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ export type Environment = {
|
||||
DATABASE_URL: string;
|
||||
JWT_SECRET: string;
|
||||
SENTRY_DSN: string;
|
||||
SENTRY_TRACES_SAMPLE_RATE?: number;
|
||||
SENTRY_PROFILES_SAMPLE_RATE?: number;
|
||||
LOG_LEVEL: keyof typeof logLevels;
|
||||
REDIS_URL: string;
|
||||
STRIPE_API_KEY: string;
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { getEnv } from "@/env";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import * as Sentry from "@sentry/nestjs";
|
||||
import { nodeProfilingIntegration } from "@sentry/profiling-node";
|
||||
|
||||
if (process.env.SENTRY_DSN) {
|
||||
// Ensure to call this before requiring any other modules!
|
||||
Sentry.init({
|
||||
dsn: getEnv("SENTRY_DSN"),
|
||||
// Add Performance Monitoring by setting tracesSampleRate
|
||||
// We recommend adjusting this value in production
|
||||
// todo: Evaluate? tracesSampleRate: 1.0
|
||||
integrations: [nodeProfilingIntegration()],
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: getEnv("SENTRY_TRACES_SAMPLE_RATE") ?? 1.0, // Capture 100% of the transactions
|
||||
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
||||
profilesSampleRate: getEnv("SENTRY_PROFILES_SAMPLE_RATE") ?? 1.0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ function PageWrapper(props: AppProps) {
|
||||
pageStatus = "404";
|
||||
} else if (router.pathname === "/500") {
|
||||
pageStatus = "500";
|
||||
} else if (router.pathname === "/403") {
|
||||
pageStatus = "403";
|
||||
}
|
||||
|
||||
// On client side don't let nonce creep into DOM
|
||||
|
||||
@@ -31,6 +31,8 @@ function PageWrapper(props: PageWrapperProps) {
|
||||
pageStatus = "404";
|
||||
} else if (pathname === "/500") {
|
||||
pageStatus = "500";
|
||||
} else if (pathname === "/403") {
|
||||
pageStatus = "403";
|
||||
}
|
||||
|
||||
// On client side don't let nonce creep into DOM
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { Trans } from "next-i18next";
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
SettingsToggle,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
InputField,
|
||||
DialogClose,
|
||||
Button,
|
||||
} from "@calcom/ui";
|
||||
|
||||
interface DisableEmailsSettingProps {
|
||||
checked: boolean;
|
||||
onCheckedChange: (e: boolean) => void;
|
||||
recipient: "attendees" | "hosts";
|
||||
t: TFunction;
|
||||
}
|
||||
|
||||
export const DisableAllEmailsSetting = ({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
recipient,
|
||||
t,
|
||||
}: DisableEmailsSettingProps) => {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [confirmText, setConfirmText] = useState("");
|
||||
|
||||
const title =
|
||||
recipient === "attendees" ? t("disable_all_emails_to_attendees") : t("disable_all_emails_to_hosts");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog open={dialogOpen} onOpenChange={(e) => setDialogOpen(e)}>
|
||||
<DialogContent title={title} Icon="circle-alert">
|
||||
<p className="text-default text-sm">
|
||||
<Trans i18nKey="disable_attendees_emails_description">
|
||||
This will disable all emails to {{ recipient }}. This includes booking confirmations, requests,
|
||||
reschedules and reschedule requests, cancellation emails, and any other emails related to
|
||||
booking updates.
|
||||
<br />
|
||||
<br />
|
||||
It is your responsibility to ensure that your {{ recipient }} are aware of any bookings and
|
||||
changes to their bookings.
|
||||
</Trans>
|
||||
</p>
|
||||
<p className="text-default mb-1 mt-2 text-sm">{t("type_confirm_to_continue")}</p>
|
||||
<InputField
|
||||
value={confirmText}
|
||||
onChange={(e) => {
|
||||
setConfirmText(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
<Button
|
||||
disabled={confirmText !== "confirm"}
|
||||
onClick={(e) => {
|
||||
onCheckedChange(true);
|
||||
setDialogOpen(false);
|
||||
}}>
|
||||
{t("disable_email")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<SettingsToggle
|
||||
labelClassName="text-sm"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
||||
title={title}
|
||||
description={t("disable_all_emails_description")}
|
||||
checked={!!checked}
|
||||
onCheckedChange={() => {
|
||||
checked ? onCheckedChange(!checked) : setDialogOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,8 +1,12 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ParsedUrlQuery } from "querystring";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getCRMContactOwnerForRRLeadSkip } from "@calcom/app-store/_utils/CRMRoundRobinSkip";
|
||||
import { ROUTING_FORM_RESPONSE_ID_QUERY_STRING } from "@calcom/app-store/routing-forms/lib/constants";
|
||||
import { enabledAppSlugs } from "@calcom/app-store/routing-forms/lib/enabledApps";
|
||||
import { zodRoutes as routesSchema } from "@calcom/app-store/routing-forms/zod";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booking";
|
||||
@@ -79,6 +83,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
}
|
||||
|
||||
const eventData = team.eventTypes[0];
|
||||
const eventTypeId = eventData.id;
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (rescheduleUid) {
|
||||
@@ -92,7 +97,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
return {
|
||||
props: {
|
||||
eventData: {
|
||||
eventTypeId: eventData.id,
|
||||
eventTypeId,
|
||||
entity: {
|
||||
fromRedirectOfNonOrgLink,
|
||||
considerUnpublished: isUnpublished && !fromRedirectOfNonOrgLink,
|
||||
@@ -112,21 +117,133 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
isInstantMeeting: eventData && queryIsInstantMeeting ? true : false,
|
||||
themeBasis: null,
|
||||
orgBannerUrl: team.parent?.bannerUrl ?? "",
|
||||
teamMemberEmail: await getTeamMemberEmail(eventData, email as string),
|
||||
teamMemberEmail: await handleGettingTeamMemberEmail(query, eventTypeId, eventData),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
async function getTeamMemberEmail(
|
||||
eventData: {
|
||||
id: number;
|
||||
isInstantEvent: boolean;
|
||||
schedulingType: SchedulingType | null;
|
||||
metadata: Prisma.JsonValue | null;
|
||||
length: number;
|
||||
},
|
||||
email?: string
|
||||
): Promise<string | null> {
|
||||
interface EventData {
|
||||
id: number;
|
||||
isInstantEvent: boolean;
|
||||
schedulingType: SchedulingType | null;
|
||||
metadata: Prisma.JsonValue | null;
|
||||
length: number;
|
||||
}
|
||||
|
||||
async function handleGettingTeamMemberEmail(
|
||||
query: ParsedUrlQuery,
|
||||
eventTypeId: number,
|
||||
eventData: EventData
|
||||
) {
|
||||
if (
|
||||
!query.email ||
|
||||
typeof query.email !== "string" ||
|
||||
eventData.schedulingType !== SchedulingType.ROUND_ROBIN
|
||||
)
|
||||
return null;
|
||||
|
||||
// Check if a routing form was completed and an routing form option is enabled
|
||||
if (
|
||||
ROUTING_FORM_RESPONSE_ID_QUERY_STRING in query &&
|
||||
Object.values(query).some((value) => value === "true")
|
||||
) {
|
||||
const { email, skipContactOwner } = await handleRoutingFormOption(query, eventTypeId);
|
||||
|
||||
if (skipContactOwner) return null;
|
||||
if (email) return email;
|
||||
} else {
|
||||
return await getTeamMemberEmail(eventData, query.email);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function handleRoutingFormOption(query: ParsedUrlQuery, eventTypeId: number) {
|
||||
const nullReturnValue = { email: null, skipContactOwner: false };
|
||||
|
||||
if (typeof query.email !== "string") return nullReturnValue;
|
||||
|
||||
const routingFormQuery = await prisma.app_RoutingForms_Form.findFirst({
|
||||
where: {
|
||||
responses: {
|
||||
some: {
|
||||
id: Number(query[ROUTING_FORM_RESPONSE_ID_QUERY_STRING]),
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
routes: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!routingFormQuery || !routingFormQuery?.routes) return nullReturnValue;
|
||||
|
||||
const parsedRoutes = routesSchema.safeParse(routingFormQuery.routes);
|
||||
|
||||
if (!parsedRoutes.success || !parsedRoutes.data) return nullReturnValue;
|
||||
|
||||
// Find the route with the attributeRoutingConfig
|
||||
const route = parsedRoutes.data.find((route) => {
|
||||
if ("action" in route) {
|
||||
return route.action.eventTypeId === eventTypeId;
|
||||
}
|
||||
});
|
||||
|
||||
if (!route || !("attributeRoutingConfig" in route)) return nullReturnValue;
|
||||
|
||||
// Get attributeRoutingConfig for the form
|
||||
const attributeRoutingConfig = route.attributeRoutingConfig;
|
||||
|
||||
if (!attributeRoutingConfig) return nullReturnValue;
|
||||
|
||||
// If the skipContactOwner is enabled then don't return an team member email
|
||||
if (attributeRoutingConfig?.skipContactOwner) return { ...nullReturnValue, skipContactOwner: true };
|
||||
|
||||
// Determine if a routing form enabled app is in the query. Then pass it to the proper handler
|
||||
// Routing form apps will have the format cal.appSlug
|
||||
let enabledRoutingFormApp;
|
||||
|
||||
for (const key of Object.keys(query)) {
|
||||
const keySplit = key.split(".");
|
||||
|
||||
const appSlug = keySplit[1];
|
||||
|
||||
if (enabledAppSlugs.includes(appSlug)) {
|
||||
enabledRoutingFormApp = appSlug;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!enabledRoutingFormApp) return nullReturnValue;
|
||||
|
||||
const appBookingFormHandler = (await import("@calcom/app-store/routing-forms/appBookingFormHandler"))
|
||||
.default;
|
||||
const appHandler = appBookingFormHandler[enabledRoutingFormApp];
|
||||
|
||||
if (!appHandler) return nullReturnValue;
|
||||
|
||||
const { email: userEmail } = await appHandler(query.email, attributeRoutingConfig, eventTypeId);
|
||||
|
||||
if (!userEmail) return nullReturnValue;
|
||||
|
||||
// Determine if the user is a part of the event type
|
||||
const userQuery = await await prisma.user.findFirst({
|
||||
where: {
|
||||
email: userEmail,
|
||||
hosts: {
|
||||
some: {
|
||||
eventTypeId: eventTypeId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!userQuery) return nullReturnValue;
|
||||
|
||||
return { ...nullReturnValue, email: userEmail };
|
||||
}
|
||||
|
||||
async function getTeamMemberEmail(eventData: EventData, email: string): Promise<string | null> {
|
||||
// Pre-requisites
|
||||
if (!eventData || !email || eventData.schedulingType !== SchedulingType.ROUND_ROBIN) return null;
|
||||
const crmContactOwnerEmail = await getCRMContactOwnerForRRLeadSkip(email, eventData.metadata);
|
||||
|
||||
@@ -315,8 +315,23 @@ export default function Success(props: PageProps) {
|
||||
return t(`needs_to_be_confirmed_or_rejected${titleSuffix}`);
|
||||
}
|
||||
if (bookingInfo.user) {
|
||||
return t(`${titlePrefix}emailed_you_and_attendees${titleSuffix}`, {
|
||||
user: bookingInfo.user.name || bookingInfo.user.email,
|
||||
const isHost = bookingInfo.user.id === session?.user?.id;
|
||||
const isAttendee = bookingInfo.attendees.find((attendee) => attendee.email === session?.user?.email);
|
||||
const attendee = bookingInfo.attendees[0]?.name || bookingInfo.attendees[0]?.email || "Nameless";
|
||||
const host = bookingInfo.user.name || bookingInfo.user.email;
|
||||
if (isHost) {
|
||||
return t(`${titlePrefix}emailed_you_and_attendees${titleSuffix}`, {
|
||||
user: attendee,
|
||||
});
|
||||
}
|
||||
if (isAttendee) {
|
||||
return t(`${titlePrefix}emailed_you_and_attendees${titleSuffix}`, {
|
||||
user: host,
|
||||
});
|
||||
}
|
||||
return t(`${titlePrefix}emailed_host_and_attendee${titleSuffix}`, {
|
||||
host,
|
||||
attendee,
|
||||
});
|
||||
}
|
||||
return t(`emailed_you_and_attendees${titleSuffix}`);
|
||||
|
||||
@@ -18,8 +18,9 @@ import { InfiniteSkeletonLoader } from "@calcom/features/eventtypes/components/S
|
||||
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { parseEventTypeColor } from "@calcom/lib";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useCopy } from "@calcom/lib/hooks/useCopy";
|
||||
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
|
||||
import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -257,6 +258,7 @@ export const InfiniteEventTypeList = ({
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { copyToClipboard } = useCopy();
|
||||
const [parent] = useAutoAnimate<HTMLUListElement>();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deleteDialogTypeId, setDeleteDialogTypeId] = useState(0);
|
||||
@@ -566,7 +568,7 @@ export const InfiniteEventTypeList = ({
|
||||
StartIcon="link"
|
||||
onClick={() => {
|
||||
showToast(t("link_copied"), "success");
|
||||
navigator.clipboard.writeText(calLink);
|
||||
copyToClipboard(calLink);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -579,7 +581,7 @@ export const InfiniteEventTypeList = ({
|
||||
StartIcon="venetian-mask"
|
||||
onClick={() => {
|
||||
showToast(t("private_link_copied"), "success");
|
||||
navigator.clipboard.writeText(placeholderHashedLink);
|
||||
copyToClipboard(placeholderHashedLink);
|
||||
setPrivateLinkCopyIndices((prev) => {
|
||||
const prevIndex = prev[type.slug] ?? 0;
|
||||
prev[type.slug] = (prevIndex + 1) % type.hashedLink.length;
|
||||
@@ -893,6 +895,7 @@ const InfiniteScrollMain = ({
|
||||
}) => {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { data } = useTypedQuery(querySchema);
|
||||
const orgBranding = useOrgBranding();
|
||||
|
||||
if (status === "error") {
|
||||
return <Alert severity="error" title="Something went wrong" message={errorMessage} />;
|
||||
@@ -911,6 +914,18 @@ const InfiniteScrollMain = ({
|
||||
const activeEventTypeGroup =
|
||||
eventTypeGroups.filter((item) => item.teamId === data.teamId) ?? eventTypeGroups[0];
|
||||
|
||||
const bookerUrl = orgBranding ? orgBranding?.fullDomain : WEBSITE_URL;
|
||||
|
||||
// If the event type group is the same as the org branding team, or the parent team, set the bookerUrl to the org branding URL
|
||||
// This is to ensure that the bookerUrl is always the same as the one in the org branding settings
|
||||
// This keeps the app working for personal event types that were not migrated to the org (rare)
|
||||
if (
|
||||
activeEventTypeGroup[0].teamId === orgBranding?.id ||
|
||||
activeEventTypeGroup[0].parentId === orgBranding?.id
|
||||
) {
|
||||
activeEventTypeGroup[0].bookerUrl = bookerUrl;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{eventTypeGroups.length >= 1 && (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "4.6.17",
|
||||
"version": "4.6.21",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import Head from "next/head";
|
||||
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
export default function AccessDenied() {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div className="bg-subtle flex h-screen">
|
||||
<Head>
|
||||
<title>{`${t("access_denied")} | ${APP_NAME}`}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
|
||||
<h1 className="font-cal text-emphasis text-6xl">403</h1>
|
||||
<h2 className="text-emphasis mt-6 text-2xl font-medium">{t("dont_have_access_this_page")}</h2>
|
||||
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">
|
||||
{t("you_need_admin_or_owner_privileges_to_access")}
|
||||
</p>
|
||||
<Button href={WEBAPP_URL}>{t("go_back_home")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
AccessDenied.PageWrapper = PageWrapper;
|
||||
@@ -85,7 +85,10 @@ export default async function handler(req: NextApiRequest) {
|
||||
ogConfig
|
||||
) as { body: Buffer };
|
||||
|
||||
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||
return new Response(img.body, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "image/png", "cache-control": "max-age=86400" },
|
||||
});
|
||||
}
|
||||
case "app": {
|
||||
const { name, description, slug } = appSchema.parse({
|
||||
|
||||
@@ -94,6 +94,7 @@ const createTeamEventType = async (
|
||||
teamEventLength?: number;
|
||||
seatsPerTimeSlot?: number;
|
||||
managedEventUnlockedFields?: Record<string, boolean>;
|
||||
assignAllTeamMembers?: boolean;
|
||||
}
|
||||
) => {
|
||||
return await prisma.eventType.create({
|
||||
@@ -138,6 +139,7 @@ const createTeamEventType = async (
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
assignAllTeamMembers: scenario?.assignAllTeamMembers,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -154,6 +156,7 @@ const createTeamAndAddUser = async (
|
||||
index,
|
||||
orgRequestedSlug,
|
||||
schedulingType,
|
||||
assignAllTeamMembersForSubTeamEvents,
|
||||
}: {
|
||||
user: { id: number; email: string; username: string | null; role?: MembershipRole };
|
||||
isUnpublished?: boolean;
|
||||
@@ -165,6 +168,7 @@ const createTeamAndAddUser = async (
|
||||
index?: number;
|
||||
orgRequestedSlug?: string;
|
||||
schedulingType?: SchedulingType;
|
||||
assignAllTeamMembersForSubTeamEvents?: boolean;
|
||||
},
|
||||
workerInfo: WorkerInfo
|
||||
) => {
|
||||
@@ -193,6 +197,7 @@ const createTeamAndAddUser = async (
|
||||
const team = await createTeamAndAddUser({ user }, workerInfo);
|
||||
await createTeamEventType(user, team, {
|
||||
schedulingType: schedulingType,
|
||||
assignAllTeamMembers: assignAllTeamMembersForSubTeamEvents,
|
||||
});
|
||||
await createTeamWorkflow(user, team);
|
||||
data.children = { connect: [{ id: team.id }] };
|
||||
@@ -282,6 +287,8 @@ export const createUsersFixture = (
|
||||
addManagedEventToTeamMates?: boolean;
|
||||
managedEventUnlockedFields?: Record<string, boolean>;
|
||||
orgRequestedSlug?: string;
|
||||
assignAllTeamMembers?: boolean;
|
||||
assignAllTeamMembersForSubTeamEvents?: boolean;
|
||||
} = {}
|
||||
) => {
|
||||
const _user = await prisma.user.create({
|
||||
@@ -537,6 +544,7 @@ export const createUsersFixture = (
|
||||
organizationId: opts?.organizationId,
|
||||
orgRequestedSlug: scenario.orgRequestedSlug,
|
||||
schedulingType: scenario.schedulingType,
|
||||
assignAllTeamMembersForSubTeamEvents: scenario.assignAllTeamMembersForSubTeamEvents,
|
||||
},
|
||||
workerInfo
|
||||
);
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 221 KiB |
@@ -1,6 +1,8 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
import { getInviteLink, localize } from "../lib/testUtils";
|
||||
@@ -121,4 +123,60 @@ test.describe("Team", () => {
|
||||
).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
test("Invited member is assigned to existing managed event, after invitation is accepted", async ({
|
||||
page,
|
||||
users,
|
||||
}) => {
|
||||
const t = await localize("en");
|
||||
const teamEventSlugAndTitle = "managed-event-test";
|
||||
const teamMatesObj = [{ name: "teammate-1" }, { name: "teammate-2" }];
|
||||
const teamOwner = await users.create(
|
||||
{ name: `team-owner-${Date.now()}` },
|
||||
{
|
||||
hasTeam: true,
|
||||
teamRole: MembershipRole.ADMIN,
|
||||
teammates: teamMatesObj,
|
||||
schedulingType: SchedulingType.MANAGED,
|
||||
teamEventSlug: teamEventSlugAndTitle,
|
||||
teamEventTitle: teamEventSlugAndTitle,
|
||||
teamEventLength: 30,
|
||||
addManagedEventToTeamMates: true,
|
||||
assignAllTeamMembers: true,
|
||||
}
|
||||
);
|
||||
const invitedMember = await users.create({
|
||||
name: `invited-member-${Date.now()}`,
|
||||
email: `invited-member-${Date.now()}@example.com`,
|
||||
});
|
||||
const { team } = await teamOwner.getFirstTeamMembership();
|
||||
|
||||
await teamOwner.apiLogin();
|
||||
await page.goto(`/settings/teams/${team.id}/members`);
|
||||
await page.getByTestId("new-member-button").click();
|
||||
await page.locator('input[name="inviteUser"]').fill(invitedMember.email);
|
||||
await page.locator(`button:text("${t("send_invite")}")`).click();
|
||||
|
||||
await invitedMember.apiLogin();
|
||||
await page.goto(`/teams`);
|
||||
await page.getByTestId(`accept-invitation-${team.id}`).click();
|
||||
const response = await page.waitForResponse("/api/trpc/teams/acceptOrLeave?batch=1");
|
||||
expect(response.status()).toBe(200);
|
||||
await page.goto(`/event-types`);
|
||||
|
||||
//ensure managed event-type is created for the invited member
|
||||
await expect(page.locator(`text="${teamEventSlugAndTitle}"`)).toBeVisible();
|
||||
|
||||
//ensure the new event-type created for invited member is child of team event-type
|
||||
const parentEventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
slug: teamEventSlugAndTitle,
|
||||
teamId: team.id,
|
||||
},
|
||||
select: {
|
||||
children: true,
|
||||
},
|
||||
});
|
||||
expect(parentEventType?.children.find((et) => et.userId === invitedMember.id)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -161,6 +161,10 @@
|
||||
<symbol class="lucide lucide-chevron-up" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="chevron-up">
|
||||
<path d="m18 15-6-6-6 6" ></path>
|
||||
</symbol>
|
||||
<symbol class="lucide lucide-chevrons-down-up" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="chevrons-down-up">
|
||||
<path d="m7 20 5-5 5 5" ></path>
|
||||
<path d="m7 4 5 5 5-5" ></path>
|
||||
</symbol>
|
||||
<symbol class="lucide lucide-chevrons-left" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="chevrons-left">
|
||||
<path d="m11 17-5-5 5-5" ></path>
|
||||
<path d="m18 17-5-5 5-5" ></path>
|
||||
|
||||
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "تعطيل الويبهوك",
|
||||
"webhook_response": "رد الويبهوك",
|
||||
"webhook_test": "اختبار ويبهوك",
|
||||
"webhook_update_form": "نموذج تحديث الويب هوك",
|
||||
"webhook_update_form_description": "أضف ويب هوك إلى عميل OAuth الخاص بك.",
|
||||
"manage_your_webhook": "إدارة الويبهوك الخاص بك",
|
||||
"webhook_created_successfully": "تم إنشاء الويبهوك بنجاح!",
|
||||
"webhook_updated_successfully": "تم تحديث الويبهوك بنجاح!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "ليس لديك أي فرق حتى الآن.",
|
||||
"no_teams_description": "تسمح الفرق للآخرين بحجز الأحداث المشتركة بين زملائك في العمل.",
|
||||
"submit": "إرسال",
|
||||
"confirm_remove_attribute": "نعم، إزالة السمة",
|
||||
"remove_attribute": "إزالة السمة",
|
||||
"remove_attribute_confirmation_message": "هل أنت متأكد أنك تريد إزالة السمة '{{name}}'؟ سيتم إلغاء تعيين جميع المستخدمين المعينين لها حالياً.",
|
||||
"delete": "حذف",
|
||||
"update": "تحديث",
|
||||
"save": "حفظ",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "التنسيق",
|
||||
"uppercase_for_letters": "استخدم الحروف الكبيرة لجميع الأحرف",
|
||||
"replace_whitespaces_underscores": "استبدل المساحات البيضاء بالشرطات السفلية",
|
||||
"platform_billing": "فوترة المنصة",
|
||||
"manage_billing": "إدارة الفواتير",
|
||||
"manage_billing_description": "إدارة كل ما يخص الفواتير",
|
||||
"billing_freeplan_title": "أنت حاليًا على الخطة المجانية",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "هل تبحث عن المزيد من Insights؟",
|
||||
"filters": "الفلاتر",
|
||||
"add_filter": "إضافة عامل تصفية",
|
||||
"add_rule": "إضافة قاعدة",
|
||||
"add_rule_group": "إضافة مجموعة قواعد",
|
||||
"remove_filters": "إزالة جميع الفلاتر",
|
||||
"email_verified": "تم التحقق من البريد الإلكتروني",
|
||||
"select_user": "اختر المستخدم",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "إعداد منظمتك",
|
||||
"set_up_your_platform_organization": "إعداد منصتك",
|
||||
"organizations_description": "المنظمات هي بيئات مشتركة حيث يمكن للفرق إنشاء أنواع الأحداث المشتركة والتطبيقات ومهام سير العمل والمزيد.",
|
||||
"plans": "الخطط",
|
||||
"platform": "المنصة",
|
||||
"platform_description": "إدارة كل ما يتعلق بالمنصة.",
|
||||
"platform_organization_description": "تتيح لك منصة Cal.com دمج الجدولة بسهولة في تطبيقك باستخدام واجهات برمجة التطبيقات والعناصر.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "لا يدعم خيار المقعد فترات متعددة",
|
||||
"include_calendar_event": "إدراج فعاليات في التقويم",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "نموذج إنشاء عميل OAuth",
|
||||
"oAuth_client_creation_form_description": "هذا هو النموذج لإنشاء عميل OAuth جديد",
|
||||
"oAuth_client_updation_form": "نموذج تحديث عميل OAuth",
|
||||
"oAuth_client_updation_form_description": "هذا هو النموذج لتعديل عميل OAuth موجود",
|
||||
"recently_added": "تمت الإضافة مؤخراً",
|
||||
"connect_all_calendars": "قم بربط جميع التقويمات",
|
||||
"connect_all_calendars_description": "{{appName}} يقرأ التوافر من جميع تقاويمك الحالية.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "معلومات وقت انتهاء الحدث في المنطقة الزمنية للحاضر",
|
||||
"salesforce_create_event_on_contact": "إنشاء حدث على جهة الاتصال، إذا كانت موجودة. وإلا، الرجوع إلى العميل المحتمل",
|
||||
"salesforce_owner_name_to_change": "اسم المالك المراد تغييره",
|
||||
"field_type": "نوع الحقل",
|
||||
"when_to_write": "متى تتم الكتابة",
|
||||
"salesforce_on_booking_write_to_record": "عند الحجز، اكتب في حقل مخصص في سجل الحاضر {{record}}",
|
||||
"on_every_booking": "عند كل حجز",
|
||||
"only_if_field_is_empty": "فقط إذا كان الحقل فارغًا",
|
||||
"booking_start_date": "تاريخ بدء الحجز",
|
||||
"booking_created_date": "تاريخ إنشاء الحجز",
|
||||
"booking_reassigned_to_host": "تم إعادة تعيين الحجز إلى {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook Deaktivdir",
|
||||
"webhook_response": "Webhook cavabı",
|
||||
"webhook_test": "Webhook testi",
|
||||
"webhook_update_form": "Webhook yeniləmə forması",
|
||||
"webhook_update_form_description": "OAuthClient-ə webhook əlavə edin.",
|
||||
"manage_your_webhook": "Webhook-u idarə edin",
|
||||
"webhook_created_successfully": "Webhook uğurla yaradıldı!",
|
||||
"webhook_updated_successfully": "Webhook uğurla yeniləndi!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Hələ heç bir komandanız yoxdur.",
|
||||
"no_teams_description": "Komandalar həmkarlarınız arasında paylaşılan tədbirləri sifariş etməyə imkan verir.",
|
||||
"submit": "Təsdiqlə",
|
||||
"confirm_remove_attribute": "Bəli, atributu sil",
|
||||
"remove_attribute": "Atributu sil",
|
||||
"remove_attribute_confirmation_message": "Atributu '{{name}}' silmək istədiyinizə əminsinizmi? Hazırda ona təyin edilmiş bütün istifadəçilər təyin edilməmiş olacaq.",
|
||||
"delete": "Sil",
|
||||
"update": "Yenilə",
|
||||
"save": "Yadda saxla",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Format",
|
||||
"uppercase_for_letters": "Bütün hərfləri böyük hərflərlə yaz",
|
||||
"replace_whitespaces_underscores": "Boşluqları alt xətlərlə əvəz et",
|
||||
"platform_billing": "Platforma ödənişi",
|
||||
"manage_billing": "Ödənişləri idarə et",
|
||||
"manage_billing_description": "Bütün ödəniş işlərini idarə et",
|
||||
"billing_freeplan_title": "Hazırda PULSUZ plandasınız",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "Daha çox məlumat axtarırsınız?",
|
||||
"filters": "Filtrlər",
|
||||
"add_filter": "Filtr əlavə et",
|
||||
"add_rule": "Qayda əlavə et",
|
||||
"add_rule_group": "Qayda qrupu əlavə et",
|
||||
"remove_filters": "Bütün filtrləri təmizlə",
|
||||
"email_verified": "Email Təsdiqləndi",
|
||||
"select_user": "İstifadəçini seçin",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Təşkilatınızı qurun",
|
||||
"set_up_your_platform_organization": "Platformanızı qurun",
|
||||
"organizations_description": "Təşkilatlar komandaların paylaşılan tədbir növləri, tətbiqlər, iş axınları və daha çoxunu yarada biləcəyi paylaşılan mühitlərdir.",
|
||||
"plans": "Planlar",
|
||||
"platform": "Platforma",
|
||||
"platform_description": "Platforma ilə bağlı hər şeyi idarə edin.",
|
||||
"platform_organization_description": "Cal.com Platforması, platforma API-ləri və atomları istifadə edərək planlaşdırmanı tətbiqinizə asanlıqla inteqrasiya etməyə imkan verir.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Oturacaq seçimi bir neçə müddəti dəstəkləmir",
|
||||
"include_calendar_event": "Təqvim tədbirini daxil edin",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "OAuth müştəri yaratma forması",
|
||||
"oAuth_client_creation_form_description": "Bu, yeni OAuth müştərisi yaratmaq üçün formadır",
|
||||
"oAuth_client_updation_form": "OAuth müştəri yeniləmə forması",
|
||||
"oAuth_client_updation_form_description": "Bu, mövcud OAuth müştərisini redaktə etmək üçün formadır",
|
||||
"recently_added": "Son zamanlar əlavə olunmuş",
|
||||
"connect_all_calendars": "Bütün təqvimlərinizi qoşun",
|
||||
"connect_all_calendars_description": "{{appName}} mövcud təqvimlərinizdən mövcudluğu oxuyur.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "İştirakçı Saat Qurşağında Tədbirin Bitmə Vaxtı",
|
||||
"salesforce_create_event_on_contact": "Əgər mövcuddursa, kontakt üzərində hadisə yaradın. Əks halda, lidə qayıdın",
|
||||
"salesforce_owner_name_to_change": "Dəyişdiriləcək sahibin adı",
|
||||
"field_type": "Sahə növü",
|
||||
"when_to_write": "Nə zaman yazmaq",
|
||||
"salesforce_on_booking_write_to_record": "Rezervasiya zamanı iştirakçının {{record}} qeydinə xüsusi sahəyə yaz",
|
||||
"on_every_booking": "Hər rezervasiyada",
|
||||
"only_if_field_is_empty": "Yalnız sahə boşdursa",
|
||||
"booking_start_date": "Rezervasiyanın başlama tarixi",
|
||||
"booking_created_date": "Rezervasiyanın yaradılma tarixi",
|
||||
"booking_reassigned_to_host": "Rezervasiya {{host}}-a yenidən təyin edildi",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni sətirləri bura əlavə edin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook vypnut",
|
||||
"webhook_response": "Webhook odpověď",
|
||||
"webhook_test": "Test webhooku",
|
||||
"webhook_update_form": "Formulář pro aktualizaci webhooku",
|
||||
"webhook_update_form_description": "Přidejte webhook k vašemu OAuthClientovi.",
|
||||
"manage_your_webhook": "Spravovat webhook",
|
||||
"webhook_created_successfully": "Webhook úspěšně vytvořen!",
|
||||
"webhook_updated_successfully": "Webhook úspěšně aktualizován!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Zatím nemáte žádné týmy.",
|
||||
"no_teams_description": "Týmy ostatním umožňují rezervovat sdílené události mezi spolupracovníky.",
|
||||
"submit": "Odeslat",
|
||||
"confirm_remove_attribute": "Ano, odstranit atribut",
|
||||
"remove_attribute": "Odstranit atribut",
|
||||
"remove_attribute_confirmation_message": "Opravdu chcete odstranit atribut '{{name}}'? Všichni uživatelé, kteří jsou k němu aktuálně přiřazeni, budou od něj odpojeni.",
|
||||
"delete": "Smazat",
|
||||
"update": "Aktualizovat",
|
||||
"save": "Uložit",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Formát",
|
||||
"uppercase_for_letters": "Použít velká písmena pro všechna písmena",
|
||||
"replace_whitespaces_underscores": "Nahradit mezery podtržítky",
|
||||
"platform_billing": "Fakturace platformy",
|
||||
"manage_billing": "Spravovat fakturaci",
|
||||
"manage_billing_description": "Spravovat vše ohledně fakturace",
|
||||
"billing_freeplan_title": "Momentálně používáte tarif ZDARMA",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "Hledáte další přehledy?",
|
||||
"filters": "Filtry",
|
||||
"add_filter": "Přidat filtr",
|
||||
"add_rule": "Přidat pravidlo",
|
||||
"add_rule_group": "Přidat skupinu pravidel",
|
||||
"remove_filters": "Vymazat všechny filtry",
|
||||
"email_verified": "E-mail ověřen",
|
||||
"select_user": "Vybrat uživatele",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Nastavení organizace",
|
||||
"set_up_your_platform_organization": "Nastavte svou platformu",
|
||||
"organizations_description": "Organizace jsou sdílená prostředí, ve kterých mohou týmy vytvářet sdílené typy událostí, aplikace, pracovní postupy a mnoho dalšího.",
|
||||
"plans": "Plány",
|
||||
"platform": "Platforma",
|
||||
"platform_description": "Spravujte vše, co souvisí s platformou.",
|
||||
"platform_organization_description": "Platforma Cal.com vám umožňuje snadno integrovat plánování do vaší aplikace pomocí platformových API a atomů.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Volba místa nepodporuje více dob trvání",
|
||||
"include_calendar_event": "Zahrnout událost kalendáře",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Formulář pro vytvoření OAuth klienta",
|
||||
"oAuth_client_creation_form_description": "Toto je formulář pro vytvoření nového OAuth klienta",
|
||||
"oAuth_client_updation_form": "Formulář pro aktualizaci OAuth klienta",
|
||||
"oAuth_client_updation_form_description": "Toto je formulář pro úpravu existujícího OAuth klienta",
|
||||
"recently_added": "Nedávno přidáno",
|
||||
"connect_all_calendars": "Propojte všechny své kalendáře",
|
||||
"connect_all_calendars_description": "{{appName}} čte dostupnost ze všech vašich stávajících kalendářů.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Čas konce události v časovém pásmu účastníka",
|
||||
"salesforce_create_event_on_contact": "Vytvořit událost na kontaktu, pokud existuje. Jinak použít lead",
|
||||
"salesforce_owner_name_to_change": "Jméno vlastníka ke změně",
|
||||
"field_type": "Typ pole",
|
||||
"when_to_write": "Kdy zapisovat",
|
||||
"salesforce_on_booking_write_to_record": "Při rezervaci zapsat do vlastního pole záznamu účastníka {{record}}",
|
||||
"on_every_booking": "Při každé rezervaci",
|
||||
"only_if_field_is_empty": "Pouze pokud je pole prázdné",
|
||||
"booking_start_date": "Datum začátku rezervace",
|
||||
"booking_created_date": "Datum vytvoření rezervace",
|
||||
"booking_reassigned_to_host": "Rezervace přeřazena hostiteli {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook Deaktiveret",
|
||||
"webhook_response": "Webhook svar",
|
||||
"webhook_test": "Webhook test",
|
||||
"webhook_update_form": "Formular til opdatering af webhook",
|
||||
"webhook_update_form_description": "Tilføj en webhook til din OAuthClient.",
|
||||
"manage_your_webhook": "Administrer din webhook",
|
||||
"webhook_created_successfully": "Webhook blev oprettet!",
|
||||
"webhook_updated_successfully": "Webhook blev opdateret!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Du har ikke nogen teams endnu.",
|
||||
"no_teams_description": "Teams giver andre mulighed for at booke begivenheder, der deles mellem dine kolleger.",
|
||||
"submit": "Videre",
|
||||
"confirm_remove_attribute": "Ja, fjern attribut",
|
||||
"remove_attribute": "Fjern attribut",
|
||||
"remove_attribute_confirmation_message": "Er du sikker på, at du vil fjerne attributten '{{name}}'? Alle brugere, der i øjeblikket er tildelt den, vil blive frakoblet.",
|
||||
"delete": "Slet",
|
||||
"update": "Opdatér",
|
||||
"save": "Gem",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Formatér",
|
||||
"uppercase_for_letters": "Brug store bogstaver for alle bogstaver",
|
||||
"replace_whitespaces_underscores": "Erstat blanke tegn med understregninger",
|
||||
"platform_billing": "Platformsfakturering",
|
||||
"manage_billing": "Håndtér fakturering",
|
||||
"manage_billing_description": "Håndtér alt omkring fakturering",
|
||||
"billing_freeplan_title": "Du er i øjeblikket på den GRATIS plan",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "Leder du efter flere indsigter?",
|
||||
"filters": "Filtre",
|
||||
"add_filter": "Tilføj filter",
|
||||
"add_rule": "Tilføj regel",
|
||||
"add_rule_group": "Tilføj regelgruppe",
|
||||
"remove_filters": "Ryd alle filtre",
|
||||
"email_verified": "Email bekræftet",
|
||||
"select_user": "Vælg bruger",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Opsæt din organisation",
|
||||
"set_up_your_platform_organization": "Opsæt din platform",
|
||||
"organizations_description": "Organisationer er delte miljøer, hvor teams kan oprette delte begivenhedstyper, apps, arbejdsgange og mere.",
|
||||
"plans": "Planer",
|
||||
"platform": "Platform",
|
||||
"platform_description": "Administrer alt relateret til platformen.",
|
||||
"platform_organization_description": "Cal.com Platform lader dig integrere planlægning problemfrit i din app ved hjælp af platformens API'er og atomer.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Sædeoption understøtter ikke flere varigheder",
|
||||
"include_calendar_event": "Inkluder kalenderbegivenhed",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Formular til oprettelse af OAuth-klient",
|
||||
"oAuth_client_creation_form_description": "Dette er formularen til at oprette en ny OAuth-klient",
|
||||
"oAuth_client_updation_form": "Formular til opdatering af OAuth-klient",
|
||||
"oAuth_client_updation_form_description": "Dette er formularen til at redigere en eksisterende OAuth-klient",
|
||||
"recently_added": "For nylig tilføjet",
|
||||
"connect_all_calendars": "Forbind alle dine kalendere",
|
||||
"connect_all_calendars_description": "{{appName}} læser tilgængelighed fra alle dine eksisterende kalendere.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Begivenhedens sluttidspunkt i deltagers tidszone",
|
||||
"salesforce_create_event_on_contact": "Opret begivenhed på kontakt, hvis den eksisterer. Ellers fald tilbage til lead",
|
||||
"salesforce_owner_name_to_change": "Ejernavn der skal ændres",
|
||||
"field_type": "Felttype",
|
||||
"when_to_write": "Hvornår der skal skrives",
|
||||
"salesforce_on_booking_write_to_record": "Ved booking, skriv til et brugerdefineret felt på deltagerens {{record}}-post",
|
||||
"on_every_booking": "Ved hver booking",
|
||||
"only_if_field_is_empty": "Kun hvis feltet er tomt",
|
||||
"booking_start_date": "Bookings startdato",
|
||||
"booking_created_date": "Dato for oprettelse af booking",
|
||||
"booking_reassigned_to_host": "Booking omfordelt til {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Tilføj dine nye strenge ovenfor her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook deaktiviert",
|
||||
"webhook_response": "Webhook-Antwort",
|
||||
"webhook_test": "Webhook-Test",
|
||||
"webhook_update_form": "Webhook-Aktualisierungsformular",
|
||||
"webhook_update_form_description": "Fügen Sie Ihrem OAuth-Client einen Webhook hinzu.",
|
||||
"manage_your_webhook": "Webhook verwalten",
|
||||
"webhook_created_successfully": "Webhook erfolgreich erstellt!",
|
||||
"webhook_updated_successfully": "Webhook erfolgreich aktualisiert!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Sie haben noch keine Teams.",
|
||||
"no_teams_description": "Teams ermöglichen Termine zu buchen, an denen mehrere Mitarbeiter beteiligt sind.",
|
||||
"submit": "Abschicken",
|
||||
"confirm_remove_attribute": "Ja, Attribut entfernen",
|
||||
"remove_attribute": "Attribut entfernen",
|
||||
"remove_attribute_confirmation_message": "Sind Sie sicher, dass Sie das Attribut '{{name}}' entfernen möchten? Alle Benutzer, die derzeit zugewiesen sind, werden abgemeldet.",
|
||||
"delete": "Löschen",
|
||||
"update": "Aktualisieren",
|
||||
"save": "Speichern",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Format",
|
||||
"uppercase_for_letters": "Großbuchstaben für alle Buchstaben verwenden",
|
||||
"replace_whitespaces_underscores": "Leerzeichen durch Unterstriche ersetzen",
|
||||
"platform_billing": "Plattform-Abrechnung",
|
||||
"manage_billing": "Abrechnung verwalten",
|
||||
"manage_billing_description": "Alles zur Abrechnung verwalten",
|
||||
"billing_freeplan_title": "Sie verwenden derzeit den KOSTENLOSEN Plan",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "Möchten Sie mehr Insights?",
|
||||
"filters": "Filter",
|
||||
"add_filter": "Filter hinzufügen",
|
||||
"add_rule": "Regel hinzufügen",
|
||||
"add_rule_group": "Regelgruppe hinzufügen",
|
||||
"remove_filters": "Alle Filter löschen",
|
||||
"email_verified": "E-Mail bestätigt",
|
||||
"select_user": "Benutzer auswählen",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Ihre Organization einrichten",
|
||||
"set_up_your_platform_organization": "Richten Sie Ihre Plattform ein",
|
||||
"organizations_description": "Organizations sind geteilte Umgebungen, in denen Teams gemeinsame Termintypen, Apps, Workflows und mehr erstellen können.",
|
||||
"plans": "Pläne",
|
||||
"platform": "Plattform",
|
||||
"platform_description": "Verwalten Sie alles, was mit der Plattform zusammenhängt.",
|
||||
"platform_organization_description": "Mit der Cal.com-Plattform können Sie die Terminplanung mühelos mit Hilfe von Plattform-APIs und -Atoms in Ihre App integrieren.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Platzoption unterstützt mehrere Dauern nicht",
|
||||
"include_calendar_event": "Kalenderereignis hinzufügen",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "OAuth-Client-Erstellungsformular",
|
||||
"oAuth_client_creation_form_description": "Dies ist das Formular zur Erstellung eines neuen OAuth-Clients",
|
||||
"oAuth_client_updation_form": "OAuth-Client-Aktualisierungsformular",
|
||||
"oAuth_client_updation_form_description": "Dies ist das Formular zur Bearbeitung eines bestehenden OAuth-Clients",
|
||||
"recently_added": "Kürzlich hinzugefügt",
|
||||
"connect_all_calendars": "Verbinden Sie alle Ihre Kalender",
|
||||
"connect_all_calendars_description": "{{appName}} liest die Verfügbarkeit aus allen Ihren vorhandenen Kalendern.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Ereignis-Endzeit in der Zeitzone des Teilnehmers",
|
||||
"salesforce_create_event_on_contact": "Event für Kontakt erstellen, falls vorhanden. Andernfalls auf Lead zurückgreifen",
|
||||
"salesforce_owner_name_to_change": "Zu ändernder Besitzername",
|
||||
"field_type": "Feldtyp",
|
||||
"when_to_write": "Wann schreiben",
|
||||
"salesforce_on_booking_write_to_record": "Bei Buchung in ein benutzerdefiniertes Feld des Teilnehmer-{{record}}-Datensatzes schreiben",
|
||||
"on_every_booking": "Bei jeder Buchung",
|
||||
"only_if_field_is_empty": "Nur wenn das Feld leer ist",
|
||||
"booking_start_date": "Buchungs-Startdatum",
|
||||
"booking_created_date": "Buchungs-Erstellungsdatum",
|
||||
"booking_reassigned_to_host": "Buchung an {{host}} neu zugewiesen",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -350,9 +350,13 @@
|
||||
"other": "Other",
|
||||
"email_sign_in_subject": "Your sign-in link for {{appName}}",
|
||||
"round_robin_emailed_you_and_attendees": "You are meeting with {{user}}. We sent an email with a calendar invitation with the details to everyone.",
|
||||
"round_robin_emailed_host_and_attendee": "{{host}} is meeting with {{attendee}}. We sent an email with a calendar invitation with the details to everyone.",
|
||||
"emailed_you_and_attendees": "We sent an email with a calendar invitation with the details to everyone.",
|
||||
"emailed_host_and_attendee": "We sent an email with a calendar invitation with the details to everyone.",
|
||||
"emailed_you_and_attendees_recurring": "We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.",
|
||||
"round_robin_emailed_you_and_attendees_recurring": "You are meeting with {{user}}.We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.",
|
||||
"emailed_host_and_attendee_recurring": "We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.",
|
||||
"round_robin_emailed_you_and_attendees_recurring": "You are meeting with {{user}}. We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.",
|
||||
"round_robin_emailed_host_and_attendee_recurring": "{{host}} is meeting with {{attendee}}. We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.",
|
||||
"emailed_you_and_any_other_attendees": "We sent an email to everyone with this information.",
|
||||
"needs_to_be_confirmed_or_rejected": "Your booking still needs to be confirmed or rejected.",
|
||||
"needs_to_be_confirmed_or_rejected_recurring": "Your recurring meeting still needs to be confirmed or rejected.",
|
||||
@@ -705,6 +709,9 @@
|
||||
"no_teams": "You don't have any teams yet.",
|
||||
"no_teams_description": "Teams allow others to book events shared between your coworkers.",
|
||||
"submit": "Submit",
|
||||
"confirm_remove_attribute": "Yes, remove attribute",
|
||||
"remove_attribute": "Remove attribute",
|
||||
"remove_attribute_confirmation_message": "Are you sure you want to remove the attribute '{{name}}'? All users currently assigned to it will be unassigned.",
|
||||
"delete": "Delete",
|
||||
"update": "Update",
|
||||
"save": "Save",
|
||||
@@ -2746,6 +2753,21 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Event End Time In Attendee Timezone",
|
||||
"salesforce_create_event_on_contact": "Create event on contact, if it exists. Else fallback to lead",
|
||||
"salesforce_owner_name_to_change": "Owner name to change",
|
||||
"dont_have_access_this_page": "You don't have access to this page",
|
||||
"you_need_admin_or_owner_privileges_to_access": "In order to view or interact with this page, you must have admin or owner privileges",
|
||||
"field_type": "Field type",
|
||||
"when_to_write": "When to write",
|
||||
"salesforce_on_booking_write_to_record": "On booking, write to a custom field on the attendee {{record}} record",
|
||||
"on_every_booking": "On every booking",
|
||||
"only_if_field_is_empty": "Only if field is empty",
|
||||
"booking_start_date": "Booking start date",
|
||||
"booking_created_date": "Booking created date",
|
||||
"booking_reassigned_to_host": "Booking reassigned to {{host}}",
|
||||
"access_denied": "Access Denied",
|
||||
"salesforce_route_to_owner": "Contact owner will be the Round Robin host if available",
|
||||
"salesforce_do_not_route_to_owner": "Contact owner will not be forced (can still be host if it matches the attributes and Round Robin criteria)",
|
||||
"salesforce_route_to_custom_lookup_field": "Route to a user that matches a lookup field on an account",
|
||||
"salesforce_option": "Salesforce Option",
|
||||
"lookup_field_name": "Lookup Field Name",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook Deshabilitado",
|
||||
"webhook_response": "Respuesta del Webhook",
|
||||
"webhook_test": "Prueba de Webhook",
|
||||
"webhook_update_form": "Formulario de actualización de webhook",
|
||||
"webhook_update_form_description": "Agrega un webhook a tu cliente OAuth.",
|
||||
"manage_your_webhook": "Gestiona tu webhook",
|
||||
"webhook_created_successfully": "¡Webhook creado con éxito!",
|
||||
"webhook_updated_successfully": "¡Webhook actualizado con éxito!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Aún no tienes equipos.",
|
||||
"no_teams_description": "Los equipos permiten que otros reserven eventos compartidos entre tus compañeros de trabajo.",
|
||||
"submit": "Enviar",
|
||||
"confirm_remove_attribute": "Sí, eliminar atributo",
|
||||
"remove_attribute": "Eliminar atributo",
|
||||
"remove_attribute_confirmation_message": "¿Estás seguro de que deseas eliminar el atributo '{{name}}'? Todos los usuarios asignados actualmente serán desvinculados.",
|
||||
"delete": "Eliminar",
|
||||
"update": "Actualizar",
|
||||
"save": "Guardar",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Formato",
|
||||
"uppercase_for_letters": "Usa mayúsculas para todas las letras",
|
||||
"replace_whitespaces_underscores": "Reemplaza los espacios en blanco con guiones bajos",
|
||||
"platform_billing": "Facturación de la plataforma",
|
||||
"manage_billing": "Gestionar facturación",
|
||||
"manage_billing_description": "Gestiona todo lo relacionado con la facturación",
|
||||
"billing_freeplan_title": "Actualmente estás en el plan GRATUITO",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "¿Buscas más información?",
|
||||
"filters": "Filtros",
|
||||
"add_filter": "Agregar filtro",
|
||||
"add_rule": "Agregar regla",
|
||||
"add_rule_group": "Agregar grupo de reglas",
|
||||
"remove_filters": "Borrar todos los filtros",
|
||||
"email_verified": "Correo Electrónico Verificado",
|
||||
"select_user": "Seleccionar Usuario",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Configura tu organización",
|
||||
"set_up_your_platform_organization": "Configura tu plataforma",
|
||||
"organizations_description": "Las organizaciones son entornos compartidos donde los equipos pueden crear tipos de eventos, aplicaciones, flujos de trabajo y más compartidos.",
|
||||
"plans": "Planes",
|
||||
"platform": "Plataforma",
|
||||
"platform_description": "Gestiona todo lo relacionado con la plataforma.",
|
||||
"platform_organization_description": "La plataforma de Cal.com te permite integrar la programación sin esfuerzo en tu aplicación utilizando APIs y átomos de la plataforma.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "La opción de asiento no admite múltiples duraciones",
|
||||
"include_calendar_event": "Incluir evento en el calendario",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Formulario de creación de cliente OAuth",
|
||||
"oAuth_client_creation_form_description": "Este es el formulario para crear un nuevo cliente OAuth",
|
||||
"oAuth_client_updation_form": "Formulario de actualización de cliente OAuth",
|
||||
"oAuth_client_updation_form_description": "Este es el formulario para editar un cliente OAuth existente",
|
||||
"recently_added": "Recientemente añadido",
|
||||
"connect_all_calendars": "Conecta todos tus calendarios",
|
||||
"connect_all_calendars_description": "{{appName}} lee la disponibilidad de todos tus calendarios existentes.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Hora de finalización del evento en la zona horaria del asistente",
|
||||
"salesforce_create_event_on_contact": "Crear evento en el contacto, si existe. De lo contrario, usar lead como alternativa",
|
||||
"salesforce_owner_name_to_change": "Nombre del propietario a cambiar",
|
||||
"field_type": "Tipo de campo",
|
||||
"when_to_write": "Cuándo escribir",
|
||||
"salesforce_on_booking_write_to_record": "Al reservar, escribir en un campo personalizado en el registro {{record}} del asistente",
|
||||
"on_every_booking": "En cada reserva",
|
||||
"only_if_field_is_empty": "Solo si el campo está vacío",
|
||||
"booking_start_date": "Fecha de inicio de la reserva",
|
||||
"booking_created_date": "Fecha de creación de la reserva",
|
||||
"booking_reassigned_to_host": "Reserva reasignada a {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agrega tus nuevas cadenas arriba de esta línea ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook Desactivado",
|
||||
"webhook_response": "Respuesta del Webhook",
|
||||
"webhook_test": "Prueba del Webhook",
|
||||
"webhook_update_form": "Formulario de actualización de webhook",
|
||||
"webhook_update_form_description": "Añade un webhook a tu cliente OAuth.",
|
||||
"manage_your_webhook": "Administra tu webhook",
|
||||
"webhook_created_successfully": "¡Webhook creado con éxito!",
|
||||
"webhook_updated_successfully": "¡Webhook actualizado con éxito!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Todavía no tienes ningún equipo.",
|
||||
"no_teams_description": "Los equipos permiten que otros reserven eventos compartidos entre tus compañeros de trabajo.",
|
||||
"submit": "Enviar",
|
||||
"confirm_remove_attribute": "Sí, eliminar atributo",
|
||||
"remove_attribute": "Eliminar atributo",
|
||||
"remove_attribute_confirmation_message": "¿Estás seguro de que deseas eliminar el atributo '{{name}}'? Todos los usuarios asignados actualmente serán desvinculados.",
|
||||
"delete": "Eliminar",
|
||||
"update": "Actualizar",
|
||||
"save": "Guardar",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Formato",
|
||||
"uppercase_for_letters": "Use mayúsculas para todas las letras",
|
||||
"replace_whitespaces_underscores": "Reemplace espacios en blanco con guiones bajos",
|
||||
"platform_billing": "Facturación de la plataforma",
|
||||
"manage_billing": "Gestione la facturación",
|
||||
"manage_billing_description": "Gestione todo lo relacionado con la facturación",
|
||||
"billing_freeplan_title": "Actualmente está en el plan GRATUITO",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "¿Quiere saber más sobre Insights?",
|
||||
"filters": "Filtros",
|
||||
"add_filter": "Añadir filtro",
|
||||
"add_rule": "Añadir regla",
|
||||
"add_rule_group": "Añadir grupo de reglas",
|
||||
"remove_filters": "Borrar todos los filtros",
|
||||
"email_verified": "Correo electrónico verificado",
|
||||
"select_user": "Seleccionar usuario",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Configure su organización",
|
||||
"set_up_your_platform_organization": "Configura tu plataforma",
|
||||
"organizations_description": "Las organizaciones son entornos compartidos donde los equipos pueden crear tipos de eventos, aplicaciones, flujos de trabajo compartidos y más.",
|
||||
"plans": "Planes",
|
||||
"platform": "Plataforma",
|
||||
"platform_description": "Gestiona todo lo relacionado con la plataforma.",
|
||||
"platform_organization_description": "Cal.com Platform te permite integrar la programación sin esfuerzo en tu aplicación utilizando APIs y átomos de la plataforma.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "La opción Cupo no soporta múltiples duraciones",
|
||||
"include_calendar_event": "Incluir evento del calendario",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Formulario de creación de cliente OAuth",
|
||||
"oAuth_client_creation_form_description": "Este es el formulario para crear un nuevo cliente OAuth",
|
||||
"oAuth_client_updation_form": "Formulario de actualización de cliente OAuth",
|
||||
"oAuth_client_updation_form_description": "Este es el formulario para editar un cliente OAuth existente",
|
||||
"recently_added": "Añadido recientemente",
|
||||
"connect_all_calendars": "Conecte todos sus calendarios",
|
||||
"connect_all_calendars_description": "{{appName}} lee la disponibilidad de todos tus calendarios existentes.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Hora de finalización del evento en la zona horaria del asistente",
|
||||
"salesforce_create_event_on_contact": "Crear evento en el contacto, si existe. De lo contrario, usar el lead",
|
||||
"salesforce_owner_name_to_change": "Nombre del propietario a cambiar",
|
||||
"field_type": "Tipo de campo",
|
||||
"when_to_write": "Cuándo escribir",
|
||||
"salesforce_on_booking_write_to_record": "Al reservar, escribir en un campo personalizado en el registro {{record}} del asistente",
|
||||
"on_every_booking": "En cada reserva",
|
||||
"only_if_field_is_empty": "Solo si el campo está vacío",
|
||||
"booking_start_date": "Fecha de inicio de la reserva",
|
||||
"booking_created_date": "Fecha de creación de la reserva",
|
||||
"booking_reassigned_to_host": "Reserva reasignada a {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Veebihaak keelatud",
|
||||
"webhook_response": "Veebihaagi vastus",
|
||||
"webhook_test": "Veebihaagi test",
|
||||
"webhook_update_form": "Webhooki uuendamise vorm",
|
||||
"webhook_update_form_description": "Lisa webhook oma OAuthCliendile.",
|
||||
"manage_your_webhook": "Halda oma veebihaaki",
|
||||
"webhook_created_successfully": "Veebihaagi loomine õnnestus!",
|
||||
"webhook_updated_successfully": "Veebihaagi värskendamine õnnestus!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Teil pole veel ühtegi meeskonda.",
|
||||
"no_teams_description": "Meeskonnad võimaldavad teistel broneerida teie töökaaslaste vahel jagatud sündmusi.",
|
||||
"submit": "Esita",
|
||||
"confirm_remove_attribute": "Jah, eemalda atribuut",
|
||||
"remove_attribute": "Eemalda atribuut",
|
||||
"remove_attribute_confirmation_message": "Kas olete kindel, et soovite atribuudi '{{name}}' eemaldada? Kõik sellele määratud kasutajad eemaldatakse.",
|
||||
"delete": "Kustuta",
|
||||
"update": "Uuendus",
|
||||
"save": "Salvesta",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Formaat",
|
||||
"uppercase_for_letters": "Kasuta kõigi tähtede jaoks suurtähti",
|
||||
"replace_whitespaces_underscores": "Asenda tühikud alakriipsudega",
|
||||
"platform_billing": "Platvormi arveldus",
|
||||
"manage_billing": "Arvelduse haldamine",
|
||||
"manage_billing_description": "Hallake kõike arveldust",
|
||||
"billing_freeplan_title": "Teil on praegu TASUTA plaan",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "Kas otsite rohkem teadmisi?",
|
||||
"filters": "Filtrid",
|
||||
"add_filter": "Lisa filter",
|
||||
"add_rule": "Lisa reegel",
|
||||
"add_rule_group": "Lisa reeglite grupp",
|
||||
"remove_filters": "Tühjenda kõik filtrid",
|
||||
"email_verified": "E-post kinnitatud",
|
||||
"select_user": "Vali kasutaja",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Seadistage oma organisatsioon",
|
||||
"set_up_your_platform_organization": "Seadista oma platvorm",
|
||||
"organizations_description": "Organisatsioonid on jagatud keskkonnad, kus meeskonnad saavad luua jagatud sündmuste tüüpe, rakendusi, töövooge ja palju muud.",
|
||||
"plans": "Plaanid",
|
||||
"platform": "Platvorm",
|
||||
"platform_description": "Halda kõike platvormiga seonduvat.",
|
||||
"platform_organization_description": "Platvorm Cal.com võimaldab teil ajakava hõlpsalt oma rakendusse integreerida, kasutades platvormi apisid ja aatomeid.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Istme valik ei toeta mitut kestust",
|
||||
"include_calendar_event": "Kaasa kalendrisündmus",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "OAuth kliendi loomise vorm",
|
||||
"oAuth_client_creation_form_description": "See on vorm uue OAuth kliendi loomiseks",
|
||||
"oAuth_client_updation_form": "OAuth kliendi uuendamise vorm",
|
||||
"oAuth_client_updation_form_description": "See on vorm olemasoleva OAuth kliendi muutmiseks",
|
||||
"recently_added": "Viimati lisatud",
|
||||
"connect_all_calendars": "Ühenda kõik oma kalendrid",
|
||||
"connect_all_calendars_description": "{{appName}} loeb saadavust kõigist teie olemasolevatest kalendritest",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Sündmuse lõpuaeg osaleja ajavööndis",
|
||||
"salesforce_create_event_on_contact": "Loo sündmus kontaktil, kui see eksisteerib. Vastasel juhul kasuta liidi",
|
||||
"salesforce_owner_name_to_change": "Muudetava omaniku nimi",
|
||||
"field_type": "Välja tüüp",
|
||||
"when_to_write": "Millal kirjutada",
|
||||
"salesforce_on_booking_write_to_record": "Broneeringu korral kirjuta osaleja {{record}} kirje kohandatud väljale",
|
||||
"on_every_booking": "Iga broneeringu korral",
|
||||
"only_if_field_is_empty": "Ainult kui väli on tühi",
|
||||
"booking_start_date": "Broneeringu alguskuupäev",
|
||||
"booking_created_date": "Broneeringu loomise kuupäev",
|
||||
"booking_reassigned_to_host": "Broneering määrati ümber {{host}}-le",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook désactivé",
|
||||
"webhook_response": "Réponse du webhook",
|
||||
"webhook_test": "Test du webhook",
|
||||
"webhook_update_form": "Formulaire de mise à jour du webhook",
|
||||
"webhook_update_form_description": "Ajoutez un webhook à votre client OAuth.",
|
||||
"manage_your_webhook": "Gérez votre webhook",
|
||||
"webhook_created_successfully": "Webhook créé avec succès !",
|
||||
"webhook_updated_successfully": "Webhook mis à jour avec succès !",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Vous n'avez pas encore d'équipe.",
|
||||
"no_teams_description": "Les équipes permettent aux autres de réserver des événements partagés entre vos collègues.",
|
||||
"submit": "Envoyer",
|
||||
"confirm_remove_attribute": "Oui, supprimer l'attribut",
|
||||
"remove_attribute": "Supprimer l'attribut",
|
||||
"remove_attribute_confirmation_message": "Êtes-vous sûr de vouloir supprimer l'attribut '{{name}}' ? Tous les utilisateurs actuellement assignés à cet attribut seront désassignés.",
|
||||
"delete": "Supprimer",
|
||||
"update": "Mettre à jour",
|
||||
"save": "Enregistrer",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Format",
|
||||
"uppercase_for_letters": "Utilisez des majuscules pour toutes les lettres.",
|
||||
"replace_whitespaces_underscores": "Remplacez les espaces par des tirets bas.",
|
||||
"platform_billing": "Facturation de la plateforme",
|
||||
"manage_billing": "Gérer la facturation",
|
||||
"manage_billing_description": "Gérez tout ce qui est en lien avec la facturation.",
|
||||
"billing_freeplan_title": "Vous êtes actuellement sur le plan Gratuit",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "Vous cherchez plus de statistiques ?",
|
||||
"filters": "Filtres",
|
||||
"add_filter": "Ajouter un filtre",
|
||||
"add_rule": "Ajouter une règle",
|
||||
"add_rule_group": "Ajouter un groupe de règles",
|
||||
"remove_filters": "Effacer les filtres",
|
||||
"email_verified": "E-mail vérifié",
|
||||
"select_user": "Sélectionner un utilisateur",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Configurer votre organisation",
|
||||
"set_up_your_platform_organization": "Configurer votre plateforme",
|
||||
"organizations_description": "Les organisations sont des environnements partagés dans lesquels les équipes peuvent créer des types d'événements partagés, des applications, des workflows, etc.",
|
||||
"plans": "Plans",
|
||||
"platform": "Plateforme",
|
||||
"platform_description": "Gérez tout ce qui concerne la plateforme.",
|
||||
"platform_organization_description": "La plateforme Cal.com vous permet d'intégrer sans effort la planification dans votre application en utilisant les API et atoms de la plateforme.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "L'option par place ne prend pas en charge les durées multiples",
|
||||
"include_calendar_event": "Inclure l'événement du calendrier",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Formulaire de création de client OAuth",
|
||||
"oAuth_client_creation_form_description": "Ceci est le formulaire pour créer un nouveau client OAuth",
|
||||
"oAuth_client_updation_form": "Formulaire de mise à jour du client OAuth",
|
||||
"oAuth_client_updation_form_description": "Ceci est le formulaire pour modifier un client OAuth existant",
|
||||
"recently_added": "Ajoutées récemment",
|
||||
"connect_all_calendars": "Connectez vos calendriers",
|
||||
"connect_all_calendars_description": "{{appName}} relève la disponibilité de tous vos calendriers existants.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Heure de fin de l'événement dans le fuseau horaire du participant",
|
||||
"salesforce_create_event_on_contact": "Créer un événement sur le contact, s'il existe. Sinon, utiliser le lead comme alternative",
|
||||
"salesforce_owner_name_to_change": "Nom du propriétaire à modifier",
|
||||
"field_type": "Type de champ",
|
||||
"when_to_write": "Quand écrire",
|
||||
"salesforce_on_booking_write_to_record": "Lors de la réservation, écrire dans un champ personnalisé sur l'enregistrement {{record}} du participant",
|
||||
"on_every_booking": "À chaque réservation",
|
||||
"only_if_field_is_empty": "Seulement si le champ est vide",
|
||||
"booking_start_date": "Date de début de la réservation",
|
||||
"booking_created_date": "Date de création de la réservation",
|
||||
"booking_reassigned_to_host": "Réservation réassignée à {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook מושבת",
|
||||
"webhook_response": "תגובת Webhook",
|
||||
"webhook_test": "בדיקת Webhook",
|
||||
"webhook_update_form": "טופס עדכון Webhook",
|
||||
"webhook_update_form_description": "הוסף webhook ללקוח OAuth שלך.",
|
||||
"manage_your_webhook": "ניהול ה-Webhook שלך",
|
||||
"webhook_created_successfully": "יצירת ה-Webhook בוצעה בהצלחה!",
|
||||
"webhook_updated_successfully": "עדכון ה-Webhook בוצע בהצלחה!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "עדיין אין לך צוותים.",
|
||||
"no_teams_description": "בעזרת צוותים, אנשים אחרים יכולים לקבוע מועד לאירועים המשותפים לעמיתיך.",
|
||||
"submit": "שליחה",
|
||||
"confirm_remove_attribute": "כן, הסר את המאפיין",
|
||||
"remove_attribute": "הסר מאפיין",
|
||||
"remove_attribute_confirmation_message": "האם אתה בטוח שברצונך להסיר את המאפיין '{{name}}'? כל המשתמשים המוקצים לו יוסרו.",
|
||||
"delete": "מחיקה",
|
||||
"update": "עדכון",
|
||||
"save": "שמירה",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "פורמט",
|
||||
"uppercase_for_letters": "שימוש באותיות רישיות עבור כל האותיות",
|
||||
"replace_whitespaces_underscores": "החלפת רווחים במקפים תחתונים",
|
||||
"platform_billing": "חיוב פלטפורמה",
|
||||
"manage_billing": "ניהול החיוב",
|
||||
"manage_billing_description": "ניהול כל מה שקשור לחיובים",
|
||||
"billing_freeplan_title": "החבילה הנוכחית שלך היא החבילה החינמית",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "רוצה עוד Insights?",
|
||||
"filters": "מסננים",
|
||||
"add_filter": "הוסף סנן",
|
||||
"add_rule": "הוסף כלל",
|
||||
"add_rule_group": "הוסף קבוצת כללים",
|
||||
"remove_filters": "ניקוי כל המסננים",
|
||||
"email_verified": "כתובת הדוא״ל אומתה",
|
||||
"select_user": "בחר משתמש",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "הגדרת הארגון שלך",
|
||||
"set_up_your_platform_organization": "הגדרת הפלטפורמה שלך",
|
||||
"organizations_description": "ארגונים הם סביבות משותפות שבהן צוותים יכולים ליצור משאבים משותפים, כמו סוגי אירועים, אפליקציות, תהליכי עבודה ועוד.",
|
||||
"plans": "תוכניות",
|
||||
"platform": "פלטפורמה",
|
||||
"platform_description": "נהל את כל מה שקשור לפלטפורמה.",
|
||||
"platform_organization_description": "פלטפורמת Cal.com מאפשרת לך לשלב תזמון בקלות באפליקציה שלך באמצעות APIs ו-atoms של הפלטפורמה.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "האפשרויות של הושבה במקומות לא תומכות במשכי זמן שונים",
|
||||
"include_calendar_event": "כלילת אירוע מלוח השנה",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "טופס יצירת לקוח OAuth",
|
||||
"oAuth_client_creation_form_description": "זהו הטופס ליצירת לקוח OAuth חדש",
|
||||
"oAuth_client_updation_form": "טופס עדכון לקוח OAuth",
|
||||
"oAuth_client_updation_form_description": "זהו הטופס לעריכת לקוח OAuth קיים",
|
||||
"recently_added": "נוספו לאחרונה",
|
||||
"connect_all_calendars": "חבר את כל לוחות השנה שלך",
|
||||
"connect_all_calendars_description": "{{appName}} קורא את הזמינות מכל לוחות השנה הקיימים שלך.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "זמן סיום האירוע באזור הזמן של המשתתף",
|
||||
"salesforce_create_event_on_contact": "צור אירוע על איש קשר, אם קיים. אחרת, חזור ללידים",
|
||||
"salesforce_owner_name_to_change": "שם הבעלים לשינוי",
|
||||
"field_type": "סוג שדה",
|
||||
"when_to_write": "מתי לכתוב",
|
||||
"salesforce_on_booking_write_to_record": "בעת הזמנה, כתוב לשדה מותאם אישית ברשומת המשתתף {{record}}",
|
||||
"on_every_booking": "בכל הזמנה",
|
||||
"only_if_field_is_empty": "רק אם השדה ריק",
|
||||
"booking_start_date": "תאריך תחילת ההזמנה",
|
||||
"booking_created_date": "תאריך יצירת ההזמנה",
|
||||
"booking_reassigned_to_host": "ההזמנה הועברה למארח {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook letiltva",
|
||||
"webhook_response": "Webhook válasza",
|
||||
"webhook_test": "Webhook teszt",
|
||||
"webhook_update_form": "Webhook frissítési űrlap",
|
||||
"webhook_update_form_description": "Webhook hozzáadása az OAuthClient-hez.",
|
||||
"manage_your_webhook": "Webhookjaid kezelése",
|
||||
"webhook_created_successfully": "Webhook sikeresen létrehozva!",
|
||||
"webhook_updated_successfully": "Webhook sikeresen frissítve!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Még nincs csapatod.",
|
||||
"no_teams_description": "A csapatok lehetővé teszik mások számára, hogy lefoglalják a munkatársai között megosztott eseményeket.",
|
||||
"submit": "Beküldés",
|
||||
"confirm_remove_attribute": "Igen, az attribútum eltávolítása",
|
||||
"remove_attribute": "Attribútum eltávolítása",
|
||||
"remove_attribute_confirmation_message": "Biztosan el szeretné távolítani a(z) '{{name}}' attribútumot? Minden jelenleg hozzárendelt felhasználó hozzárendelése megszűnik.",
|
||||
"delete": "Törlés",
|
||||
"update": "Frissítés",
|
||||
"save": "Mentés",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Formátum",
|
||||
"uppercase_for_letters": "Használjon nagybetűt minden betűhöz",
|
||||
"replace_whitespaces_underscores": "Cserélje ki a szóközöket aláhúzásjelre",
|
||||
"platform_billing": "Platform számlázás",
|
||||
"manage_billing": "Számlázás kezelése",
|
||||
"manage_billing_description": "Számlázással kapcsolatos teendők",
|
||||
"billing_freeplan_title": "Jelenleg az INGYENES csomagban vagy",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "További információkra vágyik?",
|
||||
"filters": "Szűrők",
|
||||
"add_filter": "Szűrő hozzáadása",
|
||||
"add_rule": "Szabály hozzáadása",
|
||||
"add_rule_group": "Szabálycsoport hozzáadása",
|
||||
"remove_filters": "Törölje az összes szűrőt",
|
||||
"email_verified": "E-mail ellenőrizve",
|
||||
"select_user": "Felhasználó kiválasztása",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Állítsa be szervezetét",
|
||||
"set_up_your_platform_organization": "Állítsa be a platformot",
|
||||
"organizations_description": "A szervezetek megosztott környezetek, ahol a csapatok megosztott eseménytípusokat, alkalmazásokat, munkafolyamatokat és egyebeket hozhatnak létre.",
|
||||
"plans": "Csomagok",
|
||||
"platform": "Platform",
|
||||
"platform_description": "Kezelje a platformmal kapcsolatos összes tevékenységet.",
|
||||
"platform_organization_description": "A Cal.com Platform segítségével könnyedén integrálhatja az ütemezést az alkalmazásba a platform apis és atomok segítségével.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Az hely opció nem támogatja a több időtartamot",
|
||||
"include_calendar_event": "Tartalmazza a naptári eseményt",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "OAuth kliens létrehozási űrlap",
|
||||
"oAuth_client_creation_form_description": "Ez az űrlap új OAuth kliens létrehozásához",
|
||||
"oAuth_client_updation_form": "OAuth kliens frissítési űrlap",
|
||||
"oAuth_client_updation_form_description": "Ez az űrlap egy meglévő OAuth kliens szerkesztéséhez",
|
||||
"recently_added": "Nemrég hozzáadott",
|
||||
"connect_all_calendars": "Csatlakoztassa az összes naptárát",
|
||||
"connect_all_calendars_description": "A {{appName}} beolvassa az elérhetőséget az összes meglévő naptárából.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Esemény befejezési ideje a résztvevő időzónájában",
|
||||
"salesforce_create_event_on_contact": "Esemény létrehozása a kapcsolaton, ha létezik. Egyébként visszalépés a leadre",
|
||||
"salesforce_owner_name_to_change": "Módosítandó tulajdonos neve",
|
||||
"field_type": "Mező típusa",
|
||||
"when_to_write": "Mikor írjon",
|
||||
"salesforce_on_booking_write_to_record": "Foglaláskor írjon egy egyéni mezőbe a résztvevő {{record}} rekordjában",
|
||||
"on_every_booking": "Minden foglalásnál",
|
||||
"only_if_field_is_empty": "Csak ha a mező üres",
|
||||
"booking_start_date": "Foglalás kezdő dátuma",
|
||||
"booking_created_date": "Foglalás létrehozásának dátuma",
|
||||
"booking_reassigned_to_host": "Foglalás átirányítva a következő házigazdához: {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adja hozzá az új karakterláncokat fent ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook Disabilitato",
|
||||
"webhook_response": "Risposta Webhook",
|
||||
"webhook_test": "Test Webhook",
|
||||
"webhook_update_form": "Modulo di aggiornamento webhook",
|
||||
"webhook_update_form_description": "Aggiungi un webhook al tuo OAuthClient.",
|
||||
"manage_your_webhook": "Gestisci il tuo webhook",
|
||||
"webhook_created_successfully": "Webhook creato con successo!",
|
||||
"webhook_updated_successfully": "Webhook aggiornato con successo!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Non hai ancora alcun team.",
|
||||
"no_teams_description": "I team permettono ad altri di prenotare eventi in condivisione con i tuoi collaboratori.",
|
||||
"submit": "Invia",
|
||||
"confirm_remove_attribute": "Sì, rimuovi attributo",
|
||||
"remove_attribute": "Rimuovi attributo",
|
||||
"remove_attribute_confirmation_message": "Sei sicuro di voler rimuovere l'attributo '{{name}}'? Tutti gli utenti attualmente assegnati verranno disassegnati.",
|
||||
"delete": "Elimina",
|
||||
"update": "Aggiorna",
|
||||
"save": "Salva",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Formato",
|
||||
"uppercase_for_letters": "Usa tutte lettere maiuscole",
|
||||
"replace_whitespaces_underscores": "Sostituisci spazi con sottolineature",
|
||||
"platform_billing": "Fatturazione della piattaforma",
|
||||
"manage_billing": "Gestisci fatturazione",
|
||||
"manage_billing_description": "Gestisci tutto ciò che riguarda la fatturazione",
|
||||
"billing_freeplan_title": "Attualmente stai usando il piano GRATUITO",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "Ti servono più Insights?",
|
||||
"filters": "Filtri",
|
||||
"add_filter": "Aggiungi filtro",
|
||||
"add_rule": "Aggiungi regola",
|
||||
"add_rule_group": "Aggiungi gruppo di regole",
|
||||
"remove_filters": "Cancella tutti i filtri",
|
||||
"email_verified": "Email verificata",
|
||||
"select_user": "Seleziona utente",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Imposta la tua organizzazione",
|
||||
"set_up_your_platform_organization": "Configura la tua piattaforma",
|
||||
"organizations_description": "Le organizzazioni sono ambienti condivisi dove i team possono creare e condividere tipi di eventi, applicazioni, flussi di lavoro e altro ancora.",
|
||||
"plans": "Piani",
|
||||
"platform": "Piattaforma",
|
||||
"platform_description": "Gestisci tutto ciò che riguarda la piattaforma.",
|
||||
"platform_organization_description": "La piattaforma Cal.com ti permette di integrare facilmente la pianificazione nella tua app utilizzando API e atom della piattaforma.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "L'opzione di prenotazione dei posti non supporta durate multiple",
|
||||
"include_calendar_event": "Includi evento del calendario",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Modulo di creazione client OAuth",
|
||||
"oAuth_client_creation_form_description": "Questo è il modulo per creare un nuovo client OAuth",
|
||||
"oAuth_client_updation_form": "Modulo di aggiornamento client OAuth",
|
||||
"oAuth_client_updation_form_description": "Questo è il modulo per modificare un client OAuth esistente",
|
||||
"recently_added": "Aggiunti di recente",
|
||||
"connect_all_calendars": "Collega tutti i tuoi calendari",
|
||||
"connect_all_calendars_description": "{{appName}} legge la disponibilità da tutti i tuoi calendari esistenti.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Ora di fine evento nel fuso orario del partecipante",
|
||||
"salesforce_create_event_on_contact": "Crea evento sul contatto, se esiste. Altrimenti, passa al lead",
|
||||
"salesforce_owner_name_to_change": "Nome del proprietario da modificare",
|
||||
"field_type": "Tipo di campo",
|
||||
"when_to_write": "Quando scrivere",
|
||||
"salesforce_on_booking_write_to_record": "Al momento della prenotazione, scrivi in un campo personalizzato nel record {{record}} del partecipante",
|
||||
"on_every_booking": "Ad ogni prenotazione",
|
||||
"only_if_field_is_empty": "Solo se il campo è vuoto",
|
||||
"booking_start_date": "Data di inizio prenotazione",
|
||||
"booking_created_date": "Data di creazione della prenotazione",
|
||||
"booking_reassigned_to_host": "Prenotazione riassegnata a {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "ウェブフックは無効です",
|
||||
"webhook_response": "ウェブフックの応答",
|
||||
"webhook_test": "ウェブフックのテスト",
|
||||
"webhook_update_form": "Webhookアップデートフォーム",
|
||||
"webhook_update_form_description": "OAuthクライアントにWebhookを追加します。",
|
||||
"manage_your_webhook": "ウェブフックの管理",
|
||||
"webhook_created_successfully": "ウェブフックが正常に作成されました!",
|
||||
"webhook_updated_successfully": "ウェブフックが正常に更新されました!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "まだチームがありません。",
|
||||
"no_teams_description": "チームを使用することにより、同僚の間で共有されているイベントを予約できるようになります。",
|
||||
"submit": "送信",
|
||||
"confirm_remove_attribute": "はい、属性を削除します",
|
||||
"remove_attribute": "属性を削除",
|
||||
"remove_attribute_confirmation_message": "属性「{{name}}」を削除してもよろしいですか?現在この属性が割り当てられているすべてのユーザーの割り当てが解除されます。",
|
||||
"delete": "削除",
|
||||
"update": "更新",
|
||||
"save": "保存",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "書式",
|
||||
"uppercase_for_letters": "すべての文字に大文字を使用する",
|
||||
"replace_whitespaces_underscores": "空白をアンダーバー「 _ 」に置き換える",
|
||||
"platform_billing": "プラットフォーム請求",
|
||||
"manage_billing": "請求を管理する",
|
||||
"manage_billing_description": "請求に関するすべての事柄を管理する",
|
||||
"billing_freeplan_title": "あなたは現在、無料プランを利用しています",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "Insights をさらにお探しですか?",
|
||||
"filters": "フィルター",
|
||||
"add_filter": "フィルターを追加",
|
||||
"add_rule": "ルールを追加",
|
||||
"add_rule_group": "ルールグループを追加",
|
||||
"remove_filters": "すべてのフィルターをクリア",
|
||||
"email_verified": "メール確認済み",
|
||||
"select_user": "ユーザーを選択",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "組織をセットアップする",
|
||||
"set_up_your_platform_organization": "プラットフォームを設定する",
|
||||
"organizations_description": "組織は、チームが共有するイベントの種類、アプリ、ワークフローなどが作成可能な共有環境です。",
|
||||
"plans": "プラン",
|
||||
"platform": "プラットフォーム",
|
||||
"platform_description": "プラットフォームに関連するすべてを管理します。",
|
||||
"platform_organization_description": "Cal.comプラットフォームを使用すると、プラットフォームAPIとアトムを使用してスケジューリングをアプリに簡単に統合できます。",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "座席オプションは複数の期間をサポートしていません",
|
||||
"include_calendar_event": "カレンダーのイベントを含める",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "OAuthクライアント作成フォーム",
|
||||
"oAuth_client_creation_form_description": "これは新しいOAuthクライアントを作成するためのフォームです",
|
||||
"oAuth_client_updation_form": "OAuthクライアント更新フォーム",
|
||||
"oAuth_client_updation_form_description": "これは既存のOAuthクライアントを編集するためのフォームです",
|
||||
"recently_added": "最近追加されました",
|
||||
"connect_all_calendars": "すべてのカレンダーに接続",
|
||||
"connect_all_calendars_description": "{{appName}}は、すべての既存のカレンダーから可用性を読み取ります。",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "参加者のタイムゾーンでのイベント終了時間",
|
||||
"salesforce_create_event_on_contact": "連絡先が存在する場合はイベントを作成し、存在しない場合はリードにフォールバック",
|
||||
"salesforce_owner_name_to_change": "変更する所有者名",
|
||||
"field_type": "フィールドタイプ",
|
||||
"when_to_write": "書き込むタイミング",
|
||||
"salesforce_on_booking_write_to_record": "予約時、参加者の{{record}}レコードのカスタムフィールドに書き込む",
|
||||
"on_every_booking": "すべての予約時",
|
||||
"only_if_field_is_empty": "フィールドが空の場合のみ",
|
||||
"booking_start_date": "予約開始日",
|
||||
"booking_created_date": "予約作成日",
|
||||
"booking_reassigned_to_host": "予約が{{host}}に再割り当てされました",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook បានបិទ",
|
||||
"webhook_response": "ការឆ្លើយតប Webhook",
|
||||
"webhook_test": "តេស្ត Webhook",
|
||||
"webhook_update_form": "ទម្រង់បច្ចុប្បន្នភាព Webhook",
|
||||
"webhook_update_form_description": "បន្ថែម webhook ទៅកាន់ OAuthClient របស់អ្នក។",
|
||||
"manage_your_webhook": "គ្រប់គ្រង webhook របស់អ្នក។",
|
||||
"webhook_created_successfully": "Webhook បានបង្កើតដោយជោគជ័យ!",
|
||||
"webhook_updated_successfully": "Webhook បានធ្វើបច្ចុប្បន្នភាពដោយជោគជ័យ!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "អ្នកមិនមានក្រុមណាមួយនៅឡើយទេ។",
|
||||
"no_teams_description": "ក្រុមអនុញ្ញាតឱ្យអ្នកដទៃកក់ព្រឹត្តិការណ៍ដែលចែករំលែករវាងមិត្តរួមការងាររបស់អ្នក។",
|
||||
"submit": "ដាក់ស្នើ",
|
||||
"confirm_remove_attribute": "បាទ/ចាស, លុបគុណលក្ខណៈ",
|
||||
"remove_attribute": "លុបគុណលក្ខណៈ",
|
||||
"remove_attribute_confirmation_message": "តើអ្នកប្រាកដថាចង់លុបគុណលក្ខណៈ '{{name}}' មែនទេ? អ្នកប្រើប្រាស់ទាំងអស់ដែលបានផ្ដល់ឱ្យវានឹងត្រូវបានដកចេញ។",
|
||||
"delete": "លុប",
|
||||
"update": "ធ្វើបច្ចុប្បន្នភាព",
|
||||
"save": "រក្សាទុក",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "ទ្រង់ទ្រាយ",
|
||||
"uppercase_for_letters": "ប្រើអក្សរធំសម្រាប់អក្សរទាំងអស់",
|
||||
"replace_whitespaces_underscores": "ជំនួសចន្លោះទទេដោយសញ្ញាគូសក្រោម",
|
||||
"platform_billing": "ការគិតថ្លៃវេទិកា",
|
||||
"manage_billing": "គ្រប់គ្រងវិក្កយបត្រ",
|
||||
"manage_billing_description": "គ្រប់គ្រងរឿងទាំងអស់ដែលទាក់ទងនឹងវិក្កយបត្រ",
|
||||
"billing_freeplan_title": "អ្នកកំពុងប្រើផែនការមិនគិតថ្លៃ",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "កំពុងស្វែងរកការយល់ដឹងបន្ថែម?",
|
||||
"filters": "តម្រង",
|
||||
"add_filter": "បន្ថែមតម្រង",
|
||||
"add_rule": "បន្ថែមច្បាប់",
|
||||
"add_rule_group": "បន្ថែមក្រុមច្បាប់",
|
||||
"remove_filters": "សម្អាតតម្រងទាំងអស់",
|
||||
"email_verified": "អ៊ីមែលបានផ្ទៀងផ្ទាត់",
|
||||
"select_user": "ជ្រើសរើសអ្នកប្រើ",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "រៀបចំអង្គការរបស់អ្នក",
|
||||
"set_up_your_platform_organization": "រៀបចំវេទិការបស់អ្នក",
|
||||
"organizations_description": "អង្គការគឺជាបរិយាកាសចែករំលែកដែលក្រុមអាចបង្កើតប្រភេទព្រឹត្តិការណ៍ចែករំលែក កម្មវិធី ដំណើរការ និងច្រើនទៀត។",
|
||||
"plans": "គម្រោង",
|
||||
"platform": "វេទិកា",
|
||||
"platform_description": "គ្រប់គ្រងអ្វីៗទាំងអស់ដែលទាក់ទងនឹងវេទិកា។",
|
||||
"platform_organization_description": "វេទិកា Cal.com អនុញ្ញាតឱ្យអ្នករួមបញ្ចូលការកំណត់ពេលវេលាដោយគ្មានការលំបាកទៅក្នុងកម្មវិធីរបស់អ្នកដោយប្រើ apis និង atoms វេទិកា។",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "ជម្រើសកៅអីមិនគាំទ្រចំនួនរយៈពេលច្រើនទេ",
|
||||
"include_calendar_event": "រួមបញ្ចូលព្រឹត្តិការណ៍ប្រតិទិន",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "ទម្រង់បង្កើតអតិថិជន OAuth",
|
||||
"oAuth_client_creation_form_description": "នេះគឺជាទម្រង់សម្រាប់បង្កើតអតិថិជន OAuth ថ្មី",
|
||||
"oAuth_client_updation_form": "ទម្រង់ធ្វើបច្ចុប្បន្នភាពអតិថិជន OAuth",
|
||||
"oAuth_client_updation_form_description": "នេះគឺជាទម្រង់សម្រាប់កែសម្រួលអតិថិជន OAuth ដែលមានស្រាប់",
|
||||
"recently_added": "បានបន្ថែមថ្មីៗ",
|
||||
"connect_all_calendars": "ភ្ជាប់ប្រតិទិនទាំងអស់របស់អ្នក",
|
||||
"connect_all_calendars_description": "{{appName}} អានភាពទំនេរពីប្រតិទិនទាំងអស់ដែលមានរបស់អ្នក។",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "ពេលវេលាបញ្ចប់ព្រឹត្តិការណ៍នៅក្នុងតំបន់ពេលវេលារបស់អ្នកចូលរួម",
|
||||
"salesforce_create_event_on_contact": "បង្កើតព្រឹត្តិការណ៍នៅលើទំនាក់ទំនង បើមាន។ បើមិនដូច្នោះទេ ត្រឡប់ទៅ lead វិញ",
|
||||
"salesforce_owner_name_to_change": "ឈ្មោះម្ចាស់ដែលត្រូវផ្លាស់ប្តូរ",
|
||||
"field_type": "ប្រភេទវាល",
|
||||
"when_to_write": "ពេលណាត្រូវសរសេរ",
|
||||
"salesforce_on_booking_write_to_record": "នៅពេលកក់ សរសេរទៅវាលផ្ទាល់ខ្លួននៅលើកំណត់ត្រា {{record}} របស់អ្នកចូលរួម",
|
||||
"on_every_booking": "នៅរាល់ពេលកក់",
|
||||
"only_if_field_is_empty": "លុះត្រាតែវាលនោះទទេ",
|
||||
"booking_start_date": "កាលបរិច្ឆេទចាប់ផ្តើមការកក់",
|
||||
"booking_created_date": "កាលបរិច្ឆេទបង្កើតការកក់",
|
||||
"booking_reassigned_to_host": "ការកក់ត្រូវបានកំណត់ឡើងវិញទៅ {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ បន្ថែមខ្សែអក្សរថ្មីរបស់អ្នកនៅខាងលើនេះ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "웹훅 비활성화",
|
||||
"webhook_response": "웹훅 응답",
|
||||
"webhook_test": "웹훅 테스트",
|
||||
"webhook_update_form": "웹훅 업데이트 양식",
|
||||
"webhook_update_form_description": "OAuth 클라이언트에 웹훅을 추가하세요.",
|
||||
"manage_your_webhook": "웹훅 관리하기",
|
||||
"webhook_created_successfully": "웹훅이 생성되었습니다!",
|
||||
"webhook_updated_successfully": "웹훅이 업데이트되었습니다!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "아직 팀이 하나도 없습니다.",
|
||||
"no_teams_description": "팀을 통해 다른 사람들이 동료 간 공유된 이벤트를 예약할 수 있습니다.",
|
||||
"submit": "제출하기",
|
||||
"confirm_remove_attribute": "네, 속성을 제거합니다",
|
||||
"remove_attribute": "속성 제거",
|
||||
"remove_attribute_confirmation_message": "속성 '{{name}}'을(를) 제거하시겠습니까? 현재 할당된 모든 사용자가 할당 해제됩니다.",
|
||||
"delete": "지우기",
|
||||
"update": "업데이트",
|
||||
"save": "저장하기",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "형식",
|
||||
"uppercase_for_letters": "모든 문자에 대문자 사용",
|
||||
"replace_whitespaces_underscores": "공백을 밑줄로 바꾸기",
|
||||
"platform_billing": "플랫폼 결제",
|
||||
"manage_billing": "대금청구 관리",
|
||||
"manage_billing_description": "대금청구의 모든 항목 관리",
|
||||
"billing_freeplan_title": "현재 무료 요금제를 사용 중입니다",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "더 많은 인사이트를 찾고 있습니까?",
|
||||
"filters": "필터",
|
||||
"add_filter": "필터 추가",
|
||||
"add_rule": "규칙 추가",
|
||||
"add_rule_group": "규칙 그룹 추가",
|
||||
"remove_filters": "모든 필터 지우기",
|
||||
"email_verified": "이메일 인증됨",
|
||||
"select_user": "사용자 선택",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "조직 설정",
|
||||
"set_up_your_platform_organization": "플랫폼 설정",
|
||||
"organizations_description": "조직은 팀이 공유 이벤트 유형, 앱, 워크플로 등을 만들 수 있는 공유 환경입니다.",
|
||||
"plans": "요금제",
|
||||
"platform": "플랫폼",
|
||||
"platform_description": "플랫폼과 관련된 모든 것을 관리합니다.",
|
||||
"platform_organization_description": "Cal.com 플랫폼을 사용하면 플랫폼 API와 아톰을 통해 손쉽게 앱에 스케줄링을 통합할 수 있습니다.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "좌석 옵션은 여러 기간을 지원하지 않습니다",
|
||||
"include_calendar_event": "캘린더 이벤트 포함",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "OAuth 클라이언트 생성 양식",
|
||||
"oAuth_client_creation_form_description": "새로운 OAuth 클라이언트를 생성하기 위한 양식입니다",
|
||||
"oAuth_client_updation_form": "OAuth 클라이언트 수정 양식",
|
||||
"oAuth_client_updation_form_description": "기존 OAuth 클라이언트를 수정하기 위한 양식입니다",
|
||||
"recently_added": "최근 추가됨",
|
||||
"connect_all_calendars": "모든 캘린더 연결",
|
||||
"connect_all_calendars_description": "{{appName}}은(는) 모든 기존 캘린더에서 가용성을 읽어옵니다.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "참석자 시간대의 이벤트 종료 시간",
|
||||
"salesforce_create_event_on_contact": "연락처가 존재하는 경우 이벤트 생성. 그렇지 않으면 리드로 대체",
|
||||
"salesforce_owner_name_to_change": "변경할 소유자 이름",
|
||||
"field_type": "필드 유형",
|
||||
"when_to_write": "작성 시기",
|
||||
"salesforce_on_booking_write_to_record": "예약 시, 참석자 {{record}} 레코드의 사용자 정의 필드에 작성",
|
||||
"on_every_booking": "모든 예약 시",
|
||||
"only_if_field_is_empty": "필드가 비어있을 때만",
|
||||
"booking_start_date": "예약 시작 날짜",
|
||||
"booking_created_date": "예약 생성 날짜",
|
||||
"booking_reassigned_to_host": "예약이 {{host}}에게 재할당됨",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 여기에 새 문자열을 추가하세요 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook uitgeschakeld",
|
||||
"webhook_response": "Webhook melding",
|
||||
"webhook_test": "Webhook testen",
|
||||
"webhook_update_form": "Webhook-updateformulier",
|
||||
"webhook_update_form_description": "Voeg een webhook toe aan je OAuthClient.",
|
||||
"manage_your_webhook": "Beheer uw webhook",
|
||||
"webhook_created_successfully": "Webhook succesvol toegevoegd!",
|
||||
"webhook_updated_successfully": "Webhook succesvol bijgewerkt!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "U heeft nog geen teams.",
|
||||
"no_teams_description": "Teams geven anderen de mogelijkheid om gebeurtenissen te boeken die zijn gedeeld tussen uw collega's.",
|
||||
"submit": "Opslaan",
|
||||
"confirm_remove_attribute": "Ja, attribuut verwijderen",
|
||||
"remove_attribute": "Attribuut verwijderen",
|
||||
"remove_attribute_confirmation_message": "Weet u zeker dat u het attribuut '{{name}}' wilt verwijderen? Alle gebruikers die momenteel zijn toegewezen, zullen worden losgekoppeld.",
|
||||
"delete": "Verwijderen",
|
||||
"update": "Bijwerken",
|
||||
"save": "Opslaan",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Formatteren",
|
||||
"uppercase_for_letters": "Gebruik hoofdletters voor alle letters",
|
||||
"replace_whitespaces_underscores": "Vervang spaties door onderstrepingstekens",
|
||||
"platform_billing": "Platformfacturering",
|
||||
"manage_billing": "Facturering beheren",
|
||||
"manage_billing_description": "Alles beheren wat met facturering te maken heeft",
|
||||
"billing_freeplan_title": "U heeft momenteel het GRATIS abonnement",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "Op zoek naar meer Insights?",
|
||||
"filters": "Filters",
|
||||
"add_filter": "Filter toevoegen",
|
||||
"add_rule": "Regel toevoegen",
|
||||
"add_rule_group": "Regelgroep toevoegen",
|
||||
"remove_filters": "Alle filters wissen",
|
||||
"email_verified": "E-mail geverifieerd",
|
||||
"select_user": "Selecteer gebruiker",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Stel uw organisatie in",
|
||||
"set_up_your_platform_organization": "Stel je platform in",
|
||||
"organizations_description": "Organisaties zijn gedeelde omgevingen waar teams gedeelde gebeurtenistypen, apps, werkstromen en meer kunnen maken.",
|
||||
"plans": "Abonnementen",
|
||||
"platform": "Platform",
|
||||
"platform_description": "Beheer alles wat met het platform te maken heeft.",
|
||||
"platform_organization_description": "Cal.com Platform stelt je in staat om plannen moeiteloos te integreren in je app met behulp van platform-API's en -atomen.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "De plaatsoptie ondersteunt geen meerdere duren",
|
||||
"include_calendar_event": "Agendagebeurtenis opnemen",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "OAuth-client aanmaakformulier",
|
||||
"oAuth_client_creation_form_description": "Dit is het formulier om een nieuwe OAuth-client aan te maken",
|
||||
"oAuth_client_updation_form": "OAuth-client bewerkingsformulier",
|
||||
"oAuth_client_updation_form_description": "Dit is het formulier om een bestaande OAuth-client te bewerken",
|
||||
"recently_added": "Recent toegevoegd",
|
||||
"connect_all_calendars": "Koppel al uw agenda's",
|
||||
"connect_all_calendars_description": "{{appName}} leest beschikbaarheid van al je bestaande agenda's.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Eindtijd evenement in tijdzone deelnemer",
|
||||
"salesforce_create_event_on_contact": "Maak een evenement aan op het contact, indien aanwezig. Anders terugvallen op lead",
|
||||
"salesforce_owner_name_to_change": "Eigenaarsnaam om te wijzigen",
|
||||
"field_type": "Veldtype",
|
||||
"when_to_write": "Wanneer schrijven",
|
||||
"salesforce_on_booking_write_to_record": "Bij boeking, schrijf naar een aangepast veld op het {{record}}-record van de deelnemer",
|
||||
"on_every_booking": "Bij elke boeking",
|
||||
"only_if_field_is_empty": "Alleen als het veld leeg is",
|
||||
"booking_start_date": "Startdatum boeking",
|
||||
"booking_created_date": "Aanmaakdatum boeking",
|
||||
"booking_reassigned_to_host": "Boeking opnieuw toegewezen aan {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Voeg uw nieuwe strings hierboven toe ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook Deaktivert",
|
||||
"webhook_response": "Webhook respons",
|
||||
"webhook_test": "Webhook test",
|
||||
"webhook_update_form": "Skjema for oppdatering av webhook",
|
||||
"webhook_update_form_description": "Legg til en webhook i din OAuthClient.",
|
||||
"manage_your_webhook": "Administrer webhook",
|
||||
"webhook_created_successfully": "Webhook ble opprettet!",
|
||||
"webhook_updated_successfully": "Webhook ble oppdatert!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Du har ingen team ennå.",
|
||||
"no_teams_description": "Team lar andre booke hendelser som deles mellom kollegene dine.",
|
||||
"submit": "Send inn",
|
||||
"confirm_remove_attribute": "Ja, fjern attributt",
|
||||
"remove_attribute": "Fjern attributt",
|
||||
"remove_attribute_confirmation_message": "Er du sikker på at du vil fjerne attributtet '{{name}}'? Alle brukere som for øyeblikket er tildelt det, vil bli fjernet.",
|
||||
"delete": "Slett",
|
||||
"update": "Oppdater",
|
||||
"save": "Lagre",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Format",
|
||||
"uppercase_for_letters": "Bruk store bokstaver for alle bokstaver",
|
||||
"replace_whitespaces_underscores": "Erstatt mellomrom med understrek",
|
||||
"platform_billing": "Plattformfakturering",
|
||||
"manage_billing": "Administrer betaling",
|
||||
"manage_billing_description": "Administrer alt av betaling",
|
||||
"billing_freeplan_title": "Du er for øyeblikket på GRATIS planen",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "Leter du etter flere innsikter?",
|
||||
"filters": "Filtre",
|
||||
"add_filter": "Legg til filter",
|
||||
"add_rule": "Legg til regel",
|
||||
"add_rule_group": "Legg til regelgruppe",
|
||||
"remove_filters": "Fjern alle filtre",
|
||||
"email_verified": "E-post bekreftet",
|
||||
"select_user": "Velg bruker",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Sett opp din organisasjon",
|
||||
"set_up_your_platform_organization": "Sett opp din plattform",
|
||||
"organizations_description": "Organisasjoner er delte miljøer hvor team kan lage delte hendelsestyper, apper, arbeidsflyter og mer.",
|
||||
"plans": "Planer",
|
||||
"platform": "Plattform",
|
||||
"platform_description": "Administrer alt relatert til plattformen.",
|
||||
"platform_organization_description": "Cal.com Platform lar deg integrere planlegging sømløst i appen din ved hjelp av plattform-APIer og atomer.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Setealternativet støtter ikke flere varigheter",
|
||||
"include_calendar_event": "Inkluder kalenderhendelse",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Skjema for opprettelse av OAuth-klient",
|
||||
"oAuth_client_creation_form_description": "Dette er skjemaet for å opprette en ny OAuth-klient",
|
||||
"oAuth_client_updation_form": "Skjema for oppdatering av OAuth-klient",
|
||||
"oAuth_client_updation_form_description": "Dette er skjemaet for å redigere en eksisterende OAuth-klient",
|
||||
"recently_added": "Nylig lagt til",
|
||||
"connect_all_calendars": "Koble til alle kalenderne dine",
|
||||
"connect_all_calendars_description": "{{appName}} leser tilgjengelighet fra alle dine eksisterende kalendere.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Hendelsens sluttid i deltakers tidssone",
|
||||
"salesforce_create_event_on_contact": "Opprett hendelse på kontakt, hvis den eksisterer. Ellers faller tilbake til lead",
|
||||
"salesforce_owner_name_to_change": "Eiernavn som skal endres",
|
||||
"field_type": "Felttype",
|
||||
"when_to_write": "Når skal det skrives",
|
||||
"salesforce_on_booking_write_to_record": "Ved booking, skriv til et tilpasset felt på deltaker-{{record}}-posten",
|
||||
"on_every_booking": "Ved hver booking",
|
||||
"only_if_field_is_empty": "Bare hvis feltet er tomt",
|
||||
"booking_start_date": "Bookingstart-dato",
|
||||
"booking_created_date": "Dato for opprettet booking",
|
||||
"booking_reassigned_to_host": "Booking omfordelt til {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Legg til dine nye strenger over her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook Wyłączony",
|
||||
"webhook_response": "Odpowiedź Webhooka",
|
||||
"webhook_test": "Test Webhooka",
|
||||
"webhook_update_form": "Formularz aktualizacji webhooka",
|
||||
"webhook_update_form_description": "Dodaj webhook do swojego klienta OAuth.",
|
||||
"manage_your_webhook": "Zarządzaj swoim webhookiem",
|
||||
"webhook_created_successfully": "Webhook został pomyślnie utworzony!",
|
||||
"webhook_updated_successfully": "Webhook został pomyślnie zaktualizowany!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Nie masz jeszcze żadnych drużyn.",
|
||||
"no_teams_description": "Drużyny pozwalają innym na rezerwowanie wydarzeń dzielonych pomiędzy Twoich współpracowników.",
|
||||
"submit": "Prześlij",
|
||||
"confirm_remove_attribute": "Tak, usuń atrybut",
|
||||
"remove_attribute": "Usuń atrybut",
|
||||
"remove_attribute_confirmation_message": "Czy na pewno chcesz usunąć atrybut '{{name}}'? Wszyscy użytkownicy obecnie do niego przypisani zostaną odłączeni.",
|
||||
"delete": "Usuń",
|
||||
"update": "Aktualizuj",
|
||||
"save": "Zapisz",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Formatowanie",
|
||||
"uppercase_for_letters": "Użyj wielkich liter w przypadku wszystkich liter",
|
||||
"replace_whitespaces_underscores": "Zastąp spacje podkreśleniami",
|
||||
"platform_billing": "Rozliczenia platformy",
|
||||
"manage_billing": "Zarządzaj płatnościami",
|
||||
"manage_billing_description": "Zarządzaj wszystkimi kwestiami dotyczącymi płatności",
|
||||
"billing_freeplan_title": "Obecnie korzystasz z planu BEZPŁATNEGO",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "Szukasz dokładniejszych danych Insights?",
|
||||
"filters": "Filtry",
|
||||
"add_filter": "Dodaj filtr",
|
||||
"add_rule": "Dodaj regułę",
|
||||
"add_rule_group": "Dodaj grupę reguł",
|
||||
"remove_filters": "Wyczyść wszystkie filtry",
|
||||
"email_verified": "Email zweryfikowany",
|
||||
"select_user": "Wybierz użytkownika",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Skonfiguruj swoją organizację",
|
||||
"set_up_your_platform_organization": "Skonfiguruj swoją platformę",
|
||||
"organizations_description": "Organizacje to współdzielone środowiska, w których zespoły mogą tworzyć wspólne typy wydarzeń, aplikacje, przepływy pracy itd.",
|
||||
"plans": "Plany",
|
||||
"platform": "Platforma",
|
||||
"platform_description": "Zarządzaj wszystkim, co związane z platformą.",
|
||||
"platform_organization_description": "Platforma Cal.com pozwala na bezproblemową integrację harmonogramowania z Twoją aplikacją za pomocą API i atomów platformy.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Opcja Miejsce nie obsługuje wielu czasów trwania.",
|
||||
"include_calendar_event": "Uwzględnij wydarzenie z kalendarza",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Formularz tworzenia klienta OAuth",
|
||||
"oAuth_client_creation_form_description": "To jest formularz do utworzenia nowego klienta OAuth",
|
||||
"oAuth_client_updation_form": "Formularz aktualizacji klienta OAuth",
|
||||
"oAuth_client_updation_form_description": "To jest formularz do edycji istniejącego klienta OAuth",
|
||||
"recently_added": "Niedawno dodane",
|
||||
"connect_all_calendars": "Podłącz wszystkie swoje kalendarze",
|
||||
"connect_all_calendars_description": "{{appName}} odczytuje dostępność ze wszystkich Twoich istniejących kalendarzy.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Czas zakończenia wydarzenia w strefie czasowej uczestnika",
|
||||
"salesforce_create_event_on_contact": "Utwórz wydarzenie na kontakcie, jeśli istnieje. W przeciwnym razie użyj leada",
|
||||
"salesforce_owner_name_to_change": "Nazwa właściciela do zmiany",
|
||||
"field_type": "Typ pola",
|
||||
"when_to_write": "Kiedy zapisać",
|
||||
"salesforce_on_booking_write_to_record": "Przy rezerwacji, zapisz do niestandardowego pola w rekordzie {{record}} uczestnika",
|
||||
"on_every_booking": "Przy każdej rezerwacji",
|
||||
"only_if_field_is_empty": "Tylko jeśli pole jest puste",
|
||||
"booking_start_date": "Data rozpoczęcia rezerwacji",
|
||||
"booking_created_date": "Data utworzenia rezerwacji",
|
||||
"booking_reassigned_to_host": "Rezerwacja przypisana ponownie do {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodaj nowe ciągi powyżej ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook desativado",
|
||||
"webhook_response": "Resposta do Webhook",
|
||||
"webhook_test": "Teste do Webhook",
|
||||
"webhook_update_form": "Formulário de atualização de webhook",
|
||||
"webhook_update_form_description": "Adicione um webhook ao seu OAuthClient.",
|
||||
"manage_your_webhook": "Gerenciar o seu Webhook",
|
||||
"webhook_created_successfully": "Webhook criado com sucesso!",
|
||||
"webhook_updated_successfully": "Webhook atualizado com sucesso!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Você ainda não tem nenhuma equipe.",
|
||||
"no_teams_description": "Equipes permitem que outras pessoas reservem eventos compartilhados entre seus colegas de trabalho.",
|
||||
"submit": "Enviar",
|
||||
"confirm_remove_attribute": "Sim, remover atributo",
|
||||
"remove_attribute": "Remover atributo",
|
||||
"remove_attribute_confirmation_message": "Você tem certeza de que deseja remover o atributo '{{name}}'? Todos os usuários atualmente atribuídos a ele serão desassociados.",
|
||||
"delete": "Apagar",
|
||||
"update": "Atualizar",
|
||||
"save": "Salvar",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Formato",
|
||||
"uppercase_for_letters": "Use letras maiúsculas para todas as letras",
|
||||
"replace_whitespaces_underscores": "Substituir espaços em branco com sublinhados",
|
||||
"platform_billing": "Faturamento da plataforma",
|
||||
"manage_billing": "Gerencie o faturamento",
|
||||
"manage_billing_description": "Gerencie tudo do faturamento",
|
||||
"billing_freeplan_title": "No momento, você está no plano GRÁTIS",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "Procurando mais Insights?",
|
||||
"filters": "Filtros",
|
||||
"add_filter": "Adicionar filtro",
|
||||
"add_rule": "Adicionar regra",
|
||||
"add_rule_group": "Adicionar grupo de regras",
|
||||
"remove_filters": "Limpar todos os filtros",
|
||||
"email_verified": "E-mail Verificado",
|
||||
"select_user": "Selecionar usuário",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Configurar sua organização",
|
||||
"set_up_your_platform_organization": "Configurar sua plataforma",
|
||||
"organizations_description": "Organizações são ambientes compartilhados em que as equipes podem criar fluxos de trabalho, aplicativos, tipos de evento compartilhados e muito mais.",
|
||||
"plans": "Planos",
|
||||
"platform": "Plataforma",
|
||||
"platform_description": "Gerencie tudo relacionado à plataforma.",
|
||||
"platform_organization_description": "A Plataforma Cal.com permite que você facilmente integre agendadores no seu aplicativo usando APIs e módulos da plataforma.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "A opção de assento não é compatível com múltiplas durações",
|
||||
"include_calendar_event": "Incluir evento do calendário",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Formulário de criação de cliente OAuth",
|
||||
"oAuth_client_creation_form_description": "Este é o formulário para criar um novo cliente OAuth",
|
||||
"oAuth_client_updation_form": "Formulário de atualização de cliente OAuth",
|
||||
"oAuth_client_updation_form_description": "Este é o formulário para editar um cliente OAuth existente",
|
||||
"recently_added": "Adicionou recentemente",
|
||||
"connect_all_calendars": "Conecte todos os seus calendários",
|
||||
"connect_all_calendars_description": "{{appName}} lê a disponibilidade de todos os seus calendários existentes.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Hora de término do evento no fuso horário do participante",
|
||||
"salesforce_create_event_on_contact": "Criar evento no contato, se existir. Caso contrário, usar lead como alternativa",
|
||||
"salesforce_owner_name_to_change": "Nome do proprietário a ser alterado",
|
||||
"field_type": "Tipo de campo",
|
||||
"when_to_write": "Quando escrever",
|
||||
"salesforce_on_booking_write_to_record": "Ao fazer uma reserva, escrever em um campo personalizado no registro {{record}} do participante",
|
||||
"on_every_booking": "Em cada reserva",
|
||||
"only_if_field_is_empty": "Apenas se o campo estiver vazio",
|
||||
"booking_start_date": "Data de início da reserva",
|
||||
"booking_created_date": "Data de criação da reserva",
|
||||
"booking_reassigned_to_host": "Reserva reatribuída para {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adicione suas novas strings aqui em cima ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook desativado",
|
||||
"webhook_response": "Resposta do Webhook",
|
||||
"webhook_test": "Teste do Webhook",
|
||||
"webhook_update_form": "Formulário de atualização de webhook",
|
||||
"webhook_update_form_description": "Adicione um webhook ao seu OAuthClient.",
|
||||
"manage_your_webhook": "Gerir o seu Webhook",
|
||||
"webhook_created_successfully": "Webhook criado com sucesso!",
|
||||
"webhook_updated_successfully": "Webhook atualizado com sucesso!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Ainda não tem nenhuma equipa.",
|
||||
"no_teams_description": "As equipas permitem-lhe reservar eventos partilhados entre os seus colaboradores.",
|
||||
"submit": "Enviar",
|
||||
"confirm_remove_attribute": "Sim, remover atributo",
|
||||
"remove_attribute": "Remover atributo",
|
||||
"remove_attribute_confirmation_message": "Você tem certeza de que deseja remover o atributo '{{name}}'? Todos os usuários atualmente atribuídos a ele serão desassociados.",
|
||||
"delete": "Apagar",
|
||||
"update": "Atualizar",
|
||||
"save": "Guardar",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Formato",
|
||||
"uppercase_for_letters": "Utilize letras maiúsculas para todas as letras",
|
||||
"replace_whitespaces_underscores": "Substituir espaços em branco com carateres de sublinhado",
|
||||
"platform_billing": "Faturação da plataforma",
|
||||
"manage_billing": "Gerir faturação",
|
||||
"manage_billing_description": "Gerir tudo o que esteja relacionado com a faturação",
|
||||
"billing_freeplan_title": "Atualmente está a utilizar o plano GRATUITO",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "À procura de mais Insights?",
|
||||
"filters": "Filtros",
|
||||
"add_filter": "Adicionar filtro",
|
||||
"add_rule": "Adicionar regra",
|
||||
"add_rule_group": "Adicionar grupo de regras",
|
||||
"remove_filters": "Limpar todos os filtros",
|
||||
"email_verified": "E-mail verificado",
|
||||
"select_user": "Selecionar utilizador",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Configurar a sua organização",
|
||||
"set_up_your_platform_organization": "Configurar a sua plataforma",
|
||||
"organizations_description": "As organizações são ambientes partilhados onde as equipas podem criar tipos de eventos partilhados, aplicações, fluxos de trabalho e muito mais.",
|
||||
"plans": "Planos",
|
||||
"platform": "Plataforma",
|
||||
"platform_description": "Gerir tudo relacionado com a plataforma.",
|
||||
"platform_organization_description": "A plataforma Cal.com permite, de forma muito simples, integrar agendamentos na sua aplicação utilizando APIs e atoms da plataforma.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "A opção lugar não suporta múltiplas durações",
|
||||
"include_calendar_event": "Incluir evento no calendário",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Formulário de criação de cliente OAuth",
|
||||
"oAuth_client_creation_form_description": "Este é o formulário para criar um novo cliente OAuth",
|
||||
"oAuth_client_updation_form": "Formulário de atualização de cliente OAuth",
|
||||
"oAuth_client_updation_form_description": "Este é o formulário para editar um cliente OAuth existente",
|
||||
"recently_added": "Adicionados recentemente",
|
||||
"connect_all_calendars": "Associe todos os seus calendários",
|
||||
"connect_all_calendars_description": "{{appName}} lê a disponibilidade de todos os seus calendários existentes.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Hora de fim do evento no fuso horário do participante",
|
||||
"salesforce_create_event_on_contact": "Criar evento no contacto, se existir. Caso contrário, recorrer ao lead",
|
||||
"salesforce_owner_name_to_change": "Nome do proprietário a alterar",
|
||||
"field_type": "Tipo de campo",
|
||||
"when_to_write": "Quando escrever",
|
||||
"salesforce_on_booking_write_to_record": "Na reserva, escrever num campo personalizado no registo {{record}} do participante",
|
||||
"on_every_booking": "Em cada reserva",
|
||||
"only_if_field_is_empty": "Apenas se o campo estiver vazio",
|
||||
"booking_start_date": "Data de início da reserva",
|
||||
"booking_created_date": "Data de criação da reserva",
|
||||
"booking_reassigned_to_host": "Reserva reatribuída a {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook dezactivat",
|
||||
"webhook_response": "Răspuns Webhook",
|
||||
"webhook_test": "Testul Webhook",
|
||||
"webhook_update_form": "Formular de actualizare webhook",
|
||||
"webhook_update_form_description": "Adăugați un webhook la clientul dumneavoastră OAuth.",
|
||||
"manage_your_webhook": "Gestionați webhook-ul",
|
||||
"webhook_created_successfully": "Webhook creat cu succes!",
|
||||
"webhook_updated_successfully": "Webhook actualizat cu succes!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Încă nu aveți nicio echipă.",
|
||||
"no_teams_description": "Echipele permit altora să rezerve evenimente distribuite între colegii dvs.",
|
||||
"submit": "Trimiteți",
|
||||
"confirm_remove_attribute": "Da, elimină atributul",
|
||||
"remove_attribute": "Elimină atributul",
|
||||
"remove_attribute_confirmation_message": "Ești sigur că vrei să elimini atributul '{{name}}'? Toți utilizatorii care sunt în prezent asociați cu acesta vor fi dezasociați.",
|
||||
"delete": "Şterge",
|
||||
"update": "Actualizează",
|
||||
"save": "Salvează",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Format",
|
||||
"uppercase_for_letters": "Utilizați majuscule pentru toate literele",
|
||||
"replace_whitespaces_underscores": "Înlocuiți spațiile cu caractere de subliniere",
|
||||
"platform_billing": "Facturare platformă",
|
||||
"manage_billing": "Gestionați facturarea",
|
||||
"manage_billing_description": "Gestionați toate aspectele legate de facturare",
|
||||
"billing_freeplan_title": "Utilizați planul GRATUIT în prezent",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "Căutați mai multe perspective?",
|
||||
"filters": "Filtre",
|
||||
"add_filter": "Adăugare filtru",
|
||||
"add_rule": "Adaugă regulă",
|
||||
"add_rule_group": "Adaugă grup de reguli",
|
||||
"remove_filters": "Șterge toate filtrele",
|
||||
"email_verified": "Email verificat",
|
||||
"select_user": "Selectare utilizator",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Configurați organizația",
|
||||
"set_up_your_platform_organization": "Configurează-ți platforma",
|
||||
"organizations_description": "Organizațiile sunt medii partajate în care echipele pot crea tipuri de evenimente, aplicații, fluxuri de lucru comune și multe altele.",
|
||||
"plans": "Planuri",
|
||||
"platform": "Platformă",
|
||||
"platform_description": "Gestionează tot ce ține de platformă.",
|
||||
"platform_organization_description": "Platforma Cal.com îți permite să integrezi programarea fără efort în aplicația ta folosind API-uri și atomi de platformă.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Opțiunea de loc nu acceptă mai multe durate",
|
||||
"include_calendar_event": "Includeți eveniment din calendar",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Formular de creare client OAuth",
|
||||
"oAuth_client_creation_form_description": "Acesta este formularul pentru crearea unui nou client OAuth",
|
||||
"oAuth_client_updation_form": "Formular de actualizare client OAuth",
|
||||
"oAuth_client_updation_form_description": "Acesta este formularul pentru editarea unui client OAuth existent",
|
||||
"recently_added": "Adăugate recent",
|
||||
"connect_all_calendars": "Conectați-vă toate calendarele",
|
||||
"connect_all_calendars_description": "{{appName}} citește disponibilitatea din toate calendarele tale existente.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Ora de sfârșit a evenimentului în fusul orar al participantului",
|
||||
"salesforce_create_event_on_contact": "Creați evenimentul pe contact, dacă există. Altfel, reveniți la lead",
|
||||
"salesforce_owner_name_to_change": "Numele proprietarului de schimbat",
|
||||
"field_type": "Tip câmp",
|
||||
"when_to_write": "Când să se scrie",
|
||||
"salesforce_on_booking_write_to_record": "La rezervare, scrie într-un câmp personalizat în înregistrarea {{record}} a participantului",
|
||||
"on_every_booking": "La fiecare rezervare",
|
||||
"only_if_field_is_empty": "Doar dacă câmpul este gol",
|
||||
"booking_start_date": "Data de început a rezervării",
|
||||
"booking_created_date": "Data creării rezervării",
|
||||
"booking_reassigned_to_host": "Rezervare reatribuită către {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adăugați stringurile noi deasupra acestui rând ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook Onemogućen",
|
||||
"webhook_response": "Webhook Odgovor",
|
||||
"webhook_test": "Webhook test",
|
||||
"webhook_update_form": "Forma za ažuriranje webhook-a",
|
||||
"webhook_update_form_description": "Dodajte webhook vašem OAuthClient-u.",
|
||||
"manage_your_webhook": "Upravljajte sa webhook-om",
|
||||
"webhook_created_successfully": "Webhook uspešno napravljen!",
|
||||
"webhook_updated_successfully": "Webhook upsešno ažuriran!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Još uvek nemate nijedan tim.",
|
||||
"no_teams_description": "Timovi služe za rezervacije dogadjaja koje delite sa vašim kolegama.",
|
||||
"submit": "Podnesi",
|
||||
"confirm_remove_attribute": "Da, ukloni atribut",
|
||||
"remove_attribute": "Ukloni atribut",
|
||||
"remove_attribute_confirmation_message": "Da li ste sigurni da želite da uklonite atribut '{{name}}'? Svi korisnici koji su trenutno dodeljeni ovom atributu biće razdodeljeni.",
|
||||
"delete": "Obriši",
|
||||
"update": "Ažuriraj",
|
||||
"save": "Snimi",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Format",
|
||||
"uppercase_for_letters": "Koristite velika slova za sva slova",
|
||||
"replace_whitespaces_underscores": "Zamenite prazna mesta donjom crtom",
|
||||
"platform_billing": "Naplata platforme",
|
||||
"manage_billing": "Upravljajte naplatom",
|
||||
"manage_billing_description": "Upravljajte svim u vezi sa naplatom",
|
||||
"billing_freeplan_title": "Trenutno ste na BESPLATNOM planu",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "Tražite još Insights podataka?",
|
||||
"filters": "Filteri",
|
||||
"add_filter": "Dodajte filter",
|
||||
"add_rule": "Dodaj pravilo",
|
||||
"add_rule_group": "Dodaj grupu pravila",
|
||||
"remove_filters": "Ukloni sve filtere",
|
||||
"email_verified": "Email verifikovan",
|
||||
"select_user": "Izaberite korisnika",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Konfigurisanje profila organizacije",
|
||||
"set_up_your_platform_organization": "Postavite svoju platformu",
|
||||
"organizations_description": "Organizacije su deljena okruženja gde timovi mogu da kreiraju deljene vrste događaja, aplikacije, radne tokove i još mnogo toga.",
|
||||
"plans": "Planovi",
|
||||
"platform": "Platforma",
|
||||
"platform_description": "Upravljajte svim što je povezano sa platformom.",
|
||||
"platform_organization_description": "Cal.com platforma vam omogućava da lako integrišete zakazivanje u vašu aplikaciju koristeći platform API-je i atome.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Opcije za mesta ne podržavaju višestruko trajanje",
|
||||
"include_calendar_event": "Uključite događaj u kalendaru",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Forma za kreiranje OAuth klijenta",
|
||||
"oAuth_client_creation_form_description": "Ovo je forma za kreiranje novog OAuth klijenta",
|
||||
"oAuth_client_updation_form": "Forma za ažuriranje OAuth klijenta",
|
||||
"oAuth_client_updation_form_description": "Ovo je forma za uređivanje postojećeg OAuth klijenta",
|
||||
"recently_added": "Nedavno dodato",
|
||||
"connect_all_calendars": "Povežite sve svoje kalendare",
|
||||
"connect_all_calendars_description": "{{appName}} čita dostupnost iz svih vaših postojećih kalendara.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Vreme završetka događaja u vremenskoj zoni učesnika",
|
||||
"salesforce_create_event_on_contact": "Kreiraj događaj na kontaktu, ako postoji. U suprotnom, prebaci na lead",
|
||||
"salesforce_owner_name_to_change": "Ime vlasnika koje treba promeniti",
|
||||
"field_type": "Tip polja",
|
||||
"when_to_write": "Kada pisati",
|
||||
"salesforce_on_booking_write_to_record": "Pri rezervaciji, upiši u prilagođeno polje na {{record}} zapisu učesnika",
|
||||
"on_every_booking": "Pri svakoj rezervaciji",
|
||||
"only_if_field_is_empty": "Samo ako je polje prazno",
|
||||
"booking_start_date": "Datum početka rezervacije",
|
||||
"booking_created_date": "Datum kreiranja rezervacije",
|
||||
"booking_reassigned_to_host": "Rezervacija dodeljena {{host}}-u",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodajte svoje nove stringove iznad ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webbhook inaktiverad",
|
||||
"webhook_response": "Webhook svar",
|
||||
"webhook_test": "Webhook test",
|
||||
"webhook_update_form": "Formulär för uppdatering av webhook",
|
||||
"webhook_update_form_description": "Lägg till en webhook till din OAuthClient.",
|
||||
"manage_your_webhook": "Hantera din webhook",
|
||||
"webhook_created_successfully": "Webhook har skapats!",
|
||||
"webhook_updated_successfully": "Webhook har uppdaterats!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Du har inga team ännu.",
|
||||
"no_teams_description": "Team tillåter andra att boka gemensamma evenemang mellan dina medarbetare.",
|
||||
"submit": "Skicka",
|
||||
"confirm_remove_attribute": "Ja, ta bort attribut",
|
||||
"remove_attribute": "Ta bort attribut",
|
||||
"remove_attribute_confirmation_message": "Är du säker på att du vill ta bort attributet '{{name}}'? Alla användare som för närvarande är tilldelade det kommer att avregistreras.",
|
||||
"delete": "Radera",
|
||||
"update": "Uppdatera",
|
||||
"save": "Spara",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Formatera",
|
||||
"uppercase_for_letters": "Använd versaler för alla bokstäver",
|
||||
"replace_whitespaces_underscores": "Ersätt blanktecken med understreck",
|
||||
"platform_billing": "Plattformsfakturering",
|
||||
"manage_billing": "Hantera fakturering",
|
||||
"manage_billing_description": "Hantera allt som faktureras",
|
||||
"billing_freeplan_title": "Du använder för närvarande GRATIS-prenumerationen",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "Vill du ha mer Insights?",
|
||||
"filters": "Filter",
|
||||
"add_filter": "Lägg till filter",
|
||||
"add_rule": "Lägg till regel",
|
||||
"add_rule_group": "Lägg till regelgrupp",
|
||||
"remove_filters": "Rensa alla filter",
|
||||
"email_verified": "E-post verifierad",
|
||||
"select_user": "Välj användare",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Konfigurera din organisation",
|
||||
"set_up_your_platform_organization": "Ställ in din plattform",
|
||||
"organizations_description": "Organisationer är delade miljöer där team kan skapa delade händelsetyper, appar, arbetsflöden och mycket mer.",
|
||||
"plans": "Planer",
|
||||
"platform": "Plattform",
|
||||
"platform_description": "Hantera allt som rör plattformen.",
|
||||
"platform_organization_description": "Cal.com-plattformen låter dig integrera schemaläggning smidigt i din app med hjälp av plattformens API:er och atomer.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Platsalternativet stöder inte flera varaktigheter",
|
||||
"include_calendar_event": "Inkludera kalenderhändelse",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Formulär för skapande av OAuth-klient",
|
||||
"oAuth_client_creation_form_description": "Detta är formuläret för att skapa en ny OAuth-klient",
|
||||
"oAuth_client_updation_form": "Formulär för uppdatering av OAuth-klient",
|
||||
"oAuth_client_updation_form_description": "Detta är formuläret för att redigera en befintlig OAuth-klient",
|
||||
"recently_added": "Nyligen tillagda",
|
||||
"connect_all_calendars": "Anslut alla dina kalendrar",
|
||||
"connect_all_calendars_description": "{{appName}} läser tillgänglighet från alla dina befintliga kalendrar.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Händelsens sluttid i deltagarens tidszon",
|
||||
"salesforce_create_event_on_contact": "Skapa händelse på kontakt om den finns, annars fallback till lead",
|
||||
"salesforce_owner_name_to_change": "Ägarnamn att ändra",
|
||||
"field_type": "Fälttyp",
|
||||
"when_to_write": "När ska det skrivas",
|
||||
"salesforce_on_booking_write_to_record": "Vid bokning, skriv till ett anpassat fält på deltagarens {{record}}-post",
|
||||
"on_every_booking": "Vid varje bokning",
|
||||
"only_if_field_is_empty": "Endast om fältet är tomt",
|
||||
"booking_start_date": "Bokningens startdatum",
|
||||
"booking_created_date": "Bokningens skapandedatum",
|
||||
"booking_reassigned_to_host": "Bokning omtilldelad till {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Web Kancası Devre Dışı",
|
||||
"webhook_response": "Web kancası yanıtı",
|
||||
"webhook_test": "Web kancası testi",
|
||||
"webhook_update_form": "Webhook güncelleme formu",
|
||||
"webhook_update_form_description": "OAuthClient'ınıza bir webhook ekleyin.",
|
||||
"manage_your_webhook": "Web kancanızı yönetin",
|
||||
"webhook_created_successfully": "Web kancası başarıyla oluşturuldu!",
|
||||
"webhook_updated_successfully": "Web kancası başarıyla güncellendi!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Henüz hiç ekibiniz yok.",
|
||||
"no_teams_description": "Ekipler, iş arkadaşlarınız arasında paylaşılan etkinlik rezervasyonları için diğer kişilere izin verir.",
|
||||
"submit": "Gönder",
|
||||
"confirm_remove_attribute": "Evet, özelliği kaldır",
|
||||
"remove_attribute": "Özelliği kaldır",
|
||||
"remove_attribute_confirmation_message": "'{{name}}' özelliğini kaldırmak istediğinizden emin misiniz? Bu özelliğe atanmış tüm kullanıcıların ataması kaldırılacak.",
|
||||
"delete": "Sil",
|
||||
"update": "Güncelle",
|
||||
"save": "Kaydet",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Biçim",
|
||||
"uppercase_for_letters": "Tüm harfleri büyük harf olarak kullanın",
|
||||
"replace_whitespaces_underscores": "Boşlukları alt çizgi ile değiştir",
|
||||
"platform_billing": "Platform faturalandırması",
|
||||
"manage_billing": "Faturalandırmayı yönetin",
|
||||
"manage_billing_description": "Faturalandırmayla ilgili her şeyi yönetin",
|
||||
"billing_freeplan_title": "Şu anda ÜCRETSİZ plandasınız",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "Daha fazla Insights mı arıyorsunuz?",
|
||||
"filters": "Filtreler",
|
||||
"add_filter": "Filtre ekle",
|
||||
"add_rule": "Kural ekle",
|
||||
"add_rule_group": "Kural grubu ekle",
|
||||
"remove_filters": "Tüm filtreleri temizle",
|
||||
"email_verified": "E-posta Doğrulandı",
|
||||
"select_user": "Kullanıcı seç",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Kuruluşunuzu düzenleyin",
|
||||
"set_up_your_platform_organization": "Platformunuzu kurun",
|
||||
"organizations_description": "Kuruluşlar, ekiplerin paylaşımlı etkinlik türleri, uygulamalar, iş akışları ve daha fazlasını oluşturabileceği paylaşımlı ortamlardır.",
|
||||
"plans": "Planlar",
|
||||
"platform": "Platform",
|
||||
"platform_description": "Platformla ilgili her şeyi yönetin.",
|
||||
"platform_organization_description": "Cal.com Platformu, platform API'leri ve atomları kullanarak planlamayı uygulamanıza zahmetsizce entegre etmenizi sağlar.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Koltuk seçeneği birden fazla süreyi desteklemiyor",
|
||||
"include_calendar_event": "Takvim etkinliğini dahil et",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "OAuth istemcisi oluşturma formu",
|
||||
"oAuth_client_creation_form_description": "Bu, yeni bir OAuth istemcisi oluşturmak için kullanılan formdur",
|
||||
"oAuth_client_updation_form": "OAuth istemcisi güncelleme formu",
|
||||
"oAuth_client_updation_form_description": "Bu, mevcut bir OAuth istemcisini düzenlemek için kullanılan formdur",
|
||||
"recently_added": "Son eklenen",
|
||||
"connect_all_calendars": "Tüm takvimlerinizi bağlayın",
|
||||
"connect_all_calendars_description": "{{appName}}, mevcut tüm takvimlerinizden müsaitlik bilgilerini okur.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Katılımcı Saat Diliminde Etkinlik Bitiş Zamanı",
|
||||
"salesforce_create_event_on_contact": "Eğer varsa, kişi üzerinde etkinlik oluştur. Yoksa potansiyel müşteriye geri dön",
|
||||
"salesforce_owner_name_to_change": "Değiştirilecek sahip adı",
|
||||
"field_type": "Alan türü",
|
||||
"when_to_write": "Ne zaman yazılacak",
|
||||
"salesforce_on_booking_write_to_record": "Rezervasyon yapıldığında, katılımcının {{record}} kaydındaki özel alana yaz",
|
||||
"on_every_booking": "Her rezervasyonda",
|
||||
"only_if_field_is_empty": "Sadece alan boşsa",
|
||||
"booking_start_date": "Rezervasyon başlangıç tarihi",
|
||||
"booking_created_date": "Rezervasyon oluşturma tarihi",
|
||||
"booking_reassigned_to_host": "Rezervasyon {{host}} kişisine yeniden atandı",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni dizelerinizi yukarıya ekleyin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Вебгук вимкнено",
|
||||
"webhook_response": "Відповідь вебгуку",
|
||||
"webhook_test": "Тест вебгуку",
|
||||
"webhook_update_form": "Форма оновлення вебхука",
|
||||
"webhook_update_form_description": "Додайте вебхук до вашого OAuthClient.",
|
||||
"manage_your_webhook": "Керування вебгуком",
|
||||
"webhook_created_successfully": "Вебгук створено!",
|
||||
"webhook_updated_successfully": "Вебгук оновлено!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "У вас ще немає команд.",
|
||||
"no_teams_description": "Завдяки командам охочі можуть бронювати спільні заходи для ваших колег.",
|
||||
"submit": "Надіслати",
|
||||
"confirm_remove_attribute": "Так, видалити атрибут",
|
||||
"remove_attribute": "Видалити атрибут",
|
||||
"remove_attribute_confirmation_message": "Ви впевнені, що хочете видалити атрибут '{{name}}'? Усі користувачі, які зараз мають цей атрибут, втратять його.",
|
||||
"delete": "Видалити",
|
||||
"update": "Оновити",
|
||||
"save": "Зберегти",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Формат",
|
||||
"uppercase_for_letters": "Використовувати всі великі літери",
|
||||
"replace_whitespaces_underscores": "Заміняти пробіли підкресленнями",
|
||||
"platform_billing": "Тарифікація платформи",
|
||||
"manage_billing": "Керувати рахунками",
|
||||
"manage_billing_description": "Керувати всіма моментами виставлення рахунків",
|
||||
"billing_freeplan_title": "Зараз ви використовуєте БЕЗКОШТОВНИЙ план",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "Потрібно більше висновків Insights?",
|
||||
"filters": "Фільтри",
|
||||
"add_filter": "Додати фільтр",
|
||||
"add_rule": "Додати правило",
|
||||
"add_rule_group": "Додати групу правил",
|
||||
"remove_filters": "Очистити всі фільтри",
|
||||
"email_verified": "Електронна пошта підтверджена",
|
||||
"select_user": "Вибрати користувача",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Налаштуйте свою організацію",
|
||||
"set_up_your_platform_organization": "Налаштуйте свою платформу",
|
||||
"organizations_description": "Організації — це спільне середовище, де команди можуть створювати спільні типи заходів, застосунки, робочі процеси тощо.",
|
||||
"plans": "Плани",
|
||||
"platform": "Платформа",
|
||||
"platform_description": "Керуйте всім, що стосується платформи.",
|
||||
"platform_organization_description": "Платформа Cal.com дозволяє легко інтегрувати планування у ваш додаток за допомогою API та атомів платформи.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Параметр «Місця» не підтримує декілька варіантів тривалості",
|
||||
"include_calendar_event": "Включити календарний захід",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Форма створення OAuth клієнта",
|
||||
"oAuth_client_creation_form_description": "Це форма для створення нового OAuth клієнта",
|
||||
"oAuth_client_updation_form": "Форма оновлення OAuth клієнта",
|
||||
"oAuth_client_updation_form_description": "Це форма для редагування існуючого OAuth клієнта",
|
||||
"recently_added": "Нещодавно додані",
|
||||
"connect_all_calendars": "Можливість підключення всіх календарів",
|
||||
"connect_all_calendars_description": "{{appName}} зчитує доступність з усіх ваших існуючих календарів.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Час закінчення події в часовому поясі учасника",
|
||||
"salesforce_create_event_on_contact": "Створити подію для контакту, якщо він існує. В іншому випадку створити для ліда",
|
||||
"salesforce_owner_name_to_change": "Ім'я власника для зміни",
|
||||
"field_type": "Тип поля",
|
||||
"when_to_write": "Коли записувати",
|
||||
"salesforce_on_booking_write_to_record": "При бронюванні, записати в користувацьке поле запису {{record}} учасника",
|
||||
"on_every_booking": "При кожному бронюванні",
|
||||
"only_if_field_is_empty": "Тільки якщо поле порожнє",
|
||||
"booking_start_date": "Дата початку бронювання",
|
||||
"booking_created_date": "Дата створення бронювання",
|
||||
"booking_reassigned_to_host": "Бронювання перепризначено {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook đang tắt",
|
||||
"webhook_response": "Phản đáp của Webhook",
|
||||
"webhook_test": "Kiểm tra Webhook",
|
||||
"webhook_update_form": "Biểu mẫu cập nhật webhook",
|
||||
"webhook_update_form_description": "Thêm một webhook vào OAuthClient của bạn.",
|
||||
"manage_your_webhook": "Quản lý webhook của bạn",
|
||||
"webhook_created_successfully": "Webhook đã được tạo thành công!",
|
||||
"webhook_updated_successfully": "Webhook đã được cập nhật!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "Bạn chưa có bất kỳ nhóm nào.",
|
||||
"no_teams_description": "Các nhóm cho phép người khác đăng ký các sự kiện được chia sẻ giữa đồng nghiệp của bạn.",
|
||||
"submit": "Gửi đi",
|
||||
"confirm_remove_attribute": "Có, xóa thuộc tính",
|
||||
"remove_attribute": "Xóa thuộc tính",
|
||||
"remove_attribute_confirmation_message": "Bạn có chắc chắn muốn xóa thuộc tính '{{name}}'? Tất cả người dùng hiện đang được gán cho thuộc tính này sẽ bị gỡ gán.",
|
||||
"delete": "Xóa bỏ",
|
||||
"update": "Cập nhật",
|
||||
"save": "Lưu",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "Định dạng",
|
||||
"uppercase_for_letters": "In hoa hết tất cả các chữ cái",
|
||||
"replace_whitespaces_underscores": "Thay thế khoảng trắng bằng dấu gạch dưới",
|
||||
"platform_billing": "Thanh toán nền tảng",
|
||||
"manage_billing": "Quản lí thanh toán",
|
||||
"manage_billing_description": "Quản lí tất cả những thanh toán",
|
||||
"billing_freeplan_title": "Bạn hiện đang sử dụng gói MIỄN PHÍ",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "Bạn muốn có thêm Insights?",
|
||||
"filters": "Bộ Lọc",
|
||||
"add_filter": "Thêm bộ lọc",
|
||||
"add_rule": "Thêm quy tắc",
|
||||
"add_rule_group": "Thêm nhóm quy tắc",
|
||||
"remove_filters": "Xóa tất cả bộ lọc",
|
||||
"email_verified": "Email đã được xác minh",
|
||||
"select_user": "Chọn người dùng",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "Thiết lập tổ chức của bạn",
|
||||
"set_up_your_platform_organization": "Thiết lập nền tảng của bạn",
|
||||
"organizations_description": "Tổ chức là môi trường chia sẻ tại đó các nhóm có thể tạo loại sự kiện, ứng dụng, tiến độ công việc, v.v. để dùng chung.",
|
||||
"plans": "Các gói",
|
||||
"platform": "Nền tảng",
|
||||
"platform_description": "Quản lý mọi thứ liên quan đến nền tảng.",
|
||||
"platform_organization_description": "Nền tảng Cal.com cho phép bạn tích hợp việc lên lịch một cách dễ dàng vào ứng dụng của bạn bằng cách sử dụng các API và atoms của nền tảng.",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Tuỳ chọn ghế ngồi không hỗ trợ nhiều khoảng thời gian",
|
||||
"include_calendar_event": "Bao gồm sự kiện lịch",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Biểu mẫu tạo OAuth client",
|
||||
"oAuth_client_creation_form_description": "Đây là biểu mẫu để tạo một OAuth client mới",
|
||||
"oAuth_client_updation_form": "Biểu mẫu cập nhật OAuth client",
|
||||
"oAuth_client_updation_form_description": "Đây là biểu mẫu để chỉnh sửa một OAuth client hiện có",
|
||||
"recently_added": "Đã thêm vào gần đây",
|
||||
"connect_all_calendars": "Kết nối tất cả lịch của bạn",
|
||||
"connect_all_calendars_description": "{{appName}} đọc khả dụng từ tất cả các lịch hiện có của bạn.",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Thời gian kết thúc sự kiện theo múi giờ của Người tham dự",
|
||||
"salesforce_create_event_on_contact": "Tạo sự kiện trên liên hệ, nếu tồn tại. Nếu không, chuyển sang khách hàng tiềm năng",
|
||||
"salesforce_owner_name_to_change": "Tên chủ sở hữu cần thay đổi",
|
||||
"field_type": "Loại trường",
|
||||
"when_to_write": "Khi nào ghi",
|
||||
"salesforce_on_booking_write_to_record": "Khi đặt lịch, ghi vào trường tùy chỉnh trên bản ghi {{record}} của người tham dự",
|
||||
"on_every_booking": "Trên mỗi lần đặt lịch",
|
||||
"only_if_field_is_empty": "Chỉ khi trường trống",
|
||||
"booking_start_date": "Ngày bắt đầu đặt lịch",
|
||||
"booking_created_date": "Ngày tạo đặt lịch",
|
||||
"booking_reassigned_to_host": "Đặt lịch được chuyển giao cho {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook 已禁用",
|
||||
"webhook_response": "Webhook 回应",
|
||||
"webhook_test": "Webhook 测试",
|
||||
"webhook_update_form": "Webhook 更新表单",
|
||||
"webhook_update_form_description": "为您的 OAuth 客户端添加一个 webhook。",
|
||||
"manage_your_webhook": "管理您的 Webhook",
|
||||
"webhook_created_successfully": "Webhook 创建成功!",
|
||||
"webhook_updated_successfully": "Webhook 更新成功!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "您还没有任何团队。",
|
||||
"no_teams_description": "团队允许他人预约您和同事共同的活动。",
|
||||
"submit": "提交",
|
||||
"confirm_remove_attribute": "是的,移除属性",
|
||||
"remove_attribute": "移除属性",
|
||||
"remove_attribute_confirmation_message": "您确定要移除属性“{{name}}”吗?所有当前分配到该属性的用户将被取消分配。",
|
||||
"delete": "删除",
|
||||
"update": "更新",
|
||||
"save": "保存",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "格式",
|
||||
"uppercase_for_letters": "所有字母均使用大写",
|
||||
"replace_whitespaces_underscores": "将空格替换为下划线",
|
||||
"platform_billing": "平台计费",
|
||||
"manage_billing": "管理计费",
|
||||
"manage_billing_description": "管理所有计费事项",
|
||||
"billing_freeplan_title": "您目前正在使用免费计划",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "寻找更多的 Insights?",
|
||||
"filters": "过滤",
|
||||
"add_filter": "添加筛选器",
|
||||
"add_rule": "添加规则",
|
||||
"add_rule_group": "添加规则组",
|
||||
"remove_filters": "清楚所有过滤",
|
||||
"email_verified": "电子邮件已验证",
|
||||
"select_user": "选择用户",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "设置您的组织",
|
||||
"set_up_your_platform_organization": "设置您的平台",
|
||||
"organizations_description": "组织是一种共享环境,团队可以在其中创建共享的活动类型、应用、工作流程等。",
|
||||
"plans": "计划",
|
||||
"platform": "平台",
|
||||
"platform_description": "管理与平台相关的所有内容。",
|
||||
"platform_organization_description": "Cal.com平台让您可以使用平台API和原子轻松将调度集成到您的应用中。",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "位置选项不支持多个持续时间",
|
||||
"include_calendar_event": "包括日历活动",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "OAuth 客户端创建表单",
|
||||
"oAuth_client_creation_form_description": "这是用于创建新 OAuth 客户端的表单",
|
||||
"oAuth_client_updation_form": "OAuth 客户端更新表单",
|
||||
"oAuth_client_updation_form_description": "这是用于编辑现有 OAuth 客户端的表单",
|
||||
"recently_added": "最近添加",
|
||||
"connect_all_calendars": "连接您的所有日历",
|
||||
"connect_all_calendars_description": "{{appName}} 从您所有现有的日历中读取可用性。",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "参与者时区的事件结束时间",
|
||||
"salesforce_create_event_on_contact": "如果联系人存在,则在联系人上创建事件。否则回退到潜在客户",
|
||||
"salesforce_owner_name_to_change": "要更改的所有者名称",
|
||||
"field_type": "字段类型",
|
||||
"when_to_write": "何时写入",
|
||||
"salesforce_on_booking_write_to_record": "预约时,写入参与者 {{record}} 记录的自定义字段",
|
||||
"on_every_booking": "每次预约时",
|
||||
"only_if_field_is_empty": "仅当字段为空时",
|
||||
"booking_start_date": "预约开始日期",
|
||||
"booking_created_date": "预约创建日期",
|
||||
"booking_reassigned_to_host": "预约重新分配给 {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在此上方添加您的新字符串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"webhook_disabled": "Webhook 已關閉",
|
||||
"webhook_response": "Webhook 回應",
|
||||
"webhook_test": "Webhook 測試",
|
||||
"webhook_update_form": "Webhook 更新表單",
|
||||
"webhook_update_form_description": "為您的 OAuth 客戶端添加 webhook。",
|
||||
"manage_your_webhook": "管理 Webhook",
|
||||
"webhook_created_successfully": "新增 Webhook 成功!",
|
||||
"webhook_updated_successfully": "更新 Webhook 成功!",
|
||||
@@ -703,6 +705,9 @@
|
||||
"no_teams": "還沒有任何團隊。",
|
||||
"no_teams_description": "團隊可以讓其他人預約在同事之間分享的活動。",
|
||||
"submit": "送出",
|
||||
"confirm_remove_attribute": "是的,移除屬性",
|
||||
"remove_attribute": "移除屬性",
|
||||
"remove_attribute_confirmation_message": "您確定要移除屬性 '{{name}}' 嗎?所有目前被指派的使用者將會被取消指派。",
|
||||
"delete": "刪除",
|
||||
"update": "更新",
|
||||
"save": "儲存",
|
||||
@@ -1630,6 +1635,7 @@
|
||||
"format": "格式",
|
||||
"uppercase_for_letters": "一律使用大寫字母",
|
||||
"replace_whitespaces_underscores": "以底線取代空格",
|
||||
"platform_billing": "平台計費",
|
||||
"manage_billing": "管理付費",
|
||||
"manage_billing_description": "管理所有付費事項",
|
||||
"billing_freeplan_title": "您目前使用的是免費方案",
|
||||
@@ -2037,6 +2043,8 @@
|
||||
"looking_for_more_insights": "在找更多 Insights 嗎?",
|
||||
"filters": "篩選器",
|
||||
"add_filter": "新增篩選條件",
|
||||
"add_rule": "新增規則",
|
||||
"add_rule_group": "新增規則群組",
|
||||
"remove_filters": "清除所有篩選器",
|
||||
"email_verified": "電子郵件已驗證",
|
||||
"select_user": "選擇使用者",
|
||||
@@ -2139,6 +2147,7 @@
|
||||
"set_up_your_organization": "設定組織",
|
||||
"set_up_your_platform_organization": "設置您的平台",
|
||||
"organizations_description": "組織是一種共用環境,團隊可以在其中建立共用的活動類型、應用程式、工作流程等。",
|
||||
"plans": "方案",
|
||||
"platform": "平台",
|
||||
"platform_description": "管理與平台相關的所有事務。",
|
||||
"platform_organization_description": "Cal.com 平台讓您可以使用平台 API 和原子輕鬆將排程集成到您的應用中。",
|
||||
@@ -2276,6 +2285,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "座位選項不支援多個持續時間",
|
||||
"include_calendar_event": "包含行事曆活動",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "OAuth 客戶端創建表單",
|
||||
"oAuth_client_creation_form_description": "這是用於創建新 OAuth 客戶端的表單",
|
||||
"oAuth_client_updation_form": "OAuth 客戶端更新表單",
|
||||
"oAuth_client_updation_form_description": "這是用於編輯現有 OAuth 客戶端的表單",
|
||||
"recently_added": "最近新增",
|
||||
"connect_all_calendars": "連接您的所有行事曆",
|
||||
"connect_all_calendars_description": "{{appName}} 會從您所有現有的日曆中讀取可用性。",
|
||||
@@ -2732,5 +2745,13 @@
|
||||
"event_end_time_in_attendee_timezone_info": "參與者時區的活動結束時間",
|
||||
"salesforce_create_event_on_contact": "如果聯絡人存在,則在聯絡人上建立事件。否則回退到潛在客戶",
|
||||
"salesforce_owner_name_to_change": "要更改的擁有者名稱",
|
||||
"field_type": "欄位類型",
|
||||
"when_to_write": "何時寫入",
|
||||
"salesforce_on_booking_write_to_record": "預約時,寫入參與者 {{record}} 記錄的自定義欄位",
|
||||
"on_every_booking": "每次預約時",
|
||||
"only_if_field_is_empty": "僅當欄位為空時",
|
||||
"booking_start_date": "預約開始日期",
|
||||
"booking_created_date": "預約創建日期",
|
||||
"booking_reassigned_to_host": "預約重新分配給 {{host}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 請在此處新增您的字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -8,9 +8,7 @@ describe("Tests to Check if Event Types have empty Assignment", () => {
|
||||
checkForEmptyAssignment({
|
||||
assignedUsers: [],
|
||||
assignAllTeamMembers: false,
|
||||
hosts: [
|
||||
{ userId: 101, isFixed: false, priority: 2, weight: 100, weightAdjustment: 0, scheduleId: null },
|
||||
],
|
||||
hosts: [{ userId: 101, isFixed: false, priority: 2, weight: 100, scheduleId: null }],
|
||||
isManagedEventType: true,
|
||||
})
|
||||
).toBe(true);
|
||||
@@ -63,9 +61,7 @@ describe("Tests to Check if Event Types have empty Assignment", () => {
|
||||
checkForEmptyAssignment({
|
||||
assignedUsers: [],
|
||||
assignAllTeamMembers: false,
|
||||
hosts: [
|
||||
{ userId: 101, isFixed: false, priority: 2, weight: 100, weightAdjustment: 0, scheduleId: null },
|
||||
],
|
||||
hosts: [{ userId: 101, isFixed: false, priority: 2, weight: 100, scheduleId: null }],
|
||||
isManagedEventType: false,
|
||||
})
|
||||
).toBe(false);
|
||||
|
||||
@@ -223,8 +223,10 @@ describe("getSchedule", () => {
|
||||
);
|
||||
});
|
||||
test("correctly get slots for event with round robin and fixed hosts", async () => {
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
vi.setSystemTime("2024-05-21T00:00:13Z");
|
||||
|
||||
const plus1DateString = "2024-05-22";
|
||||
const plus2DateString = "2024-05-23";
|
||||
|
||||
const crmCredential = {
|
||||
id: 1,
|
||||
@@ -365,6 +367,135 @@ describe("getSchedule", () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
test("correctly get slots for event with only round robin hosts", async () => {
|
||||
vi.setSystemTime("2024-05-21T00:00:13Z");
|
||||
|
||||
const plus1DateString = "2024-05-22";
|
||||
const plus2DateString = "2024-05-23";
|
||||
|
||||
const crmCredential = {
|
||||
id: 1,
|
||||
type: "salesforce_crm",
|
||||
key: {
|
||||
clientId: "test-client-id",
|
||||
},
|
||||
userId: 1,
|
||||
teamId: null,
|
||||
appId: "salesforce",
|
||||
invalid: false,
|
||||
user: { email: "test@test.com" },
|
||||
};
|
||||
|
||||
await createCredentials([crmCredential]);
|
||||
|
||||
mockCrmApp("salesforce", {
|
||||
getContacts: [
|
||||
{
|
||||
id: "contact-id",
|
||||
email: "test@test.com",
|
||||
ownerEmail: "example@example.com",
|
||||
},
|
||||
],
|
||||
createContacts: [{ id: "contact-id", email: "test@test.com" }],
|
||||
});
|
||||
|
||||
await createBookingScenario({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 60,
|
||||
length: 60,
|
||||
hosts: [
|
||||
{
|
||||
userId: 101,
|
||||
isFixed: false,
|
||||
},
|
||||
{
|
||||
userId: 102,
|
||||
isFixed: false,
|
||||
},
|
||||
],
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
metadata: {
|
||||
apps: {
|
||||
salesforce: {
|
||||
enabled: true,
|
||||
appCategories: ["crm"],
|
||||
roundRobinLeadSkip: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
...TestData.users.example,
|
||||
email: "example@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.EmptyAvailability],
|
||||
},
|
||||
{
|
||||
...TestData.users.example,
|
||||
email: "example1@example.com",
|
||||
id: 102,
|
||||
schedules: [TestData.schedules.IstMorningShift],
|
||||
defaultScheduleId: 2,
|
||||
},
|
||||
],
|
||||
bookings: [],
|
||||
});
|
||||
|
||||
const scheduleWithLeadSkip = await getSchedule({
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
teamMemberEmail: "example@example.com",
|
||||
orgSlug: null,
|
||||
},
|
||||
});
|
||||
|
||||
// only slots where example@example.com is available
|
||||
expect(scheduleWithLeadSkip).toHaveDateDisabled({
|
||||
dateString: plus2DateString,
|
||||
});
|
||||
|
||||
const scheduleWithoutLeadSkip = await getSchedule({
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
orgSlug: null,
|
||||
},
|
||||
});
|
||||
|
||||
// slots where either one of the rr hosts is available
|
||||
expect(scheduleWithoutLeadSkip).toHaveTimeSlots(
|
||||
[
|
||||
`04:30:00.000Z`,
|
||||
`05:30:00.000Z`,
|
||||
`06:30:00.000Z`,
|
||||
`07:30:00.000Z`,
|
||||
`08:30:00.000Z`,
|
||||
`09:30:00.000Z`,
|
||||
`10:30:00.000Z`,
|
||||
`11:30:00.000Z`,
|
||||
`12:30:00.000Z`,
|
||||
`13:30:00.000Z`,
|
||||
`14:30:00.000Z`,
|
||||
`15:30:00.000Z`,
|
||||
],
|
||||
{
|
||||
dateString: plus2DateString,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("User Event", () => {
|
||||
|
||||
@@ -1,670 +0,0 @@
|
||||
import prismaMock from "../../../../tests/libs/__mocks__/prismaMock";
|
||||
|
||||
import { expect, it, describe } from "vitest";
|
||||
|
||||
import { getLuckyUser } from "@calcom/lib/server";
|
||||
import { buildUser, buildBooking } from "@calcom/lib/test/builder";
|
||||
import { addWeightAdjustmentToNewHosts } from "@calcom/trpc/server/routers/viewer/eventTypes/util";
|
||||
|
||||
it("can find lucky user with maximize availability", async () => {
|
||||
const user1 = buildUser({
|
||||
id: 1,
|
||||
username: "test1",
|
||||
name: "Test User 1",
|
||||
email: "test1@example.com",
|
||||
bookings: [
|
||||
{
|
||||
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
||||
},
|
||||
{
|
||||
createdAt: new Date("2022-01-25T06:30:00.000Z"),
|
||||
},
|
||||
],
|
||||
});
|
||||
const user2 = buildUser({
|
||||
id: 2,
|
||||
username: "test2",
|
||||
name: "Test User 2",
|
||||
email: "test2@example.com",
|
||||
bookings: [
|
||||
{
|
||||
createdAt: new Date("2022-01-25T04:30:00.000Z"),
|
||||
},
|
||||
],
|
||||
});
|
||||
const users = [user1, user2];
|
||||
// TODO: we may be able to use native prisma generics somehow?
|
||||
prismaMock.user.findMany.mockResolvedValue(users);
|
||||
prismaMock.booking.findMany.mockResolvedValue([]);
|
||||
|
||||
await expect(
|
||||
getLuckyUser("MAXIMIZE_AVAILABILITY", {
|
||||
availableUsers: users,
|
||||
eventType: {
|
||||
id: 1,
|
||||
isRRWeightsEnabled: false,
|
||||
},
|
||||
allRRHosts: [],
|
||||
})
|
||||
).resolves.toStrictEqual(users[1]);
|
||||
});
|
||||
|
||||
it("can find lucky user with maximize availability and priority ranking", async () => {
|
||||
const user1 = buildUser({
|
||||
id: 1,
|
||||
username: "test1",
|
||||
name: "Test User 1",
|
||||
email: "test1@example.com",
|
||||
priority: 2,
|
||||
bookings: [
|
||||
{
|
||||
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
||||
},
|
||||
{
|
||||
createdAt: new Date("2022-01-25T06:30:00.000Z"),
|
||||
},
|
||||
],
|
||||
});
|
||||
const user2 = buildUser({
|
||||
id: 2,
|
||||
username: "test2",
|
||||
name: "Test User 2",
|
||||
email: "test2@example.com",
|
||||
bookings: [
|
||||
{
|
||||
createdAt: new Date("2022-01-25T04:30:00.000Z"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const users = [user1, user2];
|
||||
// TODO: we may be able to use native prisma generics somehow?
|
||||
prismaMock.user.findMany.mockResolvedValue(users);
|
||||
prismaMock.booking.findMany.mockResolvedValue([]);
|
||||
|
||||
// both users have medium priority (one user has no priority set, default to medium) so pick least recently booked
|
||||
await expect(
|
||||
getLuckyUser("MAXIMIZE_AVAILABILITY", {
|
||||
availableUsers: users,
|
||||
eventType: {
|
||||
id: 1,
|
||||
isRRWeightsEnabled: false,
|
||||
},
|
||||
allRRHosts: [],
|
||||
})
|
||||
).resolves.toStrictEqual(users[1]);
|
||||
|
||||
const userLowest = buildUser({
|
||||
id: 1,
|
||||
username: "test1",
|
||||
name: "Test User 1",
|
||||
email: "test1@example.com",
|
||||
priority: 0,
|
||||
bookings: [
|
||||
{
|
||||
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
||||
},
|
||||
],
|
||||
});
|
||||
const userMedium = buildUser({
|
||||
id: 2,
|
||||
username: "test2",
|
||||
name: "Test User 2",
|
||||
email: "test2@example.com",
|
||||
priority: 2,
|
||||
bookings: [
|
||||
{
|
||||
createdAt: new Date("2022-01-25T04:30:00.000Z"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const userHighest = buildUser({
|
||||
id: 2,
|
||||
username: "test2",
|
||||
name: "Test User 2",
|
||||
email: "test2@example.com",
|
||||
priority: 4,
|
||||
bookings: [
|
||||
{
|
||||
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const usersWithPriorities = [userLowest, userMedium, userHighest];
|
||||
// TODO: we may be able to use native prisma generics somehow?
|
||||
prismaMock.user.findMany.mockResolvedValue(usersWithPriorities);
|
||||
prismaMock.booking.findMany.mockResolvedValue([]);
|
||||
|
||||
// pick the user with the highest priority
|
||||
await expect(
|
||||
getLuckyUser("MAXIMIZE_AVAILABILITY", {
|
||||
availableUsers: usersWithPriorities,
|
||||
eventType: {
|
||||
id: 1,
|
||||
isRRWeightsEnabled: false,
|
||||
},
|
||||
allRRHosts: [],
|
||||
})
|
||||
).resolves.toStrictEqual(usersWithPriorities[2]);
|
||||
|
||||
const userLow = buildUser({
|
||||
id: 1,
|
||||
username: "test1",
|
||||
name: "Test User 1",
|
||||
email: "test1@example.com",
|
||||
priority: 0,
|
||||
bookings: [
|
||||
{
|
||||
createdAt: new Date("2022-01-25T02:30:00.000Z"),
|
||||
},
|
||||
],
|
||||
});
|
||||
const userHighLeastRecentBooking = buildUser({
|
||||
id: 2,
|
||||
username: "test2",
|
||||
name: "Test User 2",
|
||||
email: "tes2t@example.com",
|
||||
priority: 3,
|
||||
bookings: [
|
||||
{
|
||||
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const userHighRecentBooking = buildUser({
|
||||
id: 3,
|
||||
username: "test3",
|
||||
name: "Test User 3",
|
||||
email: "test3@example.com",
|
||||
priority: 3,
|
||||
bookings: [
|
||||
{
|
||||
createdAt: new Date("2022-01-25T04:30:00.000Z"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const usersWithSamePriorities = [userLow, userHighLeastRecentBooking, userHighRecentBooking];
|
||||
// TODO: we may be able to use native prisma generics somehow?
|
||||
prismaMock.user.findMany.mockResolvedValue(usersWithSamePriorities);
|
||||
prismaMock.booking.findMany.mockResolvedValue([]);
|
||||
|
||||
// pick the least recently booked user of the two with the highest priority
|
||||
await expect(
|
||||
getLuckyUser("MAXIMIZE_AVAILABILITY", {
|
||||
availableUsers: usersWithSamePriorities,
|
||||
eventType: {
|
||||
id: 1,
|
||||
isRRWeightsEnabled: false,
|
||||
},
|
||||
allRRHosts: [],
|
||||
})
|
||||
).resolves.toStrictEqual(usersWithSamePriorities[1]);
|
||||
});
|
||||
|
||||
describe("maximize availability and weights", () => {
|
||||
it("can find lucky user if hosts have same weights", async () => {
|
||||
const user1 = buildUser({
|
||||
id: 1,
|
||||
username: "test1",
|
||||
name: "Test User 1",
|
||||
email: "test1@example.com",
|
||||
priority: 3,
|
||||
weight: 100,
|
||||
bookings: [
|
||||
{
|
||||
createdAt: new Date("2022-01-25T06:30:00.000Z"),
|
||||
},
|
||||
{
|
||||
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
||||
},
|
||||
],
|
||||
});
|
||||
const user2 = buildUser({
|
||||
id: 2,
|
||||
username: "test2",
|
||||
name: "Test User 2",
|
||||
email: "test2@example.com",
|
||||
priority: 3,
|
||||
weight: 100,
|
||||
bookings: [
|
||||
{
|
||||
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
||||
},
|
||||
{
|
||||
createdAt: new Date("2022-01-25T04:30:00.000Z"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
prismaMock.user.findMany.mockResolvedValue([user1, user2]);
|
||||
|
||||
prismaMock.booking.findMany.mockResolvedValue([
|
||||
buildBooking({
|
||||
id: 1,
|
||||
userId: 1,
|
||||
createdAt: new Date("2022-01-25T06:30:00.000Z"),
|
||||
}),
|
||||
buildBooking({
|
||||
id: 2,
|
||||
userId: 1,
|
||||
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
||||
}),
|
||||
buildBooking({
|
||||
id: 3,
|
||||
userId: 2,
|
||||
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
||||
}),
|
||||
buildBooking({
|
||||
id: 4,
|
||||
userId: 2,
|
||||
createdAt: new Date("2022-01-25T04:30:00.000Z"),
|
||||
}),
|
||||
]);
|
||||
|
||||
const allRRHosts = [
|
||||
{
|
||||
user: { id: user1.id, email: user1.email },
|
||||
weight: user1.weight,
|
||||
weightAdjustment: user1.weightAdjustment,
|
||||
},
|
||||
{
|
||||
user: { id: user2.id, email: user2.email },
|
||||
weight: user2.weight,
|
||||
weightAdjustment: user2.weightAdjustment,
|
||||
},
|
||||
];
|
||||
|
||||
await expect(
|
||||
getLuckyUser("MAXIMIZE_AVAILABILITY", {
|
||||
availableUsers: [user1, user2],
|
||||
eventType: {
|
||||
id: 1,
|
||||
isRRWeightsEnabled: true,
|
||||
},
|
||||
allRRHosts,
|
||||
})
|
||||
).resolves.toStrictEqual(user2);
|
||||
});
|
||||
|
||||
it("can find lucky user if hosts have different weights", async () => {
|
||||
const user1 = buildUser({
|
||||
id: 1,
|
||||
username: "test1",
|
||||
name: "Test User 1",
|
||||
email: "test1@example.com",
|
||||
priority: 3,
|
||||
weight: 200,
|
||||
bookings: [
|
||||
{
|
||||
createdAt: new Date("2022-01-25T08:30:00.000Z"),
|
||||
},
|
||||
{
|
||||
createdAt: new Date("2022-01-25T07:30:00.000Z"),
|
||||
},
|
||||
{
|
||||
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
||||
},
|
||||
],
|
||||
});
|
||||
const user2 = buildUser({
|
||||
id: 2,
|
||||
username: "test2",
|
||||
name: "Test User 2",
|
||||
email: "test2@example.com",
|
||||
priority: 3,
|
||||
weight: 100,
|
||||
bookings: [
|
||||
{
|
||||
createdAt: new Date("2022-01-25T06:30:00.000Z"),
|
||||
},
|
||||
{
|
||||
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
prismaMock.user.findMany.mockResolvedValue([user1, user2]);
|
||||
|
||||
prismaMock.booking.findMany.mockResolvedValue([
|
||||
buildBooking({
|
||||
id: 1,
|
||||
userId: 1,
|
||||
createdAt: new Date("2022-01-25T08:30:00.000Z"),
|
||||
}),
|
||||
buildBooking({
|
||||
id: 2,
|
||||
userId: 1,
|
||||
createdAt: new Date("2022-01-25T07:30:00.000Z"),
|
||||
}),
|
||||
buildBooking({
|
||||
id: 3,
|
||||
userId: 1,
|
||||
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
||||
}),
|
||||
buildBooking({
|
||||
id: 4,
|
||||
userId: 2,
|
||||
createdAt: new Date("2022-01-25T06:30:00.000Z"),
|
||||
}),
|
||||
buildBooking({
|
||||
id: 4,
|
||||
userId: 2,
|
||||
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
||||
}),
|
||||
]);
|
||||
|
||||
const allRRHosts = [
|
||||
{
|
||||
user: { id: user1.id, email: user1.email },
|
||||
weight: user1.weight,
|
||||
weightAdjustment: user1.weightAdjustment,
|
||||
},
|
||||
{
|
||||
user: { id: user2.id, email: user2.email },
|
||||
weight: user2.weight,
|
||||
weightAdjustment: user2.weightAdjustment,
|
||||
},
|
||||
];
|
||||
|
||||
await expect(
|
||||
getLuckyUser("MAXIMIZE_AVAILABILITY", {
|
||||
availableUsers: [user1, user2],
|
||||
eventType: {
|
||||
id: 1,
|
||||
isRRWeightsEnabled: true,
|
||||
},
|
||||
allRRHosts,
|
||||
})
|
||||
).resolves.toStrictEqual(user1);
|
||||
});
|
||||
|
||||
it("can find lucky user with weights and adjusted weights", async () => {
|
||||
const user1 = buildUser({
|
||||
id: 1,
|
||||
username: "test1",
|
||||
name: "Test User 1",
|
||||
email: "test1@example.com",
|
||||
priority: 3,
|
||||
weight: 150,
|
||||
weightAdjustment: 4, // + 3 = 7 bookings
|
||||
bookings: [
|
||||
{
|
||||
createdAt: new Date("2022-01-25T07:30:00.000Z"),
|
||||
},
|
||||
{
|
||||
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
||||
},
|
||||
{
|
||||
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
||||
},
|
||||
],
|
||||
});
|
||||
const user2 = buildUser({
|
||||
id: 2,
|
||||
username: "test2",
|
||||
name: "Test User 2",
|
||||
email: "test2@example.com",
|
||||
priority: 3,
|
||||
weight: 100,
|
||||
weightAdjustment: 3, // + 2 = 5 bookings
|
||||
bookings: [
|
||||
{
|
||||
createdAt: new Date("2022-01-25T06:30:00.000Z"),
|
||||
},
|
||||
{
|
||||
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
prismaMock.user.findMany.mockResolvedValue([user1, user2]);
|
||||
|
||||
prismaMock.booking.findMany.mockResolvedValue([
|
||||
buildBooking({
|
||||
id: 1,
|
||||
userId: 1,
|
||||
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
||||
}),
|
||||
buildBooking({
|
||||
id: 2,
|
||||
userId: 1,
|
||||
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
||||
}),
|
||||
buildBooking({
|
||||
id: 3,
|
||||
userId: 1,
|
||||
createdAt: new Date("2022-01-25T07:30:00.000Z"),
|
||||
}),
|
||||
buildBooking({
|
||||
id: 4,
|
||||
userId: 2,
|
||||
createdAt: new Date("2022-01-25T06:30:00.000Z"),
|
||||
}),
|
||||
buildBooking({
|
||||
id: 4,
|
||||
userId: 2,
|
||||
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
||||
}),
|
||||
]);
|
||||
|
||||
const allRRHosts = [
|
||||
{
|
||||
user: { id: user1.id, email: user1.email },
|
||||
weight: user1.weight,
|
||||
weightAdjustment: user1.weightAdjustment,
|
||||
},
|
||||
{
|
||||
user: { id: user2.id, email: user2.email },
|
||||
weight: user2.weight,
|
||||
weightAdjustment: user2.weightAdjustment,
|
||||
},
|
||||
];
|
||||
|
||||
await expect(
|
||||
getLuckyUser("MAXIMIZE_AVAILABILITY", {
|
||||
availableUsers: [user1, user2],
|
||||
eventType: {
|
||||
id: 1,
|
||||
isRRWeightsEnabled: true,
|
||||
},
|
||||
allRRHosts,
|
||||
})
|
||||
).resolves.toStrictEqual(user1);
|
||||
});
|
||||
});
|
||||
|
||||
function convertHostsToUsers(
|
||||
hosts: {
|
||||
userId: number;
|
||||
isFixed: boolean;
|
||||
priority: number;
|
||||
weight: number;
|
||||
weightAdjustment?: number;
|
||||
newHost?: boolean;
|
||||
}[]
|
||||
) {
|
||||
return hosts.map((host) => {
|
||||
return {
|
||||
id: host.userId,
|
||||
email: `test${host.userId}@example.com`,
|
||||
hosts: host.newHost
|
||||
? []
|
||||
: [
|
||||
{
|
||||
isFixed: host.isFixed,
|
||||
priority: host.priority,
|
||||
weightAdjustment: host.weightAdjustment,
|
||||
weight: host.weight,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
describe("addWeightAdjustmentToNewHosts", () => {
|
||||
it("weight adjustment is correctly added to host with two hosts that have the same weight", async () => {
|
||||
const hosts = [
|
||||
{
|
||||
userId: 1,
|
||||
isFixed: false,
|
||||
priority: 2,
|
||||
weight: 100,
|
||||
},
|
||||
{
|
||||
userId: 2,
|
||||
isFixed: false,
|
||||
priority: 2,
|
||||
weight: 100,
|
||||
newHost: true,
|
||||
},
|
||||
];
|
||||
|
||||
const users = convertHostsToUsers(hosts);
|
||||
|
||||
prismaMock.user.findMany.mockResolvedValue(users);
|
||||
|
||||
// mock for allBookings (for ongoing RR hosts)
|
||||
prismaMock.booking.findMany
|
||||
.mockResolvedValueOnce([
|
||||
buildBooking({
|
||||
id: 1,
|
||||
userId: 1,
|
||||
}),
|
||||
buildBooking({
|
||||
id: 2,
|
||||
userId: 1,
|
||||
}),
|
||||
buildBooking({
|
||||
id: 3,
|
||||
userId: 1,
|
||||
}),
|
||||
buildBooking({
|
||||
id: 4,
|
||||
userId: 1,
|
||||
}),
|
||||
])
|
||||
// mock for bookings of new RR host
|
||||
.mockResolvedValueOnce([
|
||||
buildBooking({
|
||||
id: 5,
|
||||
userId: 2,
|
||||
}),
|
||||
]);
|
||||
|
||||
const hostsWithAdjustedWeight = await addWeightAdjustmentToNewHosts({
|
||||
hosts,
|
||||
isWeightsEnabled: true,
|
||||
eventTypeId: 1,
|
||||
prisma: prismaMock,
|
||||
});
|
||||
/*
|
||||
both users have weight 100, user1 has 4 bookings user 2 has 1 bookings already
|
||||
*/
|
||||
expect(hostsWithAdjustedWeight.find((host) => host.userId === 2)?.weightAdjustment).toBe(3);
|
||||
});
|
||||
|
||||
it("weight adjustment is correctly added to host with several hosts that have different weights", async () => {
|
||||
// make different weights
|
||||
const hosts = [
|
||||
{
|
||||
userId: 1,
|
||||
isFixed: false,
|
||||
priority: 2,
|
||||
weight: 100,
|
||||
},
|
||||
{
|
||||
userId: 2,
|
||||
isFixed: false,
|
||||
priority: 2,
|
||||
weight: 200,
|
||||
newHost: true,
|
||||
},
|
||||
{
|
||||
userId: 3,
|
||||
isFixed: false,
|
||||
priority: 2,
|
||||
weight: 200,
|
||||
},
|
||||
{
|
||||
userId: 4,
|
||||
isFixed: false,
|
||||
priority: 2,
|
||||
weight: 100,
|
||||
},
|
||||
{
|
||||
userId: 5,
|
||||
isFixed: false,
|
||||
priority: 2,
|
||||
weight: 50,
|
||||
newHost: true,
|
||||
},
|
||||
];
|
||||
|
||||
const users = convertHostsToUsers(hosts);
|
||||
|
||||
prismaMock.user.findMany.mockResolvedValue(users);
|
||||
|
||||
// mock for allBookings (for ongoing RR hosts)
|
||||
prismaMock.booking.findMany
|
||||
.mockResolvedValueOnce([
|
||||
// 8 bookings for ongoing hosts (hosts that already existed before)
|
||||
buildBooking({
|
||||
id: 1,
|
||||
userId: 1,
|
||||
}),
|
||||
buildBooking({
|
||||
id: 2,
|
||||
userId: 1,
|
||||
}),
|
||||
buildBooking({
|
||||
id: 3,
|
||||
userId: 3,
|
||||
}),
|
||||
buildBooking({
|
||||
id: 4,
|
||||
userId: 3,
|
||||
}),
|
||||
buildBooking({
|
||||
id: 5,
|
||||
userId: 4,
|
||||
}),
|
||||
buildBooking({
|
||||
id: 6,
|
||||
userId: 4,
|
||||
}),
|
||||
buildBooking({
|
||||
id: 7,
|
||||
userId: 4,
|
||||
}),
|
||||
buildBooking({
|
||||
id: 8,
|
||||
userId: 4,
|
||||
}),
|
||||
])
|
||||
// mock for bookings of new RR host
|
||||
.mockResolvedValueOnce([
|
||||
buildBooking({
|
||||
id: 5,
|
||||
userId: 2,
|
||||
}),
|
||||
])
|
||||
.mockResolvedValue([]);
|
||||
|
||||
const hostsWithAdjustedWeight = await addWeightAdjustmentToNewHosts({
|
||||
hosts,
|
||||
isWeightsEnabled: true,
|
||||
eventTypeId: 1,
|
||||
prisma: prismaMock,
|
||||
});
|
||||
|
||||
// 8 bookings from ongoing hosts, 400 total weight --> average 0.02 bookings per weight unit --> 0.02 * 200 = 4 - 1 prev bookings = 3
|
||||
expect(hostsWithAdjustedWeight.find((host) => host.userId === 2)?.weightAdjustment).toBe(3);
|
||||
|
||||
// 8 bookings from ongoing hosts, 400 total weight --> average 0.02 bookings per weight unit --> 0.02 * 50 = 1 (no prev bookings)
|
||||
expect(hostsWithAdjustedWeight.find((host) => host.userId === 5)?.weightAdjustment).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -1093,6 +1093,11 @@ export const TestData = {
|
||||
],
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
EmptyAvailability: {
|
||||
name: "Empty Availability",
|
||||
availability: [],
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
IstWorkHoursWithDateOverride: (dateString: string) => ({
|
||||
name: "9:30AM to 6PM in India - 4:00AM to 12:30PM in GMT but with a Date Override for 2PM to 6PM IST(in GST time it is 8:30AM to 12:30PM)",
|
||||
availability: [
|
||||
|
||||
@@ -4,7 +4,7 @@ title: "Webhooks"
|
||||
|
||||
|
||||
Webhooks offer a great way to automate the flow with other apps when invitees schedule, cancel or reschedule events, or when the meeting ends.
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/Yy9me5E09LA?si=_2Mz-CKL2ajsFivd" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
<iframe style={{ width: "100%", maxWidth: "560px" }} height="315" src="https://www.youtube.com/embed/Yy9me5E09LA?si=_2Mz-CKL2ajsFivd" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
|
||||
The webhook subscription allows you to listen to specific trigger events, such as when a booking has been scheduled, for example. You can always listen to the webhook by providing a custom subscriber URL with your own development work. However, if you wish to trigger automations without any development work, you can use the integration with Zapier which connects Cal.com to your apps.
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ import { Connect } from "@calcom/atoms";
|
||||
|
||||
export default function ConnectCalendar() {
|
||||
return (
|
||||
<main>
|
||||
<>
|
||||
<Connect.AppleCalendar />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -23,9 +23,9 @@ For a demonstration of the Apple calendar connect integration, please refer to t
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
height="315"
|
||||
src="https://www.loom.com/embed/0db65d94754443ae91fed28f9bc10509?sid=d7b823cc-cda1-4d18-881a-aa49242b873d"
|
||||
src="https://www.loom.com/embed/0db65d94754443ae91fed28f9bc10509?sid=8389e585-ab9c-457f-944f-82ec7e595f13"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen="true"
|
||||
@@ -44,9 +44,9 @@ import { Connect } from "@calcom/atoms";
|
||||
|
||||
export default function ConnectCalendar() {
|
||||
return (
|
||||
<main>
|
||||
<>
|
||||
<Connect.AppleCalendar isMultiCalendar={true} />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -56,9 +56,9 @@ For a demonstration of the Apple calendar connect integration for multiple users
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://www.loom.com/embed/5844146fbcf840f99e35f9620a34186d?sid=a13f4ebf-f565-4488-8352-4e68080ef237"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/5844146fbcf840f99e35f9620a34186d"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen="true"
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function Availability() {
|
||||
console.log("Deleted schedule successfully");
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -35,8 +35,8 @@ For a demonstration of the availability settings atom, please refer to the video
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/6fd65789f5744227b4ea6d0189d4fc67?sid=544a6afd-2836-4704-aa56-e801cbd93cbc"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
@@ -64,7 +64,7 @@ export default function Availability() {
|
||||
console.log("Deleted schedule successfully");
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -74,8 +74,8 @@ For a demonstration of the availability settings atom with date overrides, pleas
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/aa9ba47cbc284496ac51c26e172c7b12?sid=6d2b5bb3-d2eb-4a95-adb5-c67d863d4e34"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function Booker( props : BookerProps ) {
|
||||
console.log("booking created successfully");
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -29,8 +29,8 @@ For a demonstration of the booker atom, please refer to the video below.
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/73dbb943cef0409c9bc57d5050453a6e?sid=45575c09-4786-486e-99f9-6cf7f4e8db50"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
@@ -57,7 +57,7 @@ export default function Booker( props : BookerProps ) {
|
||||
console.log("booking created successfully");
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -67,8 +67,8 @@ For a demonstration of the booker atom along with calendar overlay, please refer
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/554e9ef9f7874cbe9d179b63cc2c601b?sid=fc185a1c-1fb9-4705-a222-8ccd131bd5b6"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function CalendarSettingsComponent() {
|
||||
return (
|
||||
<>
|
||||
<CalendarSettings />
|
||||
<>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -22,8 +22,8 @@ For a demonstration of the calendar settings atom, please refer to the video bel
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/7f6969363b8743abafcec3974f281987?sid=493e7b5d-b3ae-4821-94a3-972260630e6c"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
@@ -64,7 +64,7 @@ export default function DestinationCalendars() {
|
||||
return (
|
||||
<>
|
||||
<DestinationCalendarSettings classNames="mx-5" statusLoader={loadingStatus} />
|
||||
<>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -91,7 +91,7 @@ export default function SelectedCalendars( props : SelectedCalendarsProps ) {
|
||||
return (
|
||||
<>
|
||||
<SelectedCalendarSettings classNames="mx-5 mb-6" />
|
||||
<>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -14,16 +14,16 @@ import { CreateEventType } from "@calcom/atoms";
|
||||
export default function EventType() {
|
||||
return (
|
||||
<>
|
||||
<CreateEventType
|
||||
onSuccess={(eventType) => {
|
||||
console.log("EventType created successfully", eventType);
|
||||
}}
|
||||
customClassNames={{
|
||||
atomsWrapper: "border p-4 rounded-md",
|
||||
buttons: { submit: "bg-red-500", cancel: "bg-gray-300" },
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<CreateEventType
|
||||
onSuccess={(eventType) => {
|
||||
console.log("EventType created successfully", eventType);
|
||||
}}
|
||||
customClassNames={{
|
||||
atomsWrapper: "border p-4 rounded-md",
|
||||
buttons: { submit: "bg-red-500", cancel: "bg-gray-300" },
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -33,8 +33,8 @@ For a demonstration of the create event type atom, please refer to the video bel
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/b0c5d0599b2c48cdbba288b0b772a676?sid=5a1dbf17-8fc4-41f1-8d77-b36332546989"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
@@ -52,20 +52,20 @@ Below code snippet can be used to render the create event type atom for a team.
|
||||
```js
|
||||
import { CreateEventType } from "@calcom/atoms";
|
||||
|
||||
export default function TeamEventType(teamId: number) {
|
||||
export default function TeamEventType(teamId: number) {l
|
||||
return (
|
||||
<>
|
||||
<CreateEventType
|
||||
teamId={teamId}
|
||||
onSuccess={(eventType) => {
|
||||
console.log("EventType created successfully", eventType);
|
||||
}}
|
||||
customClassNames={{
|
||||
atomsWrapper: "border p-4 rounded-md",
|
||||
buttons: { submit: "bg-red-500", cancel: "bg-gray-300" },
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<CreateEventType
|
||||
teamId={teamId}
|
||||
onSuccess={(eventType) => {
|
||||
console.log("EventType created successfully", eventType);
|
||||
}}
|
||||
customClassNames={{
|
||||
atomsWrapper: "border p-4 rounded-md",
|
||||
buttons: { submit: "bg-red-500", cancel: "bg-gray-300" },
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -75,8 +75,8 @@ For a demonstration of the create event type atom, please refer to the video bel
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/281c18a1a61c4c44b0bdfd3c88ced8e8?sid=ca68ef1d-eb08-4e23-bd94-9a22a8f7aa7d"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
@@ -114,19 +114,19 @@ Below code snippet can be used to render the event type settings atom.
|
||||
```js
|
||||
import { EventTypeSettings } from "@calcom/atoms";
|
||||
|
||||
export default function EventTypeSettings(eventTypeId: number) {
|
||||
export function EventTypeTabs(eventTypeId: number) {
|
||||
return (
|
||||
<>
|
||||
<EventTypeSettings
|
||||
id={eventTypeId}
|
||||
allowDelete={true}
|
||||
onSuccess={(eventType) => {
|
||||
console.log("EventType settings updated successfully", eventType);
|
||||
}}
|
||||
customClassNames={{ atomsWrapper: "!w-[70vw] !m-auto" }}
|
||||
/>
|
||||
<>
|
||||
)
|
||||
<EventTypeSettings
|
||||
id={eventTypeId}
|
||||
allowDelete={true}
|
||||
onSuccess={(eventType) => {
|
||||
console.log("EventType settings updated successfully", eventType);
|
||||
}}
|
||||
customClassNames={{ atomsWrapper: "!w-[70vw] !m-auto" }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -135,8 +135,8 @@ For a demonstration of the event type settings atom, please refer to the video b
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/2e94c5699dec4fd09d7804d2fd39d71e?sid=04ea09e9-25c3-43cc-be07-33b4b2f32c17"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
@@ -209,8 +209,8 @@ Below video shows a demonstration of the limits tab.
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/ac02d393c923413aaa97ab29b43eed48?sid=c130e621-d8e2-4e22-9614-fee97053a194"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
@@ -228,8 +228,8 @@ Below video shows a demonstration of the all the options you get in the advanced
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/e0121b51920544eea92251dba99245fa?sid=ac627de7-5673-41b4-aa78-c6c9e439bdb5"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
|
||||
@@ -11,9 +11,9 @@ import { Connect } from "@calcom/atoms";
|
||||
|
||||
export default function Connect() {
|
||||
return (
|
||||
<main>
|
||||
<>
|
||||
<Connect.GoogleCalendar />
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -23,8 +23,8 @@ For a demonstration of the Google calendar connect integration, please refer to
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/dcfcaf8c25754b8daec8fe309fbf7c7c?sid=3181cba5-2e16-4f6e-99de-4408a5109c37"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
@@ -42,9 +42,9 @@ import { Connect } from "@calcom/atoms";
|
||||
|
||||
export default function Connect() {
|
||||
return (
|
||||
<main>
|
||||
<>
|
||||
<Connect.GoogleCalendar isMultiCalendar={true} />
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -54,8 +54,8 @@ For a demonstration of the Google calendar connect integration for multiple user
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/7a34123b3bd94fbc80809392aab5f712?sid=12bacd7e-62b2-40aa-9fd6-30d69ab22e0a"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
|
||||
@@ -11,9 +11,9 @@ import { Connect } from "@calcom/atoms";
|
||||
|
||||
export default function ConnectCalendar() {
|
||||
return (
|
||||
<main>
|
||||
<>
|
||||
<Connect.OutlookCalendar />
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -23,8 +23,8 @@ For a demonstration of the Google calendar connect integration, please refer to
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/2da4cdfe8d314f74a55e98121dcdb7bd?sid=cdc73356-13c0-49ba-90a3-91d885251457"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
@@ -42,9 +42,9 @@ import { Connect } from "@calcom/atoms";
|
||||
|
||||
export default function ConnectCalendar() {
|
||||
return (
|
||||
<main>
|
||||
<>
|
||||
<Connect.OutlookCalendar isMultiCalendar={true} />
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -54,8 +54,8 @@ For a demonstration of the Outlook calendar connect integration for multiple use
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/b90c21c2bc9c478f9c28c1ee62bb8568?sid=fe1af433-e6e6-4149-9fe2-d3cdbc7d6ca1"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
|
||||
@@ -11,9 +11,9 @@ import { PaymentForm } from "@calcom/atoms";
|
||||
|
||||
export default function StripePaymentForm(uid: string) {
|
||||
return (
|
||||
<main>
|
||||
<>
|
||||
<PaymentForm paymentUid={uid} />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -23,8 +23,8 @@ For a demonstration of the Payment form, please refer to the video below.
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/13cf66446b554d5c9405f9d4fef53f47?sid=d2994785-027e-4de7-937a-3b90fc357182"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
@@ -60,9 +60,9 @@ import { EventTypeSettings } from "@calcom/atoms";
|
||||
|
||||
export default function EventType(id: number) {
|
||||
return (
|
||||
<main>
|
||||
<EventTypeSettings id={id} tabs={["setup", "availability", "payments"]} />
|
||||
</main>
|
||||
<>
|
||||
<EventTypeSettings id={id} allowDelete={true} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -92,7 +92,7 @@ export default function Booker( props : BookerProps ) {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -112,9 +112,9 @@ export default function StripePaymentForm() {
|
||||
const uid = pathname.split("/").pop();
|
||||
|
||||
return (
|
||||
<main>
|
||||
<>
|
||||
<PaymentForm paymentUid={uid} />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -126,8 +126,8 @@ Below video shows a demonstration of how we combined the event types atom and pa
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/0c1bc59da8c34d83b8dfcbd7557f7eaa?sid=058cf5df-1e3a-4fe3-b721-8c5a9eac4f24"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
|
||||
@@ -35,6 +35,7 @@ export default function Bookings(props: { calUsername: string; calEmail: string
|
||||
return (
|
||||
<p>{booking.title}</p>
|
||||
)
|
||||
};
|
||||
```
|
||||
|
||||
An example implementation can be found [here](https://github.com/calcom/cal.com/blob/main/packages/platform/examples/base/src/pages/%5BbookingUid%5D.tsx).
|
||||
|
||||
@@ -6,10 +6,10 @@ Install desired apps in your self-hosted instance for better integration.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card href="/self-hosting/apps/install-apps/google" title="Google" icon="google">
|
||||
Install google for supporting Google Calendar and Google Meet integrations
|
||||
Install Google for supporting Google Calendar and Google Meet integrations
|
||||
</Card>
|
||||
<Card href="/self-hosting/apps/install-apps/microsoft" title="Microsoft" icon="microsoft">
|
||||
Install microsoft to support Exchange365 integration
|
||||
Install Microsoft to support Exchange365 integration
|
||||
</Card>
|
||||
<Card href="/self-hosting/apps/install-apps/zoom" title="Zoom" icon="z">
|
||||
Install Zoom to support Zoom video conferencing integration
|
||||
@@ -17,11 +17,11 @@ Install desired apps in your self-hosted instance for better integration.
|
||||
<Card href="/self-hosting/apps/install-apps/daily" title="Daily" icon="video">
|
||||
Install Daily to support Cal Video integration
|
||||
</Card>
|
||||
<Card href="/self-hosting/apps/install-apps/hubspot" title="Hubspot" icon="hubspot">
|
||||
Install Hubspot to support Hubspot CRM integration
|
||||
<Card href="/self-hosting/apps/install-apps/hubspot" title="HubSpot" icon="hubspot">
|
||||
Install HubSpot to support HubSpot CRM integration
|
||||
</Card>
|
||||
<Card href="/self-hosting/apps/install-apps/sendgrid" title="Sendgrid" icon="envelope">
|
||||
Install Sendgrid to support Sendgrid Email integration
|
||||
<Card href="/self-hosting/apps/install-apps/sendgrid" title="SendGrid" icon="envelope">
|
||||
Install SendGrid to support SendGrid Email integration
|
||||
</Card>
|
||||
<Card href="/self-hosting/apps/install-apps/stripe" title="Stripe" icon="stripe">
|
||||
Install Stripe to support Stripe integration for paid bookings
|
||||
|
||||
@@ -200,6 +200,8 @@ checksums:
|
||||
webhook_disabled: 99fe63e5f2e5b6da44c8afed175ac40b
|
||||
webhook_response: 560022605ca28622704d7e6939d7ebf6
|
||||
webhook_test: e5e24d4aeb89ec198800793a2346eb71
|
||||
webhook_update_form: 69b9095ef27d05204fc3120006704936
|
||||
webhook_update_form_description: bed4f95c98f9b367b80d6204ee80d6f8
|
||||
manage_your_webhook: 0e98b60ceea53bb6637469cceb2bf16d
|
||||
webhook_created_successfully: a28d4b9a5427109de72d7519e6b4fdcb
|
||||
webhook_updated_successfully: 268f4f92524ec2e59868e765bfa815e5
|
||||
@@ -705,6 +707,9 @@ checksums:
|
||||
no_teams: b629f2ff400fbd38f3b34a25295f0146
|
||||
no_teams_description: 6394f038a3b71bf2036e82443e72591a
|
||||
submit: 7c91ef5f747eea9f77a9c4f23e19fb2e
|
||||
confirm_remove_attribute: 2f3b1cba07ed6ba4f87e157db59800cc
|
||||
remove_attribute: 72944218312442e483aa41eb63abee24
|
||||
remove_attribute_confirmation_message: 356cd8163f04fbc9b9861f2a74131ec9
|
||||
delete: 8bcf303dd10a645b5baacb02b47d72c9
|
||||
update: 079fc039262fd31b10532929685c2d1b
|
||||
save: f7a2929f33bc420195e59ac5a8bcd454
|
||||
@@ -1632,6 +1637,7 @@ checksums:
|
||||
format: 14dfd9f7515f22e0f6f2ba64bc6af47e
|
||||
uppercase_for_letters: 12f5601089e8e42d3fc4cf73e3ec45f4
|
||||
replace_whitespaces_underscores: d785e8b787272f0755b0334469e30fc8
|
||||
platform_billing: e5fd386e4e409e7e88196211bab2ec9c
|
||||
manage_billing: 95d7c3339a608bdb9f0ff73be0107202
|
||||
manage_billing_description: d1dcdc64ba7a022f5cbf7da37d7b8a8c
|
||||
billing_freeplan_title: eed9834cd3ca0eeb35fda88847fce65e
|
||||
@@ -2039,6 +2045,8 @@ checksums:
|
||||
looking_for_more_insights: f245400c6511b85e78c316bacefe8a47
|
||||
filters: acf5accc113ff3c1992688058576732c
|
||||
add_filter: ed5d8e9bfcb05cd1e10e4c403befbae6
|
||||
add_rule: 7b6b4f30c6359cd00c1b60ddce4d0417
|
||||
add_rule_group: 77298c5cc51f6b31f02c9466f17f6b63
|
||||
remove_filters: 0bc64da95ec15d9bc7fb12cb77f2838a
|
||||
email_verified: 1198a84576c526d433d566f764117f2e
|
||||
select_user: 44e74722fc9863dfeba9343661262440
|
||||
@@ -2141,6 +2149,7 @@ checksums:
|
||||
set_up_your_organization: b021d3fcb90cccd0c516ed144fb16d58
|
||||
set_up_your_platform_organization: 639cfd98ed2cd3ee2f6a4e0fd5a2d8a4
|
||||
organizations_description: e77f86264212f368e855f99a06f27345
|
||||
plans: c88c6f82f451eedca3fe2d458e8d70a2
|
||||
platform: c68862170146325333c7f25af11a3fa2
|
||||
platform_description: d330e223f2a867d6a2492cc3737ce11e
|
||||
platform_organization_description: 40524306837ee9de59f20fdbc00d268f
|
||||
@@ -2278,6 +2287,10 @@ checksums:
|
||||
seat_options_doesnt_multiple_durations: 0579c3f5e11a96659184889db97edfd3
|
||||
include_calendar_event: 9c1a7461cfb789aec6bd505d7c028afc
|
||||
oAuth: 834fae0a02e83662b1367c68788ba247
|
||||
oAuth_client_creation_form: 5d2c439491c05b16bafe2e4fe20dac25
|
||||
oAuth_client_creation_form_description: f89809267353e7168d4df862f27e1160
|
||||
oAuth_client_updation_form: 0e820008c0effe62067983818d81ae1c
|
||||
oAuth_client_updation_form_description: 4ac29cf935a69173e1fb0107591ecd54
|
||||
recently_added: fdc3bfbacff759fd61eaf619b4ab3920
|
||||
connect_all_calendars: 4cc7b86ae4a708e6a144ad5fc18061b0
|
||||
connect_all_calendars_description: 4f8d4df299b38226c6e4a9d213d86af7
|
||||
@@ -2734,4 +2747,12 @@ checksums:
|
||||
event_end_time_in_attendee_timezone_info: 82ffc6118c662094eb130b74a1e59627
|
||||
salesforce_create_event_on_contact: 82fe69b4ba08b1cb9c6465dc18f968b5
|
||||
salesforce_owner_name_to_change: 000522ab557b02cfe4a5d49787259ef9
|
||||
field_type: 7444da76fa3572d47ea5b5d86c50f11f
|
||||
when_to_write: 2dab9955e5743f1bf39ee5066c5c3e91
|
||||
salesforce_on_booking_write_to_record: 43d786b0d3f592a6b1d75ec567f43c70
|
||||
on_every_booking: 7b4105d56a0826f5c95fa44dd88648ae
|
||||
only_if_field_is_empty: adf0bcef1c6656b082a63f280f1b9495
|
||||
booking_start_date: ef6f4addaa24a5c7dce41b4d91868ba6
|
||||
booking_created_date: 902300aaa13a56a2295d561d46073eeb
|
||||
booking_reassigned_to_host: 9eb99d1201d75ac0366daf0f09bc360a
|
||||
ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS: 18323f2f3fabb169a7cb50ff62433850
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import type { Auth, calendar_v3 } from "googleapis";
|
||||
import { google } from "googleapis";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { renewSelectedCalendarCredentialId } from "@calcom/lib/connectedCalendar";
|
||||
import { WEBAPP_URL, WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { getAllCalendars, updateProfilePhoto } from "@calcom/lib/google";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
||||
import { GoogleRepository } from "@calcom/lib/server/repository/google";
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
import { REQUIRED_SCOPES, SCOPE_USERINFO_PROFILE } from "../lib/constants";
|
||||
import { getGoogleAppKeys } from "../lib/getGoogleAppKeys";
|
||||
import { getAllCalendars } from "../lib/utils";
|
||||
|
||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { code } = req.query;
|
||||
@@ -82,13 +81,9 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
await updateProfilePhoto(oAuth2Client, req.session.user.id);
|
||||
}
|
||||
|
||||
const credential = await prisma.credential.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
key,
|
||||
userId: req.session.user.id,
|
||||
appId: "google-calendar",
|
||||
},
|
||||
const gcalCredential = await GoogleRepository.createGoogleCalendarCredential({
|
||||
key,
|
||||
userId: req.session.user.id,
|
||||
});
|
||||
|
||||
// If we still don't have a primary calendar skip creating the selected calendar.
|
||||
@@ -110,18 +105,17 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
// Wrapping in a try/catch to reduce chance of race conditions-
|
||||
// also this improves performance for most of the happy-paths.
|
||||
try {
|
||||
await prisma.selectedCalendar.create({
|
||||
data: {
|
||||
credentialId: credential.id,
|
||||
...selectedCalendarWhereUnique,
|
||||
},
|
||||
await GoogleRepository.createSelectedCalendar({
|
||||
credentialId: gcalCredential.id,
|
||||
externalId: selectedCalendarWhereUnique.externalId,
|
||||
userId: selectedCalendarWhereUnique.userId,
|
||||
});
|
||||
} catch (error) {
|
||||
let errorMessage = "something_went_wrong";
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
|
||||
// it is possible a selectedCalendar was orphaned, in this situation-
|
||||
// we want to recover by connecting the existing selectedCalendar to the new Credential.
|
||||
if (await renewSelectedCalendarCredentialId(selectedCalendarWhereUnique, credential.id)) {
|
||||
if (await renewSelectedCalendarCredentialId(selectedCalendarWhereUnique, gcalCredential.id)) {
|
||||
res.redirect(
|
||||
getSafeRedirectUrl(state?.returnTo) ??
|
||||
getInstalledAppPath({ variant: "calendar", slug: "google-calendar" })
|
||||
@@ -131,7 +125,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
// else
|
||||
errorMessage = "account_already_linked";
|
||||
}
|
||||
await prisma.credential.delete({ where: { id: credential.id } });
|
||||
await CredentialRepository.deleteById({ id: gcalCredential.id });
|
||||
res.redirect(
|
||||
`${
|
||||
getSafeRedirectUrl(state?.onErrorReturnTo) ??
|
||||
@@ -151,11 +145,8 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingGoogleMeetCredential = await prisma.credential.findFirst({
|
||||
where: {
|
||||
userId: req.session.user.id,
|
||||
type: "google_video",
|
||||
},
|
||||
const existingGoogleMeetCredential = await GoogleRepository.findGoogleMeetCredential({
|
||||
userId: req.session.user.id,
|
||||
});
|
||||
|
||||
// If the user already has a google meet credential, there's nothing to do in here
|
||||
@@ -168,45 +159,13 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
}
|
||||
|
||||
// Create a new google meet credential
|
||||
await prisma.credential.create({
|
||||
data: {
|
||||
type: "google_video",
|
||||
key: {},
|
||||
userId: req.session.user.id,
|
||||
appId: "google-meet",
|
||||
},
|
||||
});
|
||||
|
||||
await GoogleRepository.createGoogleMeetsCredential({ userId: req.session.user.id });
|
||||
res.redirect(
|
||||
getSafeRedirectUrl(`${WEBAPP_URL}/apps/installed/conferencing?hl=google-meet`) ??
|
||||
getInstalledAppPath({ variant: "conferencing", slug: "google-meet" })
|
||||
);
|
||||
}
|
||||
|
||||
async function updateProfilePhoto(oAuth2Client: Auth.OAuth2Client, userId: number) {
|
||||
try {
|
||||
const oauth2 = google.oauth2({ version: "v2", auth: oAuth2Client });
|
||||
const userDetails = await oauth2.userinfo.get();
|
||||
if (userDetails.data?.picture) {
|
||||
// Using updateMany here since if the user already has a profile it would throw an error
|
||||
// because no records were found to update the profile picture
|
||||
await prisma.user.updateMany({
|
||||
where: {
|
||||
id: userId,
|
||||
avatarUrl: {
|
||||
equals: null,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
avatarUrl: userDetails.data.picture,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error updating avatarUrl from google calendar connect", error);
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
CREDENTIAL_SYNC_SECRET_HEADER_NAME,
|
||||
} from "@calcom/lib/constants";
|
||||
import { formatCalEvent } from "@calcom/lib/formatCalendarEvent";
|
||||
import { getAllCalendars } from "@calcom/lib/google";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -36,7 +37,6 @@ import { markTokenAsExpired } from "../../_utils/oauth/markTokenAsExpired";
|
||||
import { OAuth2UniversalSchema } from "../../_utils/oauth/universalSchema";
|
||||
import { metadata } from "../_metadata";
|
||||
import { getGoogleAppKeys } from "./getGoogleAppKeys";
|
||||
import { getAllCalendars } from "./utils";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["app-store/googlecalendar/lib/CalendarService"] });
|
||||
interface GoogleCalError extends Error {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import routingFormBookingFormHandler from "../salesforce/lib/routingFormBookingFormHandler";
|
||||
import type { AttributeRoutingConfig } from "./types/types";
|
||||
|
||||
type AppBookingFormHandler = (
|
||||
attendeeEmail: string,
|
||||
attributeRoutingConfig: AttributeRoutingConfig,
|
||||
eventTypeId: number
|
||||
) => Promise<{ email: string | null }>;
|
||||
|
||||
const appBookingFormHandler: Record<string, AppBookingFormHandler> = {
|
||||
salesforce: routingFormBookingFormHandler,
|
||||
};
|
||||
|
||||
export default appBookingFormHandler;
|
||||
@@ -0,0 +1,5 @@
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
export const routingFormAppComponents = {
|
||||
salesforce: dynamic(() => import("../salesforce/components/RoutingFormOptions")),
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { routingFormOptions as salesforce_routing_form_schema } from "../salesforce/zod";
|
||||
|
||||
export const routingFormAppDataSchemas = {
|
||||
salesforce: salesforce_routing_form_schema,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Route, AttributeRoutingConfig } from "../types/types";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default function DynamicAppComponent<T extends Record<string, React.ComponentType<any>>>(props: {
|
||||
componentMap: T;
|
||||
slug: string;
|
||||
appData: any;
|
||||
route: Route;
|
||||
setAttributeRoutingConfig: (id: string, attributeRoutingConfig: Partial<AttributeRoutingConfig>) => void;
|
||||
wrapperClassName?: string;
|
||||
}) {
|
||||
const { componentMap, slug, wrapperClassName, appData, route, setAttributeRoutingConfig, ...rest } = props;
|
||||
|
||||
// There can be apps with no matching component
|
||||
if (!componentMap[slug]) return null;
|
||||
|
||||
const Component = componentMap[slug];
|
||||
|
||||
return (
|
||||
<div className={wrapperClassName || ""}>
|
||||
<Component
|
||||
appData={appData}
|
||||
route={route}
|
||||
setAttributeRoutingConfig={setAttributeRoutingConfig}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -17,13 +17,11 @@ export default function RoutingNavBar({
|
||||
{
|
||||
name: "Routing",
|
||||
href: `${appUrl}/route-builder/${form?.id}`,
|
||||
className: "pointer-events-none opacity-30 lg:pointer-events-auto lg:opacity-100",
|
||||
},
|
||||
{
|
||||
name: "Reporting",
|
||||
target: "_blank",
|
||||
href: `${appUrl}/reporting/${form?.id}`,
|
||||
className: "pointer-events-none opacity-30 lg:pointer-events-auto lg:opacity-100",
|
||||
},
|
||||
];
|
||||
return (
|
||||
|
||||
@@ -612,7 +612,7 @@ function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleF
|
||||
CTA={<Actions form={form} mutation={mutation} />}>
|
||||
<div className="-mx-4 mt-4 px-4 sm:px-6 md:-mx-8 md:mt-0 md:px-8">
|
||||
<div className="flex flex-col items-center items-baseline md:flex-row md:items-start">
|
||||
<div className="lg:min-w-72 lg:max-w-72 mb-6 md:mr-6">
|
||||
<div className="lg:min-w-72 lg:max-w-72 md:max-w-56 mb-6 w-full md:mr-6">
|
||||
<TextField
|
||||
type="text"
|
||||
containerClassName="mb-6"
|
||||
@@ -648,7 +648,6 @@ function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleF
|
||||
userId: userId,
|
||||
priority: 2,
|
||||
weight: 100,
|
||||
weightAdjustment: 0,
|
||||
scheduleId: 1,
|
||||
}))}
|
||||
onChange={(value) => {
|
||||
|
||||
@@ -40,7 +40,10 @@ export const getServerSidePropsForSingleFormView = async function getServerSideP
|
||||
const isFormCreateEditAllowed = (await import("../lib/isFormCreateEditAllowed")).isFormCreateEditAllowed;
|
||||
if (!(await isFormCreateEditAllowed({ userId: user.id, formId, targetTeamId: null }))) {
|
||||
return {
|
||||
notFound: true,
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/403",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -126,3 +126,16 @@
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.rule--body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.cal-query-builder .rule--field,
|
||||
.cal-query-builder .rule--operator,
|
||||
.cal-query-builder .rule--value {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const ROUTING_FORM_RESPONSE_ID_QUERY_STRING = "cal.routingFormResponseId";
|
||||
@@ -0,0 +1,2 @@
|
||||
/** This is a list of apps which have functionality in routing forms */
|
||||
export const enabledAppSlugs = ["salesforce"];
|
||||
@@ -0,0 +1,26 @@
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { enabledAppSlugs } from "./enabledApps";
|
||||
import type { Prisma } from ".prisma/client";
|
||||
|
||||
const getEventTypeAppMetadata = (metadata: Prisma.JsonValue) => {
|
||||
const eventTypeMetadataParse = EventTypeMetaDataSchema.safeParse(metadata);
|
||||
|
||||
if (!eventTypeMetadataParse.success || !eventTypeMetadataParse.data) return;
|
||||
|
||||
const appMetadata = eventTypeMetadataParse.data.apps;
|
||||
|
||||
const eventTypeAppMetadata: Record<string, any> = {};
|
||||
|
||||
if (appMetadata) {
|
||||
for (const appSlug of Object.keys(appMetadata)) {
|
||||
if (enabledAppSlugs.includes(appSlug)) {
|
||||
eventTypeAppMetadata[appSlug] = appMetadata[appSlug as keyof typeof appMetadata];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return eventTypeAppMetadata;
|
||||
};
|
||||
|
||||
export default getEventTypeAppMetadata;
|
||||
@@ -23,9 +23,10 @@ import {
|
||||
TextField,
|
||||
Badge,
|
||||
Divider,
|
||||
Switch,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import { routingFormAppComponents } from "../../appComponents";
|
||||
import DynamicAppComponent from "../../components/DynamicAppComponent";
|
||||
import type { RoutingFormWithResponseCount } from "../../components/SingleForm";
|
||||
import SingleForm, {
|
||||
getServerSidePropsForSingleFormView as getServerSideProps,
|
||||
@@ -33,6 +34,7 @@ import SingleForm, {
|
||||
import "../../components/react-awesome-query-builder/styles.css";
|
||||
import { RoutingPages } from "../../lib/RoutingPages";
|
||||
import { createFallbackRoute } from "../../lib/createFallbackRoute";
|
||||
import getEventTypeAppMetadata from "../../lib/getEventTypeAppMetadata";
|
||||
import {
|
||||
getQueryBuilderConfigForFormFields,
|
||||
getQueryBuilderConfigForAttributes,
|
||||
@@ -41,62 +43,24 @@ import {
|
||||
} from "../../lib/getQueryBuilderConfig";
|
||||
import isRouter from "../../lib/isRouter";
|
||||
import type { SerializableForm } from "../../types/types";
|
||||
import type { GlobalRoute, LocalRoute, SerializableRoute, Attribute } from "../../types/types";
|
||||
import type {
|
||||
GlobalRoute,
|
||||
LocalRoute,
|
||||
SerializableRoute,
|
||||
Attribute,
|
||||
EditFormRoute,
|
||||
AttributeRoutingConfig,
|
||||
} from "../../types/types";
|
||||
import { RouteActionType } from "../../zod";
|
||||
|
||||
type FormFieldsQueryBuilderState = {
|
||||
tree: ImmutableTree;
|
||||
config: FormFieldsQueryBuilderConfigWithRaqbFields;
|
||||
};
|
||||
|
||||
type AttributesQueryBuilderState = {
|
||||
tree: ImmutableTree;
|
||||
config: AttributesQueryBuilderConfigWithRaqbFields;
|
||||
};
|
||||
|
||||
type LocalRouteWithRaqbStates = LocalRoute & {
|
||||
formFieldsQueryBuilderState: FormFieldsQueryBuilderState;
|
||||
attributesQueryBuilderState: AttributesQueryBuilderState | null;
|
||||
fallbackAttributesQueryBuilderState: AttributesQueryBuilderState | null;
|
||||
};
|
||||
|
||||
type EventTypesByGroup = RouterOutputs["viewer"]["eventTypes"]["getByViewer"];
|
||||
|
||||
type Form = inferSSRProps<typeof getServerSideProps>["form"];
|
||||
|
||||
type Route = LocalRouteWithRaqbStates | GlobalRoute;
|
||||
type SetRoute = (id: string, route: Partial<Route>) => void;
|
||||
|
||||
const RoundRobinContactOwnerOverrideSwitch = ({
|
||||
route,
|
||||
setAttributeRoutingConfig,
|
||||
}: {
|
||||
route: LocalRouteWithRaqbStates;
|
||||
setAttributeRoutingConfig: (id: string, attributeRoutingConfig: Partial<AttributeRoutingConfig>) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="mt-4 flex flex-col">
|
||||
<Switch
|
||||
label={
|
||||
route.attributeRoutingConfig?.skipContactOwner
|
||||
? "Contact owner will not be forced (can still be host if it matches the attributes and Round Robin criteria)"
|
||||
: "Contact owner will be the Round Robin host if available"
|
||||
}
|
||||
tooltip="Contact owner can only be used if the routed event has it enabled through Salesforce app"
|
||||
checked={route.attributeRoutingConfig?.skipContactOwner ?? false}
|
||||
onCheckedChange={(skipContactOwner) => {
|
||||
setAttributeRoutingConfig(route.id, {
|
||||
skipContactOwner,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
type SetRoute = (id: string, route: Partial<EditFormRoute>) => void;
|
||||
|
||||
type AttributesQueryValue = NonNullable<LocalRoute["attributesQueryValue"]>;
|
||||
type FormFieldsQueryValue = LocalRoute["queryValue"];
|
||||
type AttributeRoutingConfig = NonNullable<LocalRoute["attributeRoutingConfig"]>;
|
||||
|
||||
/**
|
||||
* We need eventTypeId in every redirect url action now for Rerouting to work smoothly.
|
||||
@@ -107,7 +71,7 @@ function useEnsureEventTypeIdInRedirectUrlAction({
|
||||
eventOptions,
|
||||
setRoute,
|
||||
}: {
|
||||
route: Route;
|
||||
route: EditFormRoute;
|
||||
eventOptions: { label: string; value: string; eventTypeId: number }[];
|
||||
setRoute: SetRoute;
|
||||
}) {
|
||||
@@ -134,7 +98,7 @@ function useEnsureEventTypeIdInRedirectUrlAction({
|
||||
}, [eventOptions, setRoute, route.id, (route as unknown as any).action?.value]);
|
||||
}
|
||||
|
||||
const hasRules = (route: Route) => {
|
||||
const hasRules = (route: EditFormRoute) => {
|
||||
if (isRouter(route)) return false;
|
||||
route.queryValue.children1 && Object.keys(route.queryValue.children1).length;
|
||||
};
|
||||
@@ -169,13 +133,19 @@ const buildEventsData = ({
|
||||
}: {
|
||||
eventTypesByGroup: EventTypesByGroup | undefined;
|
||||
form: Form;
|
||||
route: Route;
|
||||
route: EditFormRoute;
|
||||
}) => {
|
||||
const eventOptions: { label: string; value: string; eventTypeId: number }[] = [];
|
||||
const eventOptions: {
|
||||
label: string;
|
||||
value: string;
|
||||
eventTypeId: number;
|
||||
eventTypeAppMetadata?: Record<string, any>;
|
||||
}[] = [];
|
||||
const eventTypesMap = new Map<
|
||||
number,
|
||||
{
|
||||
schedulingType: SchedulingType | null;
|
||||
eventTypeAppMetadata?: Record<string, any>;
|
||||
}
|
||||
>();
|
||||
eventTypesByGroup?.eventTypeGroups.forEach((group) => {
|
||||
@@ -199,13 +169,19 @@ const buildEventsData = ({
|
||||
if (!isRouteAlreadyInUse && !eventTypeValidInContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass app data that works with routing forms
|
||||
const eventTypeAppMetadata = getEventTypeAppMetadata(eventType.metadata);
|
||||
|
||||
eventTypesMap.set(eventType.id, {
|
||||
eventTypeAppMetadata,
|
||||
schedulingType: eventType.schedulingType,
|
||||
});
|
||||
eventOptions.push({
|
||||
label: uniqueSlug,
|
||||
value: uniqueSlug,
|
||||
eventTypeId: eventType.id,
|
||||
eventTypeAppMetadata,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -230,13 +206,13 @@ const Route = ({
|
||||
eventTypesByGroup,
|
||||
}: {
|
||||
form: Form;
|
||||
route: Route;
|
||||
routes: Route[];
|
||||
route: EditFormRoute;
|
||||
routes: EditFormRoute[];
|
||||
setRoute: SetRoute;
|
||||
setAttributeRoutingConfig: (id: string, attributeRoutingConfig: Partial<AttributeRoutingConfig>) => void;
|
||||
formFieldsQueryBuilderConfig: FormFieldsQueryBuilderConfigWithRaqbFields;
|
||||
attributesQueryBuilderConfig: AttributesQueryBuilderConfigWithRaqbFields | null;
|
||||
setRoutes: React.Dispatch<React.SetStateAction<Route[]>>;
|
||||
setRoutes: React.Dispatch<React.SetStateAction<EditFormRoute[]>>;
|
||||
fieldIdentifiers: string[];
|
||||
moveUp?: { fn: () => void; check: () => boolean } | null;
|
||||
moveDown?: { fn: () => void; check: () => boolean } | null;
|
||||
@@ -271,7 +247,7 @@ const Route = ({
|
||||
});
|
||||
|
||||
const onChangeFormFieldsQuery = (
|
||||
route: Route,
|
||||
route: EditFormRoute,
|
||||
immutableTree: ImmutableTree,
|
||||
config: FormFieldsQueryBuilderConfigWithRaqbFields
|
||||
) => {
|
||||
@@ -283,7 +259,7 @@ const Route = ({
|
||||
};
|
||||
|
||||
const onChangeTeamMembersQuery = (
|
||||
route: Route,
|
||||
route: EditFormRoute,
|
||||
immutableTree: ImmutableTree,
|
||||
config: AttributesQueryBuilderConfigWithRaqbFields
|
||||
) => {
|
||||
@@ -295,7 +271,7 @@ const Route = ({
|
||||
};
|
||||
|
||||
const onChangeFallbackTeamMembersQuery = (
|
||||
route: Route,
|
||||
route: EditFormRoute,
|
||||
immutableTree: ImmutableTree,
|
||||
config: AttributesQueryBuilderConfigWithRaqbFields
|
||||
) => {
|
||||
@@ -330,11 +306,8 @@ const Route = ({
|
||||
setRoutes(newRoutes);
|
||||
},
|
||||
}}
|
||||
label={
|
||||
<div>
|
||||
<span className="mr-2">{`Route ${index + 1}`}</span>
|
||||
</div>
|
||||
}
|
||||
isLabelEditable={false}
|
||||
label={route.name ?? `Route ${index + 1}`}
|
||||
className="mb-6">
|
||||
<div className="-mt-3">
|
||||
<Link href={`${appUrl}/route-builder/${route.id}`}>
|
||||
@@ -404,11 +377,17 @@ const Route = ({
|
||||
and use only the Team Members that match the following criteria (matches all by default)
|
||||
</span>
|
||||
|
||||
{isRoundRobinEventSelectedForRedirect ? (
|
||||
<RoundRobinContactOwnerOverrideSwitch
|
||||
route={route}
|
||||
setAttributeRoutingConfig={setAttributeRoutingConfig}
|
||||
/>
|
||||
{eventTypeRedirectUrlSelectedOption?.eventTypeAppMetadata &&
|
||||
"salesforce" in eventTypeRedirectUrlSelectedOption.eventTypeAppMetadata ? (
|
||||
<div className="mt-4 px-2.5">
|
||||
<DynamicAppComponent
|
||||
componentMap={routingFormAppComponents}
|
||||
slug="salesforce"
|
||||
appData={eventTypeRedirectUrlSelectedOption?.eventTypeAppMetadata["salesforce"]}
|
||||
route={route}
|
||||
setAttributeRoutingConfig={setAttributeRoutingConfig}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-2">
|
||||
@@ -460,7 +439,11 @@ const Route = ({
|
||||
className="mb-6"
|
||||
moveUp={moveUp}
|
||||
moveDown={moveDown}
|
||||
label={route.isFallback ? "Fallback Route" : `Route ${index + 1}`}
|
||||
label={route.name ?? (route.isFallback ? "Fallback Route" : `Route ${index + 1}`)}
|
||||
isLabelEditable={!route.isFallback}
|
||||
onLabelChange={(label) => {
|
||||
setRoute(route.id, { name: label });
|
||||
}}
|
||||
deleteField={{
|
||||
check: () => routes.length !== 1 && !route.isFallback,
|
||||
fn: () => {
|
||||
@@ -468,45 +451,47 @@ const Route = ({
|
||||
setRoutes(newRoutes);
|
||||
},
|
||||
}}>
|
||||
<div className="-mx-4 mb-4 flex w-full items-center sm:mx-0">
|
||||
<div className="mb-4 flex w-full items-center sm:mx-0">
|
||||
<div className="cal-query-builder w-full ">
|
||||
{formFieldsQueryBuilder}
|
||||
<div>
|
||||
<div className="text-emphasis flex w-full items-center text-sm">
|
||||
<div className="flex flex-grow-0 whitespace-nowrap">
|
||||
<span>{t("send_booker_to")}</span>
|
||||
<div className="flex w-full flex-col gap-2 text-sm lg:flex-row">
|
||||
<div className="flex flex-grow items-center gap-2">
|
||||
<div className="flex flex-grow-0 whitespace-nowrap">
|
||||
<span>{t("send_booker_to")}</span>
|
||||
</div>
|
||||
<Select
|
||||
isDisabled={disabled}
|
||||
className="data-testid-select-routing-action block w-full flex-grow"
|
||||
required
|
||||
value={RoutingPages.find((page) => page.value === route.action?.type)}
|
||||
onChange={(item) => {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const action: LocalRoute["action"] = {
|
||||
type: item.value,
|
||||
value: "",
|
||||
};
|
||||
|
||||
if (action.type === "customPageMessage") {
|
||||
action.value = "We are not ready for you yet :(";
|
||||
} else {
|
||||
action.value = "";
|
||||
}
|
||||
|
||||
setRoute(route.id, { action });
|
||||
}}
|
||||
options={RoutingPages}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
isDisabled={disabled}
|
||||
className="data-testid-select-routing-action block w-full flex-grow px-2"
|
||||
required
|
||||
value={RoutingPages.find((page) => page.value === route.action?.type)}
|
||||
onChange={(item) => {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const action: LocalRoute["action"] = {
|
||||
type: item.value,
|
||||
value: "",
|
||||
};
|
||||
|
||||
if (action.type === "customPageMessage") {
|
||||
action.value = "We are not ready for you yet :(";
|
||||
} else {
|
||||
action.value = "";
|
||||
}
|
||||
|
||||
setRoute(route.id, { action });
|
||||
}}
|
||||
options={RoutingPages}
|
||||
/>
|
||||
{route.action?.type ? (
|
||||
route.action?.type === "customPageMessage" ? (
|
||||
<TextArea
|
||||
required
|
||||
disabled={disabled}
|
||||
name="customPageMessage"
|
||||
className="border-default flex w-full flex-grow"
|
||||
className="border-default flex flex-grow lg:w-fit"
|
||||
value={route.action.value}
|
||||
onChange={(e) => {
|
||||
setRoute(route.id, { action: { ...route.action, value: e.target.value } });
|
||||
@@ -516,8 +501,8 @@ const Route = ({
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
name="externalRedirectUrl"
|
||||
className="border-default flex w-full flex-grow text-sm"
|
||||
containerClassName="w-full mt-2"
|
||||
className="border-default flex flex-grow text-sm"
|
||||
containerClassName="flex-grow"
|
||||
type="url"
|
||||
required
|
||||
labelSrOnly
|
||||
@@ -528,7 +513,7 @@ const Route = ({
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
) : (
|
||||
<div className="block w-full">
|
||||
<div className="flex-grow">
|
||||
<Select
|
||||
required
|
||||
className="data-testid-eventTypeRedirectUrl-select"
|
||||
@@ -541,11 +526,13 @@ const Route = ({
|
||||
if (option.value !== "custom") {
|
||||
setRoute(route.id, {
|
||||
action: { ...route.action, value: option.value, eventTypeId: option.eventTypeId },
|
||||
attributeRoutingConfig: {},
|
||||
});
|
||||
setCustomEventTypeSlug("");
|
||||
} else {
|
||||
setRoute(route.id, {
|
||||
action: { ...route.action, value: "custom", eventTypeId: 0 },
|
||||
attributeRoutingConfig: {},
|
||||
});
|
||||
setCustomEventTypeSlug("");
|
||||
}
|
||||
@@ -560,7 +547,7 @@ const Route = ({
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
className="border-default flex w-full flex-grow text-sm"
|
||||
containerClassName="w-full mt-2"
|
||||
containerClassName="flex-grow mt-2"
|
||||
addOnLeading={eventTypePrefix}
|
||||
required
|
||||
value={customEventTypeSlug}
|
||||
@@ -625,7 +612,7 @@ const deserializeRoute = ({
|
||||
route: Exclude<SerializableRoute, GlobalRoute>;
|
||||
formFieldsQueryBuilderConfig: FormFieldsQueryBuilderConfigWithRaqbFields;
|
||||
attributesQueryBuilderConfig: AttributesQueryBuilderConfigWithRaqbFields | null;
|
||||
}): Route => {
|
||||
}): EditFormRoute => {
|
||||
const attributesQueryBuilderState =
|
||||
route.attributesQueryValue && attributesQueryBuilderConfig
|
||||
? buildState({
|
||||
@@ -707,13 +694,14 @@ function useRoutes({
|
||||
return newRoutes;
|
||||
});
|
||||
|
||||
function getRoutesToSave(routes: Route[]) {
|
||||
function getRoutesToSave(routes: EditFormRoute[]) {
|
||||
return routes.map((route) => {
|
||||
if (isRouter(route)) {
|
||||
return route;
|
||||
}
|
||||
return {
|
||||
id: route.id,
|
||||
name: route.name,
|
||||
attributeRoutingConfig: route.attributeRoutingConfig,
|
||||
action: route.action,
|
||||
isFallback: route.isFallback,
|
||||
@@ -868,7 +856,7 @@ const Routes = ({
|
||||
});
|
||||
}
|
||||
|
||||
const setRoute = (id: string, route: Partial<Route>) => {
|
||||
const setRoute = (id: string, route: Partial<EditFormRoute>) => {
|
||||
const index = routes.findIndex((route) => route.id === id);
|
||||
const existingRoute = routes[index];
|
||||
const newRoutes = [...routes];
|
||||
@@ -990,7 +978,7 @@ const Routes = ({
|
||||
id: routerId,
|
||||
name: option.name,
|
||||
description: option.description,
|
||||
} as Route,
|
||||
} as EditFormRoute,
|
||||
]);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
|
||||
import { ROUTING_FORM_RESPONSE_ID_QUERY_STRING } from "../../lib/constants";
|
||||
import getFieldIdentifier from "../../lib/getFieldIdentifier";
|
||||
import type { FormResponse, LocalRoute } from "../../types/types";
|
||||
import type { getServerSideProps } from "./getServerSideProps";
|
||||
@@ -80,6 +81,25 @@ export function getUrlSearchParamsToForward({
|
||||
}
|
||||
}
|
||||
|
||||
const attributeRoutingConfigParams: Record<string, any> = {};
|
||||
|
||||
if (attributeRoutingConfig) {
|
||||
for (const key of Object.keys(attributeRoutingConfig)) {
|
||||
if (key === "skipContactOwner" && attributeRoutingConfig[key]) {
|
||||
attributeRoutingConfigParams["cal.skipContactOwner"] = "true";
|
||||
}
|
||||
|
||||
// TODO: How do we move this logic to their respective app packages
|
||||
if (key === "salesforce") {
|
||||
const salesforceData = attributeRoutingConfig[key];
|
||||
|
||||
if (salesforceData?.rrSkipToAccountLookupField && salesforceData.rrSKipToAccountLookupFieldName) {
|
||||
attributeRoutingConfigParams["cal.salesforce.rrSkipToAccountLookupField"] = "true";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const allQueryParams: Params = {
|
||||
...paramsFromCurrentUrl,
|
||||
// In case of conflict b/w paramsFromResponse and paramsFromCurrentUrl, paramsFromResponse should win as the booker probably improved upon the prefilled value.
|
||||
@@ -87,8 +107,8 @@ export function getUrlSearchParamsToForward({
|
||||
...(teamMembersMatchingAttributeLogic
|
||||
? { ["cal.routedTeamMemberIds"]: teamMembersMatchingAttributeLogic.join(",") }
|
||||
: null),
|
||||
["cal.routingFormResponseId"]: String(formResponseId),
|
||||
...(attributeRoutingConfig?.skipContactOwner ? { ["cal.skipContactOwner"]: "true" } : {}),
|
||||
[ROUTING_FORM_RESPONSE_ID_QUERY_STRING]: String(formResponseId),
|
||||
...attributeRoutingConfigParams,
|
||||
...(reroutingFormResponses
|
||||
? { ["cal.reroutingFormResponses"]: JSON.stringify(reroutingFormResponses) }
|
||||
: null),
|
||||
|
||||
@@ -28,7 +28,7 @@ const Identifiers = {
|
||||
};
|
||||
|
||||
async function enableContactOwnerOverride(page: Page) {
|
||||
await page.click("text=Contact owner will be the Round Robin host");
|
||||
await page.click("text=Contact owner will be the Round Robin host if available");
|
||||
}
|
||||
|
||||
async function selectFirstAttributeOption({ fromLocator }: { fromLocator: Locator }) {
|
||||
@@ -327,12 +327,13 @@ test.describe("Routing Forms", () => {
|
||||
// @see https://github.com/calcom/cal.com/pull/17229
|
||||
"Routed To",
|
||||
"Booked At",
|
||||
"Submitted At",
|
||||
]);
|
||||
/* Last two columns are "Routed To" and "Booked At" */
|
||||
expect(responses).toEqual([
|
||||
["event-routing", "Option-2", "Option-2", "Option-2", "Option-2", "", ""],
|
||||
["external-redirect", "Option-2", "Option-2", "Option-2", "Option-2", "", ""],
|
||||
["custom-page", "Option-2", "Option-2", "Option-2", "Option-2", "", ""],
|
||||
["custom-page", "Option-2", "Option-2", "Option-2", "Option-2", "", "", expect.any(String)],
|
||||
["external-redirect", "Option-2", "Option-2", "Option-2", "Option-2", "", "", expect.any(String)],
|
||||
["event-routing", "Option-2", "Option-2", "Option-2", "Option-2", "", "", expect.any(String)],
|
||||
]);
|
||||
|
||||
await page.goto(`apps/routing-forms/route-builder/${routingForm.id}`);
|
||||
@@ -574,7 +575,6 @@ test.describe("Routing Forms", () => {
|
||||
await selectNewRoute(page);
|
||||
// This would select Round Robin event that we created above
|
||||
await selectFirstEventRedirectOption(page);
|
||||
await enableContactOwnerOverride(page);
|
||||
await addAttributeRoutingRule(page);
|
||||
await saveCurrentForm(page);
|
||||
|
||||
@@ -592,7 +592,9 @@ test.describe("Routing Forms", () => {
|
||||
await page.click('[data-testid="test-routing"]');
|
||||
await page.waitForSelector("text=Attribute logic matched: No");
|
||||
await page.waitForSelector("text=Attribute logic fallback matched: Yes");
|
||||
await page.waitForSelector("text=All assigned members of the team event type. Consider adding some attribute rules to fallback.");
|
||||
await page.waitForSelector(
|
||||
"text=All assigned members of the team event type. Consider adding some attribute rules to fallback."
|
||||
);
|
||||
await page.click('[data-testid="dialog-rejection"]');
|
||||
})();
|
||||
});
|
||||
|
||||
@@ -61,6 +61,9 @@ const getRows = async ({ ctx: { prisma }, input }: ReportHandlerOptions) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
take,
|
||||
skip,
|
||||
});
|
||||
@@ -120,13 +123,14 @@ function presenter(args: {
|
||||
const formatDate = makeFormatDate(ctx.user.locale, ctx.user.timeZone);
|
||||
return {
|
||||
nextCursor,
|
||||
headers: [...headers, "Routed To", "Booked At"],
|
||||
headers: [...headers, "Routed To", "Booked At", "Submitted At"],
|
||||
responses: responses.map((r, i) => {
|
||||
const currentRow = rows[i];
|
||||
return [
|
||||
...r,
|
||||
currentRow.routedToBooking?.user?.email || "",
|
||||
currentRow.routedToBooking?.createdAt ? formatDate(currentRow.routedToBooking.createdAt) : "",
|
||||
formatDate(currentRow.createdAt),
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import type { App_RoutingForms_Form } from "@prisma/client";
|
||||
import type { ImmutableTree, Config } from "react-awesome-query-builder";
|
||||
import type z from "zod";
|
||||
|
||||
import type { AttributeType } from "@calcom/prisma/client";
|
||||
import type { RoutingFormSettings } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import type QueryBuilderInitialConfig from "../components/react-awesome-query-builder/config/config";
|
||||
import type {
|
||||
FormFieldsQueryBuilderConfigWithRaqbFields,
|
||||
AttributesQueryBuilderConfigWithRaqbFields,
|
||||
} from "../lib/getQueryBuilderConfig";
|
||||
import type { zodRouterRouteView, zodNonRouterRoute, zodFieldsView, zodRoutesView } from "../zod";
|
||||
|
||||
export type RoutingForm = SerializableForm<App_RoutingForms_Form>;
|
||||
@@ -74,3 +79,23 @@ export type Attribute = {
|
||||
export type AttributesQueryValue = NonNullable<LocalRoute["attributesQueryValue"]>;
|
||||
export type FormFieldsQueryValue = LocalRoute["queryValue"];
|
||||
export type SerializableField = NonNullable<SerializableForm<App_RoutingForms_Form>["fields"]>[number];
|
||||
|
||||
export type AttributeRoutingConfig = NonNullable<LocalRoute["attributeRoutingConfig"]>;
|
||||
|
||||
export type FormFieldsQueryBuilderState = {
|
||||
tree: ImmutableTree;
|
||||
config: FormFieldsQueryBuilderConfigWithRaqbFields;
|
||||
};
|
||||
|
||||
export type AttributesQueryBuilderState = {
|
||||
tree: ImmutableTree;
|
||||
config: AttributesQueryBuilderConfigWithRaqbFields;
|
||||
};
|
||||
|
||||
export type LocalRouteWithRaqbStates = LocalRoute & {
|
||||
formFieldsQueryBuilderState: FormFieldsQueryBuilderState;
|
||||
attributesQueryBuilderState: AttributesQueryBuilderState | null;
|
||||
fallbackAttributesQueryBuilderState: AttributesQueryBuilderState | null;
|
||||
};
|
||||
|
||||
export type EditFormRoute = LocalRouteWithRaqbStates | GlobalRoute;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { routingFormAppDataSchemas } from "./appDataSchemas";
|
||||
|
||||
export const zodNonRouterField = z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
@@ -128,13 +130,17 @@ export const queryValueSaveValidationSchema = queryValueSchema
|
||||
)
|
||||
.nullish();
|
||||
|
||||
export const attributeRoutingConfigSchema = z
|
||||
.object({
|
||||
skipContactOwner: z.boolean().optional(),
|
||||
salesforce: routingFormAppDataSchemas["salesforce"],
|
||||
})
|
||||
.nullish();
|
||||
|
||||
export const zodNonRouterRoute = z.object({
|
||||
id: z.string(),
|
||||
attributeRoutingConfig: z
|
||||
.object({
|
||||
skipContactOwner: z.boolean().optional(),
|
||||
})
|
||||
.nullish(),
|
||||
name: z.string().optional(),
|
||||
attributeRoutingConfig: attributeRoutingConfigSchema,
|
||||
|
||||
// TODO: It should be renamed to formFieldsQueryValue but it would take some effort
|
||||
/**
|
||||
@@ -168,6 +174,7 @@ export const zodNonRouterRouteView = zodNonRouterRoute;
|
||||
export const zodRouterRoute = z.object({
|
||||
// This is the id of the Form being used as router
|
||||
id: z.string(),
|
||||
name: z.string().optional(),
|
||||
isRouter: z.literal(true),
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,12 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { Switch, Alert, Select, Button, InputField, showToast } from "@calcom/ui";
|
||||
|
||||
import { SalesforceRecordEnum } from "../lib/recordEnum";
|
||||
import {
|
||||
SalesforceRecordEnum,
|
||||
WhenToWriteToRecord,
|
||||
SalesforceFieldType,
|
||||
DateFieldTypeData,
|
||||
} from "../lib/enums";
|
||||
import type { appDataSchema } from "../zod";
|
||||
|
||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||
@@ -32,6 +37,8 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
const onBookingChangeRecordOwnerName = getAppData("onBookingChangeRecordOwnerName") ?? [];
|
||||
const sendNoShowAttendeeData = getAppData("sendNoShowAttendeeData") ?? false;
|
||||
const sendNoShowAttendeeDataField = getAppData("sendNoShowAttendeeDataField") ?? "";
|
||||
const onBookingWriteToRecord = getAppData("onBookingWriteToRecord") ?? false;
|
||||
const onBookingWriteToRecordFields = getAppData("onBookingWriteToRecordFields") ?? {};
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -44,6 +51,34 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
recordOptions.find((option) => option.value === createEventOn) ?? recordOptions[0]
|
||||
);
|
||||
|
||||
const fieldTypeOptions = [
|
||||
{ label: t("text"), value: SalesforceFieldType.TEXT },
|
||||
{ label: t("date"), value: SalesforceFieldType.DATE },
|
||||
];
|
||||
|
||||
const [writeToPersonObjectFieldType, setWriteToPersonObjectFieldType] = useState(fieldTypeOptions[0]);
|
||||
|
||||
const whenToWriteToRecordOptions = [
|
||||
{ label: t("on_every_booking"), value: WhenToWriteToRecord.EVERY_BOOKING },
|
||||
{ label: t("only_if_field_is_empty"), value: WhenToWriteToRecord.FIELD_EMPTY },
|
||||
];
|
||||
|
||||
const [whenToWriteToPersonRecord, setWhenToWriteToPersonRecord] = useState(whenToWriteToRecordOptions[0]);
|
||||
|
||||
const dateFieldValueOptions = [
|
||||
{ label: t("booking_start_date"), value: DateFieldTypeData.BOOKING_START_DATE },
|
||||
{ label: t("booking_created_date"), value: DateFieldTypeData.BOOKING_CREATED_DATE },
|
||||
];
|
||||
|
||||
const [dateFieldValue, setDateValue] = useState(dateFieldValueOptions[0]);
|
||||
|
||||
const [newOnBookingWriteToPersonObjectField, setNewOnBookingWriteToPersonObjectField] = useState({
|
||||
field: "",
|
||||
fieldType: writeToPersonObjectFieldType.value,
|
||||
value: "",
|
||||
whenToWrite: WhenToWriteToRecord.FIELD_EMPTY,
|
||||
});
|
||||
|
||||
const checkOwnerOptions = [
|
||||
{ label: t("contact"), value: SalesforceRecordEnum.CONTACT },
|
||||
{ label: t("salesforce_lead"), value: SalesforceRecordEnum.LEAD },
|
||||
@@ -226,6 +261,185 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<Switch
|
||||
label={t("salesforce_on_booking_write_to_record", { record: createEventOn })}
|
||||
labelOnLeading
|
||||
checked={onBookingWriteToRecord}
|
||||
onCheckedChange={(checked) => {
|
||||
setAppData("onBookingWriteToRecord", checked);
|
||||
}}
|
||||
/>
|
||||
{onBookingWriteToRecord ? (
|
||||
<div className="ml-2 mt-2">
|
||||
<div className="grid grid-cols-5 gap-4">
|
||||
<div>{t("field_name")}</div>
|
||||
<div>{t("field_type")}</div>
|
||||
<div>{t("value")}</div>
|
||||
<div>{t("when_to_write")}</div>
|
||||
</div>
|
||||
<div>
|
||||
{...Object.keys(onBookingWriteToRecordFields).map((key) => (
|
||||
<div className="mt-2 grid grid-cols-5 gap-4" key={key}>
|
||||
<div>
|
||||
<InputField value={key} readOnly />
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
value={fieldTypeOptions.find(
|
||||
(option) => option.value === onBookingWriteToRecordFields[key].fieldType
|
||||
)}
|
||||
isDisabled={true}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{onBookingWriteToRecordFields[key].fieldType === SalesforceFieldType.DATE ? (
|
||||
<Select
|
||||
value={dateFieldValueOptions.find(
|
||||
(option) => option.value === onBookingWriteToRecordFields[key].value
|
||||
)}
|
||||
isDisabled={true}
|
||||
/>
|
||||
) : (
|
||||
<InputField value={onBookingWriteToRecordFields[key].value} readOnly />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
value={whenToWriteToRecordOptions.find(
|
||||
(option) => option.value === onBookingWriteToRecordFields[key].whenToWrite
|
||||
)}
|
||||
isDisabled={true}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
StartIcon="trash"
|
||||
variant="icon"
|
||||
color="destructive"
|
||||
onClick={() => {
|
||||
const newObject = onBookingWriteToRecordFields;
|
||||
delete onBookingWriteToRecordFields[key];
|
||||
setAppData("onBookingWriteToRecordFields", newObject);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-2 grid grid-cols-5 gap-4">
|
||||
<div>
|
||||
<InputField
|
||||
value={newOnBookingWriteToPersonObjectField.field}
|
||||
onChange={(e) =>
|
||||
setNewOnBookingWriteToPersonObjectField({
|
||||
...newOnBookingWriteToPersonObjectField,
|
||||
field: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
options={fieldTypeOptions}
|
||||
value={writeToPersonObjectFieldType}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setWriteToPersonObjectFieldType(e);
|
||||
setNewOnBookingWriteToPersonObjectField({
|
||||
...newOnBookingWriteToPersonObjectField,
|
||||
fieldType: e.value,
|
||||
...(e.value === SalesforceFieldType.DATE && { value: dateFieldValue.value }),
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{writeToPersonObjectFieldType.value === SalesforceFieldType.DATE ? (
|
||||
<Select
|
||||
options={dateFieldValueOptions}
|
||||
value={dateFieldValue}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setDateValue(e);
|
||||
setNewOnBookingWriteToPersonObjectField({
|
||||
...newOnBookingWriteToPersonObjectField,
|
||||
value: e.value,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<InputField
|
||||
value={newOnBookingWriteToPersonObjectField.value}
|
||||
onChange={(e) =>
|
||||
setNewOnBookingWriteToPersonObjectField({
|
||||
...newOnBookingWriteToPersonObjectField,
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
options={whenToWriteToRecordOptions}
|
||||
value={whenToWriteToPersonRecord}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setWhenToWriteToPersonRecord(e);
|
||||
setNewOnBookingWriteToPersonObjectField({
|
||||
...newOnBookingWriteToPersonObjectField,
|
||||
whenToWrite: e.value,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="mt-2"
|
||||
size="sm"
|
||||
disabled={
|
||||
!(
|
||||
newOnBookingWriteToPersonObjectField.field &&
|
||||
newOnBookingWriteToPersonObjectField.fieldType &&
|
||||
newOnBookingWriteToPersonObjectField.value &&
|
||||
newOnBookingWriteToPersonObjectField.whenToWrite
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
if (
|
||||
Object.keys(onBookingWriteToRecordFields).includes(
|
||||
newOnBookingWriteToEventObjectField.field.trim()
|
||||
)
|
||||
) {
|
||||
showToast("Field already exists", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
setAppData("onBookingWriteToRecordFields", {
|
||||
...onBookingWriteToRecordFields,
|
||||
[newOnBookingWriteToPersonObjectField.field.trim()]: {
|
||||
fieldType: newOnBookingWriteToPersonObjectField.fieldType,
|
||||
value: newOnBookingWriteToPersonObjectField.value,
|
||||
whenToWrite: newOnBookingWriteToPersonObjectField.whenToWrite,
|
||||
},
|
||||
});
|
||||
setNewOnBookingWriteToPersonObjectField({
|
||||
field: "",
|
||||
fieldType: writeToPersonObjectFieldType.value,
|
||||
value: "",
|
||||
whenToWrite: WhenToWriteToRecord.FIELD_EMPTY,
|
||||
});
|
||||
}}>
|
||||
{t("add_new_field")}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<Switch
|
||||
label="Change record owner on booking"
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Select, Input, Label } from "@calcom/ui";
|
||||
|
||||
import type { LocalRouteWithRaqbStates, AttributeRoutingConfig } from "../../routing-forms/types/types";
|
||||
import { appDataSchema } from "../zod";
|
||||
|
||||
enum SalesforceRoutingConfig {
|
||||
ROUTE_OWNER = "route_owner",
|
||||
SKIP_OWNER = "skip_owner",
|
||||
ACCOUNT_FIELD_LOOKUP = "account_field_lookup",
|
||||
}
|
||||
|
||||
const RoutingFormOptions = ({
|
||||
appData,
|
||||
route,
|
||||
setAttributeRoutingConfig,
|
||||
}: {
|
||||
appData: any;
|
||||
route: LocalRouteWithRaqbStates;
|
||||
setAttributeRoutingConfig: (id: string, attributeRoutingConfig: Partial<AttributeRoutingConfig>) => void;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const salesforceRoutingOptions = [
|
||||
{
|
||||
label: t("salesforce_route_to_owner"),
|
||||
value: SalesforceRoutingConfig.ROUTE_OWNER,
|
||||
},
|
||||
{
|
||||
label: t("salesforce_do_not_route_to_owner"),
|
||||
value: SalesforceRoutingConfig.SKIP_OWNER,
|
||||
},
|
||||
{
|
||||
label: t("salesforce_route_to_custom_lookup_field"),
|
||||
value: SalesforceRoutingConfig.ACCOUNT_FIELD_LOOKUP,
|
||||
},
|
||||
];
|
||||
|
||||
const getInitialOption = () => {
|
||||
if (route.attributeRoutingConfig?.salesforce?.rrSkipToAccountLookupField) {
|
||||
return salesforceRoutingOptions.find(
|
||||
(option) => option.value === SalesforceRoutingConfig.ACCOUNT_FIELD_LOOKUP
|
||||
);
|
||||
}
|
||||
|
||||
if (route.attributeRoutingConfig?.skipContactOwner) {
|
||||
return salesforceRoutingOptions.find((option) => option.value === SalesforceRoutingConfig.SKIP_OWNER);
|
||||
} else {
|
||||
return salesforceRoutingOptions.find((option) => option.value === SalesforceRoutingConfig.ROUTE_OWNER);
|
||||
}
|
||||
};
|
||||
|
||||
const [selectedRoutingOption, setSelectedRoutingOption] = useState(getInitialOption());
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedRoutingOption(getInitialOption());
|
||||
}, [route]);
|
||||
|
||||
const [rrSkipToAccountLookupFieldName, setRRSkipToAccountLookupFieldName] = useState(
|
||||
route.attributeRoutingConfig?.salesforce?.rrSKipToAccountLookupFieldName ?? ""
|
||||
);
|
||||
|
||||
const parsedAppData = appDataSchema.safeParse(appData);
|
||||
|
||||
if (!parsedAppData.success) return null;
|
||||
|
||||
appData = parsedAppData.data;
|
||||
|
||||
const isAppDisabled = !appData?.enabled;
|
||||
|
||||
const appDisabledOption = {
|
||||
label: "Salesforce is not enabled on event type",
|
||||
value: "disabled",
|
||||
};
|
||||
|
||||
const onRRSkipToAccountLookupFieldNameChange = (value: string) => {
|
||||
setAttributeRoutingConfig(route.id, {
|
||||
...route.attributeRoutingConfig,
|
||||
salesforce: { ...route.attributeRoutingConfig?.salesforce, rrSKipToAccountLookupFieldName: value },
|
||||
});
|
||||
};
|
||||
|
||||
const onOptionChange = (value: SalesforceRoutingConfig) => {
|
||||
switch (value) {
|
||||
case SalesforceRoutingConfig.ROUTE_OWNER:
|
||||
setAttributeRoutingConfig(route.id, {
|
||||
skipContactOwner: false,
|
||||
salesforce: {
|
||||
rrSkipToAccountLookupField: false,
|
||||
},
|
||||
});
|
||||
break;
|
||||
case SalesforceRoutingConfig.SKIP_OWNER:
|
||||
setAttributeRoutingConfig(route.id, {
|
||||
skipContactOwner: true,
|
||||
salesforce: {
|
||||
rrSkipToAccountLookupField: false,
|
||||
},
|
||||
});
|
||||
break;
|
||||
case SalesforceRoutingConfig.ACCOUNT_FIELD_LOOKUP:
|
||||
setAttributeRoutingConfig(route.id, {
|
||||
skipContactOwner: false,
|
||||
salesforce: {
|
||||
rrSkipToAccountLookupField: true,
|
||||
rrSKipToAccountLookupFieldName: rrSkipToAccountLookupFieldName,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex">
|
||||
<span className="text-emphasis flex items-center text-sm">
|
||||
<img src="/app-store/salesforce/icon.png" className="mr-2 h-6 w-6" /> {t("salesforce_option")}
|
||||
</span>
|
||||
{isAppDisabled ? (
|
||||
<Select value={appDisabledOption} isDisabled className="ml-2 w-full" />
|
||||
) : (
|
||||
<Select
|
||||
options={salesforceRoutingOptions}
|
||||
value={selectedRoutingOption}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
onOptionChange(e.value);
|
||||
setSelectedRoutingOption(e);
|
||||
}
|
||||
}}
|
||||
className="ml-2 w-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{selectedRoutingOption?.value === SalesforceRoutingConfig.ACCOUNT_FIELD_LOOKUP ? (
|
||||
<div>
|
||||
<Label className="mt-2">{t("lookup_field_name")}</Label>
|
||||
<Input
|
||||
value={rrSkipToAccountLookupFieldName}
|
||||
onChange={(e) => {
|
||||
setRRSkipToAccountLookupFieldName(e.target.value);
|
||||
onRRSkipToAccountLookupFieldNameChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoutingFormOptions;
|
||||
@@ -16,7 +16,7 @@ import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import type { ParseRefreshTokenResponse } from "../../_utils/oauth/parseRefreshTokenResponse";
|
||||
import parseRefreshTokenResponse from "../../_utils/oauth/parseRefreshTokenResponse";
|
||||
import { default as appMeta } from "../config.json";
|
||||
import { SalesforceRecordEnum } from "./recordEnum";
|
||||
import { SalesforceRecordEnum, SalesforceFieldType, WhenToWriteToRecord, DateFieldTypeData } from "./enums";
|
||||
|
||||
type ExtendedTokenResponse = TokenResponse & {
|
||||
instance_url: string;
|
||||
@@ -59,6 +59,7 @@ export default class SalesforceCRMService implements CRM {
|
||||
private calWarnings: string[] = [];
|
||||
private appOptions: any;
|
||||
private doNotCreateEvent = false;
|
||||
private fallbackToContact = false;
|
||||
|
||||
constructor(credential: CredentialPayload, appOptions: any) {
|
||||
this.integrationName = "salesforce_other_calendar";
|
||||
@@ -84,7 +85,6 @@ export default class SalesforceCRMService implements CRM {
|
||||
throw new HttpError({ statusCode: 400, message: "Salesforce consumer secret missing." });
|
||||
|
||||
const credentialKey = credential.key as unknown as ExtendedTokenResponse;
|
||||
|
||||
try {
|
||||
/* XXX: This code results in 'Bad Request', which indicates something is wrong with our salesforce integration.
|
||||
Needs further investigation ASAP */
|
||||
@@ -136,10 +136,10 @@ export default class SalesforceCRMService implements CRM {
|
||||
}
|
||||
};
|
||||
|
||||
private getSalesforceUserFromOwnerId = async (ownerId: string) => {
|
||||
private getSalesforceUserFromUserId = async (userId: string) => {
|
||||
const conn = await this.conn;
|
||||
|
||||
return await conn.query(`SELECT Id, Email, Name FROM User WHERE Id = '${ownerId}' AND IsActive = true`);
|
||||
return await conn.query(`SELECT Id, Email, Name FROM User WHERE Id = '${userId}' AND IsActive = true`);
|
||||
};
|
||||
|
||||
private getSalesforceEventBody = (event: CalendarEvent): string => {
|
||||
@@ -185,7 +185,7 @@ export default class SalesforceCRMService implements CRM {
|
||||
} = {};
|
||||
|
||||
for (const field of customFieldInputs) {
|
||||
confirmedCustomFieldInputs[field] = appOptions.onBookingWriteToEventObjectMap[field];
|
||||
confirmedCustomFieldInputs[field.name] = appOptions.onBookingWriteToEventObjectMap[field.name];
|
||||
}
|
||||
|
||||
const ownerId = await this.getSalesforceUserIdFromEmail(event.organizer.email);
|
||||
@@ -214,6 +214,9 @@ export default class SalesforceCRMService implements CRM {
|
||||
if (appOptions.onBookingChangeRecordOwner && appOptions.onBookingChangeRecordOwnerName && ownerId) {
|
||||
await this.checkRecordOwnerNameFromRecordId(contacts[0].id, ownerId);
|
||||
}
|
||||
if (appOptions.onBookingWriteToRecord && appOptions.onBookingWriteToRecordFields) {
|
||||
await this.writeToPersonRecord(contacts[0].id, event.startTime, event.organizer.email, event?.uid);
|
||||
}
|
||||
return createdEvent;
|
||||
};
|
||||
|
||||
@@ -358,8 +361,10 @@ export default class SalesforceCRMService implements CRM {
|
||||
)}')`
|
||||
);
|
||||
|
||||
if (contactSearch && contactSearch.records.length > 0)
|
||||
if (contactSearch && contactSearch.records.length > 0) {
|
||||
records = contactSearch.records as ContactRecord[];
|
||||
this.setFallbackToContact(true);
|
||||
}
|
||||
} else if (!results || !results.records.length) {
|
||||
return [];
|
||||
}
|
||||
@@ -374,7 +379,7 @@ export default class SalesforceCRMService implements CRM {
|
||||
|
||||
const ownersQuery = (await Promise.all(
|
||||
Array.from(ownerIds).map(async (ownerId) => {
|
||||
return this.getSalesforceUserFromOwnerId(ownerId);
|
||||
return this.getSalesforceUserFromUserId(ownerId);
|
||||
})
|
||||
)) as { records: ContactRecord[] }[];
|
||||
const contactsWithOwners = records.map((record) => {
|
||||
@@ -527,7 +532,7 @@ export default class SalesforceCRMService implements CRM {
|
||||
const fields = salesforceEntity.fields;
|
||||
const noShowField = fields.find((field) => field.name === sendNoShowAttendeeDataField);
|
||||
|
||||
if (!noShowField || (!noShowField.type as unknown as string) !== "boolean") {
|
||||
if (!noShowField || (noShowField.type as unknown as string) !== "boolean") {
|
||||
this.log.warn(
|
||||
`No show field on Salesforce doesn't exist or is not of type boolean for bookingUid ${bookingUid}`
|
||||
);
|
||||
@@ -651,7 +656,7 @@ export default class SalesforceCRMService implements CRM {
|
||||
const conn = await this.conn;
|
||||
|
||||
const fieldSet = new Set(fieldsToTest);
|
||||
const foundFields: string[] = [];
|
||||
const foundFields: jsforce.Field[] = [];
|
||||
|
||||
try {
|
||||
const salesforceEntity = await conn.describe(sobject);
|
||||
@@ -661,7 +666,7 @@ export default class SalesforceCRMService implements CRM {
|
||||
if (foundFields.length === fieldSet.size) break;
|
||||
|
||||
if (fieldSet.has(field.name)) {
|
||||
foundFields.push(field.name);
|
||||
foundFields.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,7 +690,7 @@ export default class SalesforceCRMService implements CRM {
|
||||
|
||||
const ownerId = recordQuery.records[0].OwnerId;
|
||||
|
||||
const ownerQuery = await this.getSalesforceUserFromOwnerId(ownerId);
|
||||
const ownerQuery = await this.getSalesforceUserFromUserId(ownerId);
|
||||
|
||||
if (!ownerQuery || !ownerQuery.records.length) return;
|
||||
|
||||
@@ -711,4 +716,192 @@ export default class SalesforceCRMService implements CRM {
|
||||
|
||||
return this.getDominantAccountId(response.records as { AccountId: string }[]);
|
||||
}
|
||||
|
||||
private setFallbackToContact(boolean: boolean) {
|
||||
this.fallbackToContact = boolean;
|
||||
}
|
||||
|
||||
private getFallbackToContact() {
|
||||
return this.fallbackToContact;
|
||||
}
|
||||
|
||||
private async writeToPersonRecord(
|
||||
contactId: string,
|
||||
startTime: string,
|
||||
organizerEmail: string,
|
||||
bookingUid?: string | null
|
||||
) {
|
||||
const conn = await this.conn;
|
||||
const { createEventOn, onBookingWriteToRecordFields } = this.getAppOptions();
|
||||
|
||||
// Determine record type (Contact or Lead)
|
||||
const personRecordType = this.determinePersonRecordType(createEventOn);
|
||||
|
||||
// Search the fields and ensure 1. they exist 2. they're the right type
|
||||
const fieldsToWriteOn = Object.keys(onBookingWriteToRecordFields);
|
||||
const existingFields = await this.ensureFieldsExistOnObject(fieldsToWriteOn, personRecordType);
|
||||
|
||||
const personRecord = await this.fetchPersonRecord(contactId, existingFields, personRecordType);
|
||||
if (!personRecord) return;
|
||||
|
||||
const writeOnRecordBody = await this.buildRecordUpdatePayload({
|
||||
existingFields,
|
||||
personRecord,
|
||||
onBookingWriteToRecordFields,
|
||||
startTime,
|
||||
bookingUid,
|
||||
organizerEmail,
|
||||
});
|
||||
|
||||
// Update the person record
|
||||
await conn.sobject(personRecordType).update({
|
||||
Id: contactId,
|
||||
...writeOnRecordBody,
|
||||
});
|
||||
}
|
||||
|
||||
private async buildRecordUpdatePayload({
|
||||
existingFields,
|
||||
personRecord,
|
||||
onBookingWriteToRecordFields,
|
||||
startTime,
|
||||
bookingUid,
|
||||
organizerEmail,
|
||||
}: {
|
||||
existingFields: jsforce.Field[];
|
||||
personRecord: Record<string, any>;
|
||||
onBookingWriteToRecordFields: Record<string, any>;
|
||||
startTime: string;
|
||||
bookingUid?: string | null;
|
||||
organizerEmail: string;
|
||||
}): Promise<Record<string, any>> {
|
||||
const writeOnRecordBody: Record<string, any> = {};
|
||||
|
||||
for (const field of existingFields) {
|
||||
const fieldConfig = onBookingWriteToRecordFields[field.name];
|
||||
|
||||
// Skip if field should only be written when empty and already has a value
|
||||
if (fieldConfig.whenToWrite === WhenToWriteToRecord.FIELD_EMPTY && personRecord[field.name]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle different field types
|
||||
if (fieldConfig.fieldType === field.type) {
|
||||
if (field.type === SalesforceFieldType.TEXT) {
|
||||
writeOnRecordBody[field.name] = fieldConfig.value.substring(0, field.length);
|
||||
} else if (field.type === SalesforceFieldType.DATE) {
|
||||
const dateValue = await this.getDateFieldValue(
|
||||
fieldConfig.value,
|
||||
startTime,
|
||||
bookingUid,
|
||||
organizerEmail
|
||||
);
|
||||
if (dateValue) {
|
||||
writeOnRecordBody[field.name] = dateValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return writeOnRecordBody;
|
||||
}
|
||||
|
||||
private async getDateFieldValue(
|
||||
fieldValue: string,
|
||||
startTime: string,
|
||||
bookingUid?: string | null,
|
||||
organizerEmail?: string
|
||||
): Promise<string | null> {
|
||||
if (fieldValue === DateFieldTypeData.BOOKING_START_DATE) {
|
||||
return new Date(startTime).toISOString();
|
||||
}
|
||||
if (fieldValue === DateFieldTypeData.BOOKING_CREATED_DATE && bookingUid) {
|
||||
const booking = await prisma.booking.findFirst({
|
||||
where: { uid: bookingUid },
|
||||
select: { createdAt: true },
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
this.log.warn(`No booking found for ${bookingUid}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Date(booking.createdAt).toISOString();
|
||||
}
|
||||
|
||||
if (!bookingUid) {
|
||||
this.log.warn(`No uid for booking with organizer ${organizerEmail}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private determinePersonRecordType(createEventOn: string): SalesforceRecordEnum {
|
||||
return createEventOn === SalesforceRecordEnum.LEAD &&
|
||||
this.appOptions.createEventOnLeadCheckForContact &&
|
||||
this.getFallbackToContact()
|
||||
? SalesforceRecordEnum.CONTACT
|
||||
: this.appOptions.createEventOn;
|
||||
}
|
||||
|
||||
private async fetchPersonRecord(
|
||||
contactId: string,
|
||||
existingFields: jsforce.Field[],
|
||||
personRecordType: SalesforceRecordEnum
|
||||
): Promise<Record<string, any> | null> {
|
||||
const conn = await this.conn;
|
||||
const existingFieldNames = existingFields.map((field) => field.name);
|
||||
|
||||
const query = await conn.query(
|
||||
`SELECT ${existingFieldNames.join(", ")} FROM ${personRecordType} WHERE Id = '${contactId}'`
|
||||
);
|
||||
|
||||
if (!query.records.length) {
|
||||
this.log.warn(`Could not find person record with id ${contactId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return query.records[0] as Record<string, any>;
|
||||
}
|
||||
|
||||
async findUserEmailFromLookupField(
|
||||
attendeeEmail: string,
|
||||
fieldName: string,
|
||||
salesforceObject: SalesforceRecordEnum
|
||||
) {
|
||||
const conn = await this.conn;
|
||||
|
||||
// Ensure the field exists on the record
|
||||
const existingFields = await this.ensureFieldsExistOnObject([fieldName], salesforceObject);
|
||||
|
||||
if (!existingFields.length) return;
|
||||
|
||||
const lookupField = existingFields[0];
|
||||
|
||||
if (salesforceObject === SalesforceRecordEnum.ACCOUNT) {
|
||||
const accountId = await this.getAccountIdBasedOnEmailDomainOfContacts(attendeeEmail);
|
||||
|
||||
if (!accountId) return;
|
||||
|
||||
const accountQuery = (await conn.query(
|
||||
`SELECT ${lookupField.name} FROM ${SalesforceRecordEnum.ACCOUNT} WHERE Id = '${accountId}'`
|
||||
)) as {
|
||||
records: { [key: string]: any };
|
||||
};
|
||||
|
||||
if (!accountQuery.records.length) return;
|
||||
|
||||
const lookupFieldUserId = accountQuery.records[0][lookupField.name];
|
||||
|
||||
if (!lookupFieldUserId) return;
|
||||
|
||||
const userQuery = await this.getSalesforceUserFromUserId(lookupFieldUserId);
|
||||
|
||||
if (!userQuery.records.length) return;
|
||||
|
||||
const user = userQuery.records[0] as { Email: string };
|
||||
|
||||
return user.Email;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export enum SalesforceRecordEnum {
|
||||
CONTACT = "Contact",
|
||||
LEAD = "Lead",
|
||||
ACCOUNT = "Account",
|
||||
}
|
||||
|
||||
export enum WhenToWriteToRecord {
|
||||
EVERY_BOOKING = "every_booking",
|
||||
FIELD_EMPTY = "field_empty",
|
||||
}
|
||||
|
||||
export enum SalesforceFieldType {
|
||||
DATE = "date",
|
||||
TEXT = "string",
|
||||
}
|
||||
|
||||
export enum DateFieldTypeData {
|
||||
BOOKING_START_DATE = "booking_start_date",
|
||||
BOOKING_CREATED_DATE = "booking_created_date",
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export enum SalesforceRecordEnum {
|
||||
CONTACT = "Contact",
|
||||
LEAD = "Lead",
|
||||
ACCOUNT = "Account",
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { CredentialRepository } from "../../../lib/server/repository/credential";
|
||||
import { EventTypeService } from "../../../lib/server/service/eventType";
|
||||
import type { AttributeRoutingConfig } from "../../routing-forms/types/types";
|
||||
import SalesforceCRMService from "./CrmService";
|
||||
import { SalesforceRecordEnum } from "./enums";
|
||||
|
||||
const routingFormBookingFormHandler = async (
|
||||
attendeeEmail: string,
|
||||
attributeRoutingConfig: AttributeRoutingConfig,
|
||||
eventTypeId: number
|
||||
) => {
|
||||
const salesforceSettings = attributeRoutingConfig?.salesforce;
|
||||
|
||||
if (
|
||||
!salesforceSettings ||
|
||||
!salesforceSettings.rrSkipToAccountLookupField ||
|
||||
!salesforceSettings.rrSKipToAccountLookupFieldName
|
||||
)
|
||||
return { email: null };
|
||||
|
||||
const appData = await EventTypeService.getEventTypeAppDataFromId(eventTypeId, "salesforce");
|
||||
|
||||
const credentialId = appData.credentialId;
|
||||
|
||||
const credential = await CredentialRepository.findFirstByIdWithKeyAndUser({ id: credentialId });
|
||||
|
||||
if (!credential) return { email: null };
|
||||
|
||||
const crm = new SalesforceCRMService(credential, {});
|
||||
|
||||
const userLookupEmail = await crm.findUserEmailFromLookupField(
|
||||
attendeeEmail,
|
||||
salesforceSettings.rrSKipToAccountLookupFieldName,
|
||||
SalesforceRecordEnum.ACCOUNT
|
||||
);
|
||||
|
||||
return { email: userLookupEmail ?? null };
|
||||
};
|
||||
|
||||
export default routingFormBookingFormHandler;
|
||||
@@ -1,7 +1,20 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { eventTypeAppCardZod } from "../eventTypeAppCardZod";
|
||||
import { SalesforceRecordEnum } from "./lib/recordEnum";
|
||||
import { SalesforceRecordEnum, WhenToWriteToRecord, SalesforceFieldType } from "./lib/enums";
|
||||
|
||||
const writeToRecordEntry = z.object({
|
||||
value: z.string(),
|
||||
fieldType: z.nativeEnum(SalesforceFieldType),
|
||||
whenToWrite: z.nativeEnum(WhenToWriteToRecord),
|
||||
});
|
||||
|
||||
export const routingFormOptions = z
|
||||
.object({
|
||||
rrSkipToAccountLookupField: z.boolean().optional(),
|
||||
rrSKipToAccountLookupFieldName: z.string().optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
export const appDataSchema = eventTypeAppCardZod.extend({
|
||||
roundRobinLeadSkip: z.boolean().optional(),
|
||||
@@ -20,6 +33,8 @@ export const appDataSchema = eventTypeAppCardZod.extend({
|
||||
onBookingChangeRecordOwnerName: z.string().optional(),
|
||||
sendNoShowAttendeeData: z.boolean().optional(),
|
||||
sendNoShowAttendeeDataField: z.string().optional(),
|
||||
onBookingWriteToRecord: z.boolean().optional(),
|
||||
onBookingWriteToRecordFields: z.record(z.string(), writeToRecordEntry).optional(),
|
||||
});
|
||||
|
||||
export const appKeysSchema = z.object({
|
||||
|
||||
@@ -35,12 +35,24 @@ export class ModalBox extends HTMLElement {
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
|
||||
close() {
|
||||
private isLoaderRunning() {
|
||||
const state = this.getAttribute("state");
|
||||
return !state || state === "loading" || state === "reopening";
|
||||
}
|
||||
|
||||
private explicitClose() {
|
||||
this.show(false);
|
||||
const event = new Event("close");
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.isLoaderRunning()) {
|
||||
return;
|
||||
}
|
||||
this.explicitClose();
|
||||
}
|
||||
|
||||
hideIframe() {
|
||||
const iframe = this.querySelector("iframe");
|
||||
if (iframe) {
|
||||
@@ -92,14 +104,14 @@ export class ModalBox extends HTMLElement {
|
||||
this.showIframe();
|
||||
this.getLoaderElement().style.display = "none";
|
||||
} else if (newValue == "closed") {
|
||||
this.close();
|
||||
this.explicitClose();
|
||||
} else if (newValue === "failed") {
|
||||
this.getLoaderElement().style.display = "none";
|
||||
this.getErrorElement().style.display = "inline-block";
|
||||
const errorString = getErrorString(this.dataset.errorCode);
|
||||
this.getErrorElement().innerText = errorString;
|
||||
} else if (newValue === "prerendering") {
|
||||
this.close();
|
||||
this.explicitClose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,13 +129,16 @@ export class ModalBox extends HTMLElement {
|
||||
once: true,
|
||||
}
|
||||
);
|
||||
|
||||
// The backdrop is inside the host element, and a click on host element is only possible if the user clicks outside the iframe.
|
||||
// So, it is backdrop click handler
|
||||
this.shadowRoot.host.addEventListener("click", () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
if (closeEl) {
|
||||
closeEl.onclick = () => {
|
||||
this.close();
|
||||
this.explicitClose();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,6 +554,7 @@ function main() {
|
||||
document.getElementsByTagName("main")[0] ||
|
||||
document.documentElement;
|
||||
if (e.target.contains(mainElement)) {
|
||||
// Because the iframe can take the entire width but the actual content could still be smaller and everything beyond that would be considered backdrop
|
||||
sdkActionManager?.fire("__closeIframe", {});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -564,7 +564,7 @@ export const getOptions = ({
|
||||
if (account.type === "credentials") {
|
||||
// return token if credentials,saml-idp
|
||||
if (account.provider === "saml-idp") {
|
||||
return token;
|
||||
return { ...token, upId: user.profile?.upId ?? token.upId ?? null } as JWT;
|
||||
}
|
||||
// any other credentials, add user info
|
||||
return {
|
||||
|
||||
@@ -12,7 +12,7 @@ export const createOrUpdateMemberships = async ({
|
||||
}: {
|
||||
user: Pick<User, "id">;
|
||||
team: Pick<Team, "id" | "parentId" | "isOrganization"> & {
|
||||
organizationSettings: OrganizationSettings | null;
|
||||
organizationSettings?: Pick<OrganizationSettings, "orgAutoAcceptEmail"> | null;
|
||||
};
|
||||
}) => {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
|
||||
@@ -236,7 +236,7 @@ function preprocess<T extends z.ZodType>({
|
||||
}
|
||||
|
||||
if (bookingField.type === "phone") {
|
||||
if (!(await phoneSchema.safeParseAsync(value)).success) {
|
||||
if (!bookingField.hidden && !(await phoneSchema.safeParseAsync(value)).success) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("invalid_number") });
|
||||
}
|
||||
continue;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user