Compare commits

...
10 Commits
Author SHA1 Message Date
Alex van Andel aedc0f5a03 chore: release v5.9.10
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-12-07 14:25:55 +00:00
Keith Williams ff88ec0eae fix: Docker build OOM issues in v5.9.9 (#25678) 2025-12-07 14:22:24 +00:00
Kartik Labhshetwar 110b5ceaf6 style: adjust dark hover background for user page to subtle (#25662) 2025-12-06 13:24:41 +00:00
Kartik Labhshetwar 7c53d2c9ce fix: improve spacing in provide link to team member when OOO list (#25529) 2025-12-06 09:12:48 +00:00
Volnei Munhoz eccebff4c6 Companion build (#25648) 2025-12-05 21:22:31 +00:00
cal.com 0a36dd7f61 revert: perf: handleChildrenEventType update existing events without mapping 2025-12-05 22:24:15 +02:00
cal.com 5143083efb perf: handleChildrenEventType update existing events without mapping 2025-12-05 22:00:33 +02:00
AbhishekandDhairyashil Shinde b874e0d55a fix: data table toolbar styling (#25632)
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2025-12-05 16:01:05 +00:00
Dhairyashil Shinde 3ee24586a7 feat(api): PATCH Event Type V2 API to support all current locations (#25084)
* feat(api): PATCH Event Type V2 API to support all current locations

* docs(api): update locations documentation and add E2E tests for new integrations

- Updated locations property documentation in create-event-type.input.ts
  and update-event-type.input.ts to clarify app installation requirements
- Explained that only Google Meet, MS Teams, and Zoom can be installed via API
- Noted that Cal Video is installed by default
- Added E2E tests for creating and updating event types with newly supported
  integration locations (jitsi, zoom, google-meet, whereby, huddle, element-call)
- Regenerated openapi.json with updated API documentation

Addresses feedback from Lauris regarding platform API location support.

* fix(api): use supportedIntegrations list for app validation

Updated checkAppIsValidAndConnected to use the full supportedIntegrations
list from locations.input.ts instead of hardcoded array. This allows all
27 supported conferencing apps to be set as event type locations via API,
as long as they are already connected by the user.

* fix(api): add slug mapping for all conferencing integrations

Added comprehensive slug mapping to translate API integration names
(e.g., 'facetime-video', 'whereby-video') to actual app slugs
(e.g., 'facetime', 'whereby'). This ensures the app lookup works
correctly for all 27 supported conferencing integrations.

Addresses AI bot feedback about slug mismatches.

* fix(api): add missing huddle to huddle01 slug mapping

Added mapping for huddle -> huddle01. Other apps like tandem, jitsi,
cal-video, google-meet, and zoom don't need mapping as their API names
already match their app slugs (handled by the fallback || appSlug).

* update key

* update ket

* test(api): update E2E tests to validate newly supported integrations

Replaced end-to-end tests with validation-focused tests that follow
the existing pattern. The new test creates event types with various
newly supported integrations (jitsi, whereby-video, huddle, tandem,
element-call-video) directly in the database (bypassing app connection
checks) and verifies the API correctly returns them.

This approach tests that the input validation accepts all 27 supported
integration types without requiring actual app installations in the
test environment.

* fix(api): correct slug mappings for whatsapp, shimmer, and jelly integrations

- Fixed whatsapp-video mapping from 'whatsappvideo' to 'whatsapp'
- Fixed shimmer-video mapping from 'shimmer' to 'shimmervideo'
- Fixed jelly-conferencing mapping from 'jelly-conferencing' to 'jelly'

All slug mappings now correctly match the actual app slugs in
packages/app-store/*/config.json files. This ensures proper app
validation when users create/update event types with these locations.

Addresses feedback from @pedroccastro

* updated openapi.json file because of main branch code

* test(api): add negative test for unsupported integration locations

- Added E2E test to validate 400 error when creating event type with unsupported integration
- Test verifies exact error message listing all supported integrations
- Uses imported supportedIntegrations constant for maintainability
- Follows same pattern as booking fields validation tests

Addresses feedback from @supalarry

* test(api): add negative test for patching event type with unconnected integration

- Added E2E test to validate 400 error when user tries to PATCH event type with jitsi integration they haven't connected
- Test verifies exact error message 'jitsi not connected.'
- Follows existing test patterns with proper cleanup
2025-12-05 19:51:58 +05:30
Dhairyashil Shinde b014ded5fd feat: api v2 event types ordering - user, team, org (#25177)
* feat: api-v2-event-types-ordering

* sort team and org event types

* revert: remove accidental changes to api-auth.strategy.ts

* docs: add ordering documentation and test for event types endpoints

- Added test assertion to verify event types are returned in descending order by ID (newest first)
- Added API documentation to user event types endpoint describing default ordering behavior
- Added API documentation to team event types endpoint describing default ordering behavior
- Added API documentation to organization event types endpoints describing default ordering behavior

Addresses PR feedback to document and test the ordering behavior introduced in the API v2 event types ordering feature.

* feat: add optional sortCreatedAt parameter to event types endpoints

- Add sortCreatedAt query parameter (SortOrderType: "asc" | "desc") to all event types endpoints
- Define SortOrder enum and SortOrderType in pagination.input.ts for reusability
- When not provided, no explicit ordering is applied (backward compatible)
- Update user, team, and organization event types endpoints
- Add comprehensive e2e tests for all sorting scenarios
- Fix circular dependency in platform-types import
- Thread sortCreatedAt through all service layers
- Use spread pattern for conditional orderBy to avoid empty array issues

Addresses PR feedback to make ordering opt-in rather than changing default behavior
2025-12-05 19:51:48 +05:30
24 changed files with 528 additions and 57 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ ARG CALCOM_TELEMETRY_DISABLED
ARG DATABASE_URL
ARG NEXTAUTH_SECRET=secret
ARG CALENDSO_ENCRYPTION_KEY=secret
ARG MAX_OLD_SPACE_SIZE=4096
ARG MAX_OLD_SPACE_SIZE=6144
ARG NEXT_PUBLIC_API_V2_URL
ARG CSP_POLICY
@@ -34,6 +34,7 @@ import { SchedulingType } from "@calcom/platform-libraries";
import {
BaseConfirmationPolicy_2024_06_14,
TeamEventTypeOutput_2024_06_14,
supportedIntegrations,
type ApiSuccessResponse,
type CreateEventTypeInput_2024_06_14,
type EventTypeOutput_2024_06_14,
@@ -640,9 +641,9 @@ describe("Event types Endpoints", () => {
hiddenEventType = responseBody.data;
});
it(`/GET/event-types by username`, async () => {
it(`/GET/event-types by username with sortCreatedAt=desc`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}`)
.get(`/api/v2/event-types?username=${user.username}&sortCreatedAt=desc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
@@ -653,6 +654,12 @@ describe("Event types Endpoints", () => {
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(2);
// Verify ordering: event types are returned newest to oldest when sortCreatedAt=desc
// hiddenEventType was created after eventType, so it should have a higher ID and appear first
expect(responseBody.data[0].id).toBeGreaterThan(responseBody.data[1].id);
expect(responseBody.data[0].id).toEqual(hiddenEventType.id);
expect(responseBody.data[1].id).toEqual(eventType.id);
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
const fetchedHiddenEventType = responseBody.data?.find((et) => et.id === hiddenEventType.id);
@@ -711,6 +718,46 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType?.hidden).toEqual(false);
});
it(`/GET/event-types by username with sortCreatedAt=asc`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}&sortCreatedAt=asc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(2);
// Verify ordering: event types are returned oldest to newest when sortCreatedAt=asc
// eventType was created before hiddenEventType, so it should have a lower ID and appear first
expect(responseBody.data[0].id).toBeLessThan(responseBody.data[1].id);
expect(responseBody.data[0].id).toEqual(eventType.id);
expect(responseBody.data[1].id).toEqual(hiddenEventType.id);
});
it(`/GET/event-types by username without sortCreatedAt parameter`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(2);
// Without sortCreatedAt, no specific order is guaranteed
// Just verify both event types are present
const ids = responseBody.data.map((et) => et.id);
expect(ids).toContain(eventType.id);
expect(ids).toContain(hiddenEventType.id);
});
it(`/GET/event-types by username should not return hidden event type if no auth provided`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}`)
@@ -2855,6 +2902,119 @@ describe("Event types Endpoints", () => {
});
});
it("should accept newly supported integration locations in input validation", async () => {
// Test that the API accepts various newly supported integrations by creating event types
// with these location types directly in the database and then retrieving them
// Note: Database uses underscores in integration types, API returns hyphens
const integrationTests = [
{ internal: "integrations:jitsi", api: "jitsi" },
{ internal: "integrations:whereby_video", api: "whereby-video" },
{ internal: "integrations:huddle01", api: "huddle" },
{ internal: "integrations:tandem", api: "tandem" },
{ internal: "integrations:element-call_video", api: "element-call-video" },
];
for (const { internal, api } of integrationTests) {
const eventTypeInput = {
title: `event type ${api}`,
description: "event type description",
length: 30,
hidden: false,
slug: `event-type-${api}`,
locations: [
{
type: internal,
link: `https://example.com/${api}/meeting`,
},
],
schedulingType: SchedulingType.ROUND_ROBIN,
bookingFields: [],
};
const legacyEventType = await eventTypesRepositoryFixture.create(eventTypeInput, user.id);
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types/${legacyEventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const fetchedEventType = responseBody.data;
// Verify the integration location is returned correctly
expect(fetchedEventType.locations).toHaveLength(1);
expect(fetchedEventType.locations[0]).toMatchObject({
type: "integration",
integration: api,
});
// Clean up
await eventTypesRepositoryFixture.delete(legacyEventType.id);
}
});
it("should reject with 400 if creating event type with unsupported integration location", async () => {
const createPayload = {
title: "Event with unsupported integration",
slug: "event-with-unsupported-integration",
lengthInMinutes: 30,
locations: [
{
type: "integration",
integration: "unsupported-integration",
},
],
};
const response = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(createPayload);
expect(response.status).toBe(400);
expect(response.body.error.message).toBe(
`Validation failed for integration location: integration must be one of the following values: ${supportedIntegrations.join(
", "
)}`
);
});
it("should reject with 400 if patching event type with integration that user has not connected", async () => {
const createPayload = {
title: "Event for patch test",
slug: "event-patch-test-unconnected-integration",
lengthInMinutes: 30,
};
const createResponse = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(createPayload)
.expect(201);
const createdEventType = createResponse.body.data;
const patchPayload = {
locations: [
{
type: "integration",
integration: "jitsi",
},
],
};
const patchResponse = await request(app.getHttpServer())
.patch(`/api/v2/event-types/${createdEventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(patchPayload);
expect(patchResponse.status).toBe(400);
expect(patchResponse.body.error.message).toBe("jitsi not connected.");
// Cleanup
await eventTypesRepositoryFixture.delete(createdEventType.id);
});
describe("EventType Hidden Property", () => {
let createdEventTypeId: number;
@@ -155,6 +155,8 @@ export class EventTypesController_2024_06_14 {
summary: "Get all event types",
description: `Hidden event types are returned only if authentication is provided and it belongs to the event type owner.
Use the optional \`sortCreatedAt\` query parameter to order results by creation date (by ID). Accepts "asc" (oldest first) or "desc" (newest first). When not provided, no explicit ordering is applied.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -3,6 +3,7 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import type { SortOrderType } from "@calcom/platform-types";
import type { Prisma } from "@calcom/prisma/client";
@Injectable()
@@ -67,21 +68,23 @@ export class EventTypesRepository_2024_06_14 {
});
}
async getUserEventTypes(userId: number) {
async getUserEventTypes(userId: number, sortCreatedAt?: SortOrderType) {
return this.dbRead.prisma.eventType.findMany({
where: {
userId,
},
...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }),
include: { users: true, schedule: true, destinationCalendar: true },
});
}
async getUserEventTypesPublic(userId: number) {
async getUserEventTypesPublic(userId: number, sortCreatedAt?: SortOrderType) {
return this.dbRead.prisma.eventType.findMany({
where: {
userId,
hidden: false,
},
...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }),
include: { users: true, schedule: true, destinationCalendar: true },
});
}
@@ -22,7 +22,7 @@ import {
getEventTypesPublic,
EventTypesPublic,
} from "@calcom/platform-libraries/event-types";
import type { GetEventTypesQuery_2024_06_14 } from "@calcom/platform-types";
import type { GetEventTypesQuery_2024_06_14, SortOrderType } from "@calcom/platform-types";
import type { EventType } from "@calcom/prisma/client";
@Injectable()
@@ -160,15 +160,16 @@ export class EventTypesService_2024_06_14 {
orgSlug?: string;
orgId?: number;
authUser?: AuthOptionalUser;
sortCreatedAt?: SortOrderType;
}) {
const user = await this.usersRepository.findByUsername(params.username, params.orgSlug, params.orgId);
if (!user) {
return [];
}
if (params.authUser?.id !== user.id) {
return await this.getUserEventTypesPublic(user.id);
return await this.getUserEventTypesPublic(user.id, params.sortCreatedAt);
}
return await this.getUserEventTypes(user.id);
return await this.getUserEventTypes(user.id, params.sortCreatedAt);
}
async getUserToCreateEvent(user: UserWithProfile) {
@@ -211,16 +212,16 @@ export class EventTypesService_2024_06_14 {
};
}
async getUserEventTypes(userId: number) {
const eventTypes = await this.eventTypesRepository.getUserEventTypes(userId);
async getUserEventTypes(userId: number, sortCreatedAt?: SortOrderType) {
const eventTypes = await this.eventTypesRepository.getUserEventTypes(userId, sortCreatedAt);
return eventTypes.map((eventType) => {
return { ownerId: userId, ...eventType };
});
}
async getUserEventTypesPublic(userId: number) {
const eventTypes = await this.eventTypesRepository.getUserEventTypesPublic(userId);
async getUserEventTypesPublic(userId: number, sortCreatedAt?: SortOrderType) {
const eventTypes = await this.eventTypesRepository.getUserEventTypesPublic(userId, sortCreatedAt);
return eventTypes.map((eventType) => {
return { ownerId: userId, ...eventType };
@@ -237,7 +238,7 @@ export class EventTypesService_2024_06_14 {
}
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14, authUser?: AuthOptionalUser) {
const { username, eventSlug, usernames, orgSlug, orgId } = queryParams;
const { username, eventSlug, usernames, orgSlug, orgId, sortCreatedAt } = queryParams;
if (username && eventSlug) {
const eventType = await this.getEventTypeByUsernameAndSlug({
username,
@@ -255,6 +256,7 @@ export class EventTypesService_2024_06_14 {
orgSlug,
orgId,
authUser,
sortCreatedAt,
});
}
@@ -264,7 +266,7 @@ export class EventTypesService_2024_06_14 {
}
if (authUser?.id) {
return await this.getUserEventTypes(authUser.id);
return await this.getUserEventTypes(authUser.id, sortCreatedAt);
}
return [];
@@ -39,6 +39,7 @@ import {
InputBookingField_2024_06_14,
OutputUnknownLocation_2024_06_14,
UpdateEventTypeInput_2024_06_14,
supportedIntegrations,
} from "@calcom/platform-types";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
@@ -542,14 +543,40 @@ export class InputEventTypesService_2024_06_14 {
}
async checkAppIsValidAndConnected(user: UserWithProfile, appSlug: string) {
const conferencingApps = ["google-meet", "office365-video", "zoom"];
const conferencingApps = supportedIntegrations as readonly string[];
if (!conferencingApps.includes(appSlug)) {
throw new BadRequestException("Invalid app, available apps are: ", conferencingApps.join(", "));
}
if (appSlug === "office365-video") {
appSlug = "msteams";
}
// Map API integration names to actual app slugs
const slugMap: Record<string, string> = {
"office365-video": "msteams",
"facetime-video": "facetime",
"whereby-video": "whereby",
"whatsapp-video": "whatsapp",
"webex-video": "webex",
"telegram-video": "telegram",
"sylaps-video": "sylapsvideo",
"skype-video": "skype",
"sirius-video": "sirius_video",
"signal-video": "signal",
"shimmer-video": "shimmervideo",
"salesroom-video": "salesroom",
"roam-video": "roam",
"riverside-video": "riverside",
"ping-video": "ping",
"mirotalk-video": "mirotalk",
"jelly-video": "jelly",
"jelly-conferencing": "jelly",
"huddle": "huddle01",
"element-call-video": "element-call",
"eightxeight-video": "eightxeight",
"discord-video": "discord",
"demodesk-video": "demodesk",
"campfire-video": "campfire",
};
appSlug = slugMap[appSlug] || appSlug;
const credentials = await getUsersCredentialsIncludeServiceAccountKey(user);
@@ -47,8 +47,8 @@ import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
import { handleCreatePhoneCall } from "@calcom/platform-libraries";
import {
CreateTeamEventTypeInput_2024_06_14,
GetOrganizationEventTypesQuery_2024_06_14,
GetTeamEventTypesQuery_2024_06_14,
SkipTakePagination,
TeamEventTypeOutput_2024_06_14,
UpdateTeamEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
@@ -159,12 +159,16 @@ export class OrganizationsEventTypesController {
@UseGuards(IsOrgGuard, IsTeamInOrg, IsAdminAPIEnabledGuard)
@Get("/teams/:teamId/event-types")
@ApiOperation({ summary: "Get team event types" })
@ApiOperation({
summary: "Get team event types",
description:
'Use the optional `sortCreatedAt` query parameter to order results by creation date (by ID). Accepts "asc" (oldest first) or "desc" (newest first). When not provided, no explicit ordering is applied.',
})
async getTeamEventTypes(
@Param("teamId", ParseIntPipe) teamId: number,
@Query() queryParams: GetTeamEventTypesQuery_2024_06_14
): Promise<GetTeamEventTypesOutput> {
const { eventSlug, hostsLimit } = queryParams;
const { eventSlug, hostsLimit, sortCreatedAt } = queryParams;
if (eventSlug) {
const eventType = await this.organizationsEventTypesService.getTeamEventTypeBySlug(
@@ -179,7 +183,7 @@ export class OrganizationsEventTypesController {
};
}
const eventTypes = await this.organizationsEventTypesService.getTeamEventTypes(teamId);
const eventTypes = await this.organizationsEventTypesService.getTeamEventTypes(teamId, sortCreatedAt);
return {
status: SUCCESS_STATUS,
@@ -191,16 +195,21 @@ export class OrganizationsEventTypesController {
@PlatformPlan("ESSENTIALS")
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@Get("/teams/event-types")
@ApiOperation({ summary: "Get all team event types" })
@ApiOperation({
summary: "Get all team event types",
description:
'Use the optional `sortCreatedAt` query parameter to order results by creation date (by ID). Accepts "asc" (oldest first) or "desc" (newest first). When not provided, no explicit ordering is applied.',
})
async getTeamsEventTypes(
@Param("orgId", ParseIntPipe) orgId: number,
@Query() queryParams: SkipTakePagination
@Query() queryParams: GetOrganizationEventTypesQuery_2024_06_14
): Promise<GetTeamEventTypesOutput> {
const { skip, take } = queryParams;
const { skip, take, sortCreatedAt } = queryParams;
const eventTypes = await this.organizationsEventTypesService.getOrganizationsTeamsEventTypes(
orgId,
skip,
take
take,
sortCreatedAt
);
return {
@@ -1,16 +1,24 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { Injectable } from "@nestjs/common";
import type { SortOrderType } from "@calcom/platform-types";
@Injectable()
export class OrganizationsEventTypesRepository {
constructor(private readonly dbRead: PrismaReadService) {}
async getOrganizationTeamsEventTypes(orgId: number, skip: number, take: number) {
async getOrganizationTeamsEventTypes(
orgId: number,
skip: number,
take: number,
sortCreatedAt?: SortOrderType
) {
return this.dbRead.prisma.eventType.findMany({
where: {
team: {
parentId: orgId,
},
},
...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }),
skip,
take,
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
@@ -1,4 +1,6 @@
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import type { SortOrderType } from "@calcom/platform-types";
import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository";
import {
TransformedCreateTeamEventTypeInput,
@@ -83,16 +85,22 @@ export class OrganizationsEventTypesService {
return this.teamsEventTypesService.getTeamEventTypeBySlug(teamId, eventTypeSlug, hostsLimit);
}
async getTeamEventTypes(teamId: number): Promise<DatabaseTeamEventType[]> {
return await this.teamsEventTypesService.getTeamEventTypes(teamId);
async getTeamEventTypes(teamId: number, sortCreatedAt?: SortOrderType): Promise<DatabaseTeamEventType[]> {
return await this.teamsEventTypesService.getTeamEventTypes(teamId, sortCreatedAt);
}
async getOrganizationsTeamsEventTypes(
orgId: number,
skip = 0,
take = 250
take = 250,
sortCreatedAt?: SortOrderType
): Promise<DatabaseTeamEventType[]> {
return await this.organizationEventTypesRepository.getOrganizationTeamsEventTypes(orgId, skip, take);
return await this.organizationEventTypesRepository.getOrganizationTeamsEventTypes(
orgId,
skip,
take,
sortCreatedAt
);
}
async updateOrganizationTeamEventType(
@@ -136,12 +136,16 @@ export class TeamsEventTypesController {
}
@Get("/")
@ApiOperation({ summary: "Get a team event type" })
@ApiOperation({
summary: "Get team event types",
description:
'Use the optional `sortCreatedAt` query parameter to order results by creation date (by ID). Accepts "asc" (oldest first) or "desc" (newest first). When not provided, no explicit ordering is applied.',
})
async getTeamEventTypes(
@Param("teamId", ParseIntPipe) teamId: number,
@Query() queryParams: GetTeamEventTypesQuery_2024_06_14
): Promise<GetTeamEventTypesOutput> {
const { eventSlug, hostsLimit } = queryParams;
const { eventSlug, hostsLimit, sortCreatedAt } = queryParams;
if (eventSlug) {
const eventType = await this.teamsEventTypesService.getTeamEventTypeBySlug(
@@ -156,7 +160,7 @@ export class TeamsEventTypesController {
};
}
const eventTypes = await this.teamsEventTypesService.getTeamEventTypes(teamId);
const eventTypes = await this.teamsEventTypesService.getTeamEventTypes(teamId, sortCreatedAt);
return {
status: SUCCESS_STATUS,
@@ -11,6 +11,8 @@ import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, NotFoundException, Logger } from "@nestjs/common";
import type { SortOrderType } from "@calcom/platform-types";
import { createEventType, updateEventType } from "@calcom/platform-libraries/event-types";
@Injectable()
@@ -101,8 +103,8 @@ export class TeamsEventTypesService {
return eventType;
}
async getTeamEventTypes(teamId: number): Promise<DatabaseTeamEventType[]> {
return await this.teamsEventTypesRepository.getTeamEventTypes(teamId);
async getTeamEventTypes(teamId: number, sortCreatedAt?: SortOrderType): Promise<DatabaseTeamEventType[]> {
return await this.teamsEventTypesRepository.getTeamEventTypes(teamId, sortCreatedAt);
}
async updateTeamEventType(
@@ -2,6 +2,8 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import type { SortOrderType } from "@calcom/platform-types";
@Injectable()
export class TeamsEventTypesRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
@@ -79,11 +81,12 @@ export class TeamsEventTypesRepository {
});
}
async getTeamEventTypes(teamId: number) {
async getTeamEventTypes(teamId: number, sortCreatedAt?: SortOrderType) {
return this.dbRead.prisma.eventType.findMany({
where: {
teamId,
},
...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }),
include: {
users: true,
schedule: true,
@@ -115,7 +115,7 @@ export function UserPage(props: PageProps) {
eventType: type,
});
}}
className="bg-default border-subtle dark:bg-cal-muted dark:hover:bg-emphasis hover:bg-cal-muted group relative border-b transition first:rounded-t-md last:rounded-b-md last:border-b-0"
className="bg-default border-subtle dark:bg-cal-muted dark:hover:bg-subtle hover:bg-cal-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"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "5.9.9",
"version": "5.9.10",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+83
View File
@@ -0,0 +1,83 @@
import { createReadStream, existsSync } from "fs";
import { promises as fs } from "fs";
import { IncomingMessage, ServerResponse } from "http";
import { extname, join, normalize } from "path";
const distDir = join(process.cwd(), "dist");
const mimeTypes: Record<string, string> = {
".html": "text/html; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json; charset=utf-8",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".svg": "image/svg+xml",
".ico": "image/x-icon",
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".map": "application/json; charset=utf-8",
};
const getSafePath = (urlPath: string): string => {
const decodedPath = decodeURIComponent(urlPath.split("?")[0]);
const normalized = normalize(decodedPath).replace(/^(\.\.(\/|\\|$))+/, "");
return normalized.startsWith("/") ? normalized : `/${normalized}`;
};
const resolveFilePath = async (safePath: string): Promise<string> => {
let candidate = join(distDir, safePath);
try {
const stats = await fs.stat(candidate);
if (stats.isDirectory()) {
candidate = join(candidate, "index.html");
}
return candidate;
} catch {
const fallback = join(distDir, "index.html");
if (existsSync(fallback)) {
return fallback;
}
throw new Error("Dist folder missing. Run `npm run build` before deploying.");
}
};
const sendFile = (res: ServerResponse, filePath: string) => {
const ext = extname(filePath).toLowerCase();
const contentType = mimeTypes[ext] || "application/octet-stream";
res.setHeader("Content-Type", contentType);
res.setHeader(
"Cache-Control",
ext === ".html" ? "no-cache, no-store, must-revalidate" : "public, max-age=31536000, immutable"
);
const stream = createReadStream(filePath);
stream.on("error", (error) => {
console.error("Failed to read file:", error);
res.statusCode = 500;
res.end("Internal Server Error");
});
stream.pipe(res);
};
export default async function handler(req: IncomingMessage, res: ServerResponse) {
if (req.method && !["GET", "HEAD"].includes(req.method)) {
res.statusCode = 405;
res.setHeader("Allow", "GET, HEAD");
res.end("Method Not Allowed");
return;
}
try {
const urlPath = req.url ?? "/";
const safePath = getSafePath(urlPath);
const filePath = await resolveFilePath(safePath);
sendFile(res, filePath);
} catch (error) {
console.error("Server render error:", error);
res.statusCode = 500;
res.end("Internal Server Error");
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"routes": [
{
"handle": "filesystem"
},
{
"src": "/.*",
"dest": "/index.html"
}
]
}
+94 -6
View File
@@ -2819,6 +2819,7 @@
"organization.attributes.update",
"organization.attributes.delete",
"organization.attributes.create",
"organization.attributes.editUsers",
"routingForm.create",
"routingForm.read",
"routingForm.update",
@@ -4470,6 +4471,7 @@
"get": {
"operationId": "OrganizationsEventTypesController_getTeamEventTypes",
"summary": "Get team event types",
"description": "Use the optional `sortCreatedAt` query parameter to order results by creation date (by ID). Accepts \"asc\" (oldest first) or \"desc\" (newest first). When not provided, no explicit ordering is applied.",
"parameters": [
{
"name": "Authorization",
@@ -4523,6 +4525,16 @@
"schema": {
"type": "number"
}
},
{
"name": "sortCreatedAt",
"required": false,
"in": "query",
"description": "Sort event types by creation date. When not provided, no explicit ordering is applied.",
"schema": {
"enum": ["asc", "desc"],
"type": "string"
}
}
],
"responses": {
@@ -4816,6 +4828,7 @@
"get": {
"operationId": "OrganizationsEventTypesController_getTeamsEventTypes",
"summary": "Get all team event types",
"description": "Use the optional `sortCreatedAt` query parameter to order results by creation date (by ID). Accepts \"asc\" (oldest first) or \"desc\" (newest first). When not provided, no explicit ordering is applied.",
"parameters": [
{
"name": "Authorization",
@@ -4876,6 +4889,16 @@
"default": 0,
"type": "number"
}
},
{
"name": "sortCreatedAt",
"required": false,
"in": "query",
"description": "Sort event types by creation date. When not provided, no explicit ordering is applied.",
"schema": {
"enum": ["asc", "desc"],
"type": "string"
}
}
],
"responses": {
@@ -11552,7 +11575,7 @@
"get": {
"operationId": "EventTypesController_2024_06_14_getEventTypes",
"summary": "Get all event types",
"description": "Hidden event types are returned only if authentication is provided and it belongs to the event type owner.\n \n <Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>\n ",
"description": "Hidden event types are returned only if authentication is provided and it belongs to the event type owner.\n \n Use the optional `sortCreatedAt` query parameter to order results by creation date (by ID). Accepts \"asc\" (oldest first) or \"desc\" (newest first). When not provided, no explicit ordering is applied.\n \n <Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>\n ",
"parameters": [
{
"name": "cal-api-version",
@@ -11609,6 +11632,16 @@
"type": "number"
}
},
{
"name": "sortCreatedAt",
"required": false,
"in": "query",
"description": "Sort event types by creation date. When not provided, no explicit ordering is applied.",
"schema": {
"enum": ["asc", "desc"],
"type": "string"
}
},
{
"name": "Authorization",
"in": "header",
@@ -14420,6 +14453,7 @@
"get": {
"operationId": "TeamsEventTypesController_getTeamEventTypes",
"summary": "Get a team event type",
"description": "Use the optional `sortCreatedAt` query parameter to order results by creation date (by ID). Accepts \"asc\" (oldest first) or \"desc\" (newest first). When not provided, no explicit ordering is applied.",
"parameters": [
{
"name": "teamId",
@@ -14446,6 +14480,16 @@
"schema": {
"type": "number"
}
},
{
"name": "sortCreatedAt",
"required": false,
"in": "query",
"description": "Sort event types by creation date. When not provided, no explicit ordering is applied.",
"schema": {
"enum": ["asc", "desc"],
"type": "string"
}
}
],
"responses": {
@@ -16781,7 +16825,37 @@
"integration": {
"type": "string",
"example": "cal-video",
"enum": ["cal-video", "google-meet", "office365-video", "zoom"]
"enum": [
"cal-video",
"google-meet",
"zoom",
"whereby-video",
"whatsapp-video",
"webex-video",
"telegram-video",
"tandem",
"sylaps-video",
"skype-video",
"sirius-video",
"signal-video",
"shimmer-video",
"salesroom-video",
"roam-video",
"riverside-video",
"ping-video",
"office365-video",
"mirotalk-video",
"jitsi",
"jelly-video",
"jelly-conferencing",
"huddle",
"facetime-video",
"element-call-video",
"eightxeight-video",
"discord-video",
"demodesk-video",
"campfire-video"
]
}
},
"required": ["type", "integration"]
@@ -17989,7 +18063,7 @@
},
"locations": {
"type": "array",
"description": "Locations where the event will take place. If not provided, cal video link will be used as the location.",
"description": "Locations where the event will take place. If not provided, cal video link will be used as the location. Note: Setting a location to a conferencing app does not install the app - the app must already be installed. Via API, only Google Meet (google-meet), Microsoft Teams (office365-video), and Zoom (zoom) can be installed. Cal Video (cal-video) is installed by default. All other conferencing apps must be connected via the Cal.com web app and are not available for Platform plan customers. You can only set an event type location to an app that has already been installed or connected.",
"items": {
"oneOf": [
{
@@ -19920,7 +19994,7 @@
},
"locations": {
"type": "array",
"description": "Locations where the event will take place. If not provided, cal video link will be used as the location.",
"description": "Locations where the event will take place. If not provided, cal video link will be used as the location. Note: Setting a location to a conferencing app does not install the app - the app must already be installed. Via API, only Google Meet (google-meet), Microsoft Teams (office365-video), and Zoom (zoom) can be installed. Cal Video (cal-video) is installed by default. All other conferencing apps must be connected via the Cal.com web app and are not available for Platform plan customers. You can only set an event type location to an app that has already been installed or connected.",
"items": {
"oneOf": [
{
@@ -21561,7 +21635,7 @@
},
"locations": {
"type": "array",
"description": "Locations where the event will take place. If not provided, cal video link will be used as the location.",
"description": "Locations where the event will take place. If not provided, cal video link will be used as the location. Note: Setting a location to a conferencing app does not install the app - the app must already be installed. Via API, only Google Meet (google-meet), Microsoft Teams (office365-video), and Zoom (zoom) can be installed. Cal Video (cal-video) is installed by default. All other conferencing apps must be connected via the Cal.com web app and are not available for Platform plan customers. You can only set an event type location to an app that has already been installed or connected.",
"items": {
"oneOf": [
{
@@ -22024,7 +22098,7 @@
},
"locations": {
"type": "array",
"description": "Locations where the event will take place. If not provided, cal video link will be used as the location.",
"description": "Locations where the event will take place. If not provided, cal video link will be used as the location. Note: Setting a location to a conferencing app does not install the app - the app must already be installed. Via API, only Google Meet (google-meet), Microsoft Teams (office365-video), and Zoom (zoom) can be installed. Cal Video (cal-video) is installed by default. All other conferencing apps must be connected via the Cal.com web app and are not available for Platform plan customers. You can only set an event type location to an app that has already been installed or connected.",
"items": {
"oneOf": [
{
@@ -26294,6 +26368,7 @@
"organization.attributes.update",
"organization.attributes.delete",
"organization.attributes.create",
"organization.attributes.editUsers",
"routingForm.create",
"routingForm.read",
"routingForm.update",
@@ -26397,6 +26472,7 @@
"organization.attributes.update",
"organization.attributes.delete",
"organization.attributes.create",
"organization.attributes.editUsers",
"routingForm.create",
"routingForm.read",
"routingForm.update",
@@ -26528,6 +26604,7 @@
"organization.attributes.update",
"organization.attributes.delete",
"organization.attributes.create",
"organization.attributes.editUsers",
"routingForm.create",
"routingForm.read",
"routingForm.update",
@@ -26630,6 +26707,7 @@
"organization.attributes.update",
"organization.attributes.delete",
"organization.attributes.create",
"organization.attributes.editUsers",
"routingForm.create",
"routingForm.read",
"routingForm.update",
@@ -27467,6 +27545,14 @@
"type": "string",
"nullable": true
},
"avatarUrl": {
"type": "string",
"nullable": true
},
"bio": {
"type": "string",
"nullable": true
},
"timeFormat": {
"type": "number"
},
@@ -27493,6 +27579,8 @@
"username",
"email",
"name",
"avatarUrl",
"bio",
"timeFormat",
"defaultScheduleId",
"weekStart",
@@ -381,7 +381,7 @@ export const CreateOrEditOutOfOfficeEntryModal = ({
onChange={(e) => setSearchRedirectMember(e.target.value)}
value={searchRedirectMember}
/>
<div className="scroll-bar bg-default mt-2 flex h-[150px] flex-col gap-0.5 overflow-y-scroll rounded-[10px] border p-1">
<div className="scroll-bar bg-default mt-2 flex h-[150px] flex-col gap-0.5 overflow-y-scroll rounded-[10px] border p-2 pl-5">
{redirectToMemberListOptions
.filter((member) => member.value !== getValues("forUserId"))
.map((member) => (
@@ -518,7 +518,7 @@ export class CreateEventTypeInput_2024_06_14 extends BaseCreateEventTypeInput {
@ValidateLocations_2024_06_14()
@DocsPropertyOptional({
description:
"Locations where the event will take place. If not provided, cal video link will be used as the location.",
"Locations where the event will take place. If not provided, cal video link will be used as the location. Note: Setting a location to a conferencing app does not install the app - the app must already be installed. Via API, only Google Meet (google-meet), Microsoft Teams (office365-video), and Zoom (zoom) can be installed. Cal Video (cal-video) is installed by default. All other conferencing apps must be connected via the Cal.com web app and are not available for Platform plan customers. You can only set an event type location to an app that has already been installed or connected.",
oneOf: [
{ $ref: getSchemaPath(InputAddressLocation_2024_06_14) },
{ $ref: getSchemaPath(InputLinkLocation_2024_06_14) },
@@ -607,7 +607,7 @@ export class CreateTeamEventTypeInput_2024_06_14 extends BaseCreateEventTypeInpu
@ValidateTeamLocations_2024_06_14()
@DocsPropertyOptional({
description:
"Locations where the event will take place. If not provided, cal video link will be used as the location.",
"Locations where the event will take place. If not provided, cal video link will be used as the location. Note: Setting a location to a conferencing app does not install the app - the app must already be installed. Via API, only Google Meet (google-meet), Microsoft Teams (office365-video), and Zoom (zoom) can be installed. Cal Video (cal-video) is installed by default. All other conferencing apps must be connected via the Cal.com web app and are not available for Platform plan customers. You can only set an event type location to an app that has already been installed or connected.",
oneOf: [
{ $ref: getSchemaPath(InputAddressLocation_2024_06_14) },
{ $ref: getSchemaPath(InputLinkLocation_2024_06_14) },
@@ -1,6 +1,8 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsNumber, IsOptional, IsString } from "class-validator";
import { IsEnum, IsNumber, IsOptional, IsString } from "class-validator";
import { SkipTakePagination, SortOrder, SortOrderType } from "../../../pagination/pagination.input";
export class GetEventTypesQuery_2024_06_14 {
@IsOptional()
@@ -46,6 +48,14 @@ export class GetEventTypesQuery_2024_06_14 {
type: Number,
})
orgId?: number;
@ApiPropertyOptional({
enum: SortOrder,
description: "Sort event types by creation date. When not provided, no explicit ordering is applied.",
})
@IsOptional()
@IsEnum(SortOrder)
sortCreatedAt?: SortOrderType;
}
export class GetTeamEventTypesQuery_2024_06_14 {
@@ -64,6 +74,24 @@ export class GetTeamEventTypesQuery_2024_06_14 {
"Specifies the maximum number of hosts to include in the response. This limit helps optimize performance. If not provided, all Hosts will be fetched.",
})
hostsLimit?: number;
@ApiPropertyOptional({
enum: SortOrder,
description: "Sort event types by creation date. When not provided, no explicit ordering is applied.",
})
@IsOptional()
@IsEnum(SortOrder)
sortCreatedAt?: SortOrderType;
}
export class GetOrganizationEventTypesQuery_2024_06_14 extends SkipTakePagination {
@ApiPropertyOptional({
enum: SortOrder,
description: "Sort event types by creation date. When not provided, no explicit ordering is applied.",
})
@IsOptional()
@IsEnum(SortOrder)
sortCreatedAt?: SortOrderType;
}
function TransformUsernames() {
@@ -54,7 +54,37 @@ export class InputLinkLocation_2024_06_14 {
public!: boolean;
}
export const supportedIntegrations = ["cal-video", "google-meet", "office365-video", "zoom"] as const;
export const supportedIntegrations = [
"cal-video",
"google-meet",
"zoom",
"whereby-video",
"whatsapp-video",
"webex-video",
"telegram-video",
"tandem",
"sylaps-video",
"skype-video",
"sirius-video",
"signal-video",
"shimmer-video",
"salesroom-video",
"roam-video",
"riverside-video",
"ping-video",
"office365-video",
"mirotalk-video",
"jitsi",
"jelly-video",
"jelly-conferencing",
"huddle",
"facetime-video",
"element-call-video",
"eightxeight-video",
"discord-video",
"demodesk-video",
"campfire-video",
] as const;
export type Integration_2024_06_14 = (typeof supportedIntegrations)[number];
export class InputIntegrationLocation_2024_06_14 {
@@ -462,7 +462,7 @@ export class UpdateEventTypeInput_2024_06_14 extends BaseUpdateEventTypeInput {
@ValidateLocations_2024_06_14()
@DocsPropertyOptional({
description:
"Locations where the event will take place. If not provided, cal video link will be used as the location.",
"Locations where the event will take place. If not provided, cal video link will be used as the location. Note: Setting a location to a conferencing app does not install the app - the app must already be installed. Via API, only Google Meet (google-meet), Microsoft Teams (office365-video), and Zoom (zoom) can be installed. Cal Video (cal-video) is installed by default. All other conferencing apps must be connected via the Cal.com web app and are not available for Platform plan customers. You can only set an event type location to an app that has already been installed or connected.",
oneOf: [
{ $ref: getSchemaPath(InputAddressLocation_2024_06_14) },
{ $ref: getSchemaPath(InputLinkLocation_2024_06_14) },
@@ -521,7 +521,7 @@ export class UpdateTeamEventTypeInput_2024_06_14 extends BaseUpdateEventTypeInpu
@ValidateTeamLocations_2024_06_14()
@DocsPropertyOptional({
description:
"Locations where the event will take place. If not provided, cal video link will be used as the location.",
"Locations where the event will take place. If not provided, cal video link will be used as the location. Note: Setting a location to a conferencing app does not install the app - the app must already be installed. Via API, only Google Meet (google-meet), Microsoft Teams (office365-video), and Zoom (zoom) can be installed. Cal Video (cal-video) is installed by default. All other conferencing apps must be connected via the Cal.com web app and are not available for Platform plan customers. You can only set an event type location to an app that has already been installed or connected.",
oneOf: [
{ $ref: getSchemaPath(InputAddressLocation_2024_06_14) },
{ $ref: getSchemaPath(InputLinkLocation_2024_06_14) },
@@ -2,6 +2,13 @@ import { ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsNumber, Min, Max, IsOptional } from "class-validator";
export enum SortOrder {
ASC = "asc",
DESC = "desc",
}
export type SortOrderType = `${SortOrder}`;
export class Pagination {
@ApiPropertyOptional({ description: "The number of items to return", example: 10 })
@Transform(({ value }: { value: string }) => value && parseInt(value))
+1 -5
View File
@@ -22,10 +22,6 @@ export function InputLeading(props: JSX.IntrinsicElements["div"]) {
);
}
type PasswordFieldTranslations = {
showPasswordText?: string;
hidePasswordText?: string;
};
export const PasswordField = forwardRef<HTMLInputElement, InputFieldProps>(function PasswordField(
props,
@@ -194,7 +190,7 @@ export const FilterSearchField = forwardRef<HTMLInputElement, InputFieldProps>(f
ref={ref}
addOnLeading={<Icon name="search" className="h-4 w-4 stroke-[2.5px]" data-testid="search-icon" />}
placeholder="Search"
containerClassName="mx-3 mt-2"
containerClassName="mt-1"
{...props}
/>
);