Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b2161cc19 | ||
|
|
b997c656df | ||
|
|
c9c5493de6 | ||
|
|
20a5775f31 | ||
|
|
b9b0a86b32 | ||
|
|
ad93066cf6 | ||
|
|
e6d76b630c | ||
|
|
b941b716d5 | ||
|
|
1ab9622416 | ||
|
|
15156c2225 | ||
|
|
cc202ed847 | ||
|
|
79d51a4989 | ||
|
|
53a846d10c |
+1
-2
@@ -355,8 +355,6 @@ E2E_TEST_OIDC_USER_PASSWORD=
|
||||
# provide a value between 0 and 100 to ensure the percentage of traffic
|
||||
# redirected from the legacy to the future pages
|
||||
AB_TEST_BUCKET_PROBABILITY=50
|
||||
# whether we redirect to the future/event-types from event-types or not
|
||||
APP_ROUTER_EVENT_TYPES_ENABLED=0
|
||||
APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=0
|
||||
APP_ROUTER_APPS_SLUG_ENABLED=0
|
||||
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
|
||||
@@ -365,6 +363,7 @@ APP_ROUTER_APPS_CATEGORIES_ENABLED=0
|
||||
# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
|
||||
APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
|
||||
APP_ROUTER_APPS_ENABLED=0
|
||||
APP_ROUTER_TEAM_ENABLED=0
|
||||
APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0
|
||||
APP_ROUTER_AUTH_LOGIN_ENABLED=0
|
||||
APP_ROUTER_AUTH_LOGOUT_ENABLED=0
|
||||
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
@@ -29,7 +29,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.79",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.81",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
|
||||
@@ -59,6 +59,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
let team1EventTypeId: number;
|
||||
let team2EventTypeId: number;
|
||||
let phoneOnlyEventTypeId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
@@ -205,6 +206,83 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
team1EventTypeId = team1EventType.id;
|
||||
|
||||
const phoneOnlyEventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
team: {
|
||||
connect: { id: team1.id },
|
||||
},
|
||||
title: "Phone Only Event Type",
|
||||
slug: "phone-only-event-type",
|
||||
length: 15,
|
||||
assignAllTeamMembers: false,
|
||||
hosts: {
|
||||
connectOrCreate: [
|
||||
{
|
||||
where: {
|
||||
userId_eventTypeId: {
|
||||
userId: teamUser.id,
|
||||
eventTypeId: team1EventTypeId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
userId: teamUser.id,
|
||||
isFixed: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
bookingFields: [
|
||||
{
|
||||
name: "name",
|
||||
type: "name",
|
||||
label: "your name",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
variant: "fullName",
|
||||
editable: "system",
|
||||
required: true,
|
||||
defaultLabel: "your_name",
|
||||
variantsConfig: {
|
||||
variants: {
|
||||
fullName: {
|
||||
fields: [{ name: "fullName", type: "text", label: "your name", required: true }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
type: "email",
|
||||
label: "your email",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system",
|
||||
required: false,
|
||||
defaultLabel: "email_address",
|
||||
},
|
||||
{
|
||||
name: "attendeePhoneNumber",
|
||||
type: "phone",
|
||||
label: "phone_number",
|
||||
sources: [{ id: "user", type: "user", label: "User", fieldRequired: true }],
|
||||
editable: "user",
|
||||
required: true,
|
||||
placeholder: "",
|
||||
},
|
||||
{
|
||||
name: "rescheduleReason",
|
||||
type: "textarea",
|
||||
views: [{ id: "reschedule", label: "Reschedule View" }],
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "reason_for_reschedule",
|
||||
defaultPlaceholder: "reschedule_placeholder",
|
||||
},
|
||||
],
|
||||
locations: [],
|
||||
});
|
||||
|
||||
phoneOnlyEventTypeId = phoneOnlyEventType.id;
|
||||
|
||||
const team2EventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "COLLECTIVE",
|
||||
team: {
|
||||
@@ -322,6 +400,60 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a phone based booking", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
|
||||
eventTypeId: phoneOnlyEventTypeId,
|
||||
attendee: {
|
||||
name: "alice",
|
||||
phoneNumber: "+919876543210",
|
||||
timeZone: "Europe/Madrid",
|
||||
language: "es",
|
||||
},
|
||||
meetingUrl: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts.length).toEqual(1);
|
||||
expect(data.hosts[0].id).toEqual(teamUser.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 15, 15, 0)).toISOString());
|
||||
expect(data.duration).toEqual(15);
|
||||
expect(data.eventTypeId).toEqual(phoneOnlyEventTypeId);
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: "919876543210@sms.cal.com",
|
||||
phoneNumber: body.attendee.phoneNumber,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
});
|
||||
expect(data.meetingUrl).toEqual(body.meetingUrl);
|
||||
expect(data.absentHost).toEqual(false);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a team 2 booking", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
|
||||
@@ -398,7 +530,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(1);
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data[0].eventTypeId).toEqual(team1EventTypeId);
|
||||
});
|
||||
});
|
||||
@@ -436,7 +568,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data.length).toEqual(3);
|
||||
expect(data.find((booking) => booking.eventTypeId === team1EventTypeId)).toBeDefined();
|
||||
expect(data.find((booking) => booking.eventTypeId === team2EventTypeId)).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -13,7 +13,6 @@ import { Request } from "express";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
handleNewBooking,
|
||||
handleNewRecurringBooking,
|
||||
getAllUserBookings,
|
||||
handleInstantMeeting,
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
handleMarkNoShow,
|
||||
confirmBookingHandler,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { handleNewBooking } from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
CreateBookingInput,
|
||||
|
||||
@@ -160,9 +160,14 @@ export class InputBookingsService_2024_08_13 {
|
||||
? {
|
||||
...inputBooking.bookingFieldsResponses,
|
||||
name: inputBooking.attendee.name,
|
||||
email: inputBooking.attendee.email,
|
||||
email: inputBooking.attendee.email ?? "",
|
||||
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
|
||||
}
|
||||
: { name: inputBooking.attendee.name, email: inputBooking.attendee.email },
|
||||
: {
|
||||
name: inputBooking.attendee.name,
|
||||
email: inputBooking.attendee.email ?? "",
|
||||
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ type DatabaseBooking = Booking & {
|
||||
email: string;
|
||||
timeZone: string;
|
||||
locale: string | null;
|
||||
phoneNumber?: string | null;
|
||||
noShow: boolean | null;
|
||||
bookingSeat?: BookingSeat | null;
|
||||
}[];
|
||||
@@ -121,6 +122,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
absent: !!attendee.noShow,
|
||||
phoneNumber: attendee.phoneNumber ?? undefined,
|
||||
})),
|
||||
guests: bookingResponses.guests,
|
||||
location,
|
||||
|
||||
+1
@@ -128,6 +128,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
const bookingFields = databaseEventType.bookingFields
|
||||
? this.transformBookingFields(databaseEventType.bookingFields)
|
||||
: this.getDefaultBookingFields(isOrgTeamEvent);
|
||||
|
||||
const recurrence = this.transformRecurringEvent(databaseEventType.recurringEvent);
|
||||
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
|
||||
const users = this.transformUsers(databaseEventType.users || []);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
@Injectable()
|
||||
export class IsUserOOO implements CanActivate {
|
||||
constructor(private oooRepo: UserOOORepository) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const oooId: string = request.params.oooId;
|
||||
const userId: string = request.params.userId;
|
||||
|
||||
if (!userId) {
|
||||
throw new ForbiddenException("No user id found in request params.");
|
||||
}
|
||||
|
||||
if (!oooId) {
|
||||
throw new ForbiddenException("No ooo entry id found in request params.");
|
||||
}
|
||||
|
||||
const ooo = await this.oooRepo.getUserOOOByIdAndUserId(Number(oooId), Number(userId));
|
||||
|
||||
if (ooo) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new ForbiddenException("This OOO entry does not belong to this user.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
import { ApiProperty, ApiPropertyOptional, PartialType } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsDate, IsInt, IsOptional, IsString, IsEnum, isDate } from "class-validator";
|
||||
|
||||
export enum OutOfOfficeReason {
|
||||
UNSPECIFIED = "unspecified",
|
||||
VACATION = "vacation",
|
||||
TRAVEL = "travel",
|
||||
SICK_LEAVE = "sick",
|
||||
PUBLIC_HOLIDAY = "public_holiday",
|
||||
}
|
||||
|
||||
export type OutOfOfficeReasonType = `${OutOfOfficeReason}`;
|
||||
|
||||
const isDateString = (dateString: string) => {
|
||||
try {
|
||||
const isoDateRegex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{3})?Z$/;
|
||||
return isoDateRegex.test(dateString);
|
||||
} catch {
|
||||
throw new BadRequestException("Invalid Date.");
|
||||
}
|
||||
};
|
||||
|
||||
export class CreateOutOfOfficeEntryDto {
|
||||
@Transform(({ value }: { value: string }) => {
|
||||
if (isDateString(value)) {
|
||||
const date = new Date(value);
|
||||
date.setUTCHours(0, 0, 0, 0);
|
||||
return date;
|
||||
}
|
||||
throw new BadRequestException("Invalid Date.");
|
||||
})
|
||||
@IsDate()
|
||||
@ApiProperty({
|
||||
description: "The start date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
example: "2023-05-01T00:00:00.000Z",
|
||||
})
|
||||
start!: Date;
|
||||
|
||||
@Transform(({ value }: { value: string }) => {
|
||||
if (isDateString(value)) {
|
||||
const date = new Date(value);
|
||||
date.setUTCHours(23, 59, 59, 999);
|
||||
return date;
|
||||
}
|
||||
throw new BadRequestException("Invalid Date.");
|
||||
})
|
||||
@IsDate()
|
||||
@ApiProperty({
|
||||
description: "The end date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
example: "2023-05-10T23:59:59.999Z",
|
||||
})
|
||||
end!: Date;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "Optional notes for the out of office entry.",
|
||||
example: "Vacation in Hawaii",
|
||||
})
|
||||
notes?: string;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "The ID of the user covering for the out of office period, if applicable.",
|
||||
example: 2,
|
||||
})
|
||||
toUserId?: number;
|
||||
|
||||
@IsEnum(OutOfOfficeReason)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "the reason for the out of office entry, if applicable",
|
||||
example: "vacation",
|
||||
})
|
||||
reason?: OutOfOfficeReasonType;
|
||||
}
|
||||
|
||||
export class UpdateOutOfOfficeEntryDto extends PartialType(CreateOutOfOfficeEntryDto) {}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { OutOfOfficeReason } from "@/modules/ooo/inputs/ooo.input";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsInt, IsEnum, ValidateNested, IsString, IsDateString, IsOptional } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class UserOooOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@ApiProperty({
|
||||
description: "The ID of the user.",
|
||||
example: 2,
|
||||
})
|
||||
readonly userId!: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "The ID of the user covering for the out of office period, if applicable.",
|
||||
example: 2,
|
||||
})
|
||||
@Expose()
|
||||
readonly toUserId?: number;
|
||||
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@ApiProperty({
|
||||
description: "The ID of the ooo entry.",
|
||||
example: 2,
|
||||
})
|
||||
readonly id!: number;
|
||||
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiProperty({
|
||||
description: "The UUID of the ooo entry.",
|
||||
example: 2,
|
||||
})
|
||||
readonly uuid!: string;
|
||||
|
||||
@IsDateString()
|
||||
@ApiProperty({
|
||||
description: "The start date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
example: "2023-05-01T00:00:00.000Z",
|
||||
})
|
||||
@Expose()
|
||||
start!: Date;
|
||||
|
||||
@IsDateString()
|
||||
@ApiProperty({
|
||||
description: "The end date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
example: "2023-05-10T23:59:59.999Z",
|
||||
})
|
||||
@Expose()
|
||||
end!: Date;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "Optional notes for the out of office entry.",
|
||||
example: "Vacation in Hawaii",
|
||||
})
|
||||
@Expose()
|
||||
notes?: string;
|
||||
|
||||
@IsEnum(OutOfOfficeReason)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "the reason for the out of office entry, if applicable",
|
||||
example: "vacation",
|
||||
})
|
||||
@Expose()
|
||||
reason?: OutOfOfficeReason;
|
||||
}
|
||||
|
||||
export class UserOooOutputResponseDto {
|
||||
@Expose()
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => UserOooOutputDto)
|
||||
data!: UserOooOutputDto;
|
||||
}
|
||||
|
||||
export class UserOoosOutputResponseDto {
|
||||
@Expose()
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => UserOooOutputDto)
|
||||
data!: UserOooOutputDto[];
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
import { PrismaWriteService } from "../../prisma/prisma-write.service";
|
||||
|
||||
type OOOInputData = Omit<Prisma.OutOfOfficeEntryCreateInput, "user" | "toUser" | "reason" | "uuid"> & {
|
||||
toUserId?: number;
|
||||
userId: number;
|
||||
reasonId?: number;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class UserOOORepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async createUserOOO(data: OOOInputData) {
|
||||
const uuid = uuidv4();
|
||||
return this.dbWrite.prisma.outOfOfficeEntry.create({
|
||||
data: { ...data, uuid },
|
||||
include: { reason: true },
|
||||
});
|
||||
}
|
||||
|
||||
async updateUserOOO(oooId: number, data: Partial<OOOInputData>) {
|
||||
return this.dbWrite.prisma.outOfOfficeEntry.update({
|
||||
where: { id: oooId },
|
||||
data,
|
||||
include: { reason: true },
|
||||
});
|
||||
}
|
||||
|
||||
async getUserOOOById(oooId: number) {
|
||||
return this.dbRead.prisma.outOfOfficeEntry.findFirst({
|
||||
where: { id: oooId },
|
||||
include: { reason: true },
|
||||
});
|
||||
}
|
||||
|
||||
async getUserOOOByIdAndUserId(oooId: number, userId: number) {
|
||||
return this.dbRead.prisma.outOfOfficeEntry.findFirst({
|
||||
where: { id: oooId, userId },
|
||||
include: { reason: true },
|
||||
});
|
||||
}
|
||||
|
||||
async getUserOOOPaginated(userId: number, skip: number, take: number) {
|
||||
return this.dbRead.prisma.outOfOfficeEntry.findMany({
|
||||
where: { userId },
|
||||
skip,
|
||||
take,
|
||||
include: { reason: true },
|
||||
});
|
||||
}
|
||||
|
||||
async deleteUserOOO(oooId: number) {
|
||||
return this.dbWrite.prisma.outOfOfficeEntry.delete({
|
||||
where: { id: oooId },
|
||||
include: { reason: true },
|
||||
});
|
||||
}
|
||||
|
||||
async findExistingOooRedirect(userId: number, start: Date, end: Date, toUserId?: number) {
|
||||
const existingOutOfOfficeEntry = await this.dbRead.prisma.outOfOfficeEntry.findFirst({
|
||||
select: {
|
||||
userId: true,
|
||||
toUserId: true,
|
||||
},
|
||||
where: {
|
||||
...(toUserId && { userId: toUserId }),
|
||||
toUserId: userId,
|
||||
// Check for time overlap or collision
|
||||
OR: [
|
||||
// Outside of range
|
||||
{
|
||||
AND: [{ start: { lte: end } }, { end: { gte: start } }],
|
||||
},
|
||||
// Inside of range
|
||||
{
|
||||
AND: [{ start: { gte: start } }, { end: { lte: end } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
return existingOutOfOfficeEntry;
|
||||
}
|
||||
|
||||
async getOooByUserIdAndTime(userId: number, start: Date, end: Date) {
|
||||
return await this.dbRead.prisma.outOfOfficeEntry.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
CreateOutOfOfficeEntryDto,
|
||||
UpdateOutOfOfficeEntryDto,
|
||||
OutOfOfficeReason,
|
||||
} from "@/modules/ooo/inputs/ooo.input";
|
||||
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, ConflictException, Injectable } from "@nestjs/common";
|
||||
|
||||
import { OutOfOfficeEntry } from "@calcom/prisma/client";
|
||||
|
||||
const OOO_REASON_ID_TO_REASON = {
|
||||
1: OutOfOfficeReason["UNSPECIFIED"],
|
||||
2: OutOfOfficeReason["VACATION"],
|
||||
3: OutOfOfficeReason["TRAVEL"],
|
||||
4: OutOfOfficeReason["SICK_LEAVE"],
|
||||
5: OutOfOfficeReason["PUBLIC_HOLIDAY"],
|
||||
};
|
||||
|
||||
const OOO_REASON_TO_REASON_ID = {
|
||||
[OutOfOfficeReason["UNSPECIFIED"]]: 1,
|
||||
[OutOfOfficeReason["VACATION"]]: 2,
|
||||
[OutOfOfficeReason["TRAVEL"]]: 3,
|
||||
[OutOfOfficeReason["SICK_LEAVE"]]: 4,
|
||||
[OutOfOfficeReason["PUBLIC_HOLIDAY"]]: 5,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class UserOOOService {
|
||||
constructor(
|
||||
private readonly oooRepository: UserOOORepository,
|
||||
private readonly usersRepository: UsersRepository
|
||||
) {}
|
||||
|
||||
formatOooReason(ooo: OutOfOfficeEntry) {
|
||||
return {
|
||||
...ooo,
|
||||
reason: ooo.reasonId
|
||||
? OOO_REASON_ID_TO_REASON[ooo.reasonId as keyof typeof OOO_REASON_ID_TO_REASON]
|
||||
: OOO_REASON_ID_TO_REASON[1],
|
||||
};
|
||||
}
|
||||
|
||||
isStartBeforeEnd(start?: Date, end?: Date) {
|
||||
if ((end && !start) || (start && !end)) {
|
||||
throw new BadRequestException("Please specify both ooo start and end time.");
|
||||
}
|
||||
|
||||
if (start && end) {
|
||||
if (start.getTime() > end.getTime()) {
|
||||
throw new BadRequestException("Start date must be before end date.");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async checkUserEligibleForRedirect(userId: number, toUserId?: number) {
|
||||
if (toUserId) {
|
||||
const user = await this.usersRepository.findUserOOORedirectEligible(userId, toUserId);
|
||||
if (!user) {
|
||||
throw new BadRequestException("Cannot redirect to this user.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async checkExistingOooRedirect(userId: number, start?: Date, end?: Date, toUserId?: number) {
|
||||
if (start && end) {
|
||||
const existingOooRedirect = await this.oooRepository.findExistingOooRedirect(
|
||||
userId,
|
||||
start,
|
||||
end,
|
||||
toUserId
|
||||
);
|
||||
|
||||
if (existingOooRedirect) {
|
||||
throw new BadRequestException("Booking redirect infinite not allowed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async checkDuplicateOOOEntry(userId: number, start?: Date, end?: Date) {
|
||||
if (start && end) {
|
||||
const duplicateEntry = await this.oooRepository.getOooByUserIdAndTime(userId, start, end);
|
||||
|
||||
if (duplicateEntry) {
|
||||
throw new ConflictException("Ooo entry already exists.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkRedirectToSelf(userId: number, toUserId?: number) {
|
||||
if (toUserId && toUserId === userId) {
|
||||
throw new BadRequestException("Cannot redirect to self.");
|
||||
}
|
||||
}
|
||||
|
||||
async checkIsValidOOO(userId: number, ooo: CreateOutOfOfficeEntryDto | UpdateOutOfOfficeEntryDto) {
|
||||
this.isStartBeforeEnd(ooo.start, ooo.end);
|
||||
await this.checkExistingOooRedirect(userId, ooo.start, ooo.end, ooo.toUserId);
|
||||
await this.checkDuplicateOOOEntry(userId, ooo.start, ooo.end);
|
||||
await this.checkRedirectToSelf(userId, ooo.toUserId);
|
||||
await this.checkUserEligibleForRedirect(userId, ooo.toUserId);
|
||||
}
|
||||
|
||||
async createUserOOO(userId: number, body: CreateOutOfOfficeEntryDto) {
|
||||
await this.checkIsValidOOO(userId, body);
|
||||
const { reason, ...rest } = body;
|
||||
const ooo = await this.oooRepository.createUserOOO({
|
||||
...rest,
|
||||
userId,
|
||||
reasonId: OOO_REASON_TO_REASON_ID[reason ?? OutOfOfficeReason["UNSPECIFIED"]],
|
||||
});
|
||||
return this.formatOooReason(ooo);
|
||||
}
|
||||
|
||||
async updateUserOOO(userId: number, oooId: number, body: UpdateOutOfOfficeEntryDto) {
|
||||
await this.checkIsValidOOO(userId, body);
|
||||
const { reason, ...rest } = body;
|
||||
const data = reason
|
||||
? { ...rest, reasonId: OOO_REASON_TO_REASON_ID[reason ?? OutOfOfficeReason["UNSPECIFIED"]] }
|
||||
: rest;
|
||||
const ooo = await this.oooRepository.updateUserOOO(oooId, data);
|
||||
return this.formatOooReason(ooo);
|
||||
}
|
||||
|
||||
async deleteUserOOO(oooId: number) {
|
||||
const ooo = await this.oooRepository.deleteUserOOO(oooId);
|
||||
return this.formatOooReason(ooo);
|
||||
}
|
||||
|
||||
async getUserOOOPaginated(userId: number, skip: number, take: number) {
|
||||
const ooos = await this.oooRepository.getUserOOOPaginated(userId, skip, take);
|
||||
return ooos.map((ooo) => this.formatOooReason(ooo));
|
||||
}
|
||||
}
|
||||
+92
@@ -577,6 +577,98 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should be able to configure phone-only event type", async () => {
|
||||
const body: UpdateTeamEventTypeInput_2024_06_14 = {
|
||||
bookingFields: [
|
||||
{
|
||||
type: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
label: "Phone number",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${collectiveEventType.id}`)
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const data = responseBody.data;
|
||||
expect(data.bookingFields).toEqual([
|
||||
{
|
||||
isDefault: true,
|
||||
type: "name",
|
||||
slug: "name",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "email",
|
||||
slug: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "text",
|
||||
slug: "title",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "multiemail",
|
||||
slug: "guests",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "rescheduleReason",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should assign all members to managed event-type", async () => {
|
||||
const body: UpdateTeamEventTypeInput_2024_06_14 = {
|
||||
assignAllTeamMembers: true,
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard";
|
||||
import { IsUserOOO } from "@/modules/ooo/guards/is-user-ooo";
|
||||
import { CreateOutOfOfficeEntryDto, UpdateOutOfOfficeEntryDto } from "@/modules/ooo/inputs/ooo.input";
|
||||
import {
|
||||
UserOooOutputDto,
|
||||
UserOooOutputResponseDto,
|
||||
UserOoosOutputResponseDto,
|
||||
} from "@/modules/ooo/outputs/ooo.output";
|
||||
import { UserOOOService } from "@/modules/ooo/services/ooo.service";
|
||||
import {
|
||||
Controller,
|
||||
UseGuards,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Delete,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Body,
|
||||
UseInterceptors,
|
||||
Query,
|
||||
} from "@nestjs/common";
|
||||
import { ClassSerializerInterceptor } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/organizations/:orgId/users/:userId/ooo",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseInterceptors(ClassSerializerInterceptor)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@UseGuards(IsOrgGuard)
|
||||
@DocsTags("Orgs / Users / OOO")
|
||||
export class OrganizationsUsersOOOController {
|
||||
constructor(private readonly userOOOService: UserOOOService) {}
|
||||
|
||||
@Get()
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg)
|
||||
@ApiOperation({ summary: "Get all ooo entries of a user" })
|
||||
async getOrganizationUserOOO(
|
||||
@Param("userId", ParseIntPipe) userId: number,
|
||||
@Query() query: SkipTakePagination
|
||||
): Promise<UserOoosOutputResponseDto> {
|
||||
const ooos = await this.userOOOService.getUserOOOPaginated(userId, query.skip ?? 0, query.take ?? 250);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: ooos.map((ooo) => plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" })),
|
||||
};
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg)
|
||||
@ApiOperation({ summary: "Create an ooo entry for user" })
|
||||
async createOrganizationUserOOO(
|
||||
@Param("userId", ParseIntPipe) userId: number,
|
||||
@Body() input: CreateOutOfOfficeEntryDto
|
||||
): Promise<UserOooOutputResponseDto> {
|
||||
const ooo = await this.userOOOService.createUserOOO(userId, input);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("/:oooId")
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg, IsUserOOO)
|
||||
@ApiOperation({ summary: "Update ooo entry of a user" })
|
||||
async updateOrganizationUserOOO(
|
||||
@Param("userId", ParseIntPipe) userId: number,
|
||||
@Param("oooId", ParseIntPipe) oooId: number,
|
||||
|
||||
@Body() input: UpdateOutOfOfficeEntryDto
|
||||
): Promise<UserOooOutputResponseDto> {
|
||||
const ooo = await this.userOOOService.updateUserOOO(userId, oooId, input);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/:oooId")
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg, IsUserOOO)
|
||||
@ApiOperation({ summary: "Delete ooo entry of a user" })
|
||||
async deleteOrganizationUserOOO(
|
||||
@Param("oooId", ParseIntPipe) oooId: number
|
||||
): Promise<UserOooOutputResponseDto> {
|
||||
const ooo = await this.userOOOService.deleteUserOOO(oooId);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
}
|
||||
+421
@@ -0,0 +1,421 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { UserOooOutputDto } from "@/modules/ooo/outputs/ooo.output";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Organizations User OOO Endpoints", () => {
|
||||
describe("User Authentication - User is Org Admin", () => {
|
||||
let app: INestApplication;
|
||||
let oooCreatedViaApiId: number;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
|
||||
let org: Team;
|
||||
let team: Team;
|
||||
let falseTestOrg: Team;
|
||||
let falseTestTeam: Team;
|
||||
|
||||
const userEmail = "org-admin-ooo-controller-e222e@api.com";
|
||||
let userAdmin: User;
|
||||
|
||||
const teammate1Email = "teammate111ooo@team.com";
|
||||
const teammate2Email = "teammate221ooo@team.com";
|
||||
const falseTestUserEmail = "false-user-ooo@false-team.com";
|
||||
let teammate1: User;
|
||||
let teammate2: User;
|
||||
let falseTestUser: User;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
|
||||
userAdmin = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
role: "ADMIN",
|
||||
});
|
||||
|
||||
teammate1 = await userRepositoryFixture.create({
|
||||
email: teammate1Email,
|
||||
username: teammate1Email,
|
||||
});
|
||||
|
||||
teammate2 = await userRepositoryFixture.create({
|
||||
email: teammate2Email,
|
||||
username: teammate2Email,
|
||||
});
|
||||
|
||||
falseTestUser = await userRepositoryFixture.create({
|
||||
email: falseTestUserEmail,
|
||||
username: falseTestUserEmail,
|
||||
});
|
||||
|
||||
org = await organizationsRepositoryFixture.create({
|
||||
name: "Test Organization ooo",
|
||||
isOrganization: true,
|
||||
});
|
||||
|
||||
falseTestOrg = await organizationsRepositoryFixture.create({
|
||||
name: "False test org ooo",
|
||||
isOrganization: true,
|
||||
});
|
||||
|
||||
team = await teamsRepositoryFixture.create({
|
||||
name: "Test org team ooo",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
falseTestTeam = await teamsRepositoryFixture.create({
|
||||
name: "Outside org team ooo",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: falseTestOrg.id } },
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${userAdmin.id}`,
|
||||
username: userEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: org.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: userAdmin.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${teammate1.id}`,
|
||||
username: teammate1Email,
|
||||
organization: {
|
||||
connect: {
|
||||
id: org.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: teammate1.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "ADMIN",
|
||||
user: { connect: { id: userAdmin.id } },
|
||||
team: { connect: { id: org.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: teammate1.id } },
|
||||
team: { connect: { id: team.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: teammate2.id } },
|
||||
team: { connect: { id: team.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: falseTestUser.id } },
|
||||
team: { connect: { id: falseTestTeam.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(userRepositoryFixture).toBeDefined();
|
||||
expect(organizationsRepositoryFixture).toBeDefined();
|
||||
expect(userAdmin).toBeDefined();
|
||||
expect(org).toBeDefined();
|
||||
});
|
||||
|
||||
it("should create a ooo entry without redirect", async () => {
|
||||
const body = {
|
||||
start: "2025-05-01T01:00:00.000Z",
|
||||
end: "2025-05-10T13:59:59.999Z",
|
||||
notes: "ooo numero uno",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data as UserOooOutputDto;
|
||||
expect(data.reason).toEqual("unspecified");
|
||||
expect(data.userId).toEqual(teammate1.id);
|
||||
expect(data.start).toEqual("2025-05-01T00:00:00.000Z");
|
||||
expect(data.end).toEqual("2025-05-10T23:59:59.999Z");
|
||||
oooCreatedViaApiId = data.id;
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a ooo entry with redirect", async () => {
|
||||
const body = {
|
||||
start: "2025-08-01T01:00:00.000Z",
|
||||
end: "2025-10-10T13:59:59.999Z",
|
||||
notes: "ooo numero dos with redirect",
|
||||
toUserId: teammate2.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const data = responseBody.data as UserOooOutputDto;
|
||||
expect(data.reason).toEqual("unspecified");
|
||||
expect(data.userId).toEqual(teammate1.id);
|
||||
expect(data.start).toEqual("2025-08-01T00:00:00.000Z");
|
||||
expect(data.end).toEqual("2025-10-10T23:59:59.999Z");
|
||||
expect(data.toUserId).toEqual(teammate2.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail to create a ooo entry with start after end", async () => {
|
||||
const body = {
|
||||
start: "2025-07-01T00:00:00.000Z",
|
||||
end: "2025-05-10T23:59:59.999Z",
|
||||
notes: "ooo numero uno duplicate",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should fail to create a duplicate ooo entry", async () => {
|
||||
const body = {
|
||||
start: "2025-05-01T00:00:00.000Z",
|
||||
end: "2025-05-10T23:59:59.999Z",
|
||||
notes: "ooo numero uno duplicate",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.send(body)
|
||||
.expect(409);
|
||||
});
|
||||
|
||||
it("should fail to create an ooo entry that redirects to self", async () => {
|
||||
const body = {
|
||||
start: "2025-05-02T00:00:00.000Z",
|
||||
end: "2025-05-03T23:59:59.999Z",
|
||||
notes: "ooo infinite redirect",
|
||||
toUserId: teammate1.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should fail to create an ooo entry that redirects to member outside of org", async () => {
|
||||
const body = {
|
||||
start: "2025-05-02T00:00:00.000Z",
|
||||
end: "2025-05-03T23:59:59.999Z",
|
||||
notes: "ooo invalid redirect",
|
||||
toUserId: falseTestUser.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should update a ooo entry without redirect", async () => {
|
||||
const body = {
|
||||
start: "2025-06-01T01:00:00.000Z",
|
||||
end: "2025-06-10T13:59:59.999Z",
|
||||
notes: "ooo numero uno",
|
||||
reason: "vacation",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data as UserOooOutputDto;
|
||||
expect(data.reason).toEqual("vacation");
|
||||
expect(data.userId).toEqual(teammate1.id);
|
||||
expect(data.start).toEqual("2025-06-01T00:00:00.000Z");
|
||||
expect(data.end).toEqual("2025-06-10T23:59:59.999Z");
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail to update a ooo entry with redirect outside of org", async () => {
|
||||
const body = {
|
||||
toUserId: falseTestUser.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should fail to update a ooo entry with start after end ", async () => {
|
||||
const body = {
|
||||
start: "2025-07-01T00:00:00.000Z",
|
||||
end: "2025-05-10T23:59:59.999Z",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should fail to update a ooo entry with redirect to self ", async () => {
|
||||
const body = {
|
||||
toUserId: teammate1.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should fail to update a ooo entry with duplicate time", async () => {
|
||||
const body = {
|
||||
start: "2025-06-01T01:00:00.000Z",
|
||||
end: "2025-06-10T13:59:59.999Z",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.send(body)
|
||||
.expect(409);
|
||||
});
|
||||
|
||||
it("should update a ooo entry without redirect", async () => {
|
||||
const body = {
|
||||
toUserId: teammate2.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data as UserOooOutputDto;
|
||||
expect(data.reason).toEqual("vacation");
|
||||
expect(data.toUserId).toEqual(teammate2.id);
|
||||
expect(data.userId).toEqual(teammate1.id);
|
||||
expect(data.start).toEqual("2025-06-01T00:00:00.000Z");
|
||||
expect(data.end).toEqual("2025-06-10T23:59:59.999Z");
|
||||
});
|
||||
});
|
||||
|
||||
it("should get 2 ooo entries", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data as UserOooOutputDto[];
|
||||
expect(data.length).toEqual(2);
|
||||
const oooUno = data.find((ooo) => ooo.id === oooCreatedViaApiId);
|
||||
expect(oooUno).toBeDefined();
|
||||
if (oooUno) {
|
||||
expect(oooUno.reason).toEqual("vacation");
|
||||
expect(oooUno.toUserId).toEqual(teammate2.id);
|
||||
expect(oooUno.userId).toEqual(teammate1.id);
|
||||
expect(oooUno.start).toEqual("2025-06-01T00:00:00.000Z");
|
||||
expect(oooUno.end).toEqual("2025-06-10T23:59:59.999Z");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should delete ooo entry", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it("user should have 1 ooo entries", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const data = responseBody.data as UserOooOutputDto[];
|
||||
expect(data.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(userAdmin.email);
|
||||
await userRepositoryFixture.deleteByEmail(teammate1.email);
|
||||
await userRepositoryFixture.deleteByEmail(teammate2.email);
|
||||
await userRepositoryFixture.deleteByEmail(falseTestUser.email);
|
||||
await teamsRepositoryFixture.delete(team.id);
|
||||
await teamsRepositoryFixture.delete(falseTestTeam.id);
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
await organizationsRepositoryFixture.delete(falseTestOrg.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,8 @@ import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/
|
||||
import { EmailModule } from "@/modules/email/email.module";
|
||||
import { EmailService } from "@/modules/email/email.service";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
|
||||
import { UserOOOService } from "@/modules/ooo/services/ooo.service";
|
||||
import { OrganizationsOptionsAttributesController } from "@/modules/organizations/controllers/attributes/organizations-attributes-options.controller";
|
||||
import { OrganizationsAttributesController } from "@/modules/organizations/controllers/attributes/organizations-attributes.controller";
|
||||
import { OrganizationsEventTypesController } from "@/modules/organizations/controllers/event-types/organizations-event-types.controller";
|
||||
@@ -12,6 +14,7 @@ import { OrganizationsSchedulesController } from "@/modules/organizations/contro
|
||||
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller";
|
||||
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/teams/organizations-teams.controller";
|
||||
import { OrganizationsTeamsSchedulesController } from "@/modules/organizations/controllers/teams/schedules/organizations-teams-schedules.controller";
|
||||
import { OrganizationsUsersOOOController } from "@/modules/organizations/controllers/users/ooo/organizations-users-ooo-controller";
|
||||
import { OrganizationsUsersController } from "@/modules/organizations/controllers/users/organizations-users.controller";
|
||||
import { OrganizationsWebhooksController } from "@/modules/organizations/controllers/webhooks/organizations-webhooks.controller";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
@@ -86,6 +89,8 @@ import { Module } from "@nestjs/common";
|
||||
WebhooksRepository,
|
||||
WebhooksService,
|
||||
OutputTeamEventTypesResponsePipe,
|
||||
UserOOOService,
|
||||
UserOOORepository,
|
||||
],
|
||||
exports: [
|
||||
OrganizationsService,
|
||||
@@ -118,6 +123,7 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsOptionsAttributesController,
|
||||
OrganizationsWebhooksController,
|
||||
OrganizationsTeamsSchedulesController,
|
||||
OrganizationsUsersOOOController,
|
||||
],
|
||||
})
|
||||
export class OrganizationsModule {}
|
||||
|
||||
@@ -265,4 +265,27 @@ export class UsersRepository {
|
||||
where: { id: userId },
|
||||
});
|
||||
}
|
||||
|
||||
async findUserOOORedirectEligible(userId: number, toTeamUserId: number) {
|
||||
return await this.dbRead.prisma.user.findUnique({
|
||||
where: {
|
||||
id: toTeamUserId,
|
||||
teams: {
|
||||
some: {
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2684,6 +2684,146 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/users/{userId}/ooo": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsUsersOOOController_getOrganizationUserOOO",
|
||||
"summary": "Get all ooo entries of a user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "take",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to return",
|
||||
"example": 10,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to skip",
|
||||
"example": 0,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Users / OOO"
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"operationId": "OrganizationsUsersOOOController_createOrganizationUserOOO",
|
||||
"summary": "Create an ooo entry for user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateOutOfOfficeEntryDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Users / OOO"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/users/{userId}/ooo/{oooId}": {
|
||||
"patch": {
|
||||
"operationId": "OrganizationsUsersOOOController_updateOrganizationUserOOO",
|
||||
"summary": "Update ooo entry of a user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "oooId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateOutOfOfficeEntryDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Users / OOO"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "OrganizationsUsersOOOController_deleteOrganizationUserOOO",
|
||||
"summary": "Delete ooo entry of a user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "oooId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Users / OOO"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/webhooks": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsWebhooksController_getAllOrganizationWebhooks",
|
||||
@@ -7778,6 +7918,9 @@
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": {
|
||||
"type": "object"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -7789,6 +7932,7 @@
|
||||
"required": [
|
||||
"type",
|
||||
"label",
|
||||
"required",
|
||||
"placeholder"
|
||||
]
|
||||
},
|
||||
@@ -8488,6 +8632,10 @@
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": {
|
||||
"type": "object",
|
||||
"default": true
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -8504,16 +8652,13 @@
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"default": "email"
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"required",
|
||||
"isDefault",
|
||||
"slug",
|
||||
"required"
|
||||
"slug"
|
||||
]
|
||||
},
|
||||
"NameDefaultFieldOutput_2024_06_14": {
|
||||
@@ -10112,7 +10257,7 @@
|
||||
},
|
||||
"customName": {
|
||||
"type": "string",
|
||||
"description": "Customizable event name with valid variables: \n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name}, \n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION}, \n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
|
||||
"description": "Customizable event name with valid variables:\n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name},\n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION},\n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
|
||||
"example": "{Event type title} between {Organiser} and {Scheduler}"
|
||||
},
|
||||
"destinationCalendar": {
|
||||
@@ -12538,7 +12683,7 @@
|
||||
},
|
||||
"customName": {
|
||||
"type": "string",
|
||||
"description": "Customizable event name with valid variables: \n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name}, \n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION}, \n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
|
||||
"description": "Customizable event name with valid variables:\n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name},\n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION},\n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
|
||||
"example": "{Event type title} between {Organiser} and {Scheduler}"
|
||||
},
|
||||
"destinationCalendar": {
|
||||
@@ -13449,6 +13594,74 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateOutOfOfficeEntryDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"start": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"description": "The start date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
"example": "2023-05-01T00:00:00.000Z"
|
||||
},
|
||||
"end": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"description": "The end date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
"example": "2023-05-10T23:59:59.999Z"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string",
|
||||
"description": "Optional notes for the out of office entry.",
|
||||
"example": "Vacation in Hawaii"
|
||||
},
|
||||
"toUserId": {
|
||||
"type": "number",
|
||||
"description": "The ID of the user covering for the out of office period, if applicable.",
|
||||
"example": 2
|
||||
},
|
||||
"reason": {
|
||||
"type": "object",
|
||||
"description": "the reason for the out of office entry, if applicable",
|
||||
"example": "vacation"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"start",
|
||||
"end"
|
||||
]
|
||||
},
|
||||
"UpdateOutOfOfficeEntryDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"start": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"description": "The start date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
"example": "2023-05-01T00:00:00.000Z"
|
||||
},
|
||||
"end": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"description": "The end date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
"example": "2023-05-10T23:59:59.999Z"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string",
|
||||
"description": "Optional notes for the out of office entry.",
|
||||
"example": "Vacation in Hawaii"
|
||||
},
|
||||
"toUserId": {
|
||||
"type": "number",
|
||||
"description": "The ID of the user covering for the out of office period, if applicable.",
|
||||
"example": 2
|
||||
},
|
||||
"reason": {
|
||||
"type": "object",
|
||||
"description": "the reason for the out of office entry, if applicable",
|
||||
"example": "vacation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StripConnectOutputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -14471,6 +14684,11 @@
|
||||
"description": "The time zone of the attendee.",
|
||||
"example": "America/New_York"
|
||||
},
|
||||
"phoneNumber": {
|
||||
"type": "string",
|
||||
"description": "The phone number of the attendee in international format.",
|
||||
"example": "+919876543210"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -14524,7 +14742,6 @@
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"email",
|
||||
"timeZone"
|
||||
]
|
||||
},
|
||||
@@ -15132,6 +15349,10 @@
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"phoneNumber": {
|
||||
"type": "string",
|
||||
"example": "+1234567890"
|
||||
},
|
||||
"seatUid": {
|
||||
"type": "string",
|
||||
"example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1"
|
||||
|
||||
@@ -5,7 +5,6 @@ import { NextResponse, URLPattern } from "next/server";
|
||||
import { FUTURE_ROUTES_ENABLED_COOKIE_NAME, FUTURE_ROUTES_OVERRIDE_COOKIE_NAME } from "@calcom/lib/constants";
|
||||
|
||||
const ROUTES: [URLPattern, boolean][] = [
|
||||
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
|
||||
["/apps/installed/:category", process.env.APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED === "1"] as const,
|
||||
["/apps/:slug", process.env.APP_ROUTER_APPS_SLUG_ENABLED === "1"] as const,
|
||||
["/apps/:slug/setup", process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED === "1"] as const,
|
||||
@@ -19,6 +18,7 @@ const ROUTES: [URLPattern, boolean][] = [
|
||||
["/auth/error", process.env.APP_ROUTER_AUTH_ERROR_ENABLED === "1"] as const,
|
||||
["/auth/platform/:path*", process.env.APP_ROUTER_AUTH_PLATFORM_ENABLED === "1"] as const,
|
||||
["/auth/oauth2/:path*", process.env.APP_ROUTER_AUTH_OAUTH2_ENABLED === "1"] as const,
|
||||
["/team", process.env.APP_ROUTER_TEAM_ENABLED === "1"] as const,
|
||||
].map(([pathname, enabled]) => [
|
||||
new URLPattern({
|
||||
pathname,
|
||||
|
||||
+1
-20
@@ -3,8 +3,7 @@ import i18next from "i18next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import type { MeetingImageProps } from "@calcom/lib/OgImages";
|
||||
import { constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
|
||||
import { constructGenericImage } from "@calcom/lib/OgImages";
|
||||
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants";
|
||||
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
||||
import { truncateOnWord } from "@calcom/lib/text";
|
||||
@@ -104,21 +103,3 @@ export const _generateMetadata = async (
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const generateMeetingMetadata = async (
|
||||
meeting: MeetingImageProps,
|
||||
getTitle: (t: TFunction<string, undefined>) => string,
|
||||
getDescription: (t: TFunction<string, undefined>) => string,
|
||||
hideBranding?: boolean,
|
||||
origin?: string
|
||||
) => {
|
||||
const metadata = await _generateMetadataWithoutImage(getTitle, getDescription, hideBranding, origin);
|
||||
const image = SEO_IMG_OGIMG + constructMeetingImage(meeting);
|
||||
return {
|
||||
...metadata,
|
||||
openGraph: {
|
||||
...metadata.openGraph,
|
||||
images: [image],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const pageProps = await getData(legacyCtx);
|
||||
|
||||
const { booking, user: username, slug: eventSlug, isTeamEvent, isBrandingHidden } = pageProps;
|
||||
const { booking, user: username, slug: eventSlug, isTeamEvent } = pageProps;
|
||||
const rescheduleUid = booking?.uid;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req);
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
@@ -34,8 +34,7 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
|
||||
const title = event?.title ?? "";
|
||||
return await _generateMetadata(
|
||||
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`,
|
||||
isBrandingHidden
|
||||
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+4
-5
@@ -4,11 +4,10 @@ import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { EventType } from "@calcom/atoms/monorepo";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/event-types/[type]/getServerSideProps";
|
||||
import type { PageProps as EventTypePageProps } from "@lib/event-types/[type]/getServerSideProps";
|
||||
|
||||
import EventTypePageWrapper from "~/event-types/views/event-types-single-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
@@ -21,5 +20,5 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
const Page = ({ type, ...rest }: EventTypePageProps) => <EventType {...rest} id={type} isAppDir={true} />;
|
||||
export default WithLayout({ getLayout: null, getData, Page })<"P">;
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, Page: EventTypePageWrapper })<"P">;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { generateEventBookingPageMetadata } from "app/generateBookingPageMetadata";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { headers, cookies } from "next/headers";
|
||||
|
||||
@@ -18,7 +18,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
|
||||
const { booking, user: username, slug: eventSlug, isSEOIndexable, eventData, isBrandingHidden } = props;
|
||||
const { booking, user: username, slug: eventSlug } = props;
|
||||
const rescheduleUid = booking?.uid;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
|
||||
|
||||
@@ -31,21 +31,12 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
});
|
||||
|
||||
const profileName = event?.profile?.name ?? "";
|
||||
const profileImage = event?.profile.image;
|
||||
const title = event?.title ?? "";
|
||||
const metadata = await generateEventBookingPageMetadata({
|
||||
event: {
|
||||
hidden: event?.hidden ?? false,
|
||||
title,
|
||||
users: event?.users ?? [],
|
||||
},
|
||||
hideBranding: isBrandingHidden,
|
||||
orgSlug: eventData?.entity.orgSlug ?? null,
|
||||
isSEOIndexable: !!isSEOIndexable,
|
||||
profile: { name: profileName, image: profileImage ?? "" },
|
||||
isReschedule: !!rescheduleUid,
|
||||
});
|
||||
return metadata;
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
|
||||
);
|
||||
};
|
||||
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { generateUserProfilePageMetadata } from "app/generateBookingPageMetadata";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { headers, cookies } from "next/headers";
|
||||
|
||||
@@ -14,25 +14,11 @@ import LegacyPage from "~/users/views/users-public-view";
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
|
||||
const { profile, markdownStrippedBio, isOrgSEOIndexable, entity } = props;
|
||||
|
||||
const isOrg = !!profile?.organization;
|
||||
const allowSEOIndexing = !!(
|
||||
(!isOrg && profile.allowSEOIndexing) ||
|
||||
(isOrg && isOrgSEOIndexable && profile.allowSEOIndexing)
|
||||
const { profile, markdownStrippedBio } = props;
|
||||
return await _generateMetadata(
|
||||
() => profile.name,
|
||||
() => markdownStrippedBio
|
||||
);
|
||||
return await generateUserProfilePageMetadata({
|
||||
profile: {
|
||||
name: profile.name,
|
||||
image: profile.image ?? "",
|
||||
username: profile.username ?? "",
|
||||
markdownStrippedBio: markdownStrippedBio,
|
||||
},
|
||||
event: null,
|
||||
hideBranding: false,
|
||||
orgSlug: entity.orgSlug ?? null,
|
||||
isSEOIndexable: allowSEOIndexing,
|
||||
});
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||
@@ -1,21 +1,10 @@
|
||||
import { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]";
|
||||
import Page from "@pages/org/[orgSlug]/[user]/[type]/embed";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { type OrgTypePageProps } from "app/org/[orgSlug]/[user]/[type]/page";
|
||||
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
|
||||
import type { PageProps as TeamTypePageProps } from "~/team/type-view";
|
||||
import TeamTypePage from "~/team/type-view";
|
||||
import UserTypePage from "~/users/views/users-type-public-view";
|
||||
import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view";
|
||||
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir<OrgTypePageProps>(getServerSideProps);
|
||||
|
||||
const Page = async (props: OrgTypePageProps) => {
|
||||
if ((props as TeamTypePageProps)?.teamId) {
|
||||
return <TeamTypePage {...(props as TeamTypePageProps)} />;
|
||||
}
|
||||
return <UserTypePage {...(props as UserTypePageProps)} />;
|
||||
};
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, ServerPage: Page });
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page });
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import Page, { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, Page });
|
||||
@@ -1,21 +1,8 @@
|
||||
import { getServerSideProps, type PageProps } from "@pages/org/[orgSlug]/[user]";
|
||||
import Page from "@pages/org/[orgSlug]/[user]/embed";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { type OrgPageProps } from "app/org/[orgSlug]/[user]/page";
|
||||
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps";
|
||||
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
|
||||
|
||||
import type { PageProps as TeamPageProps } from "~/team/team-view";
|
||||
import TeamPage from "~/team/team-view";
|
||||
import UserPage from "~/users/views/users-public-view";
|
||||
import type { PageProps as UserPageProps } from "~/users/views/users-public-view";
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir<OrgPageProps>(getServerSideProps);
|
||||
|
||||
const Page = async (props: OrgPageProps) => {
|
||||
if ((props as TeamPageProps)?.team) {
|
||||
return <TeamPage {...(props as TeamPageProps)} />;
|
||||
}
|
||||
return <UserPage {...(props as UserPageProps)} />;
|
||||
};
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, ServerPage: Page });
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page });
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import Page, { getServerSideProps, type PageProps } from "@pages/org/[orgSlug]/[user]";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, Page });
|
||||
+3
-4
@@ -15,7 +15,7 @@ import Page from "~/org/[orgSlug]/instant-meeting/team/[slug]/[type]/instant-mee
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const { slug: eventSlug, user: username, isBrandingHidden } = await getData(context);
|
||||
const { slug: eventSlug, user: username } = await getData(context);
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
@@ -33,10 +33,9 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
|
||||
|
||||
return await _generateMetadata(
|
||||
() => `${title} | ${profileName}`,
|
||||
() => `${title}`,
|
||||
isBrandingHidden
|
||||
() => `${title}`
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
export default WithLayout({ getData, Page, isBookingPage: true })<"P">;
|
||||
export default WithLayout({ getData, Page })<"P">;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/team/team-view";
|
||||
import TeamPage from "~/team/team-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => props.team.name || t("nameless_team"),
|
||||
(t) => props.team.name || t("nameless_team")
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: TeamPage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { EventRepository } from "@calcom/lib/server/repository/event";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/team/type-view";
|
||||
import TypePage from "~/team/type-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
const { user: username, slug: eventSlug, booking } = props;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
|
||||
|
||||
const event = await EventRepository.getPublicEvent({
|
||||
username,
|
||||
eventSlug,
|
||||
isTeamEvent: true,
|
||||
org: isValidOrgDomain ? currentOrgDomain : null,
|
||||
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
|
||||
});
|
||||
|
||||
const profileName = event?.profile?.name ?? "";
|
||||
const title = event?.title ?? "";
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
(t) => `${booking?.uid ? t("reschedule") : ""} ${title}`
|
||||
);
|
||||
};
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: TypePage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/team/team-view";
|
||||
import TeamPage from "~/team/team-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => props.team.name || t("nameless_team"),
|
||||
(t) => props.team.name || t("nameless_team")
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: TeamPage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
+9
-22
@@ -1,6 +1,6 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { generateEventBookingPageMetadata } from "app/generateBookingPageMetadata";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
@@ -15,7 +15,7 @@ import LegacyPage, { type PageProps as LegacyPageProps } from "~/team/type-view"
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
const { user: username, slug: eventSlug, booking, isSEOIndexable, eventData, isBrandingHidden } = props;
|
||||
const { user: username, slug: eventSlug, booking } = props;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
|
||||
|
||||
const event = await EventRepository.getPublicEvent({
|
||||
@@ -26,26 +26,13 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
|
||||
});
|
||||
|
||||
return await generateEventBookingPageMetadata({
|
||||
profile: {
|
||||
name: event?.profile?.name ?? "",
|
||||
image: event?.profile.image ?? "",
|
||||
},
|
||||
event: {
|
||||
title: event?.title ?? "",
|
||||
hidden: event?.hidden ?? false,
|
||||
users: [
|
||||
...(event?.users || []).map((user) => ({
|
||||
name: `${user.name}`,
|
||||
username: `${user.username}`,
|
||||
})),
|
||||
],
|
||||
},
|
||||
hideBranding: isBrandingHidden,
|
||||
orgSlug: eventData?.entity.orgSlug ?? null,
|
||||
isSEOIndexable,
|
||||
isReschedule: !!booking,
|
||||
});
|
||||
const profileName = event?.profile?.name ?? "";
|
||||
const title = event?.title ?? "";
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
(t) => `${booking?.uid ? t("reschedule") : ""} ${title}`
|
||||
);
|
||||
};
|
||||
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { generateTeamProfilePageMetadata } from "app/generateBookingPageMetadata";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
|
||||
@@ -13,21 +11,12 @@ import type { PageProps } from "~/team/team-view";
|
||||
import LegacyPage from "~/team/team-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const { team, markdownStrippedBio, isSEOIndexable, currentOrgDomain } = await getData(
|
||||
buildLegacyCtx(headers(), cookies(), params, searchParams)
|
||||
);
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
|
||||
return await generateTeamProfilePageMetadata({
|
||||
profile: {
|
||||
teamName: team.name,
|
||||
image: getOrgOrTeamAvatar(team),
|
||||
markdownStrippedBio: markdownStrippedBio,
|
||||
},
|
||||
event: null,
|
||||
hideBranding: false,
|
||||
orgSlug: currentOrgDomain ?? null,
|
||||
isSEOIndexable: !!isSEOIndexable,
|
||||
});
|
||||
return await _generateMetadata(
|
||||
(t) => props.team.name || t("nameless_team"),
|
||||
(t) => props.team.name || t("nameless_team")
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
@@ -36,5 +25,4 @@ export default WithLayout({
|
||||
Page: LegacyPage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
@@ -1,151 +0,0 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
|
||||
import { generateMeetingMetadata } from "./_utils";
|
||||
|
||||
type MetadataPayload = {
|
||||
title: (t: TFunction) => string;
|
||||
description: (t: TFunction) => string;
|
||||
meeting: {
|
||||
title: string;
|
||||
profile: { name: string; image: string };
|
||||
users?: { name: string; username: string }[];
|
||||
};
|
||||
robots: {
|
||||
index: boolean;
|
||||
follow: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type UserProfilePageProfile = {
|
||||
name: string | null;
|
||||
username: string | null;
|
||||
image: string | null;
|
||||
markdownStrippedBio: string | null | undefined;
|
||||
};
|
||||
|
||||
type TeamProfilePageProfile = {
|
||||
teamName: string | null;
|
||||
image: string | null;
|
||||
markdownStrippedBio: string | null | undefined;
|
||||
};
|
||||
|
||||
type EventBookingPageProfile = { name: string; image: string };
|
||||
type BaseMetadataArgs = {
|
||||
hideBranding: boolean;
|
||||
orgSlug: string | null;
|
||||
isSEOIndexable: boolean;
|
||||
isReschedule?: boolean;
|
||||
};
|
||||
|
||||
type TeamProfilePageArgs = BaseMetadataArgs & {
|
||||
profile: TeamProfilePageProfile;
|
||||
event: null;
|
||||
};
|
||||
|
||||
type UserProfilePageArgs = BaseMetadataArgs & {
|
||||
profile: UserProfilePageProfile;
|
||||
event: null;
|
||||
};
|
||||
|
||||
type EventBookingPageArgs = BaseMetadataArgs & {
|
||||
profile: EventBookingPageProfile;
|
||||
event: {
|
||||
title: string;
|
||||
hidden: boolean;
|
||||
users: { name: string | null; username: string | null }[];
|
||||
};
|
||||
};
|
||||
|
||||
export const generateEventBookingPageMetadata = async (props: EventBookingPageArgs) => {
|
||||
const { profile, isReschedule, hideBranding, orgSlug, isSEOIndexable, event } = props;
|
||||
const fullOrigin = getOrgFullOrigin(orgSlug);
|
||||
|
||||
const payload: MetadataPayload = {
|
||||
title: (t) => `${isReschedule ? t("reschedule") : ""} ${event.title} | ${profile.name}`,
|
||||
description: (t) => `${isReschedule ? t("reschedule") : ""} ${event.title}`,
|
||||
meeting: {
|
||||
title: event.title,
|
||||
profile: { name: profile.name, image: profile.image },
|
||||
users: event.users.map((user) => ({
|
||||
name: `${user.name}`,
|
||||
username: `${user.username}`,
|
||||
})),
|
||||
},
|
||||
robots: {
|
||||
index: !(event.hidden || !isSEOIndexable),
|
||||
follow: !(event.hidden || !isSEOIndexable),
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...(await generateMeetingMetadata(
|
||||
payload.meeting,
|
||||
payload.title,
|
||||
payload.description,
|
||||
hideBranding,
|
||||
fullOrigin
|
||||
)),
|
||||
robots: payload.robots,
|
||||
};
|
||||
};
|
||||
|
||||
export const generateUserProfilePageMetadata = async (props: UserProfilePageArgs) => {
|
||||
const { profile, hideBranding, orgSlug, isSEOIndexable } = props;
|
||||
const fullOrigin = getOrgFullOrigin(orgSlug);
|
||||
|
||||
const payload: MetadataPayload = {
|
||||
title: () => profile.name ?? "",
|
||||
description: () => profile.markdownStrippedBio ?? "",
|
||||
meeting: {
|
||||
title: profile.markdownStrippedBio ?? "",
|
||||
profile: { name: profile.name ?? "", image: profile.image ?? "" },
|
||||
users: [{ name: profile.name ?? "", username: profile.username ?? "" }],
|
||||
},
|
||||
robots: {
|
||||
index: isSEOIndexable,
|
||||
follow: isSEOIndexable,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...(await generateMeetingMetadata(
|
||||
payload.meeting,
|
||||
payload.title,
|
||||
payload.description,
|
||||
hideBranding,
|
||||
fullOrigin
|
||||
)),
|
||||
robots: payload.robots,
|
||||
};
|
||||
};
|
||||
|
||||
export const generateTeamProfilePageMetadata = async (props: TeamProfilePageArgs) => {
|
||||
const { profile, hideBranding, orgSlug, isSEOIndexable } = props;
|
||||
const fullOrigin = getOrgFullOrigin(orgSlug);
|
||||
|
||||
const payload: MetadataPayload = {
|
||||
title: (t) => profile.teamName || t("nameless_team"),
|
||||
description: () => profile.markdownStrippedBio ?? "",
|
||||
meeting: {
|
||||
title: profile.markdownStrippedBio ?? "",
|
||||
profile: { name: `${profile.teamName}`, image: profile.image ?? "" },
|
||||
},
|
||||
robots: {
|
||||
index: isSEOIndexable,
|
||||
follow: isSEOIndexable,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...(await generateMeetingMetadata(
|
||||
payload.meeting,
|
||||
payload.title,
|
||||
payload.description,
|
||||
hideBranding,
|
||||
fullOrigin
|
||||
)),
|
||||
robots: payload.robots,
|
||||
};
|
||||
};
|
||||
@@ -26,10 +26,7 @@ export const generateMetadata = () => prepareRootMetadata();
|
||||
const getInitialProps = async (url: string) => {
|
||||
const { pathname, searchParams } = new URL(url);
|
||||
|
||||
const isEmbedSnippetGeneratorPath = pathname.startsWith("/event-types");
|
||||
const isEmbed =
|
||||
(pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null) &&
|
||||
!isEmbedSnippetGeneratorPath;
|
||||
const isEmbed = pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null;
|
||||
const embedColorScheme = searchParams?.get("ui.color-scheme");
|
||||
|
||||
const req = { headers: headers(), cookies: cookies() };
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { generateEventBookingPageMetadata } from "app/generateBookingPageMetadata";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
|
||||
import type { PageProps as TeamTypePageProps } from "~/team/type-view";
|
||||
import TeamTypePage from "~/team/type-view";
|
||||
import UserTypePage from "~/users/views/users-type-public-view";
|
||||
import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view";
|
||||
|
||||
export type OrgTypePageProps = UserTypePageProps | TeamTypePageProps;
|
||||
const getData = withAppDirSsr<OrgTypePageProps>(getServerSideProps);
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
|
||||
const { booking, isSEOIndexable, eventData, isBrandingHidden } = props;
|
||||
const rescheduleUid = booking?.uid;
|
||||
|
||||
const profileName = eventData?.profile?.name ?? "";
|
||||
const profileImage = eventData?.profile.image ?? "";
|
||||
|
||||
return await generateEventBookingPageMetadata({
|
||||
profile: { name: profileName, image: profileImage },
|
||||
event: {
|
||||
title: eventData?.title ?? "",
|
||||
hidden: eventData?.hidden ?? false,
|
||||
users: [
|
||||
...(eventData?.users || []).map((user) => ({
|
||||
name: `${user.name}`,
|
||||
username: `${user.username}`,
|
||||
})),
|
||||
],
|
||||
},
|
||||
hideBranding: isBrandingHidden,
|
||||
orgSlug: eventData?.entity.orgSlug ?? null,
|
||||
isSEOIndexable: !!isSEOIndexable,
|
||||
isReschedule: !!rescheduleUid,
|
||||
});
|
||||
};
|
||||
|
||||
const Page = async (props: OrgTypePageProps) => {
|
||||
if ((props as TeamTypePageProps)?.teamId) {
|
||||
return <TeamTypePage {...(props as TeamTypePageProps)} />;
|
||||
}
|
||||
return <UserTypePage {...(props as UserTypePageProps)} />;
|
||||
};
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, ServerPage: Page, isBookingPage: true });
|
||||
@@ -1,64 +0,0 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import {
|
||||
generateTeamProfilePageMetadata,
|
||||
generateUserProfilePageMetadata,
|
||||
} from "app/generateBookingPageMetadata";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps";
|
||||
|
||||
import type { PageProps as TeamPageProps } from "~/team/team-view";
|
||||
import TeamPage from "~/team/team-view";
|
||||
import UserPage from "~/users/views/users-public-view";
|
||||
import type { PageProps as UserPageProps } from "~/users/views/users-public-view";
|
||||
|
||||
export type OrgPageProps = UserPageProps | TeamPageProps;
|
||||
const getData = withAppDirSsr<OrgPageProps>(getServerSideProps);
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
|
||||
if ((props as TeamPageProps)?.team) {
|
||||
const { team, markdownStrippedBio, isSEOIndexable, currentOrgDomain } = props as TeamPageProps;
|
||||
return await generateTeamProfilePageMetadata({
|
||||
profile: {
|
||||
teamName: team.name,
|
||||
image: getOrgOrTeamAvatar(team),
|
||||
markdownStrippedBio: markdownStrippedBio,
|
||||
},
|
||||
event: null,
|
||||
hideBranding: false,
|
||||
orgSlug: currentOrgDomain ?? null,
|
||||
isSEOIndexable: !!isSEOIndexable,
|
||||
});
|
||||
} else {
|
||||
const { profile, markdownStrippedBio, isOrgSEOIndexable, entity } = props as UserPageProps;
|
||||
|
||||
const isOrg = !!profile?.organization;
|
||||
const allowSEOIndexing =
|
||||
(!isOrg && profile.allowSEOIndexing) || (isOrg && isOrgSEOIndexable && profile.allowSEOIndexing);
|
||||
|
||||
return await generateUserProfilePageMetadata({
|
||||
profile: { name: profile.name, username: profile.username, image: profile.image, markdownStrippedBio },
|
||||
hideBranding: false,
|
||||
orgSlug: entity.orgSlug ?? null,
|
||||
isSEOIndexable: !!allowSEOIndexing,
|
||||
event: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const Page = async (props: OrgPageProps) => {
|
||||
if ((props as TeamPageProps)?.team) {
|
||||
return <TeamPage {...(props as TeamPageProps)} />;
|
||||
}
|
||||
return <UserPage {...(props as UserPageProps)} />;
|
||||
};
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, ServerPage: Page, isBookingPage: true });
|
||||
@@ -1 +0,0 @@
|
||||
export { default, generateMetadata } from "app/team/[slug]/page";
|
||||
@@ -1 +0,0 @@
|
||||
export { default, generateMetadata } from "app/team/[slug]/[type]/page";
|
||||
@@ -1 +0,0 @@
|
||||
export { default, generateMetadata } from "app/team/[slug]/page";
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
import { TrpcProvider } from "app/_trpc/trpc-provider";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import CacheProvider from "react-inlinesvg/provider";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<SessionProvider>
|
||||
<TrpcProvider>{children}</TrpcProvider>
|
||||
<TrpcProvider>
|
||||
{/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */}
|
||||
<CacheProvider>{children}</CacheProvider>
|
||||
</TrpcProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from "react";
|
||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { ScrollableArea, Badge, Button, Avatar } from "@calcom/ui";
|
||||
|
||||
@@ -58,9 +59,13 @@ const EventTypeCard: FC<EventTypeCardProps> = ({
|
||||
</small>
|
||||
</div>
|
||||
{Boolean(description) && (
|
||||
<div className="text-subtle line-clamp-4 break-words text-sm sm:max-w-[650px] [&>*:not(:first-child)]:hidden [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
|
||||
{description}
|
||||
</div>
|
||||
<div
|
||||
className="text-subtle line-clamp-4 break-words text-sm sm:max-w-[650px] [&>*:not(:first-child)]:hidden [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: markdownToSafeHTML(description),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="mt-2 flex flex-row flex-wrap gap-2">
|
||||
{Boolean(durations.length) &&
|
||||
|
||||
@@ -12,7 +12,6 @@ import type { ReadonlyURLSearchParams } from "next/navigation";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect } from "react";
|
||||
import CacheProvider from "react-inlinesvg/provider";
|
||||
|
||||
import DynamicPostHogProvider from "@calcom/features/ee/event-tracking/lib/posthog/providerDynamic";
|
||||
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
|
||||
@@ -277,10 +276,7 @@ const AppProviders = (props: PageWrapperProps) => {
|
||||
isThemeSupported={/* undefined gets treated as true */ props.isThemeSupported}
|
||||
isBookingPage={props.isBookingPage || isBookingPage}>
|
||||
<FeatureFlagsProvider>
|
||||
<OrgBrandProvider>
|
||||
{/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */}
|
||||
<CacheProvider>{props.children}</CacheProvider>
|
||||
</OrgBrandProvider>
|
||||
<OrgBrandProvider>{props.children}</OrgBrandProvider>
|
||||
</FeatureFlagsProvider>
|
||||
</CalcomThemeProvider>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -11,7 +11,6 @@ import dynamic from "next/dynamic";
|
||||
import type { ParsedUrlQuery } from "querystring";
|
||||
import type { PropsWithChildren, ReactNode } from "react";
|
||||
import { useEffect } from "react";
|
||||
import CacheProvider from "react-inlinesvg/provider";
|
||||
|
||||
import DynamicPostHogProvider from "@calcom/features/ee/event-tracking/lib/posthog/providerDynamic";
|
||||
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
|
||||
@@ -307,10 +306,7 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
||||
router={props.router}>
|
||||
<FeatureFlagsProvider>
|
||||
<OrgBrandProvider>
|
||||
{/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */}
|
||||
<CacheProvider>
|
||||
<MetaProvider>{props.children}</MetaProvider>
|
||||
</CacheProvider>
|
||||
<MetaProvider>{props.children}</MetaProvider>
|
||||
</OrgBrandProvider>
|
||||
</FeatureFlagsProvider>
|
||||
</CalcomThemeProvider>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getServerSideProps as GSSUserPage } from "@pages/[user]";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
@@ -5,8 +6,6 @@ import prisma from "@calcom/prisma";
|
||||
|
||||
import { getServerSideProps as GSSTeamPage } from "@lib/team/[slug]/getServerSideProps";
|
||||
|
||||
import { getServerSideProps as GSSUserPage } from "@server/lib/[user]/getServerSideProps";
|
||||
|
||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
|
||||
@@ -55,6 +55,7 @@ const getTheLastArrayElement = (value: ReadonlyArray<string> | string | undefine
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const slug = getTheLastArrayElement(context.query.slug) ?? getTheLastArrayElement(context.query.orgSlug);
|
||||
|
||||
const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(
|
||||
context.req,
|
||||
context.params?.orgSlug ?? context.query?.orgSlug
|
||||
|
||||
@@ -173,8 +173,7 @@ export const config = {
|
||||
*/
|
||||
"/apps/routing_forms/:path*",
|
||||
|
||||
"/event-types",
|
||||
"/future/event-types/",
|
||||
"/event-types/:path*",
|
||||
"/apps/installed/:category/",
|
||||
"/future/apps/installed/:category/",
|
||||
"/apps/:slug/",
|
||||
@@ -195,10 +194,7 @@ export const config = {
|
||||
"/settings/:path*",
|
||||
"/reschedule/:path*",
|
||||
"/availability/:path*",
|
||||
"/org/:path*",
|
||||
"/team/:path*",
|
||||
"/:user/:type/",
|
||||
"/:user/",
|
||||
"/booking/:path*",
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Booker } from "@calcom/atoms/monorepo";
|
||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
|
||||
import { type PageProps } from "@lib/d/[link]/[slug]/getServerSideProps";
|
||||
|
||||
@@ -19,6 +20,13 @@ export default function Type({
|
||||
}: PageProps) {
|
||||
return (
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||
<BookerSeo
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
rescheduleUid={booking?.uid}
|
||||
hideBranding={isBrandingHidden}
|
||||
entity={entity}
|
||||
/>
|
||||
<Booker
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import type { FC } from "react";
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { z } from "zod";
|
||||
@@ -18,7 +18,6 @@ import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsF
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { classNames, parseEventTypeColor } from "@calcom/lib";
|
||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useCopy } from "@calcom/lib/hooks/useCopy";
|
||||
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
|
||||
import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver";
|
||||
@@ -47,7 +46,6 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
EmptyScreen,
|
||||
HeadSeo,
|
||||
HorizontalTabs,
|
||||
Icon,
|
||||
Label,
|
||||
@@ -60,7 +58,6 @@ import {
|
||||
UserAvatarGroup,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import type { AppProps } from "@lib/app-providers";
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
|
||||
type GetUserEventGroupsResponse = RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"];
|
||||
@@ -256,7 +253,7 @@ export const InfiniteEventTypeList = ({
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const searchParams = useSearchParams();
|
||||
const { copyToClipboard } = useCopy();
|
||||
const [parent] = useAutoAnimate<HTMLUListElement>();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
@@ -373,7 +370,7 @@ export const InfiniteEventTypeList = ({
|
||||
|
||||
// inject selection data into url for correct router history
|
||||
const openDuplicateModal = (eventType: InfiniteEventType, group: InfiniteEventTypeGroup) => {
|
||||
const newSearchParams = new URLSearchParams(searchParams ?? undefined);
|
||||
const newSearchParams = new URLSearchParams(searchParams?.toString() ?? undefined);
|
||||
function setParamsIfDefined(key: string, value: string | number | boolean | null | undefined) {
|
||||
if (value) newSearchParams.set(key, value.toString());
|
||||
if (value === null) newSearchParams.delete(key);
|
||||
@@ -897,7 +894,7 @@ const InfiniteScrollMain = ({
|
||||
eventTypeGroups: GetUserEventGroupsResponse["eventTypeGroups"] | undefined;
|
||||
profiles: GetUserEventGroupsResponse["profiles"] | undefined;
|
||||
}) => {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const searchParams = useSearchParams();
|
||||
const { data } = useTypedQuery(querySchema);
|
||||
const orgBranding = useOrgBranding();
|
||||
|
||||
@@ -945,11 +942,9 @@ const InfiniteScrollMain = ({
|
||||
);
|
||||
};
|
||||
|
||||
const EventTypesPage: React.FC & {
|
||||
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
||||
getLayout?: AppProps["Component"]["getLayout"];
|
||||
} = () => {
|
||||
const EventTypesPage: React.FC = () => {
|
||||
const { t } = useLocale();
|
||||
const searchParams = useSearchParams();
|
||||
const { data: user } = useMeQuery();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_showProfileBanner, setShowProfileBanner] = useState(false);
|
||||
@@ -1003,18 +998,13 @@ const EventTypesPage: React.FC & {
|
||||
return (
|
||||
<Shell
|
||||
withoutMain={false}
|
||||
title="Event Types"
|
||||
description="Create events to share for people to book on your calendar."
|
||||
title={t("event_types_page_title")}
|
||||
description={t("event_types_page_subtitle")}
|
||||
withoutSeo
|
||||
heading={t("event_types_page_title")}
|
||||
hideHeadingOnMobile
|
||||
subtitle={t("event_types_page_subtitle")}
|
||||
CTA={<CTA profileOptions={profileOptions} isOrganization={!!user?.organizationId} />}>
|
||||
<HeadSeo
|
||||
title="Event Types"
|
||||
description="Create events to share for people to book on your calendar."
|
||||
/>
|
||||
|
||||
<InfiniteScrollMain
|
||||
profiles={getUserEventGroupsData?.profiles}
|
||||
eventTypeGroups={getUserEventGroupsData?.eventTypeGroups}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { EventType } from "@calcom/atoms/monorepo";
|
||||
import type { PageProps } from "@lib/event-types/[type]/getServerSideProps";
|
||||
|
||||
const EventTypePageWrapper = ({ type, ...rest }: PageProps) => {
|
||||
return <EventType {...rest} id={type} isAppDir={false} />;
|
||||
return <EventType {...rest} id={type} />;
|
||||
};
|
||||
|
||||
export default EventTypePageWrapper;
|
||||
|
||||
+12
-1
@@ -1,17 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import type { EmbedProps } from "app/WithEmbedSSR";
|
||||
|
||||
import { Booker } from "@calcom/atoms/monorepo";
|
||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
|
||||
import type { getServerSideProps } from "@lib/org/[orgSlug]/instant-meeting/team/[slug]/[type]/getServerSideProps";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import type { EmbedProps } from "app/WithEmbedSSR";
|
||||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||
|
||||
function Type({ slug, user, booking, isEmbed, isBrandingHidden, entity, eventTypeId, duration }: PageProps) {
|
||||
return (
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||
<BookerSeo
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
rescheduleUid={undefined}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent
|
||||
entity={entity}
|
||||
bookingData={booking}
|
||||
/>
|
||||
<Booker
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { usePathname } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
|
||||
import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -19,7 +20,7 @@ import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { Avatar, Button, UnpublishedEntity, UserAvatarGroup } from "@calcom/ui";
|
||||
import { Avatar, Button, HeadSeo, UnpublishedEntity, UserAvatarGroup } from "@calcom/ui";
|
||||
|
||||
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
||||
import type { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
@@ -163,87 +164,105 @@ function TeamPage({
|
||||
const profileImageSrc = getOrgOrTeamAvatar(team);
|
||||
|
||||
return (
|
||||
<main className="dark:bg-darkgray-50 bg-subtle mx-auto max-w-3xl rounded-md px-4 pb-12 pt-12">
|
||||
<div className="mx-auto mb-8 max-w-3xl text-center">
|
||||
<div className="relative">
|
||||
<Avatar alt={teamName} imageSrc={profileImageSrc} size="lg" />
|
||||
<>
|
||||
<HeadSeo
|
||||
origin={getOrgFullOrigin(currentOrgDomain)}
|
||||
title={teamName}
|
||||
description={teamName}
|
||||
meeting={{
|
||||
title: markdownStrippedBio,
|
||||
profile: {
|
||||
name: `${team.name}`,
|
||||
image: profileImageSrc,
|
||||
},
|
||||
}}
|
||||
nextSeoProps={{
|
||||
nofollow: !isSEOIndexable,
|
||||
noindex: !isSEOIndexable,
|
||||
}}
|
||||
/>
|
||||
<main className="dark:bg-darkgray-50 bg-subtle mx-auto max-w-3xl rounded-md px-4 pb-12 pt-12">
|
||||
<div className="mx-auto mb-8 max-w-3xl text-center">
|
||||
<div className="relative">
|
||||
<Avatar alt={teamName} imageSrc={profileImageSrc} size="lg" />
|
||||
</div>
|
||||
<p className="font-cal text-emphasis mb-2 text-2xl tracking-wider" data-testid="team-name">
|
||||
{team.parent && `${team.parent.name} `}
|
||||
{teamName}
|
||||
</p>
|
||||
{!isBioEmpty && (
|
||||
<>
|
||||
<div
|
||||
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: team.safeBio }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p className="font-cal text-emphasis mb-2 text-2xl tracking-wider" data-testid="team-name">
|
||||
{team.parent && `${team.parent.name} `}
|
||||
{teamName}
|
||||
</p>
|
||||
{!isBioEmpty && (
|
||||
{team.isOrganization ? (
|
||||
!teamOrOrgIsPrivate ? (
|
||||
<SubTeams />
|
||||
) : (
|
||||
<div className="w-full text-center">
|
||||
<h2 className="text-emphasis font-semibold">{t("you_cannot_see_teams_of_org")}</h2>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: team.safeBio }}
|
||||
/>
|
||||
{(showMembers.isOn || !team.eventTypes?.length) &&
|
||||
(teamOrOrgIsPrivate ? (
|
||||
<div className="w-full text-center">
|
||||
<h2 data-testid="you-cannot-see-team-members" className="text-emphasis font-semibold">
|
||||
{t("you_cannot_see_team_members")}
|
||||
</h2>
|
||||
</div>
|
||||
) : (
|
||||
<Team members={team.members} teamName={team.name} />
|
||||
))}
|
||||
{!showMembers.isOn && team.eventTypes && team.eventTypes.length > 0 && (
|
||||
<div className="mx-auto max-w-3xl ">
|
||||
<EventTypes eventTypes={team.eventTypes} />
|
||||
|
||||
{/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
|
||||
{!team.hideBookATeamMember && !teamOrOrgIsPrivate && (
|
||||
<div>
|
||||
<div className="relative mt-12">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="border-subtle w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="dark:bg-darkgray-50 bg-subtle text-subtle px-2 text-sm">
|
||||
{t("or")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="dark:text-inverted mt-8 flex justify-center text-center">
|
||||
<Button
|
||||
color="minimal"
|
||||
EndIcon="arrow-right"
|
||||
data-testid="book-a-team-member-btn"
|
||||
className="dark:hover:bg-darkgray-200"
|
||||
href={{
|
||||
pathname: `${isValidOrgDomain ? "" : "/team"}/${team.slug}`,
|
||||
query: {
|
||||
...queryParamsToForward,
|
||||
members: "1",
|
||||
},
|
||||
}}
|
||||
shallow={true}>
|
||||
{t("book_a_team_member")}
|
||||
</Button>
|
||||
</aside>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{team.isOrganization ? (
|
||||
!teamOrOrgIsPrivate ? (
|
||||
<SubTeams />
|
||||
) : (
|
||||
<div className="w-full text-center">
|
||||
<h2 className="text-emphasis font-semibold">{t("you_cannot_see_teams_of_org")}</h2>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{(showMembers.isOn || !team.eventTypes?.length) &&
|
||||
(teamOrOrgIsPrivate ? (
|
||||
<div className="w-full text-center">
|
||||
<h2 data-testid="you-cannot-see-team-members" className="text-emphasis font-semibold">
|
||||
{t("you_cannot_see_team_members")}
|
||||
</h2>
|
||||
</div>
|
||||
) : (
|
||||
<Team members={team.members} teamName={team.name} />
|
||||
))}
|
||||
{!showMembers.isOn && team.eventTypes && team.eventTypes.length > 0 && (
|
||||
<div className="mx-auto max-w-3xl ">
|
||||
<EventTypes eventTypes={team.eventTypes} />
|
||||
|
||||
{/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
|
||||
{!team.hideBookATeamMember && !teamOrOrgIsPrivate && (
|
||||
<div>
|
||||
<div className="relative mt-12">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="border-subtle w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="dark:bg-darkgray-50 bg-subtle text-subtle px-2 text-sm">
|
||||
{t("or")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="dark:text-inverted mt-8 flex justify-center text-center">
|
||||
<Button
|
||||
color="minimal"
|
||||
EndIcon="arrow-right"
|
||||
data-testid="book-a-team-member-btn"
|
||||
className="dark:hover:bg-darkgray-200"
|
||||
href={{
|
||||
pathname: `${isValidOrgDomain ? "" : "/team"}/${team.slug}`,
|
||||
query: {
|
||||
...queryParamsToForward,
|
||||
members: "1",
|
||||
},
|
||||
}}
|
||||
shallow={true}>
|
||||
{t("book_a_team_member")}
|
||||
</Button>
|
||||
</aside>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useSearchParams } from "next/navigation";
|
||||
|
||||
import { Booker } from "@calcom/atoms/monorepo";
|
||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
|
||||
import type { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
@@ -33,11 +34,33 @@ function Type({
|
||||
teamMemberEmail,
|
||||
crmOwnerRecordType,
|
||||
crmAppSlug,
|
||||
isSEOIndexable,
|
||||
}: PageProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const { profile, users, hidden, title } = eventData;
|
||||
|
||||
return (
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||
<BookerSeo
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
rescheduleUid={booking?.uid}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent
|
||||
eventData={
|
||||
profile && users && title && hidden !== undefined
|
||||
? {
|
||||
profile,
|
||||
users,
|
||||
title,
|
||||
hidden,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
entity={eventData.entity}
|
||||
bookingData={booking}
|
||||
isSEOIndexable={isSEOIndexable}
|
||||
/>
|
||||
<Booker
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, it, vi } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
|
||||
import UserPage from "./users-public-view";
|
||||
|
||||
@@ -70,5 +71,33 @@ describe("UserPage Component", () => {
|
||||
});
|
||||
|
||||
render(<UserPage {...mockData.props} />);
|
||||
|
||||
const expectedDescription = mockData.props.markdownStrippedBio;
|
||||
const expectedTitle = expectedDescription;
|
||||
expect(HeadSeo).toHaveBeenCalledWith(
|
||||
{
|
||||
origin: `${mockData.props.entity.orgSlug}.cal.local`,
|
||||
title: `${mockData.props.profile.name}`,
|
||||
description: expectedDescription,
|
||||
meeting: {
|
||||
profile: {
|
||||
name: mockData.props.profile.name,
|
||||
image: mockData.props.users[0].avatarUrl,
|
||||
},
|
||||
title: expectedTitle,
|
||||
users: [
|
||||
{
|
||||
name: mockData.props.users[0].name,
|
||||
username: mockData.props.users[0].username,
|
||||
},
|
||||
],
|
||||
},
|
||||
nextSeoProps: {
|
||||
nofollow: !mockData.props.profile.allowSEOIndexing,
|
||||
noindex: !mockData.props.profile.allowSEOIndexing,
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,11 +11,12 @@ import {
|
||||
useEmbedStyles,
|
||||
useIsEmbed,
|
||||
} from "@calcom/embed-core/embed-iframe";
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { Icon, UnpublishedEntity, UserAvatar } from "@calcom/ui";
|
||||
import { HeadSeo, Icon, UnpublishedEntity, UserAvatar } from "@calcom/ui";
|
||||
|
||||
import type { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
|
||||
|
||||
@@ -59,89 +60,112 @@ export function UserPage(props: PageProps) {
|
||||
const isEventListEmpty = eventTypes.length === 0;
|
||||
const isOrg = !!user?.profile?.organization;
|
||||
|
||||
const allowSEOIndexing = isOrg
|
||||
? isOrgSEOIndexable
|
||||
? profile.allowSEOIndexing
|
||||
: false
|
||||
: profile.allowSEOIndexing;
|
||||
|
||||
return (
|
||||
<div className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "max-w-3xl" : "")}>
|
||||
<main
|
||||
className={classNames(
|
||||
shouldAlignCentrally ? "mx-auto" : "",
|
||||
isEmbed ? "border-booker border-booker-width bg-default rounded-md" : "",
|
||||
"max-w-3xl px-4 py-24"
|
||||
)}>
|
||||
<div className="mb-8 text-center">
|
||||
<UserAvatar
|
||||
size="xl"
|
||||
user={{
|
||||
avatarUrl: user.avatarUrl,
|
||||
profile: user.profile,
|
||||
name: profile.name,
|
||||
username: profile.username,
|
||||
}}
|
||||
/>
|
||||
<h1 className="font-cal text-emphasis my-1 text-3xl" data-testid="name-title">
|
||||
{profile.name}
|
||||
{!isOrg && user.verified && (
|
||||
<Icon
|
||||
name="badge-check"
|
||||
className="mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black"
|
||||
/>
|
||||
)}
|
||||
{isOrg && (
|
||||
<Icon
|
||||
name="badge-check"
|
||||
className="mx-1 -mt-1 inline h-6 w-6 fill-yellow-500 text-white dark:text-black"
|
||||
/>
|
||||
)}
|
||||
</h1>
|
||||
{!isBioEmpty && (
|
||||
<>
|
||||
<div
|
||||
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: props.safeBio }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
<HeadSeo
|
||||
origin={getOrgFullOrigin(entity.orgSlug ?? null)}
|
||||
title={profile.name}
|
||||
description={markdownStrippedBio}
|
||||
meeting={{
|
||||
title: markdownStrippedBio,
|
||||
profile: { name: `${profile.name}`, image: user.avatarUrl || null },
|
||||
users: [{ username: `${user.username}`, name: `${user.name}` }],
|
||||
}}
|
||||
nextSeoProps={{
|
||||
noindex: !allowSEOIndexing,
|
||||
nofollow: !allowSEOIndexing,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classNames("rounded-md ", !isEventListEmpty && "border-subtle border")}
|
||||
data-testid="event-types">
|
||||
{eventTypes.map((type) => (
|
||||
<Link
|
||||
key={type.id}
|
||||
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
|
||||
prefetch={false}
|
||||
href={{
|
||||
pathname: `/${user.profile.username}/${type.slug}`,
|
||||
query,
|
||||
<div className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "max-w-3xl" : "")}>
|
||||
<main
|
||||
className={classNames(
|
||||
shouldAlignCentrally ? "mx-auto" : "",
|
||||
isEmbed ? "border-booker border-booker-width bg-default rounded-md" : "",
|
||||
"max-w-3xl px-4 py-24"
|
||||
)}>
|
||||
<div className="mb-8 text-center">
|
||||
<UserAvatar
|
||||
size="xl"
|
||||
user={{
|
||||
avatarUrl: user.avatarUrl,
|
||||
profile: user.profile,
|
||||
name: profile.name,
|
||||
username: profile.username,
|
||||
}}
|
||||
passHref
|
||||
onClick={async () => {
|
||||
sdkActionManager?.fire("eventTypeSelected", {
|
||||
eventType: type,
|
||||
});
|
||||
}}
|
||||
className="bg-default border-subtle dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b transition first:rounded-t-md last:rounded-b-md last:border-b-0"
|
||||
data-testid="event-type-link">
|
||||
<Icon
|
||||
name="arrow-right"
|
||||
className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
|
||||
<div className="block w-full p-5">
|
||||
<div className="flex flex-wrap items-center">
|
||||
<h2 className="text-default pr-2 text-sm font-semibold">{type.title}</h2>
|
||||
/>
|
||||
<h1 className="font-cal text-emphasis my-1 text-3xl" data-testid="name-title">
|
||||
{profile.name}
|
||||
{!isOrg && user.verified && (
|
||||
<Icon
|
||||
name="badge-check"
|
||||
className="mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black"
|
||||
/>
|
||||
)}
|
||||
{isOrg && (
|
||||
<Icon
|
||||
name="badge-check"
|
||||
className="mx-1 -mt-1 inline h-6 w-6 fill-yellow-500 text-white dark:text-black"
|
||||
/>
|
||||
)}
|
||||
</h1>
|
||||
{!isBioEmpty && (
|
||||
<>
|
||||
<div
|
||||
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: props.safeBio }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classNames("rounded-md ", !isEventListEmpty && "border-subtle border")}
|
||||
data-testid="event-types">
|
||||
{eventTypes.map((type) => (
|
||||
<Link
|
||||
key={type.id}
|
||||
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
|
||||
prefetch={false}
|
||||
href={{
|
||||
pathname: `/${user.profile.username}/${type.slug}`,
|
||||
query,
|
||||
}}
|
||||
passHref
|
||||
onClick={async () => {
|
||||
sdkActionManager?.fire("eventTypeSelected", {
|
||||
eventType: type,
|
||||
});
|
||||
}}
|
||||
className="bg-default border-subtle dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b transition first:rounded-t-md last:rounded-b-md last:border-b-0"
|
||||
data-testid="event-type-link">
|
||||
<Icon
|
||||
name="arrow-right"
|
||||
className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
|
||||
<div className="block w-full p-5">
|
||||
<div className="flex flex-wrap items-center">
|
||||
<h2 className="text-default pr-2 text-sm font-semibold">{type.title}</h2>
|
||||
</div>
|
||||
<EventTypeDescription eventType={type} isPublic={true} shortenDescription />
|
||||
</div>
|
||||
<EventTypeDescription eventType={type} isPublic={true} shortenDescription />
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isEventListEmpty && <EmptyPage name={profile.name || "User"} />}
|
||||
</main>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
{isEventListEmpty && <EmptyPage name={profile.name || "User"} />}
|
||||
</main>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
UserPage.isBookingPage = true;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useSearchParams } from "next/navigation";
|
||||
|
||||
import { Booker } from "@calcom/atoms/monorepo";
|
||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
@@ -22,11 +23,40 @@ export const getMultipleDurationValue = (
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
function Type({ slug, user, isEmbed, booking, isBrandingHidden, eventData, orgBannerUrl }: PageProps) {
|
||||
function Type({
|
||||
slug,
|
||||
user,
|
||||
isEmbed,
|
||||
booking,
|
||||
isBrandingHidden,
|
||||
isSEOIndexable,
|
||||
rescheduleUid,
|
||||
eventData,
|
||||
orgBannerUrl,
|
||||
}: PageProps) {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const { profile, users, hidden, title } = eventData;
|
||||
return (
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||
<BookerSeo
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
rescheduleUid={rescheduleUid ?? undefined}
|
||||
hideBranding={isBrandingHidden}
|
||||
isSEOIndexable={isSEOIndexable ?? true}
|
||||
eventData={
|
||||
profile && users && title && hidden !== undefined
|
||||
? {
|
||||
profile,
|
||||
users,
|
||||
title,
|
||||
hidden,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
entity={eventData.entity}
|
||||
bookingData={booking}
|
||||
/>
|
||||
<Booker
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "4.8.14",
|
||||
"version": "4.8.15",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import { getServerSideProps as _getServerSideProps } from "@server/lib/[user]/[type]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/users/views/users-type-public-view";
|
||||
import TypePage from "~/users/views/users-type-public-view";
|
||||
|
||||
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||
|
||||
const Type = (props: PageProps) => <TypePage {...props} />;
|
||||
|
||||
Type.PageWrapper = PageWrapper;
|
||||
|
||||
export default Type;
|
||||
export { default } from "./index";
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import type { PageProps } from "~/users/views/users-type-public-view";
|
||||
import TypePage from "~/users/views/users-type-public-view";
|
||||
|
||||
export { getServerSideProps } from "@server/lib/[user]/[type]/getServerSideProps";
|
||||
|
||||
const Type = (props: PageProps) => <TypePage {...props} />;
|
||||
|
||||
Type.PageWrapper = PageWrapper;
|
||||
|
||||
export default Type;
|
||||
@@ -1,21 +1,6 @@
|
||||
import { getServerSideProps as _getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps";
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import type { PageProps as TeamPageProps } from "~/team/team-view";
|
||||
import TeamPage from "~/team/team-view";
|
||||
import UserPage from "~/users/views/users-public-view";
|
||||
import type { PageProps as UserPageProps } from "~/users/views/users-public-view";
|
||||
|
||||
export type PageProps = UserPageProps | TeamPageProps;
|
||||
import { getServerSideProps as _getServerSideProps } from "@server/lib/[user]/getServerSideProps";
|
||||
|
||||
export { default } from "./index";
|
||||
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||
function Page(props: PageProps) {
|
||||
if ((props as TeamPageProps)?.team) return <TeamPage {...(props as TeamPageProps)} />;
|
||||
return <UserPage {...(props as UserPageProps)} />;
|
||||
}
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import User, { type PageProps } from "~/users/views/users-public-view";
|
||||
|
||||
export { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
|
||||
|
||||
const UserPage = (props: PageProps) => <User {...props} />;
|
||||
|
||||
UserPage.PageWrapper = PageWrapper;
|
||||
|
||||
export default UserPage;
|
||||
@@ -2,6 +2,7 @@ import type { IncomingMessage } from "http";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import type { AppContextType } from "next/dist/shared/lib/utils";
|
||||
import React, { useEffect } from "react";
|
||||
import CacheProvider from "react-inlinesvg/provider";
|
||||
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
@@ -20,7 +21,10 @@ function MyApp(props: AppProps) {
|
||||
|
||||
return (
|
||||
<SessionProvider session={pageProps.session ?? undefined}>
|
||||
{Component.PageWrapper ? <Component.PageWrapper {...props} /> : <Component {...pageProps} />}
|
||||
{/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */}
|
||||
<CacheProvider>
|
||||
{Component.PageWrapper ? <Component.PageWrapper {...props} /> : <Component {...pageProps} />}
|
||||
</CacheProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { PageProps } from "@lib/event-types/[type]/getServerSideProps";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import EventTypePageWrapper from "~/event-types/views/event-types-single-view";
|
||||
|
||||
export type {
|
||||
FormValues,
|
||||
CustomInputParsed,
|
||||
EventTypeSetup,
|
||||
EventTypeSetupProps,
|
||||
Host,
|
||||
} from "@calcom/features/eventtypes/lib/types";
|
||||
|
||||
const Page = (props: PageProps) => <EventTypePageWrapper {...props} />;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export { getServerSideProps } from "@lib/event-types/[type]/getServerSideProps";
|
||||
export default Page;
|
||||
@@ -1,9 +0,0 @@
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import EventTypesPage from "~/event-types/views/event-types-listing-view";
|
||||
|
||||
export { getServerSideProps } from "@lib/event-types/getServerSideProps";
|
||||
|
||||
EventTypesPage.PageWrapper = PageWrapper;
|
||||
|
||||
export default EventTypesPage;
|
||||
@@ -1,20 +1,7 @@
|
||||
import { getServerSideProps as _getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import { getServerSideProps as _getServerSideProps } from ".";
|
||||
|
||||
import type { PageProps as TeamTypePageProps } from "~/team/type-view";
|
||||
import TeamTypePage from "~/team/type-view";
|
||||
import UserTypePage from "~/users/views/users-type-public-view";
|
||||
import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view";
|
||||
|
||||
export type PageProps = UserTypePageProps | TeamTypePageProps;
|
||||
|
||||
export default function Page(props: PageProps) {
|
||||
if ((props as TeamTypePageProps)?.teamId) return <TeamTypePage {...(props as TeamTypePageProps)} />;
|
||||
return <UserTypePage {...(props as UserTypePageProps)} />;
|
||||
}
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
export { default, type PageProps } from ".";
|
||||
|
||||
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import type { PageProps as TeamTypePageProps } from "~/team/type-view";
|
||||
import TeamTypePage from "~/team/type-view";
|
||||
import UserTypePage from "~/users/views/users-type-public-view";
|
||||
import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view";
|
||||
|
||||
export { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
|
||||
export type PageProps = UserTypePageProps | TeamTypePageProps;
|
||||
|
||||
export default function Page(props: PageProps) {
|
||||
if ((props as TeamTypePageProps)?.teamId) return <TeamTypePage {...(props as TeamTypePageProps)} />;
|
||||
return <UserTypePage {...(props as UserTypePageProps)} />;
|
||||
}
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
@@ -1 +1,9 @@
|
||||
export { default, getServerSideProps } from "@pages/[user]/embed";
|
||||
"use client";
|
||||
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
import { getServerSideProps as _getServerSideProps } from "../[user]";
|
||||
|
||||
export { default } from "../[user]";
|
||||
|
||||
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import type { PageProps as TeamPageProps } from "~/team/team-view";
|
||||
import TeamPage from "~/team/team-view";
|
||||
import UserPage from "~/users/views/users-public-view";
|
||||
import type { PageProps as UserPageProps } from "~/users/views/users-public-view";
|
||||
|
||||
export { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps";
|
||||
|
||||
export type PageProps = UserPageProps | TeamPageProps;
|
||||
|
||||
function Page(props: PageProps) {
|
||||
if ((props as TeamPageProps)?.team) return <TeamPage {...(props as TeamPageProps)} />;
|
||||
return <UserPage {...(props as UserPageProps)} />;
|
||||
}
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
@@ -1,14 +1,7 @@
|
||||
import { getServerSideProps as _getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import { getServerSideProps as _getServerSideProps } from "./index";
|
||||
|
||||
import TeamPage, { type PageProps } from "~/team/team-view";
|
||||
|
||||
const Page = (props: PageProps) => <TeamPage {...props} />;
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
export { default } from "./index";
|
||||
|
||||
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { getServerSideProps, default } from "@pages/team/[slug]";
|
||||
@@ -0,0 +1,10 @@
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import type { PageProps } from "~/org/[orgSlug]/instant-meeting/team/[slug]/[type]/instant-meeting-view";
|
||||
import Type from "~/org/[orgSlug]/instant-meeting/team/[slug]/[type]/instant-meeting-view";
|
||||
|
||||
const Page = (props: PageProps) => <Type {...props} />;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export { getServerSideProps } from "@lib/org/[orgSlug]/instant-meeting/team/[slug]/[type]/getServerSideProps";
|
||||
export default Page;
|
||||
@@ -0,0 +1 @@
|
||||
export { getServerSideProps, default } from "@pages/team/[slug]/[type]";
|
||||
@@ -0,0 +1 @@
|
||||
export { getServerSideProps, default } from "@pages/team/[slug]";
|
||||
@@ -0,0 +1,10 @@
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import TeamPage, { type PageProps } from "~/team/team-view";
|
||||
|
||||
const Page = (props: PageProps) => <TeamPage {...props} />;
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
export { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
@@ -0,0 +1,10 @@
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import TypePage, { type PageProps } from "~/team/type-view";
|
||||
|
||||
const Page = (props: PageProps) => <TypePage {...props} />;
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
export { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
||||
@@ -11,8 +11,6 @@ import {
|
||||
selectSecondAvailableTimeSlotNextMonth,
|
||||
} from "./lib/testUtils";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
test("dynamic booking", async ({ page, users }) => {
|
||||
|
||||
@@ -258,8 +258,6 @@ test.describe("Manage Booking Questions", () => {
|
||||
|
||||
await test.step("Go to First Team Event", async () => {
|
||||
await page.getByTestId(`horizontal-tab-${team?.name}`).click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const $eventTypes = page.locator("[data-testid=event-types]").locator("li a");
|
||||
const firstEventTypeElement = $eventTypes.first();
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ async function verifyRobotsMetaTag({ page, orgSlug, urls, expectedContent }: Ver
|
||||
await doOnOrgDomain({ orgSlug, page }, async ({ page, goToUrlWithErrorHandling }) => {
|
||||
for (const relativeUrl of urls) {
|
||||
const { url } = await goToUrlWithErrorHandling(relativeUrl);
|
||||
const metaTag = await page.locator('head > meta[name="robots"]').first();
|
||||
const metaTag = await page.locator('head > meta[name="robots"]');
|
||||
const metaTagValue = await metaTag.getAttribute("content");
|
||||
expect(metaTagValue).toEqual(expectedContent);
|
||||
}
|
||||
@@ -103,7 +103,7 @@ test.describe("Organization Settings", () => {
|
||||
`/${orgMember.username}`,
|
||||
`/${orgMember.username}/${userEvent.slug}`,
|
||||
],
|
||||
expectedContent: "noindex, nofollow",
|
||||
expectedContent: "noindex,nofollow",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -130,7 +130,7 @@ test.describe("Organization Settings", () => {
|
||||
`/${orgMember.username}`,
|
||||
`/${orgMember.username}/${userEvent.slug}`,
|
||||
],
|
||||
expectedContent: "index, follow",
|
||||
expectedContent: "index,follow",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -148,7 +148,7 @@ test.describe("Organization Settings", () => {
|
||||
page,
|
||||
orgSlug: org.slug,
|
||||
urls: [`/${orgMember.username}/${userEvent.slug}`],
|
||||
expectedContent: "noindex, nofollow",
|
||||
expectedContent: "noindex,nofollow",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "أنت تقوم بحجز تجريبي.",
|
||||
"icon_showcase": "معرض الأيقونات",
|
||||
"icons_showcase": "معرض الأيقونات",
|
||||
"choose_an_option": "اختر خيارًا",
|
||||
"enter_option_value": "أدخل قيمة الخيار",
|
||||
"remove_option": "إزالة الخيار",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "Siz test rezervasiyası edirsiniz.",
|
||||
"icon_showcase": "İkon nümayişi",
|
||||
"icons_showcase": "İkonların nümayişi",
|
||||
"choose_an_option": "Seçim edin",
|
||||
"enter_option_value": "Seçim dəyərini daxil edin",
|
||||
"remove_option": "Seçimi sil",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni sətirləri bura əlavə edin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "Правите тестова резервация.",
|
||||
"icon_showcase": "Галерия с икони",
|
||||
"icons_showcase": "Галерия с икони",
|
||||
"choose_an_option": "Изберете опция",
|
||||
"enter_option_value": "Въведете стойност на опцията",
|
||||
"remove_option": "Премахване на опцията",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавете новите си низове над този ред ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "Estàs fent una reserva de prova.",
|
||||
"icon_showcase": "Mostra d'icones",
|
||||
"icons_showcase": "Mostra d'icones",
|
||||
"choose_an_option": "Tria una opció",
|
||||
"enter_option_value": "Introdueix el valor de l'opció",
|
||||
"remove_option": "Elimina l'opció",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Afegiu les vostres noves cadenes a dalt ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "Provádíte testovací rezervaci.",
|
||||
"icon_showcase": "Ukázka ikon",
|
||||
"icons_showcase": "Ukázka ikon",
|
||||
"choose_an_option": "Vyberte možnost",
|
||||
"enter_option_value": "Zadejte hodnotu možnosti",
|
||||
"remove_option": "Odebrat možnost",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "Du foretager en testbooking.",
|
||||
"icon_showcase": "Ikonoversigt",
|
||||
"icons_showcase": "Ikonoversigt",
|
||||
"choose_an_option": "Vælg en mulighed",
|
||||
"enter_option_value": "Indtast værdi for mulighed",
|
||||
"remove_option": "Fjern mulighed",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Tilføj dine nye strenge ovenfor her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "Sie führen eine Testbuchung durch.",
|
||||
"icon_showcase": "Icon-Übersicht",
|
||||
"icons_showcase": "Icons-Übersicht",
|
||||
"choose_an_option": "Option auswählen",
|
||||
"enter_option_value": "Optionswert eingeben",
|
||||
"remove_option": "Option entfernen",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "Κάνετε μια δοκιμαστική κράτηση.",
|
||||
"icon_showcase": "Παρουσίαση εικονιδίων",
|
||||
"icons_showcase": "Παρουσίαση εικονιδίων",
|
||||
"choose_an_option": "Επιλέξτε μια επιλογή",
|
||||
"enter_option_value": "Εισαγάγετε την τιμή της επιλογής",
|
||||
"remove_option": "Αφαίρεση επιλογής",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Προσθέστε τις νέες συμβολοσειρές σας πάνω από εδώ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "Estás haciendo una reserva de prueba.",
|
||||
"icon_showcase": "Muestra de íconos",
|
||||
"icons_showcase": "Muestra de íconos",
|
||||
"choose_an_option": "Elige una opción",
|
||||
"enter_option_value": "Ingresa el valor de la opción",
|
||||
"remove_option": "Eliminar opción",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agrega tus nuevas cadenas arriba de esta línea ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "Estás haciendo una reserva de prueba.",
|
||||
"icon_showcase": "Muestra de iconos",
|
||||
"icons_showcase": "Muestra de iconos",
|
||||
"choose_an_option": "Elige una opción",
|
||||
"enter_option_value": "Ingresa el valor de la opción",
|
||||
"remove_option": "Eliminar opción",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "Teed proovibroneeringut.",
|
||||
"icon_showcase": "Ikoonide ülevaade",
|
||||
"icons_showcase": "Ikoonide ülevaade",
|
||||
"choose_an_option": "Vali valik",
|
||||
"enter_option_value": "Sisesta valiku väärtus",
|
||||
"remove_option": "Eemalda valik",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "Proba-erreserba bat egiten ari zara.",
|
||||
"icon_showcase": "Ikonoen erakusleihoa",
|
||||
"icons_showcase": "Ikonoen erakusleihoa",
|
||||
"choose_an_option": "Aukeratu aukera bat",
|
||||
"enter_option_value": "Sartu aukeraren balioa",
|
||||
"remove_option": "Kendu aukera",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Gehitu zure kate berriak honen gainean ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "Teet testikäyntivarausta.",
|
||||
"icon_showcase": "Kuvakegalleria",
|
||||
"icons_showcase": "Kuvakegalleria",
|
||||
"choose_an_option": "Valitse vaihtoehto",
|
||||
"enter_option_value": "Syötä vaihtoehdon arvo",
|
||||
"remove_option": "Poista vaihtoehto",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Lisää uudet merkkijonot tämän yläpuolelle ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "Vous effectuez une réservation test.",
|
||||
"icon_showcase": "Présentation des icônes",
|
||||
"icons_showcase": "Présentation des icônes",
|
||||
"choose_an_option": "Choisir une option",
|
||||
"enter_option_value": "Saisir la valeur de l'option",
|
||||
"remove_option": "Supprimer l'option",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "אתה מבצע הזמנת ניסיון.",
|
||||
"icon_showcase": "תצוגת אייקון",
|
||||
"icons_showcase": "תצוגת אייקונים",
|
||||
"choose_an_option": "בחר אפשרות",
|
||||
"enter_option_value": "הזן ערך אפשרות",
|
||||
"remove_option": "הסר אפשרות",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "Tesztfoglalást végzel.",
|
||||
"icon_showcase": "Ikon bemutató",
|
||||
"icons_showcase": "Ikonok bemutatója",
|
||||
"choose_an_option": "Válassz egy opciót",
|
||||
"enter_option_value": "Add meg az opció értékét",
|
||||
"remove_option": "Opció eltávolítása",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adja hozzá az új karakterláncokat fent ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "Stai effettuando una prenotazione di prova.",
|
||||
"icon_showcase": "Galleria delle icone",
|
||||
"icons_showcase": "Galleria delle icone",
|
||||
"choose_an_option": "Scegli un'opzione",
|
||||
"enter_option_value": "Inserisci il valore dell'opzione",
|
||||
"remove_option": "Rimuovi opzione",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "テスト予約を実行中です。",
|
||||
"icon_showcase": "アイコンショーケース",
|
||||
"icons_showcase": "アイコンショーケース",
|
||||
"choose_an_option": "オプションを選択",
|
||||
"enter_option_value": "オプション値を入力",
|
||||
"remove_option": "オプションを削除",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2917,5 +2917,8 @@
|
||||
"dry_run_mode_active": "អ្នកកំពុងធ្វើការកក់សាកល្បង។",
|
||||
"icon_showcase": "ការបង្ហាញរូបតំណាង",
|
||||
"icons_showcase": "ការបង្ហាញរូបតំណាងនានា",
|
||||
"choose_an_option": "ជ្រើសរើសជម្រើសមួយ",
|
||||
"enter_option_value": "បញ្ចូលតម្លៃជម្រើស",
|
||||
"remove_option": "លុបជម្រើស",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ បន្ថែមខ្សែអក្សរថ្មីរបស់អ្នកនៅខាងលើនេះ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user