Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5ef3e1d72 | ||
|
|
0c19606e19 | ||
|
|
b8219c273d | ||
|
|
f75415e414 | ||
|
|
4bf5e3cdf5 | ||
|
|
4181b5988a | ||
|
|
c41ba9d984 | ||
|
|
2a578ea9d5 | ||
|
|
c96827e1c0 | ||
|
|
3f5363038c | ||
|
|
e2de28b933 | ||
|
|
5d88858995 | ||
|
|
82a500aeaa | ||
|
|
2408337c78 | ||
|
|
119edb5389 | ||
|
|
ae4474cd42 | ||
|
|
3290ae526a | ||
|
|
16a7cf6d04 | ||
|
|
ab2ab31eef |
+1
-1
@@ -119,7 +119,7 @@ NEXT_PUBLIC_FRESHCHAT_HOST=
|
||||
|
||||
# Google OAuth credentials
|
||||
# To enable Login with Google you need to:
|
||||
# 1. Set `GOOGLE_API_CREDENTIALS` above
|
||||
# 1. Set `GOOGLE_API_CREDENTIALS` below
|
||||
# 2. Set `GOOGLE_LOGIN_ENABLED` to `true`
|
||||
# When self-hosting please ensure you configure the Google integration as an Internal app so no one else can login to your instance
|
||||
# @see https://support.google.com/cloud/answer/6158849#public-and-internal&zippy=%2Cpublic-and-internal-applications
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
undecided:
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
@@ -10,7 +10,8 @@ import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
import { google } from "googleapis";
|
||||
import { OAuth2Client } from "googleapis-common";
|
||||
import { calendar_v3 } from "@googleapis/calendar";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SUCCESS_STATUS, GOOGLE_CALENDAR_TYPE } from "@calcom/platform-constants";
|
||||
@@ -78,7 +79,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {
|
||||
|
||||
const { client_id, client_secret } = this.gcalResponseSchema.parse(app.keys);
|
||||
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
|
||||
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirectUri);
|
||||
return oAuth2Client;
|
||||
}
|
||||
|
||||
@@ -134,8 +135,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {
|
||||
|
||||
oAuth2Client.setCredentials(key);
|
||||
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
const calendar = new calendar_v3.Calendar({
|
||||
auth: oAuth2Client,
|
||||
});
|
||||
|
||||
|
||||
@@ -78,4 +78,11 @@ export class EventTypesRepository_2024_04_15 {
|
||||
async deleteEventType(eventTypeId: number) {
|
||||
return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } });
|
||||
}
|
||||
|
||||
async getEventTypeWithDuration(eventTypeId: number) {
|
||||
return this.dbRead.prisma.eventType.findUnique({
|
||||
where: { id: eventTypeId },
|
||||
select: { length: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+120
-6
@@ -10,10 +10,12 @@ import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User, Schedule } from "@prisma/client";
|
||||
import { PlatformOAuthClient, Team, User, Schedule, EventType } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
@@ -75,18 +77,22 @@ describe("Event types Endpoints", () => {
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let schedulesRepostoryFixture: SchedulesRepositoryFixture;
|
||||
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
const userEmail = "event-types-test-e2e@api.com";
|
||||
const falseTestEmail = "false-event-types@api.com";
|
||||
const name = "bob-the-builder";
|
||||
const username = name;
|
||||
let eventType: EventTypeOutput_2024_06_14;
|
||||
let user: User;
|
||||
let orgUser: User;
|
||||
let falseTestUser: User;
|
||||
let firstSchedule: Schedule;
|
||||
let secondSchedule: Schedule;
|
||||
let falseTestSchedule: Schedule;
|
||||
|
||||
let orgUserEventType1: EventType;
|
||||
let orgUserEventType2: EventType;
|
||||
let orgUserEventType3: EventType;
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
@@ -109,8 +115,12 @@ describe("Event types Endpoints", () => {
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
schedulesRepostoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: "organization",
|
||||
slug: "event-type-2024-06-14-org-slug",
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
@@ -118,6 +128,49 @@ describe("Event types Endpoints", () => {
|
||||
username,
|
||||
});
|
||||
|
||||
orgUser = await userRepositoryFixture.create({
|
||||
email: "event-types-2024-06-14-org-user@example.com",
|
||||
name: "event-types-2024-06-14-org-user",
|
||||
username: "event-types-2024-06-14-org-user",
|
||||
});
|
||||
|
||||
profileRepositoryFixture.create({
|
||||
uid: `usr-${orgUser.id}`,
|
||||
username: orgUser.username as string,
|
||||
organization: {
|
||||
connect: {
|
||||
id: organization.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: orgUser.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
orgUserEventType1 = await eventTypesRepositoryFixture.create(
|
||||
{ title: "orgUserEventType1", slug: "org-event-type-1", length: 60, locations: [] },
|
||||
orgUser.id
|
||||
);
|
||||
|
||||
orgUserEventType2 = await eventTypesRepositoryFixture.create(
|
||||
{ title: "orgUserEventType2", slug: "org-event-type-2", length: 60, locations: [] },
|
||||
orgUser.id
|
||||
);
|
||||
|
||||
orgUserEventType3 = await eventTypesRepositoryFixture.create(
|
||||
{ title: "orgUserEventType3", slug: "org-event-type-3", length: 60, locations: [] },
|
||||
orgUser.id
|
||||
);
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: orgUser.id } },
|
||||
team: { connect: { id: organization.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
falseTestUser = await userRepositoryFixture.create({
|
||||
email: falseTestEmail,
|
||||
name: "false-test",
|
||||
@@ -342,7 +395,7 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it(`/GET/even-types by username`, async () => {
|
||||
it(`/GET/event-types by username`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${username}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
@@ -386,6 +439,61 @@ describe("Event types Endpoints", () => {
|
||||
expect(fetchedEventType.color).toEqual(eventType.color);
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username and orgSlug`, async () => {
|
||||
console.log(organization);
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(3);
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType1.id)?.id).toBeDefined();
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType2.id)?.id).toBeDefined();
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType3.id)?.id).toBeDefined();
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username and orgSlug and eventSlug`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}&eventSlug=${orgUserEventType1.slug}`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(1);
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType1.id)?.id).toBeDefined();
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username and orgId`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${orgUser.username}&orgId=${organization.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(3);
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType1.id)?.id).toBeDefined();
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType2.id)?.id).toBeDefined();
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType3.id)?.id).toBeDefined();
|
||||
});
|
||||
|
||||
it("should return an error when creating an event type with seats enabled and multiple locations", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Coding class 2",
|
||||
@@ -919,6 +1027,12 @@ describe("Event types Endpoints", () => {
|
||||
} catch (e) {
|
||||
// User might have been deleted by the test
|
||||
}
|
||||
|
||||
try {
|
||||
await userRepositoryFixture.delete(orgUser.id);
|
||||
} catch (e) {
|
||||
// User might have been deleted by the test
|
||||
}
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
+15
-11
@@ -74,8 +74,13 @@ export class EventTypesService_2024_06_14 {
|
||||
await this.checkUserOwnsSchedule(userId, body.scheduleId);
|
||||
}
|
||||
|
||||
async getEventTypeByUsernameAndSlug(username: string, eventTypeSlug: string) {
|
||||
const user = await this.usersRepository.findByUsername(username);
|
||||
async getEventTypeByUsernameAndSlug(
|
||||
username: string,
|
||||
eventTypeSlug: string,
|
||||
orgSlug?: string,
|
||||
orgId?: number
|
||||
) {
|
||||
const user = await this.usersRepository.findByUsername(username, orgSlug, orgId);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
@@ -92,8 +97,8 @@ export class EventTypesService_2024_06_14 {
|
||||
};
|
||||
}
|
||||
|
||||
async getEventTypesByUsername(username: string) {
|
||||
const user = await this.usersRepository.findByUsername(username);
|
||||
async getEventTypesByUsername(username: string, orgSlug?: string, orgId?: number) {
|
||||
const user = await this.usersRepository.findByUsername(username, orgSlug, orgId);
|
||||
if (!user) {
|
||||
return [];
|
||||
}
|
||||
@@ -152,27 +157,26 @@ export class EventTypesService_2024_06_14 {
|
||||
}
|
||||
|
||||
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14) {
|
||||
const { username, eventSlug, usernames } = queryParams;
|
||||
|
||||
const { username, eventSlug, usernames, orgSlug, orgId } = queryParams;
|
||||
if (username && eventSlug) {
|
||||
const eventType = await this.getEventTypeByUsernameAndSlug(username, eventSlug);
|
||||
const eventType = await this.getEventTypeByUsernameAndSlug(username, eventSlug, orgSlug, orgId);
|
||||
return eventType ? [eventType] : [];
|
||||
}
|
||||
|
||||
if (username) {
|
||||
return await this.getEventTypesByUsername(username);
|
||||
return await this.getEventTypesByUsername(username, orgSlug, orgId);
|
||||
}
|
||||
|
||||
if (usernames) {
|
||||
const dynamicEventType = await this.getDynamicEventType(usernames);
|
||||
const dynamicEventType = await this.getDynamicEventType(usernames, orgSlug, orgId);
|
||||
return [dynamicEventType];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async getDynamicEventType(usernames: string[]) {
|
||||
const users = await this.usersService.getByUsernames(usernames);
|
||||
async getDynamicEventType(usernames: string[], orgSlug?: string, orgId?: number) {
|
||||
const users = await this.usersService.getByUsernames(usernames, orgSlug, orgId);
|
||||
const usersFiltered: UserWithProfile[] = [];
|
||||
for (const user of users) {
|
||||
if (user) {
|
||||
|
||||
@@ -27,10 +27,7 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
import { google } from "googleapis";
|
||||
import { z } from "zod";
|
||||
|
||||
import { APPS_READ, GOOGLE_CALENDAR_TYPE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
import { google } from "googleapis";
|
||||
import { OAuth2Client } from "googleapis-common";
|
||||
import { z } from "zod";
|
||||
|
||||
@Injectable()
|
||||
@@ -21,7 +21,7 @@ export class GCalService {
|
||||
|
||||
const { client_id, client_secret } = this.gcalResponseSchema.parse(app.keys);
|
||||
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
|
||||
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirectUri);
|
||||
return oAuth2Client;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ApiTags as DocsTags, ApiCreatedResponse, ApiOkResponse, ApiOperation }
|
||||
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { SlotFormat } from "@calcom/platform-enums";
|
||||
import { getAvailableSlots } from "@calcom/platform-libraries";
|
||||
import type { AvailableSlotsType } from "@calcom/platform-libraries";
|
||||
import { RemoveSelectedSlotInput, ReserveSlotInput } from "@calcom/platform-types";
|
||||
@@ -89,9 +90,31 @@ export class SlotsController {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
time: { type: "string", format: "date-time", example: "2024-09-25T08:00:00.000Z" },
|
||||
},
|
||||
oneOf: [
|
||||
{
|
||||
properties: {
|
||||
time: {
|
||||
type: "string",
|
||||
format: "date-time",
|
||||
example: "2024-09-25T08:00:00.000Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
startTime: {
|
||||
type: "string",
|
||||
format: "date-time",
|
||||
example: "2024-09-25T08:00:00.000Z",
|
||||
},
|
||||
endTime: {
|
||||
type: "string",
|
||||
format: "date-time",
|
||||
example: "2024-09-25T08:30:00.000Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -102,11 +125,18 @@ export class SlotsController {
|
||||
status: "success",
|
||||
data: {
|
||||
slots: {
|
||||
// Default format (when slotFormat is 'time' or not provided)
|
||||
"2024-09-25": [{ time: "2024-09-25T08:00:00.000Z" }, { time: "2024-09-25T08:15:00.000Z" }],
|
||||
// Alternative format (when slotFormat is 'range')
|
||||
"2024-09-26": [
|
||||
{ time: "2024-09-26T08:00:00.000Z" },
|
||||
{ time: "2024-09-26T08:15:00.000Z" },
|
||||
{ time: "2024-09-26T08:30:00.000Z" },
|
||||
{
|
||||
startTime: "2024-09-26T08:00:00.000Z",
|
||||
endTime: "2024-09-26T08:30:00.000Z",
|
||||
},
|
||||
{
|
||||
startTime: "2024-09-26T08:15:00.000Z",
|
||||
endTime: "2024-09-26T08:45:00.000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -129,8 +159,20 @@ export class SlotsController {
|
||||
},
|
||||
});
|
||||
|
||||
const transformedSlots =
|
||||
query.slotFormat === SlotFormat.Range
|
||||
? await this.slotsService.formatSlots(
|
||||
availableSlots,
|
||||
query.duration,
|
||||
query.eventTypeId,
|
||||
query.slotFormat
|
||||
)
|
||||
: availableSlots.slots;
|
||||
|
||||
return {
|
||||
data: availableSlots,
|
||||
data: {
|
||||
slots: transformedSlots,
|
||||
},
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
|
||||
import { SlotsRepository } from "@/modules/slots/slots.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import { SlotFormat } from "@calcom/platform-enums";
|
||||
import { ReserveSlotInput } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
@@ -54,4 +55,51 @@ export class SlotsService {
|
||||
const event = await this.eventTypeRepo.getEventTypeById(eventTypeId);
|
||||
return !!event?.teamId;
|
||||
}
|
||||
|
||||
async getEventTypeWithDuration(eventTypeId: number) {
|
||||
return await this.eventTypeRepo.getEventTypeWithDuration(eventTypeId);
|
||||
}
|
||||
|
||||
async getDuration(duration?: number, eventTypeId?: number): Promise<number> {
|
||||
if (duration) {
|
||||
return duration;
|
||||
}
|
||||
|
||||
if (eventTypeId) {
|
||||
const eventType = await this.eventTypeRepo.getEventTypeWithDuration(eventTypeId);
|
||||
if (!eventType) {
|
||||
throw new Error("Event type not found");
|
||||
}
|
||||
return eventType.length;
|
||||
}
|
||||
|
||||
throw new Error("duration or eventTypeId is required");
|
||||
}
|
||||
|
||||
async formatSlots(
|
||||
availableSlots: { slots: Record<string, { time: string }[]> },
|
||||
duration?: number,
|
||||
eventTypeId?: number,
|
||||
slotFormat?: SlotFormat
|
||||
): Promise<Record<string, { startTime: string; endTime: string }[]>> {
|
||||
if (slotFormat && !Object.values(SlotFormat).includes(slotFormat)) {
|
||||
throw new BadRequestException("Invalid slot format. Must be either 'range' or 'time'");
|
||||
}
|
||||
|
||||
const slotDuration = await this.getDuration(duration, eventTypeId);
|
||||
|
||||
return Object.entries(availableSlots.slots).reduce<
|
||||
Record<string, { startTime: string; endTime: string }[]>
|
||||
>((acc, [date, slots]) => {
|
||||
acc[date] = (slots as { time: string }[]).map((slot) => {
|
||||
const startTime = new Date(slot.time);
|
||||
const endTime = new Date(startTime.getTime() + slotDuration * 60000);
|
||||
return {
|
||||
startTime: startTime.toISOString(),
|
||||
endTime: endTime.toISOString(),
|
||||
};
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ import { User } from "@calcom/prisma/client";
|
||||
export class UsersService {
|
||||
constructor(private readonly usersRepository: UsersRepository) {}
|
||||
|
||||
async getByUsernames(usernames: string[]) {
|
||||
async getByUsernames(usernames: string[], orgSlug?: string, orgId?: number) {
|
||||
const users = await Promise.all(
|
||||
usernames.map((username) => this.usersRepository.findByUsername(username))
|
||||
usernames.map((username) => this.usersRepository.findByUsername(username, orgSlug, orgId))
|
||||
);
|
||||
const usersFiltered: User[] = [];
|
||||
|
||||
|
||||
@@ -142,11 +142,21 @@ export class UsersRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findByUsername(username: string) {
|
||||
async findByUsername(username: string, orgSlug?: string, orgId?: number) {
|
||||
return this.dbRead.prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
where:
|
||||
orgId || orgSlug
|
||||
? {
|
||||
profiles: {
|
||||
some: {
|
||||
organization: orgSlug ? { slug: orgSlug } : { id: orgId },
|
||||
username: username,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
username,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4059,6 +4059,15 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "eventTypeId",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Id of the event-type, if this is provided no other parameter is required",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "usernames",
|
||||
"required": false,
|
||||
@@ -4067,6 +4076,33 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "ID of the user you want the get the event-types of, username is not needed when using this parameter",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "orgSlug",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "slug of the user's organization if he is in one, orgId is not required if using this parameter",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "orgId",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "ID of the organization of the user you want the get the event-types of, orgSlug is not needed when using this parameter",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -5005,6 +5041,20 @@
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "slotFormat",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Format of slot times in response. Use 'range' to get start and end times.",
|
||||
"example": "range",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"range",
|
||||
"time"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -5028,13 +5078,31 @@
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:00:00.000Z"
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:00:00.000Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"startTime": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:00:00.000Z"
|
||||
},
|
||||
"endTime": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:30:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5055,13 +5123,12 @@
|
||||
],
|
||||
"2024-09-26": [
|
||||
{
|
||||
"time": "2024-09-26T08:00:00.000Z"
|
||||
"startTime": "2024-09-26T08:00:00.000Z",
|
||||
"endTime": "2024-09-26T08:30:00.000Z"
|
||||
},
|
||||
{
|
||||
"time": "2024-09-26T08:15:00.000Z"
|
||||
},
|
||||
{
|
||||
"time": "2024-09-26T08:30:00.000Z"
|
||||
"startTime": "2024-09-26T08:15:00.000Z",
|
||||
"endTime": "2024-09-26T08:45:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6534,7 +6601,7 @@
|
||||
"rolling": {
|
||||
"type": "boolean",
|
||||
"example": true,
|
||||
"description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n "
|
||||
"description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always be equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n "
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -6562,7 +6629,7 @@
|
||||
"rolling": {
|
||||
"type": "boolean",
|
||||
"example": true,
|
||||
"description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n "
|
||||
"description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always be equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n "
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -13450,7 +13517,7 @@
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Host"
|
||||
"$ref": "#/components/schemas/BookingHost"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
@@ -13571,7 +13638,7 @@
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Host"
|
||||
"$ref": "#/components/schemas/BookingHost"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
@@ -13737,7 +13804,7 @@
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Host"
|
||||
"$ref": "#/components/schemas/BookingHost"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
@@ -13853,7 +13920,7 @@
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Host"
|
||||
"$ref": "#/components/schemas/BookingHost"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useSession } from "next-auth/react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import type { ComponentProps } from "react";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import type Shell from "@calcom/features/shell/Shell";
|
||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||
import { ErrorBoundary } from "@calcom/ui";
|
||||
|
||||
export default function AdminLayout({
|
||||
children,
|
||||
...rest
|
||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||
const pathname = usePathname();
|
||||
const session = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
// Force redirect on component level
|
||||
useEffect(() => {
|
||||
if (session.data && session.data.user.role !== UserPermissionRole.ADMIN) {
|
||||
router.replace("/settings/my-account/profile");
|
||||
}
|
||||
}, [session, router]);
|
||||
|
||||
const isAppsPage = pathname?.startsWith("/settings/admin/apps");
|
||||
return (
|
||||
<SettingsLayout {...rest}>
|
||||
<div className="divide-subtle bg-default mx-auto flex max-w-4xl flex-row divide-y">
|
||||
<div className={isAppsPage ? "min-w-0" : "flex flex-1 [&>*]:flex-1"}>
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export const getLayout = (page: React.ReactElement) => <AdminLayout>{page}</AdminLayout>;
|
||||
@@ -212,23 +212,27 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
];
|
||||
|
||||
const editBookingActions: ActionType[] = [
|
||||
{
|
||||
id: "reschedule",
|
||||
icon: "clock" as const,
|
||||
label: t("reschedule_booking"),
|
||||
href: `/reschedule/${booking.uid}${
|
||||
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
|
||||
}`,
|
||||
},
|
||||
{
|
||||
id: "reschedule_request",
|
||||
icon: "send" as const,
|
||||
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
||||
label: t("send_reschedule_request"),
|
||||
onClick: () => {
|
||||
setIsOpenRescheduleDialog(true);
|
||||
},
|
||||
},
|
||||
...(isBookingInPast
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: "reschedule",
|
||||
icon: "clock" as const,
|
||||
label: t("reschedule_booking"),
|
||||
href: `/reschedule/${booking.uid}${
|
||||
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
|
||||
}`,
|
||||
},
|
||||
{
|
||||
id: "reschedule_request",
|
||||
icon: "send" as const,
|
||||
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
||||
label: t("send_reschedule_request"),
|
||||
onClick: () => {
|
||||
setIsOpenRescheduleDialog(true);
|
||||
},
|
||||
},
|
||||
]),
|
||||
...(isBookingReroutable(parsedBooking)
|
||||
? [
|
||||
{
|
||||
|
||||
@@ -115,6 +115,28 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
};
|
||||
}
|
||||
|
||||
const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
|
||||
|
||||
const enrichedBookingUser = booking.user
|
||||
? await UserRepository.enrichUserWithItsProfile({ user: booking.user })
|
||||
: null;
|
||||
|
||||
const eventUrl = await buildEventUrlFromBooking({
|
||||
eventType,
|
||||
dynamicGroupSlugRef: booking.dynamicGroupSlugRef ?? null,
|
||||
profileEnrichedBookingUser: enrichedBookingUser,
|
||||
});
|
||||
|
||||
const isBookingInPast = booking.endTime && new Date(booking.endTime) < new Date();
|
||||
if (isBookingInPast) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: eventUrl,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// if booking event type is for a seated event and no seat reference uid is provided, throw not found
|
||||
if (booking?.eventType?.seatsPerTimeSlot && !maybeSeatReferenceUid) {
|
||||
const userId = session?.user?.id;
|
||||
@@ -142,18 +164,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
}
|
||||
}
|
||||
|
||||
const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
|
||||
|
||||
const enrichedBookingUser = booking.user
|
||||
? await UserRepository.enrichUserWithItsProfile({ user: booking.user })
|
||||
: null;
|
||||
|
||||
const eventUrl = await buildEventUrlFromBooking({
|
||||
eventType,
|
||||
dynamicGroupSlugRef: booking.dynamicGroupSlugRef ?? null,
|
||||
profileEnrichedBookingUser: enrichedBookingUser,
|
||||
});
|
||||
|
||||
const destinationUrlSearchParams = new URLSearchParams();
|
||||
|
||||
destinationUrlSearchParams.set("rescheduleUid", seatReferenceUid || bookingUid);
|
||||
|
||||
@@ -729,7 +729,7 @@ export default function Success(props: PageProps) {
|
||||
</span>
|
||||
|
||||
<>
|
||||
{!props.recurringBookings && (
|
||||
{!props.recurringBookings && !isBookingInPast && (
|
||||
<span className="text-default inline">
|
||||
<span className="underline" data-testid="reschedule-link">
|
||||
<Link
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "4.7.17",
|
||||
"version": "4.7.19",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -48,6 +48,9 @@
|
||||
"@dub/analytics": "^0.0.15",
|
||||
"@formkit/auto-animate": "1.0.0-beta.5",
|
||||
"@glidejs/glide": "^3.5.2",
|
||||
"@googleapis/admin": "^23.0.0",
|
||||
"@googleapis/calendar": "^9.7.9",
|
||||
"@googleapis/oauth2": "^1.0.7",
|
||||
"@hookform/error-message": "^2.0.0",
|
||||
"@hookform/resolvers": "^2.9.7",
|
||||
"@next-auth/prisma-adapter": "^1.0.4",
|
||||
@@ -85,7 +88,6 @@
|
||||
"dotenv-cli": "^6.0.0",
|
||||
"entities": "^4.4.0",
|
||||
"eslint-config-next": "^13.2.1",
|
||||
"googleapis": "^84.0.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"handlebars": "^4.7.7",
|
||||
"ical.js": "^1.4.0",
|
||||
@@ -180,6 +182,7 @@
|
||||
"deasync": "^0.1.30",
|
||||
"detect-port": "^1.3.0",
|
||||
"env-cmd": "^10.1.0",
|
||||
"google-auth-library": "^9.15.0",
|
||||
"module-alias": "^2.2.2",
|
||||
"msw": "^0.42.3",
|
||||
"node-html-parser": "^6.1.10",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { google } from "googleapis";
|
||||
import { OAuth2Client } from "googleapis-common";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
|
||||
@@ -20,7 +20,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
// use differnt callback to normal calendar connection
|
||||
const redirect_uri = `${WEBAPP_URL}/api/teams/googleworkspace/callback`;
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirect_uri);
|
||||
|
||||
const authUrl = oAuth2Client.generateAuthUrl({
|
||||
access_type: "offline",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { google } from "googleapis";
|
||||
import { OAuth2Client } from "googleapis-common";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -37,7 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(400).json({ message: "Google client_secret missing." });
|
||||
|
||||
const redirect_uri = `${WEBAPP_URL}/api/teams/googleworkspace/callback`;
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirect_uri);
|
||||
|
||||
if (!code) {
|
||||
throw new Error("No code provided");
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { testBothFutureAndLegacyRoutes } from "./lib/future-legacy-routes";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
const ensureAppDir = async (page: Page) => {
|
||||
const dataNextJsRouter = await page.evaluate(() =>
|
||||
window.document.documentElement.getAttribute("data-nextjs-router")
|
||||
);
|
||||
|
||||
expect(dataNextJsRouter).toEqual("app");
|
||||
};
|
||||
|
||||
testBothFutureAndLegacyRoutes.describe("apps/ A/B tests", (routeVariant) => {
|
||||
test.describe("apps/ A/B tests", () => {
|
||||
test("should render the /apps/installed/[category]", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
|
||||
@@ -24,10 +14,6 @@ testBothFutureAndLegacyRoutes.describe("apps/ A/B tests", (routeVariant) => {
|
||||
|
||||
const locator = page.getByRole("heading", { name: "Messaging" });
|
||||
|
||||
if (routeVariant === "future") {
|
||||
await ensureAppDir(page);
|
||||
}
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -40,10 +26,6 @@ testBothFutureAndLegacyRoutes.describe("apps/ A/B tests", (routeVariant) => {
|
||||
|
||||
const locator = page.getByRole("heading", { name: "Telegram" });
|
||||
|
||||
if (routeVariant === "future") {
|
||||
await ensureAppDir(page);
|
||||
}
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -56,10 +38,6 @@ testBothFutureAndLegacyRoutes.describe("apps/ A/B tests", (routeVariant) => {
|
||||
|
||||
const locator = page.getByRole("heading", { name: "Connect to Apple Server" });
|
||||
|
||||
if (routeVariant === "future") {
|
||||
await ensureAppDir(page);
|
||||
}
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -72,10 +50,6 @@ testBothFutureAndLegacyRoutes.describe("apps/ A/B tests", (routeVariant) => {
|
||||
|
||||
const locator = page.getByTestId("app-store-category-messaging");
|
||||
|
||||
if (routeVariant === "future") {
|
||||
await ensureAppDir(page);
|
||||
}
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -88,10 +62,6 @@ testBothFutureAndLegacyRoutes.describe("apps/ A/B tests", (routeVariant) => {
|
||||
|
||||
const locator = page.getByText(/messaging apps/i);
|
||||
|
||||
if (routeVariant === "future") {
|
||||
await ensureAppDir(page);
|
||||
}
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -104,10 +74,6 @@ testBothFutureAndLegacyRoutes.describe("apps/ A/B tests", (routeVariant) => {
|
||||
|
||||
const locator = page.getByTestId("horizontal-tab-upcoming");
|
||||
|
||||
if (routeVariant === "future") {
|
||||
await ensureAppDir(page);
|
||||
}
|
||||
|
||||
await expect(locator).toHaveClass(/bg-emphasis/);
|
||||
});
|
||||
|
||||
@@ -120,10 +86,6 @@ testBothFutureAndLegacyRoutes.describe("apps/ A/B tests", (routeVariant) => {
|
||||
|
||||
const locator = page.getByText("Apple Calendar");
|
||||
|
||||
if (routeVariant === "future") {
|
||||
await ensureAppDir(page);
|
||||
}
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { testBothFutureAndLegacyRoutes } from "./lib/future-legacy-routes";
|
||||
import { installAppleCalendar } from "./lib/testUtils";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
testBothFutureAndLegacyRoutes.describe("App Store - Authed", (routeVariant) => {
|
||||
test.describe("App Store - Authed", () => {
|
||||
test("should render /apps page", async ({ page, users, context }) => {
|
||||
test.skip(routeVariant === "future", "Future route not ready yet");
|
||||
const user = await users.create();
|
||||
|
||||
await user.apiLogin();
|
||||
|
||||
@@ -5,11 +5,10 @@ import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
import { testBothFutureAndLegacyRoutes } from "../lib/future-legacy-routes";
|
||||
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
testBothFutureAndLegacyRoutes.describe("Forgot password", async () => {
|
||||
test.describe("Forgot password", async () => {
|
||||
test("Can reset forgotten password", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { SchedulingType } from "@calcom/prisma/client";
|
||||
import type { Schedule, TimeRange } from "@calcom/types/schedule";
|
||||
|
||||
import { test, todo } from "./lib/fixtures";
|
||||
import { testBothFutureAndLegacyRoutes } from "./lib/future-legacy-routes";
|
||||
import {
|
||||
bookFirstEvent,
|
||||
bookOptinEvent,
|
||||
@@ -63,7 +62,7 @@ test("check SSR and OG - User Event Type", async ({ page, users }) => {
|
||||
|
||||
todo("check SSR and OG - Team Event Type");
|
||||
|
||||
testBothFutureAndLegacyRoutes.describe("free user", () => {
|
||||
test.describe("free user", () => {
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const free = await users.create(freeUserObj);
|
||||
await page.goto(`/${free.username}`);
|
||||
@@ -99,7 +98,7 @@ testBothFutureAndLegacyRoutes.describe("free user", () => {
|
||||
});
|
||||
});
|
||||
|
||||
testBothFutureAndLegacyRoutes.describe("pro user", () => {
|
||||
test.describe("pro user", () => {
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const pro = await users.create();
|
||||
await page.goto(`/${pro.username}`);
|
||||
@@ -343,7 +342,7 @@ testBothFutureAndLegacyRoutes.describe("pro user", () => {
|
||||
});
|
||||
});
|
||||
|
||||
testBothFutureAndLegacyRoutes.describe("prefill", () => {
|
||||
test.describe("prefill", () => {
|
||||
test("logged in", async ({ page, users }) => {
|
||||
const prefill = await users.create({ name: "Prefill User" });
|
||||
await prefill.apiLogin();
|
||||
@@ -400,7 +399,7 @@ testBothFutureAndLegacyRoutes.describe("prefill", () => {
|
||||
});
|
||||
});
|
||||
|
||||
testBothFutureAndLegacyRoutes.describe("Booking on different layouts", () => {
|
||||
test.describe("Booking on different layouts", () => {
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await page.goto(`/${user.username}`);
|
||||
@@ -457,7 +456,7 @@ testBothFutureAndLegacyRoutes.describe("Booking on different layouts", () => {
|
||||
});
|
||||
});
|
||||
|
||||
testBothFutureAndLegacyRoutes.describe("Booking round robin event", () => {
|
||||
test.describe("Booking round robin event", () => {
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const teamMatesObj = [{ name: "teammate-1" }];
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { randomString } from "@calcom/lib/random";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { testBothFutureAndLegacyRoutes } from "./lib/future-legacy-routes";
|
||||
import {
|
||||
bookTimeSlot,
|
||||
createNewEventType,
|
||||
@@ -18,7 +17,7 @@ import {
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
testBothFutureAndLegacyRoutes.describe("Event Types A/B tests", () => {
|
||||
test.describe("Event Types A/B tests", () => {
|
||||
test("should render the /future/event-types page", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
|
||||
@@ -32,7 +31,7 @@ testBothFutureAndLegacyRoutes.describe("Event Types A/B tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
testBothFutureAndLegacyRoutes.describe("Event Types tests", () => {
|
||||
test.describe("Event Types tests", () => {
|
||||
test.describe("user", () => {
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { test } from "./fixtures";
|
||||
|
||||
export type RouteVariant = "future" | "legacy";
|
||||
|
||||
const routeVariants = ["future", "legacy"];
|
||||
|
||||
/**
|
||||
* Small wrapper around test.describe().
|
||||
* When using testbothFutureLegacyRoutes.describe() instead of test.describe(), this will run the specified
|
||||
* tests twice. One with the pages route, and one with the new app dir "future" route. It will also add the route variant
|
||||
* name to the test name for easier debugging.
|
||||
* Finally it also adds a parameter routeVariant to your testBothFutureAndLegacyRoutes.describe() callback, which
|
||||
* can be used to do any conditional rendering in the test for a specific route variant (should be as little
|
||||
* as possible).
|
||||
*
|
||||
* See apps/web/playwright/event-types.e2e.ts for an example.
|
||||
*/
|
||||
export const testBothFutureAndLegacyRoutes = {
|
||||
describe: (testName: string, testFn: (routeVariant: RouteVariant) => void) => {
|
||||
routeVariants.forEach((routeVariant) => {
|
||||
test.describe(`${testName} -- ${routeVariant}`, () => {
|
||||
if (routeVariant === "future") {
|
||||
test.beforeEach(({ context }) => {
|
||||
context.addCookies([
|
||||
{ name: "x-calcom-future-routes-override", value: "1", url: "http://localhost:3000" },
|
||||
]);
|
||||
});
|
||||
}
|
||||
testFn(routeVariant as RouteVariant);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -2,14 +2,13 @@ import { expect } from "@playwright/test";
|
||||
|
||||
import { login } from "./fixtures/users";
|
||||
import { test } from "./lib/fixtures";
|
||||
import { testBothFutureAndLegacyRoutes } from "./lib/future-legacy-routes";
|
||||
import { localize } from "./lib/testUtils";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
// a test to logout requires both a succesfull login as logout, to prevent
|
||||
// a doubling of tests failing on logout & logout, we can group them.
|
||||
testBothFutureAndLegacyRoutes.describe("user can login & logout succesfully", async () => {
|
||||
test.describe("user can login & logout succesfully", async () => {
|
||||
test.afterEach(async ({ users }) => {
|
||||
await users.deleteAll();
|
||||
});
|
||||
@@ -45,7 +44,7 @@ testBothFutureAndLegacyRoutes.describe("user can login & logout succesfully", as
|
||||
});
|
||||
});
|
||||
|
||||
testBothFutureAndLegacyRoutes.describe("Login and logout tests", () => {
|
||||
test.describe("Login and logout tests", () => {
|
||||
test.afterEach(async ({ users }) => {
|
||||
await users.deleteAll();
|
||||
});
|
||||
@@ -58,7 +57,7 @@ testBothFutureAndLegacyRoutes.describe("Login and logout tests", () => {
|
||||
await expect(page.locator(`[data-testid=login-form]`)).toBeVisible();
|
||||
});
|
||||
|
||||
testBothFutureAndLegacyRoutes.describe("Login flow validations", async () => {
|
||||
test.describe("Login flow validations", async () => {
|
||||
test("Should warn when user does not exist", async ({ page }) => {
|
||||
const alertMessage = (await localize("en"))("incorrect_email_password");
|
||||
|
||||
|
||||
@@ -3,18 +3,16 @@ import { expect } from "@playwright/test";
|
||||
import { bookTimeSlot, selectFirstAvailableTimeSlotNextMonth } from "@calcom/web/playwright/lib/testUtils";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { testBothFutureAndLegacyRoutes } from "./lib/future-legacy-routes";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
testBothFutureAndLegacyRoutes.describe("Payment", (routeVariant) => {
|
||||
test.describe("Payment", () => {
|
||||
test.describe("user", () => {
|
||||
test.afterEach(async ({ users }) => {
|
||||
await users.deleteAll();
|
||||
});
|
||||
|
||||
test("should create a mock payment for a user", async ({ context, users, page }) => {
|
||||
test.skip(routeVariant === "future", "Future route not ready yet");
|
||||
test.skip(process.env.MOCK_PAYMENT_APP_ENABLED === undefined, "Skipped as Stripe is not installed");
|
||||
|
||||
const user = await users.create();
|
||||
|
||||
@@ -5,7 +5,6 @@ import { prisma } from "@calcom/prisma";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
import { test, todo } from "./lib/fixtures";
|
||||
import { testBothFutureAndLegacyRoutes } from "./lib/future-legacy-routes";
|
||||
import {
|
||||
bookTimeSlot,
|
||||
confirmReschedule,
|
||||
@@ -16,10 +15,8 @@ import {
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
testBothFutureAndLegacyRoutes.describe("Teams A/B tests", (routeVariant) => {
|
||||
test.describe("Teams A/B tests", () => {
|
||||
test("should render the /teams page", async ({ page, users, context }) => {
|
||||
// TODO: Revert until OOM issue is resolved
|
||||
test.skip(routeVariant === "future", "Future route not ready yet");
|
||||
const user = await users.create();
|
||||
|
||||
await user.apiLogin();
|
||||
@@ -34,7 +31,7 @@ testBothFutureAndLegacyRoutes.describe("Teams A/B tests", (routeVariant) => {
|
||||
});
|
||||
});
|
||||
|
||||
testBothFutureAndLegacyRoutes.describe("Teams - NonOrg", (routeVariant) => {
|
||||
test.describe("Teams - NonOrg", () => {
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
test("Can create a booking for Collective EventType", async ({ page, users }) => {
|
||||
@@ -120,7 +117,6 @@ testBothFutureAndLegacyRoutes.describe("Teams - NonOrg", (routeVariant) => {
|
||||
});
|
||||
|
||||
test("Non admin team members cannot create team in org", async ({ page, users }) => {
|
||||
test.skip(routeVariant === "future", "Future route not ready yet");
|
||||
const teamMateName = "teammate-1";
|
||||
|
||||
const owner = await users.create(undefined, {
|
||||
@@ -155,7 +151,6 @@ testBothFutureAndLegacyRoutes.describe("Teams - NonOrg", (routeVariant) => {
|
||||
});
|
||||
|
||||
test("Can create team with same name as user", async ({ page, users }) => {
|
||||
test.skip(routeVariant === "future", "Future route not ready yet");
|
||||
const user = await users.create();
|
||||
// Name to be used for both user and team
|
||||
const uniqueName = user.username!;
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "سيتمكن الأعضاء من إضافة سير العمل الشخصي إلى هذا النوع من الأحداث. سيتمكن الأعضاء من رؤية سير العمل النشط للفريق ولكن لن يتمكنوا من تعديل أي إعدادات لسير العمل.",
|
||||
"workflows_locked_by_team_admins_description": "ستتمكن من رؤية سير العمل النشط للفريق ولكن لن تتمكن من تعديل أي إعدادات لسير العمل أو إضافة سير العمل الشخصي إلى هذا النوع من الأحداث.",
|
||||
"workflows_unlocked_by_team_admins_description": "ستتمكن من تفعيل/تعطيل سير العمل الشخصي لهذا النوع من الأحداث. ستتمكن من رؤية سير العمل النشط للفريق ولكن لن تتمكن من تعديل أي إعدادات لسير العمل الخاص بالفريق.",
|
||||
"webhooks_locked_for_members_description": "لا يمكن للأعضاء إضافة الويب هوكس الشخصية الخاصة بهم إلى نوع الحدث هذا. سيتمكن الأعضاء من رؤية الويب هوكس النشطة ولكن لن يتمكنوا من تعديل أي إعدادات للويب هوكس.",
|
||||
"webhooks_unlocked_for_members_description": "سيتمكن الأعضاء من إضافة الويب هوكس الشخصية الخاصة بهم إلى نوع الحدث هذا. سيتمكن الأعضاء من رؤية الويب هوكس النشطة ولكن لن يتمكنوا من تعديل أي إعدادات للويب هوكس.",
|
||||
"webhooks_locked_by_team_admins_description": "يمكنك رؤية الويب هوكس المُدارة النشطة ولكن لن تتمكن من تعديل أي إعدادات للويب هوكس أو إضافة الويب هوكس الشخصية الخاصة بك إلى نوع الحدث هذا.",
|
||||
"webhooks_unlocked_by_team_admins_description": "يمكنك إضافة الويب هوكس إلى نوع الحدث هذا. يمكنك رؤية الويب هوكس المُدارة النشطة ولكن لن تتمكن من تعديلها.",
|
||||
"locked_by_team_admin": "مقفل من قبل المشرف",
|
||||
"app_not_connected": "أنت غير متصل بحساب {{appName}}.",
|
||||
"connect_now": "اتصل الآن",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "سنراقب عدد الحجوزات التي تمت مع كل مضيف ونقارنها مع الآخرين، مع تعطيل بعض المضيفين الذين تجاوزوا الحد بكثير لضمان توزيع الحجوزات بشكل متساوٍ.",
|
||||
"exclude_emails_that_contain": "استبعاد رسائل البريد الإلكتروني التي تحتوي على ...",
|
||||
"exclude_emails_match_found_error_message": "يرجى إدخال عنوان بريد إلكتروني صالح للعمل",
|
||||
"search_columns": "البحث في الأعمدة",
|
||||
"search_options": "خيارات البحث",
|
||||
"disable_org_url_label": "تعطيل الملف التعريفي العام للمؤسسة وإعادة التوجيه",
|
||||
"disable_org_url_description": "إعادة توجيه {{orgSlug}}.cal.com إلى {{destination}}",
|
||||
"single_select": "اختيار فردي",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Üzvlər bu tədbir növünə şəxsi iş axınlarını əlavə edə biləcəklər. Üzvlər aktiv komanda iş axınlarını görə biləcəklər, lakin heç bir iş axını parametrlərini redaktə edə bilməyəcəklər.",
|
||||
"workflows_locked_by_team_admins_description": "Siz aktiv komanda iş axınlarını görə biləcəksiniz, lakin heç bir iş axını parametrlərini redaktə edə və ya bu tədbir növünə şəxsi iş axınlarınızı əlavə edə bilməyəcəksiniz.",
|
||||
"workflows_unlocked_by_team_admins_description": "Siz bu tədbir növündə şəxsi iş axınlarını aktivləşdirə/deaktivləşdirə biləcəksiniz. Aktiv komanda iş axınlarını görə biləcəksiniz, lakin heç bir komanda iş axını parametrlərini redaktə edə bilməyəcəksiniz.",
|
||||
"webhooks_locked_for_members_description": "Üzvlər bu tədbir növünə öz şəxsi vebhukları əlavə edə bilməzlər. Üzvlər aktiv vebhukları görə biləcəklər, lakin heç bir vebhuk parametrini redaktə edə bilməyəcəklər.",
|
||||
"webhooks_unlocked_for_members_description": "Üzvlər bu tədbir növünə öz şəxsi vebhukları əlavə edə biləcəklər. Üzvlər aktiv vebhukları görə biləcəklər, lakin heç bir vebhuk parametrini redaktə edə bilməyəcəklər.",
|
||||
"webhooks_locked_by_team_admins_description": "Siz aktiv idarə olunan vebhukları görə bilərsiniz, lakin heç bir vebhuk parametrini redaktə edə və ya bu tədbir növünə öz şəxsi vebhuklarınızı əlavə edə bilməzsiniz.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Siz bu tədbir növünə vebhuklar əlavə edə bilərsiniz. Aktiv idarə olunan vebhukları görə bilərsiniz, lakin onları redaktə edə bilməzsiniz.",
|
||||
"locked_by_team_admin": "Komanda admini tərəfindən kilidlənib",
|
||||
"app_not_connected": "Siz {{appName}} hesabını bağlamamısınız.",
|
||||
"connect_now": "İndi bağlayın",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Hər bir təşkilatçı ilə neçə bron edildiyini izləyəcəyik və bunu digərləri ilə müqayisə edəcəyik, çox irəlidə olan bəzi təşkilatçıları deaktiv edərək bronların bərabər paylanmasını təmin edəcəyik.",
|
||||
"exclude_emails_that_contain": "Tərkibində olan e-poçtları istisna et...",
|
||||
"exclude_emails_match_found_error_message": "Zəhmət olmasa düzgün iş e-poçt ünvanı daxil edin",
|
||||
"search_columns": "Sütunlarda axtar",
|
||||
"search_options": "Axtarış seçimləri",
|
||||
"disable_org_url_label": "İctimai təşkilat profilini deaktiv edin və yönləndirin",
|
||||
"disable_org_url_description": "{{orgSlug}}.cal.com ünvanını {{destination}} ünvanına yönləndirir",
|
||||
"single_select": "Tək seçim",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Членовете ще могат да добавят личните си работни процеси към този тип събитие. Членовете ще могат да виждат активните екипни работни процеси, но няма да могат да редактират никакви настройки на работните процеси.",
|
||||
"workflows_locked_by_team_admins_description": "Ще можете да виждате активните екипни работни процеси, но няма да можете да редактирате никакви настройки на работните процеси или да добавяте лични работни процеси към този тип събитие.",
|
||||
"workflows_unlocked_by_team_admins_description": "Ще можете да активирате/деактивирате лични работни процеси за този тип събитие. Ще можете да виждате активните екипни работни процеси, но няма да можете да редактирате никакви настройки на екипните работни процеси.",
|
||||
"webhooks_locked_for_members_description": "Членовете не могат да добавят своите лични уебхуки към този тип събитие. Членовете ще могат да виждат активните уебхуки, но няма да могат да редактират никакви настройки на уебхуките.",
|
||||
"webhooks_unlocked_for_members_description": "Членовете ще могат да добавят своите лични уебхуки към този тип събитие. Членовете ще могат да виждат активните уебхуки, но няма да могат да редактират никакви настройки на уебхуките.",
|
||||
"webhooks_locked_by_team_admins_description": "Можете да виждате активните управлявани уебхуки, но няма да можете да редактирате никакви настройки на уебхуките или да добавяте свои лични уебхуки към този тип събитие.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Можете да добавяте уебхуки към този тип събитие. Можете да виждате активните управлявани уебхуки, но няма да можете да ги редактирате.",
|
||||
"locked_by_team_admin": "Заключено от администратора на екипа",
|
||||
"app_not_connected": "Не сте свързали {{appName}} акаунт.",
|
||||
"connect_now": "Свържете сега",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Ще следим колко резервации са направени с всеки домакин и ще ги сравняваме с останалите, като деактивираме някои домакини, които са прекалено напред, за да бъдат резервациите равномерно разпределени.",
|
||||
"exclude_emails_that_contain": "Изключи имейли, които съдържат ...",
|
||||
"exclude_emails_match_found_error_message": "Моля, въведете валиден служебен имейл адрес",
|
||||
"search_columns": "Търсене в колони",
|
||||
"search_options": "Опции за търсене",
|
||||
"disable_org_url_label": "Деактивиране на публичния профил на организацията и пренасочване",
|
||||
"disable_org_url_description": "Пренасочва {{orgSlug}}.cal.com към {{destination}}",
|
||||
"single_select": "Единичен избор",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Els membres podran afegir els seus fluxos de treball personals a aquest tipus d'esdeveniment. Els membres podran veure els fluxos de treball actius de l'equip però no podran editar cap configuració del flux de treball.",
|
||||
"workflows_locked_by_team_admins_description": "Podràs veure els fluxos de treball actius de l'equip però no podràs editar cap configuració del flux de treball ni afegir els teus fluxos de treball personals a aquest tipus d'esdeveniment.",
|
||||
"workflows_unlocked_by_team_admins_description": "Podràs activar/desactivar fluxos de treball personals en aquest tipus d'esdeveniment. Podràs veure els fluxos de treball actius de l'equip però no podràs editar cap configuració del flux de treball de l'equip.",
|
||||
"webhooks_locked_for_members_description": "Els membres no poden afegir els seus webhooks personals a aquest tipus d'esdeveniment. Els membres podran veure els webhooks actius però no podran editar cap configuració de webhook.",
|
||||
"webhooks_unlocked_for_members_description": "Els membres podran afegir els seus webhooks personals a aquest tipus d'esdeveniment. Els membres podran veure els webhooks actius però no podran editar cap configuració de webhook.",
|
||||
"webhooks_locked_by_team_admins_description": "Pots veure els webhooks gestionats actius però no podràs editar cap configuració de webhook ni afegir els teus webhooks personals a aquest tipus d'esdeveniment.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Pots afegir webhooks a aquest tipus d'esdeveniment. Pots veure els webhooks gestionats actius però no podràs editar-los.",
|
||||
"locked_by_team_admin": "Bloquejat per l'administrador de l'equip",
|
||||
"app_not_connected": "No has connectat cap compte de {{appName}}.",
|
||||
"connect_now": "Connecta ara",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Monitoritzarem quantes reserves s'han fet amb cada amfitrió i ho compararem amb els altres, desactivant alguns amfitrions que estiguin massa avançats perquè les reserves es distribueixin uniformement",
|
||||
"exclude_emails_that_contain": "Excloure correus electrònics que continguin...",
|
||||
"exclude_emails_match_found_error_message": "Si us plau, introdueix una adreça de correu electrònic de treball vàlida",
|
||||
"search_columns": "Cerca columnes",
|
||||
"search_options": "Opcions de cerca",
|
||||
"disable_org_url_label": "Desactiva el perfil públic de l'organització i redirigeix",
|
||||
"disable_org_url_description": "Redirigeix {{orgSlug}}.cal.com a {{destination}}",
|
||||
"single_select": "Selecció única",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Členové budou moci přidávat své osobní pracovní postupy k tomuto typu události. Členové budou moci vidět aktivní týmové pracovní postupy, ale nebudou moci upravovat žádná nastavení pracovních postupů.",
|
||||
"workflows_locked_by_team_admins_description": "Budete moci vidět aktivní týmové pracovní postupy, ale nebudete moci upravovat žádná nastavení pracovních postupů ani přidávat své osobní pracovní postupy k tomuto typu události.",
|
||||
"workflows_unlocked_by_team_admins_description": "Budete moci povolit/zakázat osobní pracovní postupy u tohoto typu události. Budete moci vidět aktivní týmové pracovní postupy, ale nebudete moci upravovat žádná nastavení týmových pracovních postupů.",
|
||||
"webhooks_locked_for_members_description": "Členové nemohou přidávat své osobní webhooky k tomuto typu události. Členové uvidí aktivní webhooky, ale nebudou moci upravovat žádná nastavení webhooků.",
|
||||
"webhooks_unlocked_for_members_description": "Členové budou moci přidávat své osobní webhooky k tomuto typu události. Členové uvidí aktivní webhooky, ale nebudou moci upravovat žádná nastavení webhooků.",
|
||||
"webhooks_locked_by_team_admins_description": "Můžete vidět aktivní spravované webhooky, ale nemůžete upravovat žádná nastavení webhooků ani přidávat své osobní webhooky k tomuto typu události.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Můžete přidávat webhooky k tomuto typu události. Můžete vidět aktivní spravované webhooky, ale nemůžete je upravovat.",
|
||||
"locked_by_team_admin": "Uzamčeno správcem týmu",
|
||||
"app_not_connected": "Nemáte připojený účet {{appName}}.",
|
||||
"connect_now": "Připojte se",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Budeme sledovat, kolik rezervací bylo provedeno u jednotlivých hostitelů a porovnávat je s ostatními. Některé hostitele, kteří jsou příliš napřed, dočasně deaktivujeme, aby byly rezervace rovnoměrně distribuovány.",
|
||||
"exclude_emails_that_contain": "Vyloučit e-maily obsahující...",
|
||||
"exclude_emails_match_found_error_message": "Zadejte prosím platnou pracovní e-mailovou adresu",
|
||||
"search_columns": "Hledat sloupce",
|
||||
"search_options": "Možnosti vyhledávání",
|
||||
"disable_org_url_label": "Zakázat veřejný profil organizace a přesměrování",
|
||||
"disable_org_url_description": "Přesměruje {{orgSlug}}.cal.com na {{destination}}",
|
||||
"single_select": "Jeden výběr",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Medlemmer vil kunne tilføje deres personlige arbejdsgange til denne begivenhedstype. Medlemmer vil kunne se de aktive teamarbejdsgange, men vil ikke kunne redigere nogen arbejdsgangsindstillinger.",
|
||||
"workflows_locked_by_team_admins_description": "Du vil kunne se de aktive team-workflows, men du vil ikke kunne redigere nogen workflow-indstillinger eller tilføje dine personlige workflows til denne begivenhedstype.",
|
||||
"workflows_unlocked_by_team_admins_description": "Du vil kunne aktivere/deaktivere personlige workflows for denne begivenhedstype. Du vil kunne se de aktive team-workflows, men du vil ikke kunne redigere nogen team-workflow-indstillinger.",
|
||||
"webhooks_locked_for_members_description": "Medlemmer kan ikke tilføje deres personlige webhooks til denne begivenhedstype. Medlemmer vil kunne se de aktive webhooks, men vil ikke kunne redigere nogen webhook-indstillinger.",
|
||||
"webhooks_unlocked_for_members_description": "Medlemmer vil kunne tilføje deres personlige webhooks til denne begivenhedstype. Medlemmer vil kunne se de aktive webhooks, men vil ikke kunne redigere nogen webhook-indstillinger.",
|
||||
"webhooks_locked_by_team_admins_description": "Du kan se de aktive administrerede webhooks, men vil ikke kunne redigere nogen webhook-indstillinger eller tilføje dine personlige webhooks til denne begivenhedstype.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Du kan tilføje webhooks til denne begivenhedstype. Du kan se de aktive administrerede webhooks, men vil ikke kunne redigere dem.",
|
||||
"locked_by_team_admin": "Låst af teamadministrator",
|
||||
"app_not_connected": "Du har ikke forbundet en {{appName}}-konto.",
|
||||
"connect_now": "Forbind nu",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Vi vil overvåge, hvor mange bookinger der er foretaget med hver vært og sammenligne dette med andre, og deaktivere nogle værter, der er for langt foran, så bookinger fordeles jævnt.",
|
||||
"exclude_emails_that_contain": "Ekskluder e-mails, der indeholder ...",
|
||||
"exclude_emails_match_found_error_message": "Indtast venligst en gyldig arbejds-e-mailadresse",
|
||||
"search_columns": "Søg i kolonner",
|
||||
"search_options": "Søgemuligheder",
|
||||
"disable_org_url_label": "Deaktiver offentlig organisationsprofil og omdirigering",
|
||||
"disable_org_url_description": "Omdirigerer {{orgSlug}}.cal.com til {{destination}}",
|
||||
"single_select": "Enkeltvalg",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Mitglieder werden ihre persönlichen Workflows zu diesem Termintyp hinzufügen können. Mitglieder können die aktiven Teamworkflows sehen, können aber keine Workflow-Einstellungen bearbeiten.",
|
||||
"workflows_locked_by_team_admins_description": "Sie werden die aktiven Team-Workflows sehen können, werden aber keine Workflow-Einstellungen ändern oder Ihre persönlichen Workflows zu diesem Termintypen hinzufügen können.",
|
||||
"workflows_unlocked_by_team_admins_description": "Sie werden ihre persönlichen Workflows bei diesem Termintyp aktivieren/deaktivieren können. Sie können die aktiven Teamworkflows sehen, können aber keine Team-Workflow-Einstellungen bearbeiten.",
|
||||
"webhooks_locked_for_members_description": "Mitglieder können diesem Ereignistyp keine persönlichen Webhooks hinzufügen. Mitglieder können die aktiven Webhooks sehen, aber keine Webhook-Einstellungen bearbeiten.",
|
||||
"webhooks_unlocked_for_members_description": "Mitglieder können diesem Ereignistyp ihre persönlichen Webhooks hinzufügen. Mitglieder können die aktiven Webhooks sehen, aber keine Webhook-Einstellungen bearbeiten.",
|
||||
"webhooks_locked_by_team_admins_description": "Sie können die aktiven verwalteten Webhooks sehen, aber keine Webhook-Einstellungen bearbeiten oder diesem Ereignistyp keine persönlichen Webhooks hinzufügen.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Sie können diesem Ereignistyp Webhooks hinzufügen. Sie können die aktiven verwalteten Webhooks sehen, können diese aber nicht bearbeiten.",
|
||||
"locked_by_team_admin": "Vom Team-Admin gesperrt",
|
||||
"app_not_connected": "Sie haben kein {{appName}}-Konto verbunden.",
|
||||
"connect_now": "Jetzt verbinden",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Wir überwachen, wie viele Buchungen bei jedem Host eingegangen sind und vergleichen dies mit anderen. Hosts, die zu weit voraus sind, werden temporär deaktiviert, damit die Buchungen gleichmäßig verteilt werden.",
|
||||
"exclude_emails_that_contain": "E-Mails ausschließen, die ... enthalten",
|
||||
"exclude_emails_match_found_error_message": "Bitte geben Sie eine gültige Geschäfts-E-Mail-Adresse ein",
|
||||
"search_columns": "Spalten durchsuchen",
|
||||
"search_options": "Suchoptionen",
|
||||
"disable_org_url_label": "Öffentliches Organisationsprofil deaktivieren und weiterleiten",
|
||||
"disable_org_url_description": "Leitet {{orgSlug}}.cal.com zu {{destination}} weiter",
|
||||
"single_select": "Einfachauswahl",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Τα μέλη θα μπορούν να προσθέσουν τις προσωπικές τους ροές εργασίας σε αυτόν τον τύπο συμβάντος. Τα μέλη θα μπορούν να βλέπουν τις ενεργές ροές εργασίας της ομάδας αλλά δεν θα μπορούν να επεξεργαστούν τις ρυθμίσεις τους",
|
||||
"workflows_locked_by_team_admins_description": "Θα μπορείτε να βλέπετε τις ενεργές ροές εργασίας της ομάδας, αλλά δεν θα μπορείτε να επεξεργαστείτε τις ρυθμίσεις ροής εργασίας ή να προσθέσετε τις προσωπικές σας ροές εργασίας σε αυτόν τον τύπο εκδήλωσης.",
|
||||
"workflows_unlocked_by_team_admins_description": "Θα μπορείτε να ενεργοποιήσετε/απενεργοποιήσετε προσωπικές ροές εργασίας σε αυτόν τον τύπο εκδήλωσης. Θα μπορείτε να βλέπετε τις ενεργές ροές εργασίας της ομάδας, αλλά δεν θα μπορείτε να επεξεργαστείτε τις ρυθμίσεις των ροών εργασίας της ομάδας.",
|
||||
"webhooks_locked_for_members_description": "Τα μέλη δεν μπορούν να προσθέσουν τα προσωπικά τους webhooks σε αυτόν τον τύπο εκδήλωσης. Τα μέλη θα μπορούν να βλέπουν τα ενεργά webhooks αλλά δεν θα μπορούν να επεξεργαστούν καμία ρύθμιση webhook.",
|
||||
"webhooks_unlocked_for_members_description": "Τα μέλη θα μπορούν να προσθέσουν τα προσωπικά τους webhooks σε αυτόν τον τύπο εκδήλωσης. Τα μέλη θα μπορούν να βλέπουν τα ενεργά webhooks αλλά δεν θα μπορούν να επεξεργαστούν καμία ρύθμιση webhook.",
|
||||
"webhooks_locked_by_team_admins_description": "Μπορείτε να δείτε τα ενεργά διαχειριζόμενα webhooks αλλά δεν θα μπορείτε να επεξεργαστείτε καμία ρύθμιση webhook ή να προσθέσετε τα προσωπικά σας webhooks σε αυτόν τον τύπο εκδήλωσης.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Μπορείτε να προσθέσετε webhooks σε αυτόν τον τύπο εκδήλωσης. Μπορείτε να δείτε τα ενεργά διαχειριζόμενα webhooks αλλά δεν θα μπορείτε να τα επεξεργαστείτε.",
|
||||
"locked_by_team_admin": "Κλειδωμένο από τον διαχειριστή ομάδας",
|
||||
"app_not_connected": "Δεν έχετε συνδέσει λογαριασμό {{appName}}.",
|
||||
"connect_now": "Σύνδεση τώρα",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Θα παρακολουθούμε πόσες κρατήσεις έχουν γίνει με κάθε διοργανωτή και θα τις συγκρίνουμε με άλλους, απενεργοποιώντας κάποιους διοργανωτές που έχουν προχωρήσει πολύ μπροστά ώστε οι κρατήσεις να κατανέμονται ομοιόμορφα.",
|
||||
"exclude_emails_that_contain": "Εξαίρεση email που περιέχουν ...",
|
||||
"exclude_emails_match_found_error_message": "Παρακαλώ εισάγετε μια έγκυρη διεύθυνση email εργασίας",
|
||||
"search_columns": "Αναζήτηση στηλών",
|
||||
"search_options": "Επιλογές αναζήτησης",
|
||||
"disable_org_url_label": "Απενεργοποίηση δημόσιου προφίλ οργανισμού και ανακατεύθυνση",
|
||||
"disable_org_url_description": "Ανακατευθύνει το {{orgSlug}}.cal.com στο {{destination}}",
|
||||
"single_select": "Μονή επιλογή",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Members will be able to add their personal workflows to this event type. Members will be able to see the active team workflows but will not be able to edit any workflow settings.",
|
||||
"workflows_locked_by_team_admins_description": "You will be able to see the active team workflows but will not be able to edit any workflow settings or add your personal workflows to this event type.",
|
||||
"workflows_unlocked_by_team_admins_description": "You will be able to enable/disable personal workflows on this event type. You will be able to see the active team workflows but will not be able to edit any team workflow settings.",
|
||||
"webhooks_locked_for_members_description": "Members can not add their personal webhooks to this event type. Members will be able to see the active webhooks but will not be able to edit any webhook settings.",
|
||||
"webhooks_unlocked_for_members_description": "Members will be able to add their personal webhooks to this event type. Members will be able to see the active webhooks but will not be able to edit any webhook settings.",
|
||||
"webhooks_locked_by_team_admins_description": "You are able to see the active managed webhooks but will not be able to edit any webhook settings or add your personal webhooks to this event type.",
|
||||
"webhooks_unlocked_by_team_admins_description": "You can add webhooks to this event type. You are able to see the active managed webhooks but will not be able to edit them.",
|
||||
"locked_by_team_admin": "Locked by team admin",
|
||||
"app_not_connected": "You have not connected a {{appName}} account.",
|
||||
"connect_now": "Connect now",
|
||||
@@ -2822,6 +2826,8 @@
|
||||
"rr_distribution_method_balanced_description": "We will monitor how many bookings have been made with each host and compare this with others, disabling some hosts that are too far ahead so bookings are evenly distributed.",
|
||||
"exclude_emails_that_contain": "Exclude emails that contain ...",
|
||||
"exclude_emails_match_found_error_message": "Please enter a valid work email address",
|
||||
"search_columns": "Search columns",
|
||||
"search_options": "Search options",
|
||||
"disable_org_url_label": "Disable public organization profile and redirect",
|
||||
"disable_org_url_description": "Redirects {{orgSlug}}.cal.com to {{destination}}",
|
||||
"single_select": "Single Select",
|
||||
@@ -2830,4 +2836,3 @@
|
||||
"salesforce_ignore_guests": "Do not create new records for guests added to the booking",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Los miembros podrán agregar sus flujos de trabajo personales a este tipo de evento. Los miembros podrán ver los flujos de trabajo del equipo activos pero no podrán editar ninguna configuración del flujo de trabajo.",
|
||||
"workflows_locked_by_team_admins_description": "Podrás ver los flujos de trabajo activos del equipo, pero no podrás editar ninguna configuración de flujo de trabajo ni agregar tus flujos de trabajo personales a este tipo de evento.",
|
||||
"workflows_unlocked_by_team_admins_description": "Podrás habilitar/deshabilitar flujos de trabajo personales en este tipo de evento. Podrás ver los flujos de trabajo activos del equipo, pero no podrás editar ninguna configuración de flujo de trabajo del equipo.",
|
||||
"webhooks_locked_for_members_description": "Los miembros no pueden agregar sus webhooks personales a este tipo de evento. Los miembros podrán ver los webhooks activos pero no podrán editar ninguna configuración de webhook.",
|
||||
"webhooks_unlocked_for_members_description": "Los miembros podrán agregar sus webhooks personales a este tipo de evento. Los miembros podrán ver los webhooks activos pero no podrán editar ninguna configuración de webhook.",
|
||||
"webhooks_locked_by_team_admins_description": "Puedes ver los webhooks administrados activos pero no podrás editar ninguna configuración de webhook ni agregar tus webhooks personales a este tipo de evento.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Puedes agregar webhooks a este tipo de evento. Puedes ver los webhooks administrados activos pero no podrás editarlos.",
|
||||
"locked_by_team_admin": "Bloqueado por el administrador del equipo",
|
||||
"app_not_connected": "No has conectado una cuenta de {{appName}}.",
|
||||
"connect_now": "Conectar ahora",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Monitoreamos cuántas reservas se han realizado con cada anfitrión y las comparamos con otros, deshabilitando algunos anfitriones que están muy adelantados para que las reservas se distribuyan de manera uniforme.",
|
||||
"exclude_emails_that_contain": "Excluir correos electrónicos que contengan ...",
|
||||
"exclude_emails_match_found_error_message": "Por favor, ingresa una dirección de correo electrónico laboral válida",
|
||||
"search_columns": "Buscar columnas",
|
||||
"search_options": "Opciones de búsqueda",
|
||||
"disable_org_url_label": "Deshabilitar perfil público de la organización y redireccionar",
|
||||
"disable_org_url_description": "Redirecciona {{orgSlug}}.cal.com a {{destination}}",
|
||||
"single_select": "Selección única",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Los miembros podrán agregar sus flujos de trabajo personales a este tipo de evento. Los miembros podrán ver los flujos de trabajo del equipo activos pero no podrán editar ninguna configuración del flujo de trabajo.",
|
||||
"workflows_locked_by_team_admins_description": "Podrás ver los flujos de trabajo del equipo activos pero no podrás editar ninguna configuración del flujo de trabajo ni agregar tus flujos de trabajo personales a este tipo de evento.",
|
||||
"workflows_unlocked_by_team_admins_description": "Podrás habilitar/deshabilitar flujos de trabajo personales en este tipo de evento. Podrás ver los flujos de trabajo del equipo activos pero no podrás editar ninguna configuración del flujo de trabajo del equipo.",
|
||||
"webhooks_locked_for_members_description": "Los miembros no pueden añadir sus webhooks personales a este tipo de evento. Los miembros podrán ver los webhooks activos pero no podrán editar ninguna configuración de webhook.",
|
||||
"webhooks_unlocked_for_members_description": "Los miembros podrán añadir sus webhooks personales a este tipo de evento. Los miembros podrán ver los webhooks activos pero no podrán editar ninguna configuración de webhook.",
|
||||
"webhooks_locked_by_team_admins_description": "Puedes ver los webhooks administrados activos pero no podrás editar ninguna configuración de webhook ni añadir tus webhooks personales a este tipo de evento.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Puedes añadir webhooks a este tipo de evento. Puedes ver los webhooks administrados activos pero no podrás editarlos.",
|
||||
"locked_by_team_admin": "Bloqueado por el administrador del equipo",
|
||||
"app_not_connected": "No ha conectado una cuenta de {{appName}}.",
|
||||
"connect_now": "Conectar ahora",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Monitorizaremos cuántas reservas se han realizado con cada anfitrión y las compararemos con otros, deshabilitando algunos anfitriones que estén muy por delante para que las reservas se distribuyan uniformemente.",
|
||||
"exclude_emails_that_contain": "Excluir correos que contengan...",
|
||||
"exclude_emails_match_found_error_message": "Por favor, introduce una dirección de correo electrónico laboral válida",
|
||||
"search_columns": "Buscar columnas",
|
||||
"search_options": "Opciones de búsqueda",
|
||||
"disable_org_url_label": "Desactivar perfil público de la organización y redireccionar",
|
||||
"disable_org_url_description": "Redirecciona {{orgSlug}}.cal.com a {{destination}}",
|
||||
"single_select": "Selección única",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Liikmed saavad sellele sündmusetüübile lisada oma isiklikud töövood. Liikmed näevad aktiivseid meeskonna töövooge, kuid ei saa muuta töövoo sätteid.",
|
||||
"workflows_locked_by_team_admins_description": "Näete aktiivseid meeskonnatöövooge, kuid te ei saa muuta töövoo sätteid ega lisada sellele sündmusetüübile oma isiklikke töövooge.",
|
||||
"workflows_unlocked_by_team_admins_description": "Selle sündmusetüübi puhul saate lubada/keelata isiklikke töövooge. Näete aktiivseid meeskonnatöövooge, kuid ei saa muuta meeskonna töövoo sätteid.",
|
||||
"webhooks_locked_for_members_description": "Liikmed ei saa sellele sündmuse tüübile oma isiklikke webhook'e lisada. Liikmed näevad aktiivseid webhook'e, kuid ei saa webhook'i seadeid muuta.",
|
||||
"webhooks_unlocked_for_members_description": "Liikmed saavad sellele sündmuse tüübile oma isiklikke webhook'e lisada. Liikmed näevad aktiivseid webhook'e, kuid ei saa webhook'i seadeid muuta.",
|
||||
"webhooks_locked_by_team_admins_description": "Sul on võimalik näha aktiivseid hallatavaid webhook'e, kuid sa ei saa muuta webhook'i seadeid ega lisada oma isiklikke webhook'e sellele sündmuse tüübile.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Sa saad sellele sündmuse tüübile webhook'e lisada. Sul on võimalik näha aktiivseid hallatavaid webhook'e, kuid sa ei saa neid muuta.",
|
||||
"locked_by_team_admin": "Meeskonna administraatori poolt lukustatud",
|
||||
"app_not_connected": "Te pole rakenduse {{appName}} kontot ühendanud.",
|
||||
"connect_now": "Ühenda kohe",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Jälgime, kui palju broneeringuid on tehtud iga võõrustajaga ja võrdleme seda teistega, keelates mõned võõrustajad, kes on liiga kaugel ees, et broneeringud oleksid ühtlaselt jaotatud.",
|
||||
"exclude_emails_that_contain": "Välista e-kirjad, mis sisaldavad ...",
|
||||
"exclude_emails_match_found_error_message": "Palun sisestage kehtiv tööalane e-posti aadress",
|
||||
"search_columns": "Otsi veerge",
|
||||
"search_options": "Otsingusuvandid",
|
||||
"disable_org_url_label": "Keela avalik organisatsiooni profiil ja suuna ümber",
|
||||
"disable_org_url_description": "Suunab {{orgSlug}}.cal.com ümber aadressile {{destination}}",
|
||||
"single_select": "Üksikvalik",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Kideek beren lan-fluxu pertsonalak gehitu ahal izango dituzte gertaera mota honetara. Kideek taldeko lan-fluxu aktiboak ikusi ahal izango dituzte baina ezin izango dituzte lan-fluxuen ezarpenak editatu.",
|
||||
"workflows_locked_by_team_admins_description": "Taldeko lan-fluxu aktiboak ikusi ahal izango dituzu baina ezin izango dituzu lan-fluxuen ezarpenak editatu edo zure lan-fluxu pertsonalak gehitu gertaera mota honetara.",
|
||||
"workflows_unlocked_by_team_admins_description": "Gertaera mota honetan workflow pertsonalak gaitu/desgaitu ahal izango dituzu. Taldeko workflow aktiboak ikusi ahal izango dituzu, baina ezin izango dituzu taldeko workflow-ezarpenak editatu.",
|
||||
"webhooks_locked_for_members_description": "Kideek ezin dituzte webhook pertsonalak gehitu gertaera mota honetara. Kideek webhook aktiboak ikusi ahal izango dituzte, baina ezin izango dituzte webhook ezarpenak editatu.",
|
||||
"webhooks_unlocked_for_members_description": "Kideek webhook pertsonalak gehitu ahal izango dituzte gertaera mota honetara. Kideek webhook aktiboak ikusi ahal izango dituzte, baina ezin izango dituzte webhook ezarpenak editatu.",
|
||||
"webhooks_locked_by_team_admins_description": "Kudeatutako webhook aktiboak ikusi ahal izango dituzu, baina ezin izango dituzu webhook ezarpenak editatu edo zure webhook pertsonalak gehitu gertaera mota honetara.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Webhook-ak gehitu ditzakezu gertaera mota honetara. Kudeatutako webhook aktiboak ikusi ahal izango dituzu, baina ezin izango dituzu editatu.",
|
||||
"locked_by_team_admin": "Taldeko administratzaileak blokeatuta",
|
||||
"app_not_connected": "Ez duzu {{appName}} konturik konektatu.",
|
||||
"connect_now": "Konektatu orain",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Ostalari bakoitzarekin egin diren erreserben kopurua monitorizatu eta besteekin konparatuko dugu, aurreregi doazen ostalari batzuk desgaituz erreserbak modu orekatuan banatzeko",
|
||||
"exclude_emails_that_contain": "Baztertu hau duten emailak...",
|
||||
"exclude_emails_match_found_error_message": "Mesedez, sartu baliozko lan helbide elektroniko bat",
|
||||
"search_columns": "Bilatu zutabeak",
|
||||
"search_options": "Bilaketa-aukerak",
|
||||
"disable_org_url_label": "Desgaitu erakundearen profil publikoa eta birbideratu",
|
||||
"disable_org_url_description": "{{orgSlug}}.cal.com {{destination}}-era birbideratzen du",
|
||||
"single_select": "Hautapen bakarra",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Les membres pourront ajouter leurs propres flux de travail à ce type d'évènement. Ils pourront voir les flux de travail actifs de l'équipe, mais ne pourront pas en modifier les paramètres.",
|
||||
"workflows_locked_by_team_admins_description": "Vous pourrez voir les flux de travail actifs de l'équipe, mais vous ne pourrez pas modifier les paramètres de flux de travail ni ajouter vos flux de travail personnels à ce type d'évènement.",
|
||||
"workflows_unlocked_by_team_admins_description": "Vous pourrez activer/désactiver les flux de travail personnels pour ce type d'évènement. Vous pourrez voir les flux de travail d'équipe actifs, mais vous ne pourrez pas modifier les paramètres des flux de travail d'équipe.",
|
||||
"webhooks_locked_for_members_description": "Les membres ne peuvent pas ajouter leurs webhooks personnels à ce type d'événement. Les membres pourront voir les webhooks actifs mais ne pourront pas modifier les paramètres des webhooks.",
|
||||
"webhooks_unlocked_for_members_description": "Les membres pourront ajouter leurs webhooks personnels à ce type d'événement. Les membres pourront voir les webhooks actifs mais ne pourront pas modifier les paramètres des webhooks.",
|
||||
"webhooks_locked_by_team_admins_description": "Vous pouvez voir les webhooks gérés actifs mais vous ne pourrez pas modifier les paramètres des webhooks ni ajouter vos webhooks personnels à ce type d'événement.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Vous pouvez ajouter des webhooks à ce type d'événement. Vous pouvez voir les webhooks gérés actifs mais vous ne pourrez pas les modifier.",
|
||||
"locked_by_team_admin": "Verrouillé par l'administrateur de l'équipe",
|
||||
"app_not_connected": "Vous n'avez pas connecté de compte {{appName}}.",
|
||||
"connect_now": "Connecter maintenant",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Nous surveillerons le nombre de réservations effectuées avec chaque hôte et le comparerons avec les autres, en désactivant certains hôtes trop en avance pour que les réservations soient réparties équitablement.",
|
||||
"exclude_emails_that_contain": "Exclure les e-mails contenant ...",
|
||||
"exclude_emails_match_found_error_message": "Veuillez entrer une adresse e-mail professionnelle valide",
|
||||
"search_columns": "Rechercher dans les colonnes",
|
||||
"search_options": "Options de recherche",
|
||||
"disable_org_url_label": "Désactiver le profil public de l'organisation et rediriger",
|
||||
"disable_org_url_description": "Redirige {{orgSlug}}.cal.com vers {{destination}}",
|
||||
"single_select": "Sélection unique",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "חברים יוכלו להוסיף את זרימות העבודה האישיות שלהם לסוג אירוע זה. חברים יוכלו לראות את זרימות העבודה הפעילות של הצוות אך לא יוכלו לערוך הגדרות של זרימות עבודה.",
|
||||
"workflows_locked_by_team_admins_description": "תוכלו לראות את זרימות העבודה הפעילות של הצוות אך לא תוכלו לערוך הגדרות של זרימות עבודה או להוסיף את זרימות העבודה האישיות שלכם לסוג אירוע זה.",
|
||||
"workflows_unlocked_by_team_admins_description": "תוכל להפעיל/לכבות תהליכים אישיים בסוג אירוע זה. תוכל לראות את התהליכים הפעילים של הצוות אך לא תוכל לערוך את הגדרות התהליכים של הצוות.",
|
||||
"webhooks_locked_for_members_description": "חברי הצוות אינם יכולים להוסיף וובהוקים אישיים לסוג אירוע זה. חברי הצוות יוכלו לראות את הוובהוקים הפעילים אך לא יוכלו לערוך את הגדרות הוובהוק.",
|
||||
"webhooks_unlocked_for_members_description": "חברי הצוות יוכלו להוסיף וובהוקים אישיים לסוג אירוע זה. חברי הצוות יוכלו לראות את הוובהוקים הפעילים אך לא יוכלו לערוך את הגדרות הוובהוק.",
|
||||
"webhooks_locked_by_team_admins_description": "באפשרותך לראות את הוובהוקים המנוהלים הפעילים אך לא תוכל לערוך הגדרות וובהוק או להוסיף וובהוקים אישיים לסוג אירוע זה.",
|
||||
"webhooks_unlocked_by_team_admins_description": "באפשרותך להוסיף וובהוקים לסוג אירוע זה. תוכל לראות את הוובהוקים המנוהלים הפעילים אך לא תוכל לערוך אותם.",
|
||||
"locked_by_team_admin": "ננעל על ידי מנהל/ת הצוות",
|
||||
"app_not_connected": "לא קישרת חשבון {{appName}}.",
|
||||
"connect_now": "להתחבר עכשיו",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "אנחנו נעקוב אחר מספר ההזמנות שנקבעו עם כל מארח ונשווה זאת לאחרים, תוך השבתה זמנית של מארחים שקיבלו יותר מדי הזמנות כדי להבטיח חלוקה שווה",
|
||||
"exclude_emails_that_contain": "הוצא כתובות דוא\"ל המכילות...",
|
||||
"exclude_emails_match_found_error_message": "אנא הזן כתובת דוא\"ל עבודה תקינה",
|
||||
"search_columns": "חיפוש עמודות",
|
||||
"search_options": "אפשרויות חיפוש",
|
||||
"disable_org_url_label": "השבת פרופיל ארגוני ציבורי והפנה מחדש",
|
||||
"disable_org_url_description": "מפנה מחדש את {{orgSlug}}.cal.com אל {{destination}}",
|
||||
"single_select": "בחירה בודדת",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "A tagok személyes munkafolyamataikat is hozzáadhatják ehhez az eseménytípushoz. A tagok láthatják az aktív csapatmunkafolyamatokat, de nem szerkeszthetik a munkafolyamat-beállításokat.",
|
||||
"workflows_locked_by_team_admins_description": "Látni fogja az aktív csapatmunkafolyamatokat, de nem szerkesztheti a munkafolyamat-beállításokat, és nem adhatja hozzá személyes munkafolyamatait ehhez az eseménytípushoz.",
|
||||
"workflows_unlocked_by_team_admins_description": "Engedélyezheti/letilthatja a személyes munkafolyamatokat ennél az eseménytípusnál. Látni fogja az aktív csoportmunkafolyamatokat, de nem szerkesztheti a csapatmunkafolyamat-beállításokat.",
|
||||
"webhooks_locked_for_members_description": "A tagok nem adhatnak hozzá személyes webhookokat ehhez az eseménytípushoz. A tagok láthatják az aktív webhookokat, de nem módosíthatják a webhook beállításokat.",
|
||||
"webhooks_unlocked_for_members_description": "A tagok hozzáadhatják személyes webhookjaikat ehhez az eseménytípushoz. A tagok láthatják az aktív webhookokat, de nem módosíthatják a webhook beállításokat.",
|
||||
"webhooks_locked_by_team_admins_description": "Láthatod az aktív menedzselt webhookokat, de nem módosíthatod a webhook beállításokat, és nem adhatsz hozzá személyes webhookokat ehhez az eseménytípushoz.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Hozzáadhatsz webhookokat ehhez az eseménytípushoz. Láthatod az aktív menedzselt webhookokat, de nem módosíthatod azokat.",
|
||||
"locked_by_team_admin": "Csapatadminisztrátor zárolta",
|
||||
"app_not_connected": "Nem csatlakoztatott {{appName}}-fiókot.",
|
||||
"connect_now": "Csatlakozzon most",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Figyelemmel kísérjük, hogy hány foglalás történt az egyes házigazdákkal, és összehasonlítjuk másokkal, letiltva azokat a házigazdákat, akik túl sok foglalással rendelkeznek, így a foglalások egyenletesen oszlanak el.",
|
||||
"exclude_emails_that_contain": "Zárja ki azokat az e-maileket, amelyek tartalmazzák ...",
|
||||
"exclude_emails_match_found_error_message": "Kérjük, adjon meg egy érvényes munkahelyi e-mail címet",
|
||||
"search_columns": "Oszlopok keresése",
|
||||
"search_options": "Keresési beállítások",
|
||||
"disable_org_url_label": "Nyilvános szervezeti profil és átirányítás kikapcsolása",
|
||||
"disable_org_url_description": "Átirányítja a {{orgSlug}}.cal.com címet a {{destination}} címre",
|
||||
"single_select": "Egyszeres kiválasztás",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "I membri possono aggiungere i loro flussi di lavoro personali a questo tipo di evento. I membri potranno vedere i flussi di lavoro attivi del team, ma non potranno modificare le impostazioni dei flussi di lavoro.",
|
||||
"workflows_locked_by_team_admins_description": "Potrai vedere i flussi di lavoro attivi del team, ma non potrai modificare le impostazioni dei flussi di lavoro né aggiungere i tuoi flussi di lavoro personali a questo tipo di evento.",
|
||||
"workflows_unlocked_by_team_admins_description": "Potrai attivare/disattivare flussi di lavoro personali su questo tipo di evento. Potrai vedere i flussi di lavoro attivi del team, ma non potrai modificare le impostazioni dei flussi di lavoro.",
|
||||
"webhooks_locked_for_members_description": "I membri non possono aggiungere i propri webhook personali a questo tipo di evento. I membri potranno vedere i webhook attivi ma non potranno modificare alcuna impostazione dei webhook.",
|
||||
"webhooks_unlocked_for_members_description": "I membri potranno aggiungere i propri webhook personali a questo tipo di evento. I membri potranno vedere i webhook attivi ma non potranno modificare alcuna impostazione dei webhook.",
|
||||
"webhooks_locked_by_team_admins_description": "Puoi vedere i webhook gestiti attivi ma non potrai modificare alcuna impostazione dei webhook o aggiungere i tuoi webhook personali a questo tipo di evento.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Puoi aggiungere webhook a questo tipo di evento. Puoi vedere i webhook gestiti attivi ma non potrai modificarli.",
|
||||
"locked_by_team_admin": "Bloccato dall'amministratore del team",
|
||||
"app_not_connected": "Non è stato connesso un account di {{appName}}.",
|
||||
"connect_now": "Connetti ora",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Monitoreremo quante prenotazioni sono state effettuate con ciascun host e le confronteremo con gli altri, disabilitando alcuni host che sono troppo avanti per garantire una distribuzione uniforme delle prenotazioni.",
|
||||
"exclude_emails_that_contain": "Escludi le email che contengono ...",
|
||||
"exclude_emails_match_found_error_message": "Inserisci un indirizzo email di lavoro valido",
|
||||
"search_columns": "Cerca colonne",
|
||||
"search_options": "Opzioni di ricerca",
|
||||
"disable_org_url_label": "Disabilita il profilo pubblico dell'organizzazione e reindirizza",
|
||||
"disable_org_url_description": "Reindirizza {{orgSlug}}.cal.com a {{destination}}",
|
||||
"single_select": "Selezione singola",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "メンバーはこのイベントタイプに個人のワークフローを追加できます。アクティブなチームのワークフローを確認できますが、ワークフローの設定を編集することはできません。",
|
||||
"workflows_locked_by_team_admins_description": "アクティブなチームのワークフローを確認できますが、ワークフローの設定を編集したり、個人のワークフローをこのイベントタイプに追加することはできません。",
|
||||
"workflows_unlocked_by_team_admins_description": "このイベントタイプで個人のワークフローを有効/無効にできます。アクティブなチームのワークフローを確認できますが、チームのワークフロー設定を編集することはできません。",
|
||||
"webhooks_locked_for_members_description": "メンバーは、このイベントタイプに個人のウェブフックを追加することはできません。メンバーはアクティブなウェブフックを閲覧できますが、ウェブフックの設定を編集することはできません。",
|
||||
"webhooks_unlocked_for_members_description": "メンバーは、このイベントタイプに個人のウェブフックを追加することができます。メンバーはアクティブなウェブフックを閲覧できますが、ウェブフックの設定を編集することはできません。",
|
||||
"webhooks_locked_by_team_admins_description": "管理されているアクティブなウェブフックを閲覧することはできますが、ウェブフックの設定を編集したり、このイベントタイプに個人のウェブフックを追加したりすることはできません。",
|
||||
"webhooks_unlocked_by_team_admins_description": "このイベントタイプにウェブフックを追加することができます。管理されているアクティブなウェブフックを閲覧することはできますが、それらを編集することはできません。",
|
||||
"locked_by_team_admin": "チームの管理者によりロックされています",
|
||||
"app_not_connected": "{{appName}} のアカウントに接続していません。",
|
||||
"connect_now": "今すぐ接続",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "各ホストの予約数を監視し、他のホストと比較して予約数が多すぎるホストを一時的に無効にすることで、予約を均等に配分します。",
|
||||
"exclude_emails_that_contain": "...を含むメールアドレスを除外",
|
||||
"exclude_emails_match_found_error_message": "有効な勤務用メールアドレスを入力してください",
|
||||
"search_columns": "列を検索",
|
||||
"search_options": "検索オプション",
|
||||
"disable_org_url_label": "組織の公開プロフィールを無効にしてリダイレクト",
|
||||
"disable_org_url_description": "{{orgSlug}}.cal.comを{{destination}}にリダイレクトします",
|
||||
"single_select": "単一選択",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "សមាជិកនឹងអាចបន្ថែមលំហូរការងារផ្ទាល់ខ្លួនរបស់ពួកគេទៅប្រភេទព្រឹត្តិការណ៍នេះបាន។ សមាជិកនឹងអាចមើលឃើញលំហូរការងារក្រុមសកម្ម ប៉ុន្តែពួកគេនឹងមិនអាចកែប្រែការកំណត់លំហូរការងារណាមួយបានទេ។",
|
||||
"workflows_locked_by_team_admins_description": "អ្នកនឹងអាចមើលលំហូរការងាររបស់ក្រុមដែលកំពុងដំណើរការប៉ុន្តែអ្នកនឹងមិនអាចកែប្រែការកំណត់លំហូរការងារណាមួយ ឬបន្ថែមលំហូរការងារផ្ទាល់ខ្លួនរបស់អ្នកទៅប្រភេទព្រឹត្តិការណ៍នេះទេ។",
|
||||
"workflows_unlocked_by_team_admins_description": "អ្នកនឹងអាចបើក/បិទលំហូរការងារផ្ទាល់ខ្លួននៅលើប្រភេទព្រឹត្តិការណ៍នេះ។ អ្នកនឹងអាចមើលលំហូរការងាររបស់ក្រុមដែលកំពុងដំណើរការប៉ុន្តែអ្នកនឹងមិនអាចកែប្រែការកំណត់លំហូរការងាររបស់ក្រុមណាមួយទេ។",
|
||||
"webhooks_locked_for_members_description": "សមាជិកមិនអាចបន្ថែម webhooks ផ្ទាល់ខ្លួនរបស់ពួកគេទៅក្នុងប្រភេទព្រឹត្តិការណ៍នេះបានទេ។ សមាជិកនឹងអាចមើលឃើញ webhooks ដែលកំពុងដំណើរការ ប៉ុន្តេនឹងមិនអាចកែប្រែការកំណត់ webhooks ណាមួយបានទេ។",
|
||||
"webhooks_unlocked_for_members_description": "សមាជិកនឹងអាចបន្ថែម webhooks ផ្ទាល់ខ្លួនរបស់ពួកគេទៅក្នុងប្រភេទព្រឹត្តិការណ៍នេះ។ សមាជិកនឹងអាចមើលឃើញ webhooks ដែលកំពុងដំណើរការ ប៉ុន្តេនឹងមិនអាចកែប្រែការកំណត់ webhooks ណាមួយបានទេ។",
|
||||
"webhooks_locked_by_team_admins_description": "អ្នកអាចមើលឃើញ webhooks ដែលគ្រប់គ្រងដែលកំពុងដំណើរការ ប៉ុន្តេនឹងមិនអាចកែប្រែការកំណត់ webhooks ណាមួយ ឬបន្ថែម webhooks ផ្ទាល់ខ្លួនរបស់អ្នកទៅក្នុងប្រភេទព្រឹត្តិការណ៍នេះបានទេ។",
|
||||
"webhooks_unlocked_by_team_admins_description": "អ្នកអាចបន្ថែម webhooks ទៅក្នុងប្រភេទព្រឹត្តិការណ៍នេះ។ អ្នកអាចមើលឃើញ webhooks ដែលគ្រប់គ្រងដែលកំពុងដំណើរការ ប៉ុន្តេនឹងមិនអាចកែប្រែវាបានទេ។",
|
||||
"locked_by_team_admin": "បានចាក់សោដោយអ្នកគ្រប់គ្រងក្រុម",
|
||||
"app_not_connected": "អ្នកមិនបានភ្ជាប់គណនី {{appName}} ទេ។",
|
||||
"connect_now": "ភ្ជាប់ឥឡូវនេះ",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "យើងនឹងតាមដានចំនួនការកក់ដែលបានធ្វើឡើងជាមួយម្ចាស់នីមួយៗ និងប្រៀបធៀបជាមួយអ្នកផ្សេងទៀត ដោយបិទម្ចាស់មួយចំនួនដែលមានការកក់ច្រើនពេកដើម្បីឱ្យការកក់ត្រូវបានចែកចាយស្មើៗគ្នា។",
|
||||
"exclude_emails_that_contain": "ច្រានចោលអ៊ីមែលដែលមាន ...",
|
||||
"exclude_emails_match_found_error_message": "សូមបញ្ចូលអាសយដ្ឋានអ៊ីមែលការងារដែលមានសុពលភាព",
|
||||
"search_columns": "ស្វែងរកជួរឈរ",
|
||||
"search_options": "ជម្រើសស្វែងរក",
|
||||
"disable_org_url_label": "បិទទម្រង់សាធារណៈរបស់ស្ថាប័ន និងបញ្ជូនបន្ត",
|
||||
"disable_org_url_description": "បញ្ជូនបន្តពី {{orgSlug}}.cal.com ទៅកាន់ {{destination}}",
|
||||
"single_select": "ជ្រើសរើសតែមួយ",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "회원은 이 이벤트 유형에 개인 워크플로를 추가할 수 있습니다. 회원은 활성 팀 워크플로를 볼 수 있지만 워크플로 설정을 편집할 수 없습니다.",
|
||||
"workflows_locked_by_team_admins_description": "활성 팀 워크플로를 볼 수 있지만 워크플로 설정을 편집하거나 개인 워크플로를 이 이벤트 유형에 추가할 수 없습니다.",
|
||||
"workflows_unlocked_by_team_admins_description": "이 이벤트 유형에서 개인 워크플로를 활성화/비활성화할 수 있습니다. 활성 팀 워크플로를 볼 수 있지만 팀 워크플로 설정을 편집할 수 없습니다.",
|
||||
"webhooks_locked_for_members_description": "멤버는 이 이벤트 유형에 개인 웹훅을 추가할 수 없습니다. 멤버는 활성화된 웹훅을 볼 수는 있지만 웹훅 설정을 수정할 수는 없습니다.",
|
||||
"webhooks_unlocked_for_members_description": "멤버는 이 이벤트 유형에 개인 웹훅을 추가할 수 있습니다. 멤버는 활성화된 웹훅을 볼 수는 있지만 웹훅 설정을 수정할 수는 없습니다.",
|
||||
"webhooks_locked_by_team_admins_description": "활성화된 관리 웹훅을 볼 수는 있지만, 웹훅 설정을 수정하거나 이 이벤트 유형에 개인 웹훅을 추가할 수는 없습니다.",
|
||||
"webhooks_unlocked_by_team_admins_description": "이 이벤트 유형에 웹훅을 추가할 수 있습니다. 활성화된 관리 웹훅을 볼 수는 있지만 수정할 수는 없습니다.",
|
||||
"locked_by_team_admin": "팀 관리자에 의해 잠김",
|
||||
"app_not_connected": "{{appName}} 계정을 연결하지 않았습니다.",
|
||||
"connect_now": "지금 연결",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "각 호스트의 예약 수를 모니터링하고 다른 호스트들과 비교하여, 예약이 균등하게 분배되도록 예약이 너무 많은 호스트를 일시적으로 비활성화합니다.",
|
||||
"exclude_emails_that_contain": "다음이 포함된 이메일 제외 ...",
|
||||
"exclude_emails_match_found_error_message": "유효한 업무용 이메일 주소를 입력해 주세요.",
|
||||
"search_columns": "열 검색",
|
||||
"search_options": "검색 옵션",
|
||||
"disable_org_url_label": "공개 조직 프로필 비활성화 및 리디렉션",
|
||||
"disable_org_url_description": "{{orgSlug}}.cal.com을 {{destination}}(으)로 리디렉션합니다",
|
||||
"single_select": "단일 선택",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Leden kunnen hun persoonlijke workflows toevoegen aan dit type evenement. Leden kunnen de actieve teamworkflows zien, maar kunnen geen workflow-instellingen bewerken.",
|
||||
"workflows_locked_by_team_admins_description": "Je zult de actieve teamworkflows kunnen zien, maar je kunt geen workflow-instellingen bewerken of je persoonlijke workflows toevoegen aan dit type evenement.",
|
||||
"workflows_unlocked_by_team_admins_description": "Je kunt persoonlijke workflows in- of uitschakelen voor dit type evenement. Je zult de actieve teamworkflows kunnen zien, maar je kunt geen teamworkflow-instellingen bewerken.",
|
||||
"webhooks_locked_for_members_description": "Leden kunnen geen persoonlijke webhooks toevoegen aan dit afspraaktype. Leden kunnen wel de actieve webhooks zien, maar kunnen geen webhook-instellingen bewerken.",
|
||||
"webhooks_unlocked_for_members_description": "Leden kunnen hun persoonlijke webhooks toevoegen aan dit afspraaktype. Leden kunnen de actieve webhooks zien, maar kunnen geen webhook-instellingen bewerken.",
|
||||
"webhooks_locked_by_team_admins_description": "Je kunt de actieve beheerde webhooks zien, maar je kunt geen webhook-instellingen bewerken of je persoonlijke webhooks toevoegen aan dit afspraaktype.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Je kunt webhooks toevoegen aan dit afspraaktype. Je kunt de actieve beheerde webhooks zien, maar je kunt deze niet bewerken.",
|
||||
"locked_by_team_admin": "Vergrendeld door teambeheerder",
|
||||
"app_not_connected": "U hebt geen {{appName}}-account gekoppeld.",
|
||||
"connect_now": "Nu koppelen",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "We monitoren hoeveel boekingen er bij elke host zijn gemaakt en vergelijken dit met anderen. Hosts die te ver voorlopen worden tijdelijk uitgeschakeld zodat boekingen gelijkmatig worden verdeeld.",
|
||||
"exclude_emails_that_contain": "E-mailadressen uitsluiten die ... bevatten",
|
||||
"exclude_emails_match_found_error_message": "Voer een geldig zakelijk e-mailadres in",
|
||||
"search_columns": "Kolommen zoeken",
|
||||
"search_options": "Zoekopties",
|
||||
"disable_org_url_label": "Openbaar organisatieprofiel uitschakelen en doorverwijzen",
|
||||
"disable_org_url_description": "Verwijst {{orgSlug}}.cal.com door naar {{destination}}",
|
||||
"single_select": "Enkele selectie",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Medlemmer vil kunne legge til sine personlige arbeidsflyter til denne arrangementtypen. Medlemmer vil kunne se de aktive teamarbeidsflytene, men vil ikke kunne redigere noen arbeidsflytinnstillinger.",
|
||||
"workflows_locked_by_team_admins_description": "Du vil kunne se de aktive teamarbeidsflytene, men vil ikke kunne redigere noen arbeidsflytinnstillinger eller legge til dine personlige arbeidsflyter til denne hendelsestypen.",
|
||||
"workflows_unlocked_by_team_admins_description": "Du vil kunne aktivere/deaktivere personlige arbeidsflyter for denne hendelsestypen. Du vil kunne se de aktive teamarbeidsflytene, men vil ikke kunne redigere noen teamarbeidsflytinnstillinger.",
|
||||
"webhooks_locked_for_members_description": "Medlemmer kan ikke legge til sine personlige webhooks til denne hendelsestypen. Medlemmer vil kunne se aktive webhooks, men vil ikke kunne redigere noen webhook-innstillinger.",
|
||||
"webhooks_unlocked_for_members_description": "Medlemmer vil kunne legge til sine personlige webhooks til denne hendelsestypen. Medlemmer vil kunne se aktive webhooks, men vil ikke kunne redigere noen webhook-innstillinger.",
|
||||
"webhooks_locked_by_team_admins_description": "Du kan se de aktive administrerte webhooks, men vil ikke kunne redigere noen webhook-innstillinger eller legge til dine personlige webhooks til denne hendelsestypen.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Du kan legge til webhooks til denne hendelsestypen. Du kan se de aktive administrerte webhooks, men vil ikke kunne redigere dem.",
|
||||
"locked_by_team_admin": "Låst av teamadministrator",
|
||||
"app_not_connected": "Du har ikke koblet til en {{appName}}-konto.",
|
||||
"connect_now": "Koble til nå",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Vi vil overvåke hvor mange bestillinger som er gjort med hver vert og sammenligne dette med andre, og deaktivere enkelte verter som ligger for langt foran slik at bestillinger blir jevnt fordelt.",
|
||||
"exclude_emails_that_contain": "Ekskluder e-poster som inneholder ...",
|
||||
"exclude_emails_match_found_error_message": "Vennligst skriv inn en gyldig arbeids-e-postadresse",
|
||||
"search_columns": "Søk i kolonner",
|
||||
"search_options": "Søkealternativer",
|
||||
"disable_org_url_label": "Deaktiver offentlig organisasjonsprofil og omdirigering",
|
||||
"disable_org_url_description": "Omdirigerer {{orgSlug}}.cal.com til {{destination}}",
|
||||
"single_select": "Enkeltvalg",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Członkowie będą mogli dodawać swoje osobiste przepływy pracy do tego typu wydarzenia. Członkowie będą mogli zobaczyć aktywne przepływy pracy zespołu, ale nie będą mogli edytować żadnych ustawień przepływu pracy.",
|
||||
"workflows_locked_by_team_admins_description": "Będziesz mógł zobaczyć aktywne przepływy pracy zespołu, ale nie będziesz mógł edytować żadnych ustawień przepływu pracy ani dodawać swoich osobistych przepływów pracy do tego typu wydarzenia.",
|
||||
"workflows_unlocked_by_team_admins_description": "Będziesz mógł włączać/wyłączać osobiste przepływy pracy dla tego typu wydarzenia. Będziesz mógł zobaczyć aktywne przepływy pracy zespołu, ale nie będziesz mógł edytować żadnych ustawień przepływu pracy zespołu.",
|
||||
"webhooks_locked_for_members_description": "Członkowie nie mogą dodawać swoich osobistych webhooków do tego typu wydarzenia. Członkowie będą mogli zobaczyć aktywne webhooki, ale nie będą mogli edytować żadnych ustawień webhooków.",
|
||||
"webhooks_unlocked_for_members_description": "Członkowie będą mogli dodawać swoje osobiste webhooki do tego typu wydarzenia. Członkowie będą mogli zobaczyć aktywne webhooki, ale nie będą mogli edytować żadnych ustawień webhooków.",
|
||||
"webhooks_locked_by_team_admins_description": "Możesz zobaczyć aktywne zarządzane webhooki, ale nie będziesz mógł edytować żadnych ustawień webhooków ani dodawać swoich osobistych webhooków do tego typu wydarzenia.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Możesz dodawać webhooki do tego typu wydarzenia. Możesz zobaczyć aktywne zarządzane webhooki, ale nie będziesz mógł ich edytować.",
|
||||
"locked_by_team_admin": "Zablokowane przez administratora zespołu",
|
||||
"app_not_connected": "Nie podłączono konta {{appName}}.",
|
||||
"connect_now": "Podłącz teraz",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "System będzie monitorować liczbę rezerwacji przypisanych do każdego gospodarza i porównywać ją z innymi, dezaktywując tymczasowo gospodarzy z większą liczbą spotkań, aby zapewnić równomierną dystrybucję.",
|
||||
"exclude_emails_that_contain": "Wyklucz e-maile, które zawierają ...",
|
||||
"exclude_emails_match_found_error_message": "Proszę wprowadzić prawidłowy adres e-mail służbowy",
|
||||
"search_columns": "Szukaj w kolumnach",
|
||||
"search_options": "Opcje wyszukiwania",
|
||||
"disable_org_url_label": "Wyłącz publiczny profil organizacji i przekierowanie",
|
||||
"disable_org_url_description": "Przekierowuje {{orgSlug}}.cal.com na {{destination}}",
|
||||
"single_select": "Pojedynczy wybór",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Membros poderão adicionar seus fluxos de trabalho pessoais a este tipo de evento. Membros poderão ver os fluxos de trabalho ativos da equipe mas não poderão editar configurações de fluxo de trabalho.",
|
||||
"workflows_locked_by_team_admins_description": "Você pode ver os fluxos de trabalho ativos da equipe mas não poderá editar as configurações ou adicionar seus fluxos de trabalho pessoais a este tipo de evento.",
|
||||
"workflows_unlocked_by_team_admins_description": "Você poderá habilitar/desabilitar fluxos de trabalho pessoais neste tipo de evento. Você poderá ver os fluxos de trabalho ativos da equipe mas não poderá editar configurações de fluxo de trabalho.",
|
||||
"webhooks_locked_for_members_description": "Os membros não podem adicionar seus webhooks pessoais a este tipo de evento. Os membros poderão ver os webhooks ativos, mas não poderão editar nenhuma configuração de webhook.",
|
||||
"webhooks_unlocked_for_members_description": "Os membros poderão adicionar seus webhooks pessoais a este tipo de evento. Os membros poderão ver os webhooks ativos, mas não poderão editar nenhuma configuração de webhook.",
|
||||
"webhooks_locked_by_team_admins_description": "Você pode ver os webhooks gerenciados ativos, mas não poderá editar nenhuma configuração de webhook ou adicionar seus webhooks pessoais a este tipo de evento.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Você pode adicionar webhooks a este tipo de evento. Você pode ver os webhooks gerenciados ativos, mas não poderá editá-los.",
|
||||
"locked_by_team_admin": "Bloqueado pelo administrador da equipe",
|
||||
"app_not_connected": "Você não conectou uma conta do {{appName}}.",
|
||||
"connect_now": "Conectar agora",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Monitoraremos quantas reservas foram feitas com cada anfitrião e compararemos com os outros, desativando alguns anfitriões que estão muito à frente para que as reservas sejam distribuídas uniformemente.",
|
||||
"exclude_emails_that_contain": "Excluir e-mails que contêm ...",
|
||||
"exclude_emails_match_found_error_message": "Por favor, insira um endereço de e-mail corporativo válido",
|
||||
"search_columns": "Buscar colunas",
|
||||
"search_options": "Opções de busca",
|
||||
"disable_org_url_label": "Desativar perfil público da organização e redirecionar",
|
||||
"disable_org_url_description": "Redireciona {{orgSlug}}.cal.com para {{destination}}",
|
||||
"single_select": "Seleção única",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Os membros poderão adicionar os seus fluxos de trabalho pessoais a este tipo de evento. Os membros poderão ver os fluxos de trabalho da equipa ativos, mas não poderão editar quaisquer configurações de fluxo de trabalho.",
|
||||
"workflows_locked_by_team_admins_description": "Você poderá ver os fluxos de trabalho da equipa ativos, mas não poderá editar quaisquer configurações de fluxo de trabalho nem adicionar os seus fluxos de trabalho pessoais a este tipo de evento.",
|
||||
"workflows_unlocked_by_team_admins_description": "Você poderá ativar/desativar fluxos de trabalho pessoais neste tipo de evento. Você poderá ver os fluxos de trabalho da equipa ativos, mas não poderá editar quaisquer configurações de fluxo de trabalho da equipa.",
|
||||
"webhooks_locked_for_members_description": "Os membros não podem adicionar seus webhooks pessoais a este tipo de evento. Os membros poderão ver os webhooks ativos, mas não poderão editar nenhuma configuração de webhook.",
|
||||
"webhooks_unlocked_for_members_description": "Os membros poderão adicionar seus webhooks pessoais a este tipo de evento. Os membros poderão ver os webhooks ativos, mas não poderão editar nenhuma configuração de webhook.",
|
||||
"webhooks_locked_by_team_admins_description": "Você pode ver os webhooks gerenciados ativos, mas não poderá editar nenhuma configuração de webhook ou adicionar seus webhooks pessoais a este tipo de evento.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Você pode adicionar webhooks a este tipo de evento. Você pode ver os webhooks gerenciados ativos, mas não poderá editá-los.",
|
||||
"locked_by_team_admin": "Bloqueado pelo administrador da equipa",
|
||||
"app_not_connected": "Não associou uma conta {{appName}}.",
|
||||
"connect_now": "Associar agora",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Monitoraremos quantas reservas foram feitas com cada anfitrião e compararemos com os outros, desativando alguns anfitriões que estão muito à frente para que as reservas sejam distribuídas uniformemente.",
|
||||
"exclude_emails_that_contain": "Excluir emails que contêm ...",
|
||||
"exclude_emails_match_found_error_message": "Por favor, insira um endereço de email de trabalho válido",
|
||||
"search_columns": "Pesquisar colunas",
|
||||
"search_options": "Opções de pesquisa",
|
||||
"disable_org_url_label": "Desativar perfil público da organização e redirecionar",
|
||||
"disable_org_url_description": "Redireciona {{orgSlug}}.cal.com para {{destination}}",
|
||||
"single_select": "Seleção única",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Membrii vor putea adăuga fluxurile lor de lucru personale la acest tip de eveniment. Membrii vor putea vedea fluxurile de lucru active ale echipei, dar nu vor putea edita nicio setare a fluxurilor de lucru.",
|
||||
"workflows_locked_by_team_admins_description": "Veți putea vedea fluxurile de lucru active ale echipei, dar nu veți putea edita nicio setare a fluxurilor de lucru sau adăuga fluxurile dvs. personale la acest tip de eveniment.",
|
||||
"workflows_unlocked_by_team_admins_description": "Veți putea activa/dezactiva fluxurile de lucru personale pentru acest tip de eveniment. Veți putea vedea fluxurile de lucru active ale echipei, dar nu veți putea edita nicio setare a fluxurilor de lucru ale echipei.",
|
||||
"webhooks_locked_for_members_description": "Membrii nu pot adăuga webhook-uri personale la acest tip de eveniment. Membrii vor putea vedea webhook-urile active, dar nu vor putea modifica setările webhook-urilor.",
|
||||
"webhooks_unlocked_for_members_description": "Membrii vor putea adăuga webhook-uri personale la acest tip de eveniment. Membrii vor putea vedea webhook-urile active, dar nu vor putea modifica setările webhook-urilor.",
|
||||
"webhooks_locked_by_team_admins_description": "Poți vedea webhook-urile gestionate active, dar nu vei putea modifica setările webhook-urilor sau adăuga webhook-uri personale la acest tip de eveniment.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Poți adăuga webhook-uri la acest tip de eveniment. Poți vedea webhook-urile gestionate active, dar nu le vei putea modifica.",
|
||||
"locked_by_team_admin": "Blocat de administratorul echipei",
|
||||
"app_not_connected": "Nu ați conectat un cont {{appName}}.",
|
||||
"connect_now": "Conectați-l acum",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Vom monitoriza câte rezervări au fost făcute cu fiecare host și vom compara acest lucru cu ceilalți, dezactivând temporar hosții care au prea multe programări, astfel încât rezervările să fie distribuite uniform.",
|
||||
"exclude_emails_that_contain": "Excludează emailurile care conțin ...",
|
||||
"exclude_emails_match_found_error_message": "Vă rugăm să introduceți o adresă de email de serviciu validă",
|
||||
"search_columns": "Caută coloane",
|
||||
"search_options": "Opțiuni de căutare",
|
||||
"disable_org_url_label": "Dezactivează profilul public al organizației și redirecționează",
|
||||
"disable_org_url_description": "Redirecționează {{orgSlug}}.cal.com către {{destination}}",
|
||||
"single_select": "Selecție unică",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Участники смогут добавлять свои личные рабочие процессы к этому типу событий. Участники смогут видеть активные рабочие процессы команды, но не смогут изменять настройки любых рабочих процессов.",
|
||||
"workflows_locked_by_team_admins_description": "Вы сможете видеть активные рабочие процессы команды, но не сможете изменять настройки любых рабочих процессов или добавлять свои личные рабочие процессы к этому типу событий.",
|
||||
"workflows_unlocked_by_team_admins_description": "Вы сможете включать/отключать личные рабочие процессы для этого типа событий. Вы сможете видеть активные рабочие процессы команды, но не сможете изменять настройки любых командных рабочих процессов.",
|
||||
"webhooks_locked_for_members_description": "Участники не могут добавлять свои личные вебхуки к этому типу событий. Участники смогут видеть активные вебхуки, но не смогут редактировать их настройки.",
|
||||
"webhooks_unlocked_for_members_description": "Участники смогут добавлять свои личные вебхуки к этому типу событий. Участники смогут видеть активные вебхуки, но не смогут редактировать их настройки.",
|
||||
"webhooks_locked_by_team_admins_description": "Вы можете видеть активные управляемые вебхуки, но не сможете редактировать их настройки или добавлять свои личные вебхуки к этому типу событий.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Вы можете добавлять вебхуки к этому типу событий. Вы можете видеть активные управляемые вебхуки, но не сможете их редактировать.",
|
||||
"locked_by_team_admin": "Заблокировано администратором команды",
|
||||
"app_not_connected": "Вы не подключили аккаунт {{appName}}.",
|
||||
"connect_now": "Подключить",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Мы будем отслеживать количество бронирований у каждого организатора и сравнивать их между собой, отключая тех организаторов, у которых слишком много встреч, чтобы обеспечить равномерное распределение.",
|
||||
"exclude_emails_that_contain": "Исключить email-адреса, содержащие...",
|
||||
"exclude_emails_match_found_error_message": "Пожалуйста, введите действительный рабочий email-адрес",
|
||||
"search_columns": "Поиск по столбцам",
|
||||
"search_options": "Параметры поиска",
|
||||
"disable_org_url_label": "Отключить публичный профиль организации и перенаправление",
|
||||
"disable_org_url_description": "Перенаправляет {{orgSlug}}.cal.com на {{destination}}",
|
||||
"single_select": "Одиночный выбор",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Členovia budú môcť pridať svoje osobné pracovné postupy k tomuto typu udalosti. Členovia budú môcť vidieť aktívne tímové pracovné postupy, ale nebudú môcť upravovať žiadne nastavenia pracovných postupov.",
|
||||
"workflows_locked_by_team_admins_description": "Budete môcť vidieť aktívne tímové pracovné postupy, ale nebudete môcť upravovať žiadne nastavenia pracovných postupov alebo pridať svoje osobné pracovné postupy k tomuto typu udalosti.",
|
||||
"workflows_unlocked_by_team_admins_description": "Budete môcť povoliť/zakázať osobné pracovné postupy pre tento typ udalosti. Budete môcť vidieť aktívne tímové pracovné postupy, ale nebudete môcť upravovať žiadne nastavenia tímových pracovných postupov.",
|
||||
"webhooks_locked_for_members_description": "Členovia nemôžu pridávať svoje osobné webhoky k tomuto typu udalosti. Členovia budú môcť vidieť aktívne webhoky, ale nebudú môcť upravovať žiadne nastavenia webhokov.",
|
||||
"webhooks_unlocked_for_members_description": "Členovia budú môcť pridávať svoje osobné webhoky k tomuto typu udalosti. Členovia budú môcť vidieť aktívne webhoky, ale nebudú môcť upravovať žiadne nastavenia webhokov.",
|
||||
"webhooks_locked_by_team_admins_description": "Môžete vidieť aktívne spravované webhoky, ale nebudete môcť upravovať žiadne nastavenia webhokov ani pridávať svoje osobné webhoky k tomuto typu udalosti.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Môžete pridávať webhoky k tomuto typu udalosti. Môžete vidieť aktívne spravované webhoky, ale nebudete ich môcť upravovať.",
|
||||
"locked_by_team_admin": "Zamknuté tímovým administrátorom",
|
||||
"app_not_connected": "Nemáte pripojený účet {{appName}}.",
|
||||
"connect_now": "Pripojiť teraz",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Budeme sledovať počet rezervácií u každého hostiteľa a porovnávať ich s ostatnými. Hostiteľov, ktorí majú príliš veľa rezervácií, dočasne deaktivujeme, aby boli stretnutia distribuované rovnomerne.",
|
||||
"exclude_emails_that_contain": "Vylúčiť e-maily, ktoré obsahujú...",
|
||||
"exclude_emails_match_found_error_message": "Zadajte platnú pracovnú e-mailovú adresu",
|
||||
"search_columns": "Vyhľadať stĺpce",
|
||||
"search_options": "Možnosti vyhľadávania",
|
||||
"disable_org_url_label": "Zakázať verejný profil organizácie a presmerovanie",
|
||||
"disable_org_url_description": "Presmeruje {{orgSlug}}.cal.com na {{destination}}",
|
||||
"single_select": "Jednoduchý výber",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Članovi će moći da dodaju svoje lične tokove rada za ovaj tip događaja. Članovi će moći da vide aktivne timske tokove rada, ali neće moći da uređuju bilo koja podešavanja tokova rada.",
|
||||
"workflows_locked_by_team_admins_description": "Moći ćete da vidite aktivne timske tokove rada, ali nećete moći da uređujete bilo koja podešavanja tokova rada ili da dodajete svoje lične tokove rada za ovaj tip događaja.",
|
||||
"workflows_unlocked_by_team_admins_description": "Moći ćete da omogućite/onemogućite lične tokove rada za ovaj tip događaja. Moći ćete da vidite aktivne timske tokove rada, ali nećete moći da uređujete bilo koja podešavanja timskih tokova rada.",
|
||||
"webhooks_locked_for_members_description": "Članovi ne mogu dodati svoje lične webhook-ove ovom tipu događaja. Članovi će moći da vide aktivne webhook-ove ali neće moći da menjaju podešavanja webhook-ova.",
|
||||
"webhooks_unlocked_for_members_description": "Članovi će moći da dodaju svoje lične webhook-ove ovom tipu događaja. Članovi će moći da vide aktivne webhook-ove ali neće moći da menjaju podešavanja webhook-ova.",
|
||||
"webhooks_locked_by_team_admins_description": "Možete da vidite aktivne upravljane webhook-ove ali nećete moći da menjate podešavanja webhook-ova niti da dodate svoje lične webhook-ove ovom tipu događaja.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Možete dodati webhook-ove ovom tipu događaja. Možete da vidite aktivne upravljane webhook-ove ali nećete moći da ih menjate.",
|
||||
"locked_by_team_admin": "Zaključao administrator tima",
|
||||
"app_not_connected": "Niste povezali {{appName}} nalog.",
|
||||
"connect_now": "Povežite odmah",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Pratićemo broj zakazanih sastanaka kod svakog domaćina i upoređivati ga sa ostalima, privremeno onemogućavajući domaćine koji imaju previše sastanaka kako bi distribucija bila ravnomerna.",
|
||||
"exclude_emails_that_contain": "Isključi e-poruke koje sadrže ...",
|
||||
"exclude_emails_match_found_error_message": "Molimo unesite važeću poslovnu e-mail adresu",
|
||||
"search_columns": "Pretraži kolone",
|
||||
"search_options": "Opcije pretrage",
|
||||
"disable_org_url_label": "Onemogući javni profil organizacije i preusmeravanje",
|
||||
"disable_org_url_description": "Preusmerava {{orgSlug}}.cal.com na {{destination}}",
|
||||
"single_select": "Jednostruki izbor",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Medlemmar kommer att kunna lägga till sina personliga arbetsflöden till denna händelsetyp. Medlemmar kommer att kunna se de aktiva teamarbetsflödena men inte kunna redigera några arbetsflödesinställningar.",
|
||||
"workflows_locked_by_team_admins_description": "Du kommer att kunna se de aktiva teamarbetsflödena men inte kunna redigera några arbetsflödesinställningar eller lägga till dina personliga arbetsflöden till denna händelsetyp.",
|
||||
"workflows_unlocked_by_team_admins_description": "Du kommer att kunna aktivera/inaktivera personliga arbetsflöden för denna händelsetyp. Du kommer att kunna se de aktiva teamarbetsflödena men inte kunna redigera några teamarbetsflödesinställningar.",
|
||||
"webhooks_locked_for_members_description": "Medlemmar kan inte lägga till sina personliga webhooks till denna händelsetyp. Medlemmar kommer att kunna se aktiva webhooks men kommer inte att kunna redigera några webhook-inställningar.",
|
||||
"webhooks_unlocked_for_members_description": "Medlemmar kommer att kunna lägga till sina personliga webhooks till denna händelsetyp. Medlemmar kommer att kunna se aktiva webhooks men kommer inte att kunna redigera några webhook-inställningar.",
|
||||
"webhooks_locked_by_team_admins_description": "Du kan se de aktiva hanterade webhooks men kommer inte att kunna redigera några webhook-inställningar eller lägga till dina personliga webhooks till denna händelsetyp.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Du kan lägga till webhooks till denna händelsetyp. Du kan se de aktiva hanterade webhooks men kommer inte att kunna redigera dem.",
|
||||
"locked_by_team_admin": "Låst av teamadministratör",
|
||||
"app_not_connected": "Du har inte anslutit ett {{appName}}-konto.",
|
||||
"connect_now": "Anslut nu",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Vi övervakar hur många bokningar som har gjorts med varje värd och jämför detta med andra, och inaktiverar vissa värdar som ligger för långt före så att bokningarna fördelas jämnt.",
|
||||
"exclude_emails_that_contain": "Uteslut e-postmeddelanden som innehåller ...",
|
||||
"exclude_emails_match_found_error_message": "Ange en giltig arbets-e-postadress",
|
||||
"search_columns": "Sök kolumner",
|
||||
"search_options": "Sökalternativ",
|
||||
"disable_org_url_label": "Inaktivera offentlig organisationsprofil och omdirigering",
|
||||
"disable_org_url_description": "Omdirigerar {{orgSlug}}.cal.com till {{destination}}",
|
||||
"single_select": "Enkelval",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Üyeler bu etkinlik türüne kişisel iş akışlarını ekleyebilecek. Üyeler aktif takım iş akışlarını görebilecek ancak herhangi bir iş akışı ayarını düzenleyemeyecek.",
|
||||
"workflows_locked_by_team_admins_description": "Aktif takım iş akışlarını görebileceksiniz ancak herhangi bir iş akışı ayarını düzenleyemeyecek veya bu etkinlik türüne kişisel iş akışlarınızı ekleyemeyeceksiniz.",
|
||||
"workflows_unlocked_by_team_admins_description": "Bu etkinlik türünde kişisel iş akışlarını etkinleştirebilir/devre dışı bırakabilirsiniz. Aktif takım iş akışlarını görebileceksiniz ancak herhangi bir takım iş akışı ayarını düzenleyemeyeceksiniz.",
|
||||
"webhooks_locked_for_members_description": "Üyeler bu etkinlik türüne kişisel webhook'larını ekleyemezler. Üyeler aktif webhook'ları görebilir ancak webhook ayarlarını düzenleyemezler.",
|
||||
"webhooks_unlocked_for_members_description": "Üyeler bu etkinlik türüne kişisel webhook'larını ekleyebilirler. Üyeler aktif webhook'ları görebilir ancak webhook ayarlarını düzenleyemezler.",
|
||||
"webhooks_locked_by_team_admins_description": "Yönetilen aktif webhook'ları görebilirsiniz ancak webhook ayarlarını düzenleyemez veya bu etkinlik türüne kişisel webhook'larınızı ekleyemezsiniz.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Bu etkinlik türüne webhook'lar ekleyebilirsiniz. Yönetilen aktif webhook'ları görebilirsiniz ancak bunları düzenleyemezsiniz.",
|
||||
"locked_by_team_admin": "Ekip yöneticisi tarafından kilitlendi",
|
||||
"app_not_connected": "{{appName}} hesabı bağlamadınız.",
|
||||
"connect_now": "Hemen bağlan",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Her ev sahibiyle yapılan rezervasyon sayısını takip edip diğerleriyle karşılaştıracağız ve rezervasyonların eşit dağılımını sağlamak için çok ileri giden bazı ev sahiplerini devre dışı bırakacağız.",
|
||||
"exclude_emails_that_contain": "... içeren e-postaları hariç tut",
|
||||
"exclude_emails_match_found_error_message": "Lütfen geçerli bir iş e-posta adresi girin",
|
||||
"search_columns": "Sütunlarda ara",
|
||||
"search_options": "Arama seçenekleri",
|
||||
"disable_org_url_label": "Genel kurum profilini devre dışı bırak ve yönlendir",
|
||||
"disable_org_url_description": "{{orgSlug}}.cal.com adresini {{destination}} adresine yönlendirir",
|
||||
"single_select": "Tekli Seçim",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Учасники зможуть додавати свої особисті робочі процеси до цього типу подій. Учасники зможуть бачити активні робочі процеси команди, але не зможуть редагувати жодні налаштування робочих процесів.",
|
||||
"workflows_locked_by_team_admins_description": "Ви зможете бачити активні робочі процеси команди, але не зможете редагувати жодні налаштування робочих процесів або додавати свої особисті робочі процеси до цього типу подій.",
|
||||
"workflows_unlocked_by_team_admins_description": "Ви зможете вмикати/вимикати особисті робочі процеси для цього типу подій. Ви зможете бачити активні робочі процеси команди, але не зможете редагувати жодні налаштування робочих процесів команди.",
|
||||
"webhooks_locked_for_members_description": "Учасники не можуть додавати свої особисті вебхуки до цього типу події. Учасники зможуть бачити активні вебхуки, але не зможуть редагувати налаштування вебхуків.",
|
||||
"webhooks_unlocked_for_members_description": "Учасники зможуть додавати свої особисті вебхуки до цього типу події. Учасники зможуть бачити активні вебхуки, але не зможуть редагувати налаштування вебхуків.",
|
||||
"webhooks_locked_by_team_admins_description": "Ви можете бачити активні керовані вебхуки, але не зможете редагувати налаштування вебхуків або додавати свої особисті вебхуки до цього типу події.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Ви можете додавати вебхуки до цього типу події. Ви можете бачити активні керовані вебхуки, але не зможете їх редагувати.",
|
||||
"locked_by_team_admin": "Заблоковано адміністратором команди",
|
||||
"app_not_connected": "Ви не під’єднали обліковий запис {{appName}}.",
|
||||
"connect_now": "Під’єднати",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Ми будемо відстежувати кількість бронювань для кожного хоста та порівнювати їх з іншими, деактивуючи тих хостів, які значно випереджають інших, щоб бронювання розподілялися рівномірно.",
|
||||
"exclude_emails_that_contain": "Виключити електронні адреси, які містять ...",
|
||||
"exclude_emails_match_found_error_message": "Будь ласка, введіть дійсну робочу електронну адресу",
|
||||
"search_columns": "Пошук стовпців",
|
||||
"search_options": "Параметри пошуку",
|
||||
"disable_org_url_label": "Вимкнути публічний профіль організації та перенаправлення",
|
||||
"disable_org_url_description": "Перенаправляє {{orgSlug}}.cal.com на {{destination}}",
|
||||
"single_select": "Одиночний вибір",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "Thành viên sẽ có thể thêm quy trình làm việc cá nhân vào loại sự kiện này. Thành viên sẽ có thể xem các quy trình làm việc của nhóm đang hoạt động nhưng sẽ không thể chỉnh sửa bất kỳ cài đặt quy trình làm việc nào.",
|
||||
"workflows_locked_by_team_admins_description": "Bạn sẽ có thể xem các quy trình làm việc của nhóm đang hoạt động nhưng sẽ không thể chỉnh sửa bất kỳ cài đặt quy trình làm việc nào hoặc thêm quy trình làm việc cá nhân vào loại sự kiện này.",
|
||||
"workflows_unlocked_by_team_admins_description": "Bạn sẽ có thể bật/tắt quy trình làm việc cá nhân trên loại sự kiện này. Bạn sẽ có thể xem các quy trình làm việc của nhóm đang hoạt động nhưng sẽ không thể chỉnh sửa bất kỳ cài đặt quy trình làm việc nào của nhóm.",
|
||||
"webhooks_locked_for_members_description": "Thành viên không thể thêm webhook cá nhân vào loại sự kiện này. Thành viên có thể xem các webhook đang hoạt động nhưng không thể chỉnh sửa bất kỳ cài đặt webhook nào.",
|
||||
"webhooks_unlocked_for_members_description": "Thành viên có thể thêm webhook cá nhân vào loại sự kiện này. Thành viên có thể xem các webhook đang hoạt động nhưng không thể chỉnh sửa bất kỳ cài đặt webhook nào.",
|
||||
"webhooks_locked_by_team_admins_description": "Bạn có thể xem các webhook được quản lý đang hoạt động nhưng không thể chỉnh sửa bất kỳ cài đặt webhook nào hoặc thêm webhook cá nhân vào loại sự kiện này.",
|
||||
"webhooks_unlocked_by_team_admins_description": "Bạn có thể thêm webhook vào loại sự kiện này. Bạn có thể xem các webhook được quản lý đang hoạt động nhưng không thể chỉnh sửa chúng.",
|
||||
"locked_by_team_admin": "Bị khoá bởi quản trị viên của nhóm",
|
||||
"app_not_connected": "Bạn không có tài khoản {{appName}} đã được kết nối.",
|
||||
"connect_now": "Kết nối ngay",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "Chúng tôi sẽ theo dõi số lượng đặt lịch đã được thực hiện với từng chủ trì và so sánh với những người khác, vô hiệu hóa một số chủ trì đã nhận quá nhiều lịch để đảm bảo việc đặt lịch được phân phối đồng đều.",
|
||||
"exclude_emails_that_contain": "Loại trừ các email chứa ...",
|
||||
"exclude_emails_match_found_error_message": "Vui lòng nhập địa chỉ email công việc hợp lệ",
|
||||
"search_columns": "Tìm kiếm cột",
|
||||
"search_options": "Tùy chọn tìm kiếm",
|
||||
"disable_org_url_label": "Vô hiệu hóa hồ sơ tổ chức công khai và chuyển hướng",
|
||||
"disable_org_url_description": "Chuyển hướng {{orgSlug}}.cal.com đến {{destination}}",
|
||||
"single_select": "Chọn một",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "成员将能够将他们的个人工作流程添加到此事件类型。成员将能够看到活动的团队工作流程,但将无法编辑任何工作流程设置。",
|
||||
"workflows_locked_by_team_admins_description": "您将能够看到活动的团队工作流程,但将无法编辑任何工作流程设置或将您的个人工作流程添加到此事件类型。",
|
||||
"workflows_unlocked_by_team_admins_description": "您将能够在此事件类型上启用/禁用个人工作流程。您将能够看到活动的团队工作流程,但将无法编辑任何团队工作流程设置。",
|
||||
"webhooks_locked_for_members_description": "成员无法将其个人 webhook 添加到此日程类型。成员可以查看活动的 webhook,但无法编辑任何 webhook 设置。",
|
||||
"webhooks_unlocked_for_members_description": "成员可以将其个人 webhook 添加到此日程类型。成员可以查看活动的 webhook,但无法编辑任何 webhook 设置。",
|
||||
"webhooks_locked_by_team_admins_description": "您可以查看活动的托管 webhook,但无法编辑任何 webhook 设置或将个人 webhook 添加到此日程类型。",
|
||||
"webhooks_unlocked_by_team_admins_description": "您可以将 webhook 添加到此日程类型。您可以查看活动的托管 webhook,但无法编辑它们。",
|
||||
"locked_by_team_admin": "被团队管理员锁定",
|
||||
"app_not_connected": "您尚未连接 {{appName}} 帐户。",
|
||||
"connect_now": "立即连接",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "系统会监控每位主持人已接受的预订数量并与其他人进行比较,如果某些主持人的预订数量远超他人,系统会暂时停用这些主持人,以确保预订均匀分配。",
|
||||
"exclude_emails_that_contain": "排除包含...的电子邮件",
|
||||
"exclude_emails_match_found_error_message": "请输入有效的工作电子邮件地址",
|
||||
"search_columns": "搜索列",
|
||||
"search_options": "搜索选项",
|
||||
"disable_org_url_label": "禁用公开组织资料并重定向",
|
||||
"disable_org_url_description": "将 {{orgSlug}}.cal.com 重定向到 {{destination}}",
|
||||
"single_select": "单选",
|
||||
|
||||
@@ -2040,6 +2040,10 @@
|
||||
"workflows_unlocked_for_members_description": "成員將能夠將他們的個人工作流程添加到此事件類型中。成員將能夠看到活躍的團隊工作流程,但無法編輯任何工作流程設置。",
|
||||
"workflows_locked_by_team_admins_description": "您將能夠看到活躍的團隊工作流程,但無法編輯任何工作流程設置或將個人工作流程添加到此事件類型中。",
|
||||
"workflows_unlocked_by_team_admins_description": "您將能夠在此事件類型中啟用/禁用個人工作流程。您將能夠看到活躍的團隊工作流程,但無法編輯任何團隊工作流程設置。",
|
||||
"webhooks_locked_for_members_description": "成員無法將個人 webhook 新增至此活動類型。成員可以查看已啟用的 webhook,但無法編輯任何 webhook 設定。",
|
||||
"webhooks_unlocked_for_members_description": "成員可以將個人 webhook 新增至此活動類型。成員可以查看已啟用的 webhook,但無法編輯任何 webhook 設定。",
|
||||
"webhooks_locked_by_team_admins_description": "您可以查看已啟用的受管理 webhook,但無法編輯任何 webhook 設定或將個人 webhook 新增至此活動類型。",
|
||||
"webhooks_unlocked_by_team_admins_description": "您可以將 webhook 新增至此活動類型。您可以查看已啟用的受管理 webhook,但無法編輯它們。",
|
||||
"locked_by_team_admin": "已由團隊管理員鎖定",
|
||||
"app_not_connected": "您尚未連結 {{appName}} 帳號。",
|
||||
"connect_now": "立即連結",
|
||||
@@ -2818,6 +2822,8 @@
|
||||
"rr_distribution_method_balanced_description": "系統會監控每位主持人的預約數量並與其他人進行比較,當某些主持人的預約量遠超過其他人時,系統會暫時停用這些主持人,以確保預約能夠均勻分配。",
|
||||
"exclude_emails_that_contain": "排除包含...的電子郵件",
|
||||
"exclude_emails_match_found_error_message": "請輸入有效的工作電子郵件地址",
|
||||
"search_columns": "搜尋欄位",
|
||||
"search_options": "搜尋選項",
|
||||
"disable_org_url_label": "停用公開組織檔案並重新導向",
|
||||
"disable_org_url_description": "將 {{orgSlug}}.cal.com 重新導向至 {{destination}}",
|
||||
"single_select": "單選",
|
||||
|
||||
@@ -19,7 +19,7 @@ import dayjs from "@calcom/dayjs";
|
||||
import { SchedulingType, type BookingStatus } from "@calcom/prisma/enums";
|
||||
import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util";
|
||||
|
||||
import { expect } from "./getSchedule/expects";
|
||||
import { expect, expectedSlotsForSchedule } from "./getSchedule/expects";
|
||||
import { setupAndTeardown } from "./getSchedule/setupAndTeardown";
|
||||
import { timeTravelToTheBeginningOfToday } from "./getSchedule/utils";
|
||||
|
||||
@@ -89,7 +89,8 @@ describe("getSchedule", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Round robin lead skip - CRM", async () => {
|
||||
// TODO: Move these inside describe('Team Event')
|
||||
describe("Round robin lead skip(i.e. use contact owner specified by teamMemberEmail) - CRM", async () => {
|
||||
test("correctly get slots for event with only round robin hosts", async () => {
|
||||
vi.setSystemTime("2024-05-21T00:00:13Z");
|
||||
|
||||
@@ -367,7 +368,7 @@ describe("getSchedule", () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
test("correctly get slots for event with only round robin hosts", async () => {
|
||||
test("correctly get slots for event with only round robin hosts - When no availability is found", async () => {
|
||||
vi.setSystemTime("2024-05-21T00:00:13Z");
|
||||
|
||||
const plus1DateString = "2024-05-22";
|
||||
@@ -496,6 +497,119 @@ describe("getSchedule", () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
test("Negative case(i.e. skipContactOwner is true) - when teamMemberEmail is provided but not used", 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.IstEveningShift],
|
||||
},
|
||||
{
|
||||
...TestData.users.example,
|
||||
email: "example1@example.com",
|
||||
id: 102,
|
||||
schedules: [TestData.schedules.IstMorningShift],
|
||||
defaultScheduleId: 2,
|
||||
},
|
||||
],
|
||||
bookings: [],
|
||||
});
|
||||
|
||||
const scheduleWhenContactOwnerIsSkipped = 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",
|
||||
skipContactOwner: true,
|
||||
orgSlug: null,
|
||||
},
|
||||
});
|
||||
|
||||
// Both users slot would be available as contact owner(example@example.com) is skipped and we fallback to all users of RR
|
||||
expect(scheduleWhenContactOwnerIsSkipped).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", () => {
|
||||
@@ -2163,5 +2277,168 @@ describe("getSchedule", () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("Reschedule: should reschedule with same host if rescheduleWithSameRoundRobinHost is true", async () => {
|
||||
vi.setSystemTime("2024-05-21T00:00:13Z");
|
||||
|
||||
const plus1DateString = "2024-05-22";
|
||||
const plus2DateString = "2024-05-23";
|
||||
|
||||
// An event with common schedule
|
||||
await createBookingScenario({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 60,
|
||||
length: 60,
|
||||
rescheduleWithSameRoundRobinHost: true,
|
||||
hosts: [
|
||||
{
|
||||
userId: 101,
|
||||
isFixed: false,
|
||||
},
|
||||
{
|
||||
userId: 102,
|
||||
isFixed: false,
|
||||
},
|
||||
],
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
...TestData.users.example,
|
||||
email: "example@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstEveningShift],
|
||||
defaultScheduleId: 1,
|
||||
},
|
||||
{
|
||||
...TestData.users.example,
|
||||
email: "example1@example.com",
|
||||
id: 102,
|
||||
schedules: [TestData.schedules.IstMorningShift],
|
||||
defaultScheduleId: 2,
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
uid: "BOOKING_TO_RESCHEDULE_UID",
|
||||
userId: 101,
|
||||
attendees: [
|
||||
{
|
||||
email: "IntegrationTestUser102@example.com",
|
||||
},
|
||||
],
|
||||
eventTypeId: 1,
|
||||
status: "ACCEPTED",
|
||||
startTime: `${plus2DateString}T04:00:00.000Z`,
|
||||
endTime: `${plus2DateString}T04:15:00.000Z`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const schedule = await getSchedule({
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
rescheduleUid: "BOOKING_TO_RESCHEDULE_UID",
|
||||
},
|
||||
});
|
||||
|
||||
// expect only slots of IstEveningShift as this is the slots for the original host of the booking
|
||||
expect(schedule).toHaveTimeSlots(
|
||||
[`11:30:00.000Z`, `12:30:00.000Z`, `13:30:00.000Z`, `14:30:00.000Z`, `15:30:00.000Z`],
|
||||
{
|
||||
dateString: plus2DateString,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("Reschedule: should reschedule as per routedTeamMemberIds(instead of same host) even if rescheduleWithSameRoundRobinHost is true but it is a rerouting scenario", async () => {
|
||||
vi.setSystemTime("2024-05-21T00:00:13Z");
|
||||
|
||||
const plus1DateString = "2024-05-22";
|
||||
const plus2DateString = "2024-05-23";
|
||||
|
||||
// An event with common schedule
|
||||
await createBookingScenario({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 60,
|
||||
length: 60,
|
||||
rescheduleWithSameRoundRobinHost: true,
|
||||
hosts: [
|
||||
{
|
||||
userId: 101,
|
||||
isFixed: false,
|
||||
},
|
||||
{
|
||||
userId: 102,
|
||||
isFixed: false,
|
||||
},
|
||||
],
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
...TestData.users.example,
|
||||
email: "example@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstEveningShift],
|
||||
defaultScheduleId: 1,
|
||||
},
|
||||
{
|
||||
...TestData.users.example,
|
||||
email: "example1@example.com",
|
||||
id: 102,
|
||||
schedules: [TestData.schedules.IstMorningShift],
|
||||
defaultScheduleId: 2,
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
uid: "BOOKING_TO_RESCHEDULE_UID",
|
||||
userId: 101,
|
||||
attendees: [
|
||||
{
|
||||
email: "IntegrationTestUser102@example.com",
|
||||
},
|
||||
],
|
||||
eventTypeId: 1,
|
||||
status: "ACCEPTED",
|
||||
startTime: `${plus2DateString}T04:00:00.000Z`,
|
||||
endTime: `${plus2DateString}T04:15:00.000Z`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const schedule = await getSchedule({
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
rescheduleUid: "BOOKING_TO_RESCHEDULE_UID",
|
||||
routedTeamMemberIds: [102],
|
||||
},
|
||||
});
|
||||
|
||||
// expect only slots of IstEveningShift as this is the slots for the original host of the booking
|
||||
expect(schedule).toHaveTimeSlots(
|
||||
expectedSlotsForSchedule.IstMorningShift.interval["1hr"].allPossibleSlotsStartingAt430,
|
||||
{
|
||||
dateString: plus2DateString,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +30,32 @@ export const expectedSlotsForSchedule = {
|
||||
},
|
||||
},
|
||||
},
|
||||
IstMorningShift: {
|
||||
interval: {
|
||||
"1hr": {
|
||||
allPossibleSlotsStartingAt430: [
|
||||
"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",
|
||||
],
|
||||
allPossibleSlotsStartingAt4: [
|
||||
"04:00:00.000Z",
|
||||
"05:00:00.000Z",
|
||||
"06:00:00.000Z",
|
||||
"07:00:00.000Z",
|
||||
"08:00:00.000Z",
|
||||
"09:00:00.000Z",
|
||||
"10:00:00.000Z",
|
||||
"11:00:00.000Z",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1283,6 +1283,9 @@ export function getOrganizer({
|
||||
|
||||
export function getScenarioData(
|
||||
{
|
||||
/**
|
||||
* organizer has no special meaning. It is a regular user. It is supposed to be deprecated along with `usersApartFromOrganizer` and we should introduce a new `users` field instead
|
||||
*/
|
||||
organizer,
|
||||
eventTypes,
|
||||
usersApartFromOrganizer = [],
|
||||
|
||||
@@ -3849,6 +3849,15 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "eventTypeId",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Id of the event-type, if this is provided no other parameter is required",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "usernames",
|
||||
"required": false,
|
||||
@@ -3857,6 +3866,33 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "ID of the user you want the get the event-types of, username is not needed when using this parameter",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "orgSlug",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "slug of the user's organization if he is in one, orgId is not required if using this parameter",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "orgId",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "ID of the organization of the user you want the get the event-types of, orgSlug is not needed when using this parameter",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -4751,6 +4787,17 @@
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "slotFormat",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Format of slot times in response. Use 'range' to get start and end times.",
|
||||
"example": "range",
|
||||
"schema": {
|
||||
"enum": ["range", "time"],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -4774,13 +4821,31 @@
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:00:00.000Z"
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:00:00.000Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"startTime": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:00:00.000Z"
|
||||
},
|
||||
"endTime": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:30:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4801,13 +4866,12 @@
|
||||
],
|
||||
"2024-09-26": [
|
||||
{
|
||||
"time": "2024-09-26T08:00:00.000Z"
|
||||
"startTime": "2024-09-26T08:00:00.000Z",
|
||||
"endTime": "2024-09-26T08:30:00.000Z"
|
||||
},
|
||||
{
|
||||
"time": "2024-09-26T08:15:00.000Z"
|
||||
},
|
||||
{
|
||||
"time": "2024-09-26T08:30:00.000Z"
|
||||
"startTime": "2024-09-26T08:15:00.000Z",
|
||||
"endTime": "2024-09-26T08:45:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6067,7 +6131,7 @@
|
||||
"rolling": {
|
||||
"type": "boolean",
|
||||
"example": true,
|
||||
"description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n "
|
||||
"description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always be equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n "
|
||||
}
|
||||
},
|
||||
"required": ["type", "value"]
|
||||
@@ -6088,7 +6152,7 @@
|
||||
"rolling": {
|
||||
"type": "boolean",
|
||||
"example": true,
|
||||
"description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n "
|
||||
"description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always be equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n "
|
||||
}
|
||||
},
|
||||
"required": ["type", "value"]
|
||||
@@ -12100,7 +12164,7 @@
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Host"
|
||||
"$ref": "#/components/schemas/BookingHost"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
@@ -12216,7 +12280,7 @@
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Host"
|
||||
"$ref": "#/components/schemas/BookingHost"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
@@ -12371,7 +12435,7 @@
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Host"
|
||||
"$ref": "#/components/schemas/BookingHost"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
@@ -12482,7 +12546,7 @@
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Host"
|
||||
"$ref": "#/components/schemas/BookingHost"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -2042,6 +2042,10 @@ checksums:
|
||||
workflows_unlocked_for_members_description: edb59754bf0d771666984d01fcffbaa7
|
||||
workflows_locked_by_team_admins_description: 8bfac6f17bbd494bb130c00ae5fcd4de
|
||||
workflows_unlocked_by_team_admins_description: 457e6dd5da3072aa2fb1f84232bd2dbf
|
||||
webhooks_locked_for_members_description: 9b2935a9a3850d82f3165adfe6f84c21
|
||||
webhooks_unlocked_for_members_description: f5b5d7cd9110c935a64623a79b6da02b
|
||||
webhooks_locked_by_team_admins_description: 9ff386c2b6ded47b1a0092d9af1175b8
|
||||
webhooks_unlocked_by_team_admins_description: 41bd9c0d1d1393ee54eff5e4f1f84c29
|
||||
locked_by_team_admin: 539128c9579f340b95d58abbfa0880ef
|
||||
app_not_connected: 25c979da79e9f983e6f5e62dcd1c47bb
|
||||
connect_now: ca98ba0375aad91861c150bbc0fff9ec
|
||||
@@ -2820,6 +2824,8 @@ checksums:
|
||||
rr_distribution_method_balanced_description: 23fd5a95ac9d9f6283883180ba30db32
|
||||
exclude_emails_that_contain: d43255c873c75c8fe1adcb8f8b7af914
|
||||
exclude_emails_match_found_error_message: 4ca9429a41d63e53caffeceb0e4229f7
|
||||
search_columns: 4ea704f7b20d0f19dea8a5a121a639fa
|
||||
search_options: 62518ed013b62001f2abe7b4314326c7
|
||||
disable_org_url_label: fff15973948e3f4f07645d82553c15c1
|
||||
disable_org_url_description: 875a1606ef83f120547305072b1b8723
|
||||
single_select: 40d573e93a3059895cf3e9ee1e7012c6
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,17 @@
|
||||
import { oauth2_v2 } from "@googleapis/oauth2";
|
||||
import type { OAuth2Client } from "googleapis-common";
|
||||
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
|
||||
export async function updateProfilePhotoGoogle(oAuth2Client: OAuth2Client, userId: number) {
|
||||
try {
|
||||
const oauth2 = new oauth2_v2.Oauth2({ auth: oAuth2Client });
|
||||
const userDetails = await oauth2.userinfo.get();
|
||||
if (userDetails.data?.picture) {
|
||||
await UserRepository.updateAvatar({ id: userId, avatarUrl: userDetails.data.picture });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error updating avatarUrl from google calendar connect", error);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,21 @@
|
||||
import { google } from "googleapis";
|
||||
import { OAuth2Client } from "googleapis-common";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
|
||||
import { GOOGLE_CALENDAR_SCOPES, SCOPE_USERINFO_PROFILE, WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
import { SCOPES } from "../lib/constants";
|
||||
import { getGoogleAppKeys } from "../lib/getGoogleAppKeys";
|
||||
|
||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
// Get token from Google Calendar API
|
||||
const { client_id, client_secret } = await getGoogleAppKeys();
|
||||
const redirect_uri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback`;
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirect_uri);
|
||||
|
||||
const authUrl = oAuth2Client.generateAuthUrl({
|
||||
access_type: "offline",
|
||||
scope: SCOPES,
|
||||
scope: [SCOPE_USERINFO_PROFILE, ...GOOGLE_CALENDAR_SCOPES],
|
||||
// A refresh token is only returned the first time the user
|
||||
// consents to providing access. For illustration purposes,
|
||||
// setting the prompt to 'consent' will force this consent
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import { google } from "googleapis";
|
||||
import { calendar_v3 } from "@googleapis/calendar";
|
||||
import { OAuth2Client } from "googleapis-common";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { updateProfilePhotoGoogle } from "@calcom/app-store/_utils/oauth/updateProfilePhotoGoogle";
|
||||
import GoogleCalendarService from "@calcom/app-store/googlecalendar/lib/CalendarService";
|
||||
import { renewSelectedCalendarCredentialId } from "@calcom/lib/connectedCalendar";
|
||||
import { WEBAPP_URL, WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
|
||||
import {
|
||||
GOOGLE_CALENDAR_SCOPES,
|
||||
SCOPE_USERINFO_PROFILE,
|
||||
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 { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
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";
|
||||
|
||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -40,14 +45,14 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
||||
const redirect_uri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback`;
|
||||
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirect_uri);
|
||||
|
||||
if (code) {
|
||||
const token = await oAuth2Client.getToken(code);
|
||||
const key = token.tokens;
|
||||
const grantedScopes = token.tokens.scope?.split(" ") ?? [];
|
||||
// Check if we have granted all required permissions
|
||||
const hasMissingRequiredScopes = REQUIRED_SCOPES.some((scope) => !grantedScopes.includes(scope));
|
||||
const hasMissingRequiredScopes = GOOGLE_CALENDAR_SCOPES.some((scope) => !grantedScopes.includes(scope));
|
||||
if (hasMissingRequiredScopes) {
|
||||
if (!state?.fromApp) {
|
||||
throw new HttpError({
|
||||
@@ -63,28 +68,25 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the primary calendar as the first selected calendar
|
||||
|
||||
oAuth2Client.setCredentials(key);
|
||||
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
const gcalCredential = await CredentialRepository.create({
|
||||
userId: req.session.user.id,
|
||||
key,
|
||||
appId: "google-calendar",
|
||||
type: "google_calendar",
|
||||
});
|
||||
|
||||
const gCalService = new GoogleCalendarService({
|
||||
...gcalCredential,
|
||||
user: null,
|
||||
});
|
||||
|
||||
const calendar = new calendar_v3.Calendar({
|
||||
auth: oAuth2Client,
|
||||
});
|
||||
|
||||
const cals = await getAllCalendars(calendar);
|
||||
|
||||
const primaryCal = cals.find((cal) => cal.primary) ?? cals[0];
|
||||
|
||||
// Only attempt to update the user's profile photo if the user has granted the required scope
|
||||
if (grantedScopes.includes(SCOPE_USERINFO_PROFILE)) {
|
||||
await updateProfilePhoto(oAuth2Client, req.session.user.id);
|
||||
}
|
||||
|
||||
const gcalCredential = await GoogleRepository.createGoogleCalendarCredential({
|
||||
key,
|
||||
userId: req.session.user.id,
|
||||
});
|
||||
const primaryCal = await gCalService.getPrimaryCalendar(calendar);
|
||||
|
||||
// If we still don't have a primary calendar skip creating the selected calendar.
|
||||
// It can be toggled on later.
|
||||
@@ -96,6 +98,11 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only attempt to update the user's profile photo if the user has granted the required scope
|
||||
if (grantedScopes.includes(SCOPE_USERINFO_PROFILE)) {
|
||||
await updateProfilePhotoGoogle(oAuth2Client, req.session.user.id);
|
||||
}
|
||||
|
||||
const selectedCalendarWhereUnique = {
|
||||
userId: req.session.user.id,
|
||||
externalId: primaryCal.id,
|
||||
@@ -105,10 +112,8 @@ 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 GoogleRepository.upsertSelectedCalendar({
|
||||
credentialId: gcalCredential.id,
|
||||
await gCalService.upsertSelectedCalendar({
|
||||
externalId: selectedCalendarWhereUnique.externalId,
|
||||
userId: selectedCalendarWhereUnique.userId,
|
||||
});
|
||||
} catch (error) {
|
||||
let errorMessage = "something_went_wrong";
|
||||
@@ -145,8 +150,9 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingGoogleMeetCredential = await GoogleRepository.findGoogleMeetCredential({
|
||||
const existingGoogleMeetCredential = await CredentialRepository.findFirstByUserIdAndType({
|
||||
userId: req.session.user.id,
|
||||
type: "google_video",
|
||||
});
|
||||
|
||||
// If the user already has a google meet credential, there's nothing to do in here
|
||||
@@ -159,7 +165,12 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
}
|
||||
|
||||
// Create a new google meet credential
|
||||
await GoogleRepository.createGoogleMeetsCredential({ userId: req.session.user.id });
|
||||
await CredentialRepository.create({
|
||||
userId: req.session.user.id,
|
||||
type: "google_video",
|
||||
key: {},
|
||||
appId: "google-meet",
|
||||
});
|
||||
res.redirect(
|
||||
getSafeRedirectUrl(`${WEBAPP_URL}/apps/installed/conferencing?hl=google-meet`) ??
|
||||
getInstalledAppPath({ variant: "conferencing", slug: "google-meet" })
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import prismock from "../../../../tests/libs/__mocks__/prisma";
|
||||
import oAuthManagerMock, { defaultMockOAuthManager } from "../../tests/__mocks__/OAuthManager";
|
||||
import { googleapisMock, setCredentialsMock } from "./__mocks__/googleapis";
|
||||
import { adminMock, calendarMock, setCredentialsMock } from "./__mocks__/googleapis";
|
||||
|
||||
import { expect, test, vi } from "vitest";
|
||||
import { expect, test, beforeEach, vi } from "vitest";
|
||||
import "vitest-fetch-mock";
|
||||
|
||||
import { CalendarCache } from "@calcom/features/calendar-cache/calendar-cache";
|
||||
@@ -22,7 +22,21 @@ vi.mock("./getGoogleAppKeys", () => ({
|
||||
redirect_uris: ["http://localhost:3000/api/integrations/googlecalendar/callback"],
|
||||
}),
|
||||
}));
|
||||
googleapisMock.google;
|
||||
|
||||
vi.mock("googleapis-common", () => ({
|
||||
OAuth2Client: vi.fn().mockImplementation(() => ({
|
||||
setCredentials: setCredentialsMock,
|
||||
})),
|
||||
}));
|
||||
vi.mock("@googleapis/admin", () => adminMock);
|
||||
vi.mock("@googleapis/calendar", () => calendarMock);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
setCredentialsMock.mockClear();
|
||||
calendarMock.calendar_v3.Calendar.mockClear();
|
||||
adminMock.admin_directory_v1.Admin.mockClear();
|
||||
});
|
||||
|
||||
const googleTestCredentialKey = {
|
||||
scope: "https://www.googleapis.com/auth/calendar.events",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { calendar_v3 } from "@googleapis/calendar";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { calendar_v3 } from "googleapis";
|
||||
import { google } from "googleapis";
|
||||
import { OAuth2Client } from "googleapis-common";
|
||||
import { RRule } from "rrule";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
@@ -18,10 +18,9 @@ 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 { GoogleRepository } from "@calcom/lib/server/repository/google";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type {
|
||||
Calendar,
|
||||
@@ -164,8 +163,7 @@ export default class GoogleCalendarService implements Calendar {
|
||||
|
||||
public authedCalendar = async () => {
|
||||
const myGoogleAuth = await this.auth.getMyGoogleAuthWithRefreshedToken();
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
const calendar = new calendar_v3.Calendar({
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
return calendar;
|
||||
@@ -541,12 +539,12 @@ export default class GoogleCalendarService implements Calendar {
|
||||
// Only calendars of other integrations selected
|
||||
return [];
|
||||
}
|
||||
async function getCalIds() {
|
||||
const getCalIds = async () => {
|
||||
if (selectedCalendarIds.length !== 0) return selectedCalendarIds;
|
||||
const cals = await getAllCalendars(calendar, ["id"]);
|
||||
const cals = await this.getAllCalendars(calendar, ["id"]);
|
||||
if (!cals.length) return [];
|
||||
return cals.reduce((c, cal) => (cal.id ? [...c, cal.id] : c), [] as string[]);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const calsIds = await getCalIds();
|
||||
@@ -607,7 +605,7 @@ export default class GoogleCalendarService implements Calendar {
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
data: {
|
||||
items: await getAllCalendars(calendar),
|
||||
items: await this.getAllCalendars(calendar),
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -652,10 +650,8 @@ export default class GoogleCalendarService implements Calendar {
|
||||
},
|
||||
});
|
||||
const response = res.data;
|
||||
await GoogleRepository.upsertSelectedCalendar({
|
||||
userId: this.credential.userId!,
|
||||
await this.upsertSelectedCalendar({
|
||||
externalId: calendarId,
|
||||
credentialId: this.credential.id,
|
||||
googleChannelId: response?.id,
|
||||
googleChannelKind: response?.kind,
|
||||
googleChannelResourceId: response?.resourceId,
|
||||
@@ -686,10 +682,8 @@ export default class GoogleCalendarService implements Calendar {
|
||||
.catch((err) => {
|
||||
console.warn(JSON.stringify(err));
|
||||
});
|
||||
await GoogleRepository.upsertSelectedCalendar({
|
||||
userId: this.credential.userId!,
|
||||
await this.upsertSelectedCalendar({
|
||||
externalId: calendarId,
|
||||
credentialId: this.credential.id,
|
||||
googleChannelId: null,
|
||||
googleChannelKind: null,
|
||||
googleChannelResourceId: null,
|
||||
@@ -715,9 +709,97 @@ export default class GoogleCalendarService implements Calendar {
|
||||
const data = await this.fetchAvailability(parsedArgs);
|
||||
await this.setAvailabilityInCache(parsedArgs, data);
|
||||
}
|
||||
|
||||
async createSelectedCalendar(
|
||||
data: Omit<Prisma.SelectedCalendarUncheckedCreateInput, "integration" | "credentialId">
|
||||
) {
|
||||
return await SelectedCalendarRepository.create({
|
||||
...data,
|
||||
integration: this.integrationName,
|
||||
credentialId: this.credential.id,
|
||||
});
|
||||
}
|
||||
|
||||
async upsertSelectedCalendar(
|
||||
data: Omit<Prisma.SelectedCalendarUncheckedCreateInput, "integration" | "credentialId" | "userId">
|
||||
) {
|
||||
if (!this.credential.userId) {
|
||||
logger.error("upsertSelectedCalendar failed. userId is missing.");
|
||||
return;
|
||||
}
|
||||
return await SelectedCalendarRepository.upsert({
|
||||
...data,
|
||||
integration: this.integrationName,
|
||||
credentialId: this.credential.id,
|
||||
userId: this.credential.userId,
|
||||
});
|
||||
}
|
||||
|
||||
async getAllCalendars(
|
||||
calendar: calendar_v3.Calendar,
|
||||
fields: string[] = ["id", "summary", "primary", "accessRole"]
|
||||
): Promise<calendar_v3.Schema$CalendarListEntry[]> {
|
||||
let allCalendars: calendar_v3.Schema$CalendarListEntry[] = [];
|
||||
let pageToken: string | undefined;
|
||||
|
||||
try {
|
||||
do {
|
||||
const response: any = await calendar.calendarList.list({
|
||||
fields: `items(${fields.join(",")}),nextPageToken`,
|
||||
pageToken,
|
||||
maxResults: 250, // 250 is max
|
||||
});
|
||||
|
||||
allCalendars = [...allCalendars, ...(response.data.items ?? [])];
|
||||
pageToken = response.data.nextPageToken;
|
||||
} while (pageToken);
|
||||
|
||||
return allCalendars;
|
||||
} catch (error) {
|
||||
logger.error("Error fetching all Google Calendars", { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getPrimaryCalendar(
|
||||
calendar: calendar_v3.Calendar,
|
||||
fields: string[] = ["id", "summary", "primary", "accessRole"]
|
||||
): Promise<calendar_v3.Schema$CalendarListEntry | null> {
|
||||
let pageToken: string | undefined;
|
||||
let firstCalendar: calendar_v3.Schema$CalendarListEntry | undefined;
|
||||
|
||||
try {
|
||||
do {
|
||||
const response: any = await calendar.calendarList.list({
|
||||
fields: `items(${fields.join(",")}),nextPageToken`,
|
||||
pageToken,
|
||||
maxResults: 250, // 250 is max
|
||||
});
|
||||
|
||||
const cals = response.data.items ?? [];
|
||||
const primaryCal = cals.find((cal: calendar_v3.Schema$CalendarListEntry) => cal.primary);
|
||||
if (primaryCal) {
|
||||
return primaryCal;
|
||||
}
|
||||
|
||||
// Store the first calendar in case no primary is found
|
||||
if (cals.length > 0 && !firstCalendar) {
|
||||
firstCalendar = cals[0];
|
||||
}
|
||||
|
||||
pageToken = response.data.nextPageToken;
|
||||
} while (pageToken);
|
||||
|
||||
// should not be reached because Google Cal always has a primary cal
|
||||
return firstCalendar ?? null;
|
||||
} catch (error) {
|
||||
logger.error("Error in `getPrimaryCalendar`", { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MyGoogleAuth extends google.auth.OAuth2 {
|
||||
class MyGoogleAuth extends OAuth2Client {
|
||||
constructor(client_id: string, client_secret: string, redirect_uri: string) {
|
||||
super(client_id, client_secret, redirect_uri);
|
||||
}
|
||||
|
||||
@@ -1,50 +1,33 @@
|
||||
import type * as googleapis from "googleapis";
|
||||
import { beforeEach, vi } from "vitest";
|
||||
import { mockDeep, mockReset } from "vitest-mock-extended";
|
||||
|
||||
vi.mock("googleapis", () => googleapisMock);
|
||||
|
||||
beforeEach(() => {
|
||||
mockReset(googleapisMock);
|
||||
});
|
||||
|
||||
const googleapisMock = mockDeep<typeof googleapis>({
|
||||
fallbackMockImplementation: (...args) => {
|
||||
console.log(args);
|
||||
throw new Error("Unimplemented");
|
||||
},
|
||||
});
|
||||
import { vi } from "vitest";
|
||||
|
||||
const setCredentialsMock = vi.fn();
|
||||
googleapisMock.google = {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
calendar: vi.fn().mockReturnValue({
|
||||
channels: {
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
events: {
|
||||
watch: vi.fn().mockResolvedValue({
|
||||
data: {
|
||||
kind: "api#channel",
|
||||
id: "mock-channel-id",
|
||||
resourceId: "mock-resource-id",
|
||||
resourceUri: "mock-resource-uri",
|
||||
expiration: "1111111111",
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
auth: {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
OAuth2: function MockGoogleOAuth2() {
|
||||
return {
|
||||
setCredentials: setCredentialsMock,
|
||||
};
|
||||
},
|
||||
|
||||
const calendarMockImplementation = {
|
||||
channels: {
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
events: {
|
||||
watch: vi.fn().mockResolvedValue({
|
||||
data: {
|
||||
kind: "api#channel",
|
||||
id: "mock-channel-id",
|
||||
resourceId: "mock-resource-id",
|
||||
resourceUri: "mock-resource-uri",
|
||||
expiration: "1111111111",
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export default googleapisMock;
|
||||
export { googleapisMock, setCredentialsMock };
|
||||
const calendarMock = {
|
||||
calendar_v3: {
|
||||
Calendar: vi.fn().mockImplementation(() => calendarMockImplementation),
|
||||
},
|
||||
};
|
||||
const adminMock = {
|
||||
admin_directory_v1: {
|
||||
Admin: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export { calendarMock, adminMock, setCredentialsMock };
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export const SCOPE_USERINFO_PROFILE = "https://www.googleapis.com/auth/userinfo.profile";
|
||||
export const SCOPE_CALENDAR_READONLY = "https://www.googleapis.com/auth/calendar.readonly";
|
||||
export const SCOPE_CALENDAR_EVENT = "https://www.googleapis.com/auth/calendar.events";
|
||||
|
||||
export const REQUIRED_SCOPES = [SCOPE_CALENDAR_READONLY, SCOPE_CALENDAR_EVENT];
|
||||
|
||||
export const SCOPES = [...REQUIRED_SCOPES, SCOPE_USERINFO_PROFILE];
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { calendar_v3 } from "googleapis";
|
||||
|
||||
import logger from "@calcom/lib/logger";
|
||||
|
||||
export async function getAllCalendars(
|
||||
calendar: calendar_v3.Calendar,
|
||||
fields: string[] = ["id", "summary", "primary", "accessRole"]
|
||||
): Promise<calendar_v3.Schema$CalendarListEntry[]> {
|
||||
let allCalendars: calendar_v3.Schema$CalendarListEntry[] = [];
|
||||
let pageToken: string | undefined;
|
||||
|
||||
try {
|
||||
do {
|
||||
const response: any = await calendar.calendarList.list({
|
||||
fields: `items(${fields.join(",")}),nextPageToken`,
|
||||
pageToken,
|
||||
maxResults: 250, // 250 is max
|
||||
});
|
||||
|
||||
allCalendars = [...allCalendars, ...(response.data.items ?? [])];
|
||||
pageToken = response.data.nextPageToken;
|
||||
} while (pageToken);
|
||||
|
||||
return allCalendars;
|
||||
} catch (error) {
|
||||
logger.error("Error fetching all Google Calendars", { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
"description": "Google Calendar is a time management and scheduling service developed by Google. Allows users to create and edit events, with options available for type and time. Available to anyone that has a Gmail account on both mobile and web versions.",
|
||||
"dependencies": {
|
||||
"@calcom/prisma": "*",
|
||||
"googleapis": "^84.0.0"
|
||||
"@googleapis/calendar": "^9.7.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
"main": "./index.ts",
|
||||
"description": "Google Meet is Google's web-based video conferencing platform, designed to compete with major conferencing platforms.",
|
||||
"dependencies": {
|
||||
"@calcom/prisma": "*",
|
||||
"googleapis": "^84.0.0"
|
||||
"@calcom/prisma": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { calendar_v3 } from "@googleapis/calendar";
|
||||
import type { Membership, Team, UserPermissionRole } from "@prisma/client";
|
||||
import { waitUntil } from "@vercel/functions";
|
||||
import { OAuth2Client } from "googleapis-common";
|
||||
import type { AuthOptions, Session, User } from "next-auth";
|
||||
import type { JWT } from "next-auth/jwt";
|
||||
import { encode } from "next-auth/jwt";
|
||||
@@ -8,6 +10,8 @@ import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import EmailProvider from "next-auth/providers/email";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
|
||||
import { updateProfilePhotoGoogle } from "@calcom/app-store/_utils/oauth/updateProfilePhotoGoogle";
|
||||
import GoogleCalendarService from "@calcom/app-store/googlecalendar/lib/CalendarService";
|
||||
import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService";
|
||||
import createUsersAndConnectToOrg from "@calcom/features/ee/dsync/lib/users/createUsersAndConnectToOrg";
|
||||
import postHogClient from "@calcom/features/ee/event-tracking/lib/posthog/postHogClient";
|
||||
@@ -15,7 +19,12 @@ import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/Imperso
|
||||
import { getOrgFullOrigin, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { HOSTED_CAL_FEATURES, IS_CALCOM } from "@calcom/lib/constants";
|
||||
import {
|
||||
GOOGLE_CALENDAR_SCOPES,
|
||||
GOOGLE_OAUTH_SCOPES,
|
||||
HOSTED_CAL_FEATURES,
|
||||
IS_CALCOM,
|
||||
} from "@calcom/lib/constants";
|
||||
import { ENABLE_PROFILE_SWITCHER, IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { symmetricDecrypt, symmetricEncrypt } from "@calcom/lib/crypto";
|
||||
import { defaultCookies } from "@calcom/lib/default-cookies";
|
||||
@@ -23,6 +32,7 @@ import { isENVDev } from "@calcom/lib/env";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { randomString } from "@calcom/lib/random";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
@@ -251,6 +261,13 @@ if (IS_GOOGLE_LOGIN_ENABLED) {
|
||||
clientId: GOOGLE_CLIENT_ID,
|
||||
clientSecret: GOOGLE_CLIENT_SECRET,
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
authorization: {
|
||||
params: {
|
||||
scope: [...GOOGLE_OAUTH_SCOPES, ...GOOGLE_CALENDAR_SCOPES].join(" "),
|
||||
access_type: "offline",
|
||||
prompt: "consent",
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -608,6 +625,63 @@ export const getOptions = ({
|
||||
return await autoMergeIdentities();
|
||||
}
|
||||
|
||||
const grantedScopes = account.scope?.split(" ") ?? [];
|
||||
if (
|
||||
account.provider === "google" &&
|
||||
!(await CredentialRepository.findFirstByAppIdAndUserId({
|
||||
userId: user.id as number,
|
||||
appId: "google-calendar",
|
||||
})) &&
|
||||
GOOGLE_CALENDAR_SCOPES.every((scope) => grantedScopes.includes(scope))
|
||||
) {
|
||||
// Installing Google Calendar by default
|
||||
const credentialkey = {
|
||||
access_token: account.access_token,
|
||||
refresh_token: account.refresh_token,
|
||||
id_token: account.id_token,
|
||||
token_type: account.token_type,
|
||||
expires_at: account.expires_at,
|
||||
};
|
||||
const gcalCredential = await CredentialRepository.create({
|
||||
userId: user.id as number,
|
||||
key: credentialkey,
|
||||
appId: "google-calendar",
|
||||
type: "google_calendar",
|
||||
});
|
||||
const gCalService = new GoogleCalendarService({
|
||||
...gcalCredential,
|
||||
user: null,
|
||||
});
|
||||
|
||||
if (
|
||||
!(await CredentialRepository.findFirstByUserIdAndType({
|
||||
userId: user.id as number,
|
||||
type: "google_video",
|
||||
}))
|
||||
) {
|
||||
await CredentialRepository.create({
|
||||
type: "google_video",
|
||||
key: {},
|
||||
userId: user.id as number,
|
||||
appId: "google-meet",
|
||||
});
|
||||
}
|
||||
|
||||
const oAuth2Client = new OAuth2Client(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET);
|
||||
oAuth2Client.setCredentials(credentialkey);
|
||||
const calendar = new calendar_v3.Calendar({
|
||||
auth: oAuth2Client,
|
||||
});
|
||||
const primaryCal = await gCalService.getPrimaryCalendar(calendar);
|
||||
if (primaryCal?.id) {
|
||||
await gCalService.createSelectedCalendar({
|
||||
externalId: primaryCal.id,
|
||||
userId: user.id as number,
|
||||
});
|
||||
}
|
||||
await updateProfilePhotoGoogle(oAuth2Client, user.id as number);
|
||||
}
|
||||
|
||||
return {
|
||||
...token,
|
||||
id: existingUser.id,
|
||||
@@ -632,7 +706,6 @@ export const getOptions = ({
|
||||
log.debug("callbacks:session - Session callback called", safeStringify({ session, token, user }));
|
||||
const licenseKeyService = await LicenseKeySingleton.getInstance();
|
||||
const hasValidLicense = await licenseKeyService.checkLicense();
|
||||
|
||||
const profileId = token.profileId;
|
||||
const calendsoSession: Session = {
|
||||
...session,
|
||||
|
||||
@@ -619,10 +619,16 @@ async function handler(
|
||||
// freeUsers is ensured
|
||||
const originalRescheduledBookingUserId =
|
||||
originalRescheduledBooking && originalRescheduledBooking.userId;
|
||||
const isSameRoundRobinHost =
|
||||
|
||||
const isRouting = !!routedTeamMemberIds;
|
||||
const isRerouting = originalRescheduledBookingUserId && isRouting;
|
||||
const shouldUseSameRRHost =
|
||||
!!originalRescheduledBookingUserId &&
|
||||
eventType.schedulingType === SchedulingType.ROUND_ROBIN &&
|
||||
eventType.rescheduleWithSameRoundRobinHost;
|
||||
eventType.rescheduleWithSameRoundRobinHost &&
|
||||
// If it is rerouting, we should not force reschedule with same host.
|
||||
// It will be unexpected plus could cause unavailable slots as original host might not be part of routedTeamMemberIds
|
||||
!isRerouting;
|
||||
|
||||
const userIdsSet = new Set(users.map((user) => user.id));
|
||||
|
||||
@@ -646,7 +652,7 @@ async function handler(
|
||||
});
|
||||
}
|
||||
|
||||
const newLuckyUser = isSameRoundRobinHost
|
||||
const newLuckyUser = shouldUseSameRRHost
|
||||
? freeUsers.find((user) => user.id === originalRescheduledBookingUserId)
|
||||
: await getLuckyUser({
|
||||
// find a lucky user that is not already in the luckyUsers array
|
||||
|
||||
@@ -2546,6 +2546,165 @@ describe("handleNewBooking", () => {
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
test(
|
||||
"should reschedule as per routedTeamMemberIds(instead of same host) even if rescheduleWithSameRoundRobinHost is true but it is a rerouting scenario",
|
||||
async ({ emails }) => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const otherHost = getOrganizer({
|
||||
name: "RR Host 1",
|
||||
email: "rrhost1@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
|
||||
const hostOfOriginalBooking = getOrganizer({
|
||||
name: "RR Host 2",
|
||||
email: "rrhost2@example.com",
|
||||
id: 102,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const uidOfBookingToBeRescheduled = "n5Wv3eHgconAED2j4gcVhP";
|
||||
await createBookingScenario(
|
||||
getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 15,
|
||||
length: 15,
|
||||
hosts: [
|
||||
{
|
||||
userId: 101,
|
||||
isFixed: false,
|
||||
},
|
||||
{
|
||||
userId: 102,
|
||||
isFixed: false,
|
||||
},
|
||||
],
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
rescheduleWithSameRoundRobinHost: true,
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
uid: uidOfBookingToBeRescheduled,
|
||||
eventTypeId: 1,
|
||||
userId: 102,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `${plus1DateString}T05:00:00.000Z`,
|
||||
endTime: `${plus1DateString}T05:15:00.000Z`,
|
||||
metadata: {
|
||||
videoCallUrl: "https://existing-daily-video-call-url.example.com",
|
||||
},
|
||||
},
|
||||
],
|
||||
organizer: otherHost,
|
||||
usersApartFromOrganizer: [hostOfOriginalBooking],
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
mockSuccessfulVideoMeetingCreation({
|
||||
metadataLookupKey: "dailyvideo",
|
||||
});
|
||||
|
||||
mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
uid: "MOCK_ID",
|
||||
},
|
||||
update: {
|
||||
uid: "UPDATED_MOCK_ID",
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
user: otherHost.name,
|
||||
rescheduleUid: uidOfBookingToBeRescheduled,
|
||||
start: `${plus1DateString}T04:00:00.000Z`,
|
||||
end: `${plus1DateString}T04:15:00.000Z`,
|
||||
routedTeamMemberIds: [101],
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
},
|
||||
},
|
||||
});
|
||||
const { req } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData,
|
||||
});
|
||||
|
||||
const createdBooking = await handleNewBooking(req);
|
||||
|
||||
const previousBooking = await prismaMock.booking.findUnique({
|
||||
where: {
|
||||
uid: uidOfBookingToBeRescheduled,
|
||||
},
|
||||
});
|
||||
|
||||
logger.silly({
|
||||
previousBooking,
|
||||
allBookings: await prismaMock.booking.findMany(),
|
||||
});
|
||||
|
||||
// Expect previous booking to be cancelled
|
||||
await expectBookingToBeInDatabase({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
uid: uidOfBookingToBeRescheduled,
|
||||
status: BookingStatus.CANCELLED,
|
||||
});
|
||||
|
||||
expect(previousBooking?.status).toBe(BookingStatus.CANCELLED);
|
||||
/**
|
||||
* Booking Time should be new time
|
||||
*/
|
||||
expect(createdBooking.startTime?.toISOString()).toBe(`${plus1DateString}T04:00:00.000Z`);
|
||||
expect(createdBooking.endTime?.toISOString()).toBe(`${plus1DateString}T04:15:00.000Z`);
|
||||
|
||||
expect(createdBooking.userId).toBe(otherHost.id);
|
||||
|
||||
await expectBookingInDBToBeRescheduledFromTo({
|
||||
from: {
|
||||
uid: uidOfBookingToBeRescheduled,
|
||||
},
|
||||
to: {
|
||||
description: "",
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
uid: createdBooking.uid!,
|
||||
eventTypeId: mockBookingData.eventTypeId,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
location: BookingLocations.CalVideo,
|
||||
responses: expect.objectContaining({
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
expectSuccessfulRoundRobinReschedulingEmails({
|
||||
prevOrganizer: hostOfOriginalBooking,
|
||||
newOrganizer: otherHost,
|
||||
emails,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -185,14 +185,6 @@ export default async function handleChildrenEventTypes({
|
||||
*/
|
||||
rrSegmentQueryValue: undefined,
|
||||
assignRRMembersUsingSegment: false,
|
||||
|
||||
// Reserved for future releases
|
||||
/*
|
||||
webhooks: eventType.webhooks && {
|
||||
createMany: {
|
||||
data: eventType.webhooks?.map((wh) => ({ ...wh, eventTypeId: undefined })),
|
||||
},
|
||||
},*/
|
||||
},
|
||||
});
|
||||
})
|
||||
@@ -275,23 +267,6 @@ export default async function handleChildrenEventTypes({
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Reserved for future releases
|
||||
/**
|
||||
const updatedOldWebhooks = await prisma.webhook.updateMany({
|
||||
where: {
|
||||
userId: {
|
||||
in: oldUserIds,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
...eventType.webhooks,
|
||||
},
|
||||
});
|
||||
console.log(
|
||||
"handleChildrenEventTypes:updatedOldWebhooks",
|
||||
JSON.stringify({ updatedOldWebhooks }, null, 2)
|
||||
);*/
|
||||
}
|
||||
|
||||
// Old users deleted
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { AppSkeletonLoader as SkeletonLoader, Meta } from "@calcom/ui";
|
||||
|
||||
import { getLayout } from "../../../settings/layouts/SettingsLayout";
|
||||
import SSOConfiguration from "../components/SSOConfiguration";
|
||||
|
||||
const SAMLSSO = () => {
|
||||
const params = useParamsWithFallback();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
const teamId = Number(params.id);
|
||||
|
||||
const {
|
||||
data: team,
|
||||
isPending,
|
||||
error,
|
||||
} = trpc.viewer.teams.get.useQuery(
|
||||
{ teamId },
|
||||
{
|
||||
enabled: !!teamId,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!HOSTED_CAL_FEATURES) {
|
||||
router.push("/404");
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
function refactorMeWithoutEffect() {
|
||||
if (error) {
|
||||
router.replace("/teams");
|
||||
}
|
||||
},
|
||||
[error]
|
||||
);
|
||||
if (isPending) {
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
|
||||
if (!team) {
|
||||
router.push("/404");
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-default w-full sm:mx-0 xl:mt-0">
|
||||
<Meta title={t("sso_configuration")} description={t("sso_configuration_description")} />
|
||||
<SSOConfiguration teamId={teamId} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SAMLSSO.getLayout = getLayout;
|
||||
|
||||
export default SAMLSSO;
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import NoSSR from "@calcom/core/components/NoSSR";
|
||||
|
||||
import { getLayout } from "../../../settings/layouts/SettingsLayout";
|
||||
import { UsersTable } from "../components/UsersTable";
|
||||
|
||||
const DeploymentUsersListPage = () => {
|
||||
@@ -15,6 +14,4 @@ const DeploymentUsersListPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
DeploymentUsersListPage.getLayout = getLayout;
|
||||
|
||||
export default DeploymentUsersListPage;
|
||||
|
||||
@@ -240,6 +240,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
} else if (reminder.workflowStep.template === WorkflowTemplates.REMINDER) {
|
||||
emailContent = emailReminderTemplate(
|
||||
false,
|
||||
reminder.booking.user?.locale || "en",
|
||||
reminder.workflowStep.action,
|
||||
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
|
||||
reminder.booking.startTime.toISOString() || "",
|
||||
@@ -366,6 +367,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
||||
emailContent = emailReminderTemplate(
|
||||
false,
|
||||
reminder.booking.user?.locale || "en",
|
||||
WorkflowActions.EMAIL_ATTENDEE,
|
||||
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
|
||||
reminder.booking.startTime.toISOString() || "",
|
||||
|
||||
@@ -81,7 +81,7 @@ const getTimeSectionText = (trigger: WorkflowTriggerEvents, t: TFunction) => {
|
||||
};
|
||||
|
||||
export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||
const { t } = useLocale();
|
||||
const { t, i18n } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const { step, form, reload, setReload, teamId } = props;
|
||||
@@ -145,13 +145,14 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||
whatsappReminderTemplate(true, action, timeFormat)
|
||||
);
|
||||
} else {
|
||||
const reminderBodyTemplate = emailReminderTemplate(true, action, timeFormat).emailBody;
|
||||
const reminderBodyTemplate = emailReminderTemplate(true, i18n.language, action, timeFormat).emailBody;
|
||||
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, reminderBodyTemplate);
|
||||
}
|
||||
}
|
||||
if (!form.getValues(`steps.${step.stepNumber - 1}.emailSubject`)) {
|
||||
const subjectTemplate = emailReminderTemplate(
|
||||
true,
|
||||
i18n.language,
|
||||
form.getValues(`steps.${step.stepNumber - 1}.action`),
|
||||
timeFormat
|
||||
).emailSubject;
|
||||
@@ -525,6 +526,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||
} else {
|
||||
const emailReminderBody = emailReminderTemplate(
|
||||
true,
|
||||
i18n.language,
|
||||
val.value,
|
||||
timeFormat
|
||||
);
|
||||
@@ -832,11 +834,11 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||
} else {
|
||||
form.setValue(
|
||||
`steps.${step.stepNumber - 1}.reminderBody`,
|
||||
emailReminderTemplate(true, action, timeFormat).emailBody
|
||||
emailReminderTemplate(true, i18n.language, action, timeFormat).emailBody
|
||||
);
|
||||
form.setValue(
|
||||
`steps.${step.stepNumber - 1}.emailSubject`,
|
||||
emailReminderTemplate(true, action, timeFormat).emailSubject
|
||||
emailReminderTemplate(true, i18n.language, action, timeFormat).emailSubject
|
||||
);
|
||||
}
|
||||
} else if (val.value === WorkflowTemplates.RATING) {
|
||||
|
||||
@@ -185,6 +185,7 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) =>
|
||||
} else if (template === WorkflowTemplates.REMINDER) {
|
||||
emailContent = emailReminderTemplate(
|
||||
false,
|
||||
evt.organizer.language.locale,
|
||||
action,
|
||||
evt.organizer.timeFormat,
|
||||
startTime,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { WorkflowActions } from "@calcom/prisma/enums";
|
||||
|
||||
const emailReminderTemplate = (
|
||||
isEditingMode: boolean,
|
||||
locale: string,
|
||||
action?: WorkflowActions,
|
||||
timeFormat?: TimeFormat,
|
||||
startTime?: string,
|
||||
@@ -28,9 +29,9 @@ const emailReminderTemplate = (
|
||||
name = action === WorkflowActions.EMAIL_ATTENDEE ? "{ATTENDEE}" : "{ORGANIZER}";
|
||||
eventDate = `{EVENT_DATE_${dateTimeFormat}}`;
|
||||
} else {
|
||||
eventDate = dayjs(startTime).tz(timeZone).format(dateTimeFormat);
|
||||
eventDate = dayjs(startTime).tz(timeZone).locale(locale).format(dateTimeFormat);
|
||||
|
||||
endTime = dayjs(endTime).tz(timeZone).format(currentTimeFormat);
|
||||
endTime = dayjs(endTime).tz(timeZone).locale(locale).format(currentTimeFormat);
|
||||
}
|
||||
|
||||
const emailSubject = `Reminder: ${eventName} - ${eventDate}`;
|
||||
|
||||
@@ -7,8 +7,8 @@ import { useFormContext } from "react-hook-form";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { FormValues, EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
|
||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||
import EventTypeWebhookListItem from "@calcom/features/webhooks/components/EventTypeWebhookListItem";
|
||||
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
||||
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
||||
import { subscriberUrlReserved } from "@calcom/features/webhooks/lib/subscriberUrlReserved";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -103,8 +103,9 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
translate: t,
|
||||
formMethods,
|
||||
});
|
||||
const webhookLockedStatus = shouldLockDisableProps("webhooks");
|
||||
|
||||
const webhooksDisableProps = shouldLockDisableProps("webhooks", { simple: true });
|
||||
const lockedText = webhooksDisableProps.isLocked ? "locked" : "unlocked";
|
||||
const cannotEditWebhooks = isChildrenManagedEventType ? webhooksDisableProps.isLocked : false;
|
||||
return (
|
||||
<div>
|
||||
{webhooks && !isLoading && (
|
||||
@@ -112,12 +113,31 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
<div>
|
||||
<div>
|
||||
<>
|
||||
{isManagedEventType && (
|
||||
{(isManagedEventType || isChildrenManagedEventType) && (
|
||||
<Alert
|
||||
severity="neutral"
|
||||
severity={webhooksDisableProps.isLocked ? "neutral" : "green"}
|
||||
className="mb-2"
|
||||
title={t("locked_for_members")}
|
||||
message={t("locked_webhooks_description")}
|
||||
title={
|
||||
<Trans
|
||||
i18nKey={`${lockedText}_${isManagedEventType ? "for_members" : "by_team_admins"}`}>
|
||||
{lockedText[0].toUpperCase()}
|
||||
{lockedText.slice(1)} {isManagedEventType ? "for members" : "by team admins"}
|
||||
</Trans>
|
||||
}
|
||||
actions={
|
||||
<div className="flex h-full items-center">{webhooksDisableProps.LockedIcon}</div>
|
||||
}
|
||||
message={
|
||||
<Trans
|
||||
i18nKey={`webhooks_${lockedText}_${
|
||||
isManagedEventType ? "for_members" : "by_team_admins"
|
||||
}_description`}>
|
||||
{isManagedEventType ? "Members" : "You"}{" "}
|
||||
{webhooksDisableProps.isLocked
|
||||
? "will be able to see the active webhooks but will not be able to edit any webhook settings"
|
||||
: "will be able to see the active webhooks and will be able to edit any webhook settings"}
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{webhooks.length ? (
|
||||
@@ -130,7 +150,7 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
{t("add_webhook_description", { appName: APP_NAME })}
|
||||
</p>
|
||||
</div>
|
||||
{isChildrenManagedEventType && !isManagedEventType ? (
|
||||
{cannotEditWebhooks ? (
|
||||
<Button StartIcon="lock" color="secondary" disabled>
|
||||
{t("locked_by_team_admin")}
|
||||
</Button>
|
||||
@@ -142,15 +162,15 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
<div className="border-subtle my-8 rounded-md border">
|
||||
{webhooks.map((webhook, index) => {
|
||||
return (
|
||||
<WebhookListItem
|
||||
<EventTypeWebhookListItem
|
||||
key={webhook.id}
|
||||
webhook={webhook}
|
||||
lastItem={webhooks.length === index + 1}
|
||||
canEditWebhook={!webhookLockedStatus.disabled}
|
||||
onEditWebhook={() => {
|
||||
setEditModalOpen(true);
|
||||
setWebhookToEdit(webhook);
|
||||
}}
|
||||
readOnly={isChildrenManagedEventType && webhook.eventTypeId !== eventType.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -174,7 +194,7 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
headline={t("create_your_first_webhook")}
|
||||
description={t("first_event_type_webhook_description")}
|
||||
buttonRaw={
|
||||
isChildrenManagedEventType && !isManagedEventType ? (
|
||||
cannotEditWebhooks ? (
|
||||
<Button StartIcon="lock" color="secondary" disabled>
|
||||
{t("locked_by_team_admin")}
|
||||
</Button>
|
||||
|
||||
@@ -63,9 +63,6 @@ export async function translateEventTypeDescription(payload: string): Promise<vo
|
||||
eventTypeId,
|
||||
sourceLocale: userLocale,
|
||||
targetLocale,
|
||||
// Keep old fields during transition
|
||||
sourceLang: userLocale, // TODO: remove after migration
|
||||
targetLang: targetLocale, // TODO: remove after migration
|
||||
translatedText,
|
||||
userId,
|
||||
}))
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
showToast,
|
||||
Switch,
|
||||
Tooltip,
|
||||
} from "@calcom/ui";
|
||||
|
||||
type WebhookProps = {
|
||||
id: string;
|
||||
subscriberUrl: string;
|
||||
payloadTemplate: string | null;
|
||||
active: boolean;
|
||||
eventTriggers: WebhookTriggerEvents[];
|
||||
secret: string | null;
|
||||
eventTypeId: number | null;
|
||||
teamId: number | null;
|
||||
};
|
||||
|
||||
export default function EventTypeWebhookListItem(props: {
|
||||
webhook: WebhookProps;
|
||||
onEditWebhook: () => void;
|
||||
lastItem: boolean;
|
||||
readOnly?: boolean;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
const { webhook } = props;
|
||||
|
||||
const deleteWebhook = trpc.viewer.webhook.delete.useMutation({
|
||||
async onSuccess() {
|
||||
showToast(t("webhook_removed_successfully"), "success");
|
||||
await utils.viewer.webhook.getByViewer.invalidate();
|
||||
await utils.viewer.webhook.list.invalidate();
|
||||
await utils.viewer.eventTypes.get.invalidate();
|
||||
},
|
||||
});
|
||||
const toggleWebhook = trpc.viewer.webhook.edit.useMutation({
|
||||
async onSuccess(data) {
|
||||
// TODO: Better success message
|
||||
showToast(t(data?.active ? "enabled" : "disabled"), "success");
|
||||
await utils.viewer.webhook.getByViewer.invalidate();
|
||||
await utils.viewer.webhook.list.invalidate();
|
||||
await utils.viewer.eventTypes.get.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const onDeleteWebhook = () => {
|
||||
// TODO: Confimation dialog before deleting
|
||||
deleteWebhook.mutate({
|
||||
id: webhook.id,
|
||||
eventTypeId: webhook.eventTypeId || undefined,
|
||||
teamId: webhook.teamId || undefined,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex w-full justify-between p-4",
|
||||
props.lastItem ? "" : "border-subtle border-b"
|
||||
)}>
|
||||
<div className="w-full truncate">
|
||||
<div className="flex">
|
||||
<Tooltip content={webhook.subscriberUrl}>
|
||||
<p className="text-emphasis max-w-[600px] truncate text-sm font-medium">
|
||||
{webhook.subscriberUrl}
|
||||
</p>
|
||||
</Tooltip>
|
||||
{!!props.readOnly && (
|
||||
<Badge variant="gray" className="ml-2 ">
|
||||
{t("readonly")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip content={t("triggers_when")}>
|
||||
<div className="flex w-4/5 flex-wrap">
|
||||
{webhook.eventTriggers.map((trigger) => (
|
||||
<Badge
|
||||
key={trigger}
|
||||
className="mt-2.5 basis-1/5 ltr:mr-2 rtl:ml-2"
|
||||
variant="gray"
|
||||
startIcon="zap">
|
||||
{t(`${trigger.toLowerCase()}`)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{!props.readOnly && (
|
||||
<div className="ml-2 flex items-center space-x-4">
|
||||
<Switch
|
||||
defaultChecked={webhook.active}
|
||||
data-testid="webhook-switch"
|
||||
onCheckedChange={() =>
|
||||
toggleWebhook.mutate({
|
||||
id: webhook.id,
|
||||
active: !webhook.active,
|
||||
payloadTemplate: webhook.payloadTemplate,
|
||||
eventTypeId: webhook.eventTypeId || undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="hidden lg:flex"
|
||||
color="secondary"
|
||||
onClick={props.onEditWebhook}
|
||||
data-testid="webhook-edit-button">
|
||||
{t("edit")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="hidden lg:flex"
|
||||
color="destructive"
|
||||
StartIcon="trash"
|
||||
variant="icon"
|
||||
onClick={onDeleteWebhook}
|
||||
/>
|
||||
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="lg:hidden" StartIcon="ellipsis" variant="icon" color="secondary" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem StartIcon="pencil" color="secondary" onClick={props.onEditWebhook}>
|
||||
{t("edit")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem StartIcon="trash" color="destructive" onClick={onDeleteWebhook}>
|
||||
{t("delete")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,20 @@ const getWebhooks = async (options: GetSubscriberOptions, prisma: PrismaClient =
|
||||
const orgId = options.orgId ?? 0;
|
||||
const oAuthClientId = options.oAuthClientId ?? "";
|
||||
|
||||
const managedChildEventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
id: eventTypeId,
|
||||
parentId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
parentId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const managedParentEventTypeId = managedChildEventType?.parentId ?? 0;
|
||||
|
||||
// if we have userId and teamId it is a managed event type and should trigger for team and user
|
||||
const allWebhooks = await prisma.webhook.findMany({
|
||||
where: {
|
||||
@@ -32,6 +46,9 @@ const getWebhooks = async (options: GetSubscriberOptions, prisma: PrismaClient =
|
||||
{
|
||||
eventTypeId,
|
||||
},
|
||||
{
|
||||
eventTypeId: managedParentEventTypeId,
|
||||
},
|
||||
{
|
||||
teamId: {
|
||||
in: [...teamIds, orgId],
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { WebhooksByViewer } from "@calcom/trpc/server/routers/viewer/webhook/getByViewer.handler";
|
||||
import { Avatar, EmptyScreen, Meta, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||
import { Avatar, EmptyScreen, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||
|
||||
import { WebhookListItem, CreateNewWebhookButton } from "../components";
|
||||
|
||||
@@ -26,7 +26,7 @@ const SkeletonLoader = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const WebhooksView = () => {
|
||||
const WebhooksView = () => {
|
||||
const { t } = useLocale();
|
||||
const session = useSession();
|
||||
const isAdmin = session.data?.user.role === UserPermissionRole.ADMIN;
|
||||
|
||||
@@ -189,3 +189,11 @@ export const RECORDING_DEFAULT_ICON = IS_PRODUCTION
|
||||
export const RECORDING_IN_PROGRESS_ICON = IS_PRODUCTION
|
||||
? `${WEBAPP_URL}/stop-recording.svg`
|
||||
: `https://app.cal.com/stop-recording.svg`;
|
||||
|
||||
export const SCOPE_USERINFO_PROFILE = "https://www.googleapis.com/auth/userinfo.profile";
|
||||
export const SCOPE_USERINFO_EMAIL = "https://www.googleapis.com/auth/userinfo.email";
|
||||
export const GOOGLE_OAUTH_SCOPES = [SCOPE_USERINFO_PROFILE, SCOPE_USERINFO_EMAIL];
|
||||
export const GOOGLE_CALENDAR_SCOPES = [
|
||||
"https://www.googleapis.com/auth/calendar.events",
|
||||
"https://www.googleapis.com/auth/calendar.readonly",
|
||||
];
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { OAuth2Client } from "google-auth-library";
|
||||
import type { calendar_v3 } from "googleapis";
|
||||
import { google } from "googleapis";
|
||||
|
||||
import logger from "@calcom/lib/logger";
|
||||
|
||||
import { UserRepository } from "./server/repository/user";
|
||||
|
||||
export async function getAllCalendars(
|
||||
calendar: calendar_v3.Calendar,
|
||||
fields: string[] = ["id", "summary", "primary", "accessRole"]
|
||||
): Promise<calendar_v3.Schema$CalendarListEntry[]> {
|
||||
let allCalendars: calendar_v3.Schema$CalendarListEntry[] = [];
|
||||
let pageToken: string | undefined;
|
||||
|
||||
try {
|
||||
do {
|
||||
const response: any = await calendar.calendarList.list({
|
||||
fields: `items(${fields.join(",")}),nextPageToken`,
|
||||
pageToken,
|
||||
maxResults: 250, // 250 is max
|
||||
});
|
||||
|
||||
allCalendars = [...allCalendars, ...(response.data.items ?? [])];
|
||||
pageToken = response.data.nextPageToken;
|
||||
} while (pageToken);
|
||||
|
||||
return allCalendars;
|
||||
} catch (error) {
|
||||
logger.error("Error fetching all Google Calendars", { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateProfilePhoto(oAuth2Client: OAuth2Client, userId: number) {
|
||||
try {
|
||||
const oauth2 = google.oauth2({ version: "v2", auth: oAuth2Client });
|
||||
const userDetails = await oauth2.userinfo.get();
|
||||
if (userDetails.data?.picture) {
|
||||
await UserRepository.updateAvatar({ id: userId, avatarUrl: userDetails.data.picture });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error updating avatarUrl from google calendar connect", error);
|
||||
}
|
||||
}
|
||||
@@ -245,7 +245,7 @@ export function isTimeViolatingFutureLimit({
|
||||
const dateInSystemTz = dayjs(time);
|
||||
if (periodLimits.endOfRollingPeriodEndDayInBookerTz) {
|
||||
const isAfterRollingEndDay = dateInSystemTz.isAfter(periodLimits.endOfRollingPeriodEndDayInBookerTz);
|
||||
log.debug("rollingEndDayCheck", {
|
||||
log.silly("rollingEndDayCheck", {
|
||||
formattedDate: dateInSystemTz.format(),
|
||||
isAfterRollingEndDay,
|
||||
endOfRollingPeriodEndDayInBookerTz: periodLimits.endOfRollingPeriodEndDayInBookerTz.format(),
|
||||
@@ -256,7 +256,7 @@ export function isTimeViolatingFutureLimit({
|
||||
if (periodLimits.startOfRangeStartDayInEventTz && periodLimits.endOfRangeEndDayInEventTz) {
|
||||
const isBeforeRangeStart = dateInSystemTz.isBefore(periodLimits.startOfRangeStartDayInEventTz);
|
||||
const isAfterRangeEnd = dateInSystemTz.isAfter(periodLimits.endOfRangeEndDayInEventTz);
|
||||
log.debug("rangeCheck", {
|
||||
log.silly("rangeCheck", {
|
||||
formattedDate: dateInSystemTz.format(),
|
||||
isAfterRangeEnd,
|
||||
isBeforeRangeStart,
|
||||
|
||||
@@ -30,6 +30,15 @@ export class CredentialRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findFirstByAppIdAndUserId({ appId, userId }: { appId: string; userId: number }) {
|
||||
return await prisma.credential.findFirst({
|
||||
where: {
|
||||
appId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static async findFirstByUserIdAndType({ userId, type }: { userId: number; type: string }) {
|
||||
return await prisma.credential.findFirst({ where: { userId, type } });
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user