diff --git a/apps/api/v2/src/ee/event-types/controllers/event-types.controller.e2e-spec.ts b/apps/api/v2/src/ee/event-types/controllers/event-types.controller.e2e-spec.ts index 34feeb2955..4c7df52fa4 100644 --- a/apps/api/v2/src/ee/event-types/controllers/event-types.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/event-types/controllers/event-types.controller.e2e-spec.ts @@ -18,6 +18,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth"; import { SUCCESS_STATUS } from "@calcom/platform-constants"; +import { EventTypesByViewer, EventTypesPublic } from "@calcom/platform-libraries"; import { ApiSuccessResponse } from "@calcom/platform-types"; describe("Event types Endpoints", () => { @@ -60,6 +61,8 @@ describe("Event types Endpoints", () => { let eventTypesRepositoryFixture: EventTypesRepositoryFixture; const userEmail = "event-types-test-e2e@api.com"; + const name = "bob"; + const username = name; let eventType: EventType; let user: User; @@ -88,6 +91,8 @@ describe("Event types Endpoints", () => { oAuthClient = await createOAuthClient(organization.id); user = await userRepositoryFixture.create({ email: userEmail, + name, + username, }); eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); @@ -141,6 +146,44 @@ describe("Event types Endpoints", () => { expect(responseBody.data.userId).toEqual(user.id); }); + it(`/GET/`, async () => { + const response = await request(app.getHttpServer()) + .get(`/api/v2/event-types`) + // note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above + .set("Authorization", `Bearer whatever`) + .expect(200); + + const responseBody: ApiSuccessResponse = response.body; + + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + console.log("asap responseBody.data", responseBody.data); + expect(responseBody.data.eventTypeGroups).toBeDefined(); + expect(responseBody.data.eventTypeGroups).toBeDefined(); + expect(responseBody.data.eventTypeGroups[0]).toBeDefined(); + expect(responseBody.data.eventTypeGroups[0].profile).toBeDefined(); + expect(responseBody.data.eventTypeGroups?.[0]?.profile?.name).toEqual(name); + expect(responseBody.data.eventTypeGroups?.[0]?.eventTypes?.[0]?.id).toEqual(eventType.id); + expect(responseBody.data.profiles?.[0]?.name).toEqual(name); + }); + + it(`/GET/:username/public`, async () => { + const response = await request(app.getHttpServer()) + .get(`/api/v2/event-types/${username}/public`) + // note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above + .set("Authorization", `Bearer whatever`) + .expect(200); + + const responseBody: ApiSuccessResponse = response.body; + + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + console.log("asap responseBody.data", responseBody.data); + expect(responseBody.data).toBeDefined(); + expect(responseBody.data.length).toEqual(1); + expect(responseBody.data[0].id).toEqual(eventType.id); + }); + it(`/GET/:id not existing`, async () => { await request(app.getHttpServer()) .get(`/api/v2/event-types/1000`) diff --git a/apps/api/v2/src/ee/event-types/controllers/event-types.controller.ts b/apps/api/v2/src/ee/event-types/controllers/event-types.controller.ts index 3ecd826a79..9c6df0ff19 100644 --- a/apps/api/v2/src/ee/event-types/controllers/event-types.controller.ts +++ b/apps/api/v2/src/ee/event-types/controllers/event-types.controller.ts @@ -9,20 +9,23 @@ import { UserWithProfile } from "@/modules/users/users.repository"; import { Controller, UseGuards, Get, Param, Post, Body, NotFoundException } from "@nestjs/common"; import { EventType } from "@prisma/client"; +import { EventTypesByViewer } from "@calcom/lib"; import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { EventType as AtomEventType } from "@calcom/platform-libraries"; +import type { EventType as AtomEventType, EventTypesPublic } from "@calcom/platform-libraries"; +import { getEventTypesByViewer } from "@calcom/platform-libraries"; import { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types"; @Controller({ path: "event-types", version: "2", }) -@UseGuards(AccessTokenGuard, PermissionsGuard) +@UseGuards(PermissionsGuard) export class EventTypesController { constructor(private readonly eventTypesService: EventTypesService) {} @Post("/") @Permissions([EVENT_TYPE_WRITE]) + @UseGuards(AccessTokenGuard) async createEventType( @Body() body: CreateEventTypeInput, @GetUser() user: UserWithProfile @@ -37,6 +40,7 @@ export class EventTypesController { @Get("/:eventTypeId") @Permissions([EVENT_TYPE_READ]) + @UseGuards(AccessTokenGuard) async getEventType( @Param("eventTypeId") eventTypeId: string, @ForAtom() forAtom: boolean, @@ -55,4 +59,34 @@ export class EventTypesController { data: eventType, }; } + + @Get("/") + @Permissions([EVENT_TYPE_READ]) + @UseGuards(AccessTokenGuard) + async getEventTypes(@GetUser() user: UserWithProfile): Promise> { + const eventTypes = await getEventTypesByViewer({ + id: user.id, + profile: { + upId: `usr-${user.id}`, + }, + }); + + return { + status: SUCCESS_STATUS, + data: eventTypes, + }; + } + + @Get("/:username/public") + @Permissions([EVENT_TYPE_READ]) + async getPublicEventTypes( + @Param("username") username: string + ): Promise> { + const eventTypes = await this.eventTypesService.getEventTypesPublicByUsername(username); + + return { + status: SUCCESS_STATUS, + data: eventTypes, + }; + } } diff --git a/apps/api/v2/src/ee/event-types/event-types.module.ts b/apps/api/v2/src/ee/event-types/event-types.module.ts index fa48ca0bfd..1000f637e9 100644 --- a/apps/api/v2/src/ee/event-types/event-types.module.ts +++ b/apps/api/v2/src/ee/event-types/event-types.module.ts @@ -4,10 +4,11 @@ import { EventTypesService } from "@/ee/event-types/services/event-types.service import { MembershipsModule } from "@/modules/memberships/memberships.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { TokensModule } from "@/modules/tokens/tokens.module"; +import { UsersModule } from "@/modules/users/users.module"; import { Module } from "@nestjs/common"; @Module({ - imports: [PrismaModule, MembershipsModule, TokensModule], + imports: [PrismaModule, MembershipsModule, TokensModule, UsersModule], providers: [EventTypesRepository, EventTypesService], controllers: [EventTypesController], exports: [EventTypesService, EventTypesRepository], diff --git a/apps/api/v2/src/ee/event-types/services/event-types.service.ts b/apps/api/v2/src/ee/event-types/services/event-types.service.ts index aefe23944d..08c9da3c15 100644 --- a/apps/api/v2/src/ee/event-types/services/event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/services/event-types.service.ts @@ -2,14 +2,17 @@ import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/constants/constants"; import { EventTypesRepository } from "@/ee/event-types/event-types.repository"; import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { Injectable } from "@nestjs/common"; +import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository"; +import { Injectable, NotFoundException } from "@nestjs/common"; + +import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries"; @Injectable() export class EventTypesService { constructor( private readonly eventTypesRepository: EventTypesRepository, - private readonly membershipsRepository: MembershipsRepository + private readonly membershipsRepository: MembershipsRepository, + private readonly usersRepository: UsersRepository ) {} async createUserEventType(userId: number, body: CreateEventTypeInput) { @@ -30,6 +33,15 @@ export class EventTypesService { return this.eventTypesRepository.getUserEventTypeForAtom(user, isUserOrganizationAdmin, eventTypeId); } + async getEventTypesPublicByUsername(username: string): Promise { + const user = await this.usersRepository.findByUsername(username); + if (!user) { + throw new NotFoundException(`User with username "${username}" not found`); + } + + return await getEventTypesPublic(user.id); + } + async createUserDefaultEventTypes(userId: number) { const thirtyMinutes = DEFAULT_EVENT_TYPES.thirtyMinutes; const thirtyMinutesVideo = DEFAULT_EVENT_TYPES.thirtyMinutesVideo; diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto.ts index f019f43fcf..e95fd60155 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto.ts @@ -44,7 +44,7 @@ export class PlatformOAuthClientDto { @IsNumber() organizationId!: number; - @ApiProperty({ example: new Date(), type: Date }) + @ApiProperty({ example: "2024-03-23T08:33:21.851Z", type: Date }) @IsDate() createdAt!: Date; } diff --git a/apps/api/v2/src/modules/users/users.repository.ts b/apps/api/v2/src/modules/users/users.repository.ts index 7ee095fbc6..0666e7ef20 100644 --- a/apps/api/v2/src/modules/users/users.repository.ts +++ b/apps/api/v2/src/modules/users/users.repository.ts @@ -83,6 +83,14 @@ export class UsersRepository { }); } + async findByUsername(username: string) { + return this.dbRead.prisma.user.findFirst({ + where: { + username, + }, + }); + } + async update(userId: number, updateData: UpdateManagedPlatformUserInput) { this.formatInput(updateData); diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 47258d5d5c..bf15ab2caf 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -506,6 +506,22 @@ } } } + }, + "get": { + "operationId": "EventTypesController_getEventTypes", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } } }, "/api/v2/event-types/{eventTypeId}": { @@ -535,6 +551,33 @@ } } }, + "/api/v2/event-types/{username}/public": { + "get": { + "operationId": "EventTypesController_getPublicEventTypes", + "parameters": [ + { + "name": "username", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + } + }, "/api/v2/platform/gcal/oauth/auth-url": { "get": { "operationId": "GcalController_redirect", @@ -1206,7 +1249,7 @@ "createdAt": { "format": "date-time", "type": "string", - "example": "2024-03-21T16:58:29.130Z" + "example": "2024-03-23T08:33:21.851Z" } }, "required": [ diff --git a/apps/web/modules/users/views/users-public-view.getServerSideProps.tsx b/apps/web/modules/users/views/users-public-view.getServerSideProps.tsx index 1ee4fc242b..9a628ac051 100644 --- a/apps/web/modules/users/views/users-public-view.getServerSideProps.tsx +++ b/apps/web/modules/users/views/users-public-view.getServerSideProps.tsx @@ -7,16 +7,15 @@ import { handleUserRedirection } from "@calcom/features/booking-redirect/handle- import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR } from "@calcom/lib/constants"; import { getUsernameList } from "@calcom/lib/defaultEvents"; +import { getEventTypesPublic } from "@calcom/lib/event-types/getEventTypesPublic"; import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import logger from "@calcom/lib/logger"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import { safeStringify } from "@calcom/lib/safeStringify"; import { UserRepository } from "@calcom/lib/server/repository/user"; import { stripMarkdown } from "@calcom/lib/stripMarkdown"; -import prisma from "@calcom/prisma"; import { RedirectType, type EventType, type User } from "@calcom/prisma/client"; -import { baseEventTypeSelect } from "@calcom/prisma/selects"; -import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; +import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { UserProfile } from "@calcom/types/UserProfile"; import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect"; @@ -71,58 +70,6 @@ export type UserPageProps = { >)[]; } & EmbedProps; -export const getEventTypesWithHiddenFromDB = async (userId: number) => { - const eventTypes = await prisma.eventType.findMany({ - where: { - AND: [ - { - teamId: null, - }, - { - OR: [ - { - userId, - }, - { - users: { - some: { - id: userId, - }, - }, - }, - ], - }, - ], - }, - orderBy: [ - { - position: "desc", - }, - { - id: "asc", - }, - ], - select: { - ...baseEventTypeSelect, - metadata: true, - }, - }); - // map and filter metadata, exclude eventType entirely when faulty metadata is found. - // report error to exception so we don't lose the error. - return eventTypes.reduce((eventTypes, eventType) => { - const parsedMetadata = EventTypeMetaDataSchema.safeParse(eventType.metadata); - if (!parsedMetadata.success) { - log.error(parsedMetadata.error); - return eventTypes; - } - eventTypes.push({ - ...eventType, - metadata: parsedMetadata.data, - }); - return eventTypes; - }, []); -}; - export const getServerSideProps: GetServerSideProps = async (context) => { const ssr = await ssrInit(context); const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug); @@ -220,18 +167,12 @@ export const getServerSideProps: GetServerSideProps = async (cont organization: user.profile.organization, }; - const eventTypesWithHidden = await getEventTypesWithHiddenFromDB(user.id); const dataFetchEnd = Date.now(); if (context.query.log === "1") { context.res.setHeader("X-Data-Fetch-Time", `${dataFetchEnd - dataFetchStart}ms`); } - const eventTypesRaw = eventTypesWithHidden.filter((evt) => !evt.hidden); - const eventTypes = eventTypesRaw.map((eventType) => ({ - ...eventType, - metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}), - descriptionAsSafeHTML: markdownToSafeHTML(eventType.description), - })); + const eventTypes = await getEventTypesPublic(user.id); // if profile only has one public event-type, redirect to it if (eventTypes.length === 1 && context.query.redirect !== "false" && !outOfOffice) { diff --git a/packages/app-store/test-setup.ts b/packages/app-store/test-setup.ts index 98ae156feb..2070561ba8 100644 --- a/packages/app-store/test-setup.ts +++ b/packages/app-store/test-setup.ts @@ -27,6 +27,9 @@ vi.mock("@calcom/atoms/monorepo", () => ({ }, })); +vi.mock("@calcom/lib/event-types/getEventTypesByViewer", () => ({})); +vi.mock("@calcom/lib/event-types/getEventTypesPublic", () => ({})); + global.ResizeObserver = vi.fn().mockImplementation(() => ({ observe: vi.fn(), unobserve: vi.fn(), diff --git a/packages/lib/getEventTypeById.ts b/packages/lib/event-types/getEventTypeById.ts similarity index 99% rename from packages/lib/getEventTypeById.ts rename to packages/lib/event-types/getEventTypeById.ts index bfe71c4009..7326201161 100644 --- a/packages/lib/getEventTypeById.ts +++ b/packages/lib/event-types/getEventTypeById.ts @@ -14,8 +14,8 @@ import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-u import { TRPCError } from "@trpc/server"; -import { WEBSITE_URL } from "./constants"; -import { getBookerBaseUrl } from "./getBookerUrl/server"; +import { WEBSITE_URL } from "../constants"; +import { getBookerBaseUrl } from "../getBookerUrl/server"; interface getEventTypeByIdProps { eventTypeId: number; diff --git a/packages/lib/event-types/getEventTypesByViewer.ts b/packages/lib/event-types/getEventTypesByViewer.ts new file mode 100644 index 0000000000..9d41edace5 --- /dev/null +++ b/packages/lib/event-types/getEventTypesByViewer.ts @@ -0,0 +1,348 @@ +// eslint-disable-next-line no-restricted-imports +import { orderBy } from "lodash"; + +import { hasFilter } from "@calcom/features/filters/lib/hasFilter"; +import { getOrgAvatarUrl, getTeamAvatarUrl, getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; +import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client"; +import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; +import logger from "@calcom/lib/logger"; +import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; +import { safeStringify } from "@calcom/lib/safeStringify"; +import { EventTypeRepository } from "@calcom/lib/server/repository/eventType"; +import { MembershipRepository } from "@calcom/lib/server/repository/membership"; +import { ProfileRepository } from "@calcom/lib/server/repository/profile"; +import { UserRepository } from "@calcom/lib/server/repository/user"; +import { MembershipRole, SchedulingType } from "@calcom/prisma/enums"; +import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; +import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; + +import { TRPCError } from "@trpc/server"; + +const log = logger.getSubLogger({ prefix: ["viewer.eventTypes.getByViewer"] }); + +type User = { + id: number; + profile: { + upId: string; + }; +}; + +type Filters = { + teamIds?: number[]; + upIds?: string[]; +}; + +export type EventTypesByViewer = Awaited>; + +export const getEventTypesByViewer = async (user: User, filters?: Filters, forRoutingForms?: boolean) => { + const userProfile = user.profile; + const profile = await ProfileRepository.findByUpId(userProfile.upId); + const parentOrgHasLockedEventTypes = + profile?.organization?.organizationSettings?.lockEventTypeCreationForUsers; + const isFilterSet = filters && hasFilter(filters); + const isUpIdInFilter = filters?.upIds?.includes(userProfile.upId); + + let shouldListUserEvents = !isFilterSet || isUpIdInFilter; + // FIX: Handles the case when an upId != lightProfile - pretend like there is no filter. + // Results in {"eventTypeGroups":[],"profiles":[]} - this crashes all dependencies. + if (isFilterSet && filters?.upIds && !isUpIdInFilter) { + shouldListUserEvents = true; + } + const [profileMemberships, profileEventTypes] = await Promise.all([ + MembershipRepository.findAllByUpIdIncludeTeamWithMembersAndEventTypes( + { + upId: userProfile.upId, + }, + { + where: { + accepted: true, + }, + } + ), + shouldListUserEvents + ? EventTypeRepository.findAllByUpId( + { + upId: userProfile.upId, + userId: user.id, + }, + { + where: { + teamId: null, + }, + orderBy: [ + { + position: "desc", + }, + { + id: "asc", + }, + ], + } + ) + : [], + ]); + + if (!profile) { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); + } + + const memberships = profileMemberships.map((membership) => ({ + ...membership, + team: { + ...membership.team, + metadata: teamMetadataSchema.parse(membership.team.metadata), + }, + })); + + log.debug( + safeStringify({ + profileMemberships, + profileEventTypes, + }) + ); + + type UserEventTypes = (typeof profileEventTypes)[number]; + + const mapEventType = async (eventType: UserEventTypes) => ({ + ...eventType, + safeDescription: eventType?.description ? markdownToSafeHTML(eventType.description) : undefined, + users: await Promise.all( + (!!eventType?.hosts?.length ? eventType?.hosts.map((host) => host.user) : eventType.users).map( + async (u) => + await UserRepository.enrichUserWithItsProfile({ + user: u, + }) + ) + ), + metadata: eventType.metadata ? EventTypeMetaDataSchema.parse(eventType.metadata) : null, + children: await Promise.all( + (eventType.children || []).map(async (c) => ({ + ...c, + users: await Promise.all( + c.users.map( + async (u) => + await UserRepository.enrichUserWithItsProfile({ + user: u, + }) + ) + ), + })) + ), + }); + + const userEventTypes = (await Promise.all(profileEventTypes.map(mapEventType))).filter((eventType) => { + const isAChildEvent = eventType.parentId; + // A child event only has one user + const childEventAssignee = eventType.users[0]; + if (isAChildEvent && childEventAssignee.id != user.id) { + return false; + } + return true; + }); + + type EventTypeGroup = { + teamId?: number | null; + parentId?: number | null; + bookerUrl: string; + membershipRole?: MembershipRole | null; + profile: { + slug: (typeof profile)["username"] | null; + name: (typeof profile)["name"]; + image: string; + eventTypesLockedByOrg?: boolean; + }; + metadata: { + membershipCount: number; + readOnly: boolean; + }; + eventTypes: typeof userEventTypes; + }; + + let eventTypeGroups: EventTypeGroup[] = []; + + const unmanagedEventTypes = userEventTypes.filter( + (evType) => evType.schedulingType !== SchedulingType.MANAGED + ); + + log.debug(safeStringify({ profileMemberships, profileEventTypes, profile })); + + log.debug( + "Filter Settings", + safeStringify({ + isFilterSet, + isProfileIdInFilter: isUpIdInFilter, + }) + ); + + if (shouldListUserEvents) { + const bookerUrl = await getBookerBaseUrl(profile.organizationId ?? null); + eventTypeGroups.push({ + teamId: null, + bookerUrl, + membershipRole: null, + profile: { + slug: profile.username, + name: profile.name, + image: getUserAvatarUrl({ + username: profile.username, + avatarUrl: profile.avatarUrl, + profile: profile, + }), + eventTypesLockedByOrg: parentOrgHasLockedEventTypes, + }, + eventTypes: orderBy(unmanagedEventTypes, ["position", "id"], ["desc", "asc"]), + metadata: { + membershipCount: 1, + readOnly: false, + }, + }); + } + + const teamMemberships = profileMemberships.map((membership) => ({ + teamId: membership.team.id, + membershipRole: membership.role, + })); + + const filterTeamsEventTypesBasedOnInput = async (eventType: Awaited>) => { + if (!filters || !hasFilter(filters)) { + return true; + } + return filters?.teamIds?.includes(eventType?.teamId || 0) ?? false; + }; + eventTypeGroups = ([] as EventTypeGroup[]).concat( + eventTypeGroups, + await Promise.all( + memberships + .filter((mmship) => { + if (mmship.team.isOrganization) { + return false; + } else { + if (!filters || !hasFilter(filters)) { + return true; + } + return filters?.teamIds?.includes(mmship?.team?.id || 0) ?? false; + } + }) + .map(async (membership) => { + const orgMembership = teamMemberships.find( + (teamM) => teamM.teamId === membership.team.parentId + )?.membershipRole; + + const team = { + ...membership.team, + metadata: teamMetadataSchema.parse(membership.team.metadata), + }; + + let slug; + + if (forRoutingForms) { + // For Routing form we want to ensure that after migration of team to an org, the URL remains same for the team + // Once we solve this https://github.com/calcom/cal.com/issues/12399, we can remove this conditional change in slug + slug = `team/${team.slug}`; + } else { + // In an Org, a team can be accessed without /team prefix as well as with /team prefix + slug = team.slug ? (!team.parentId ? `team/${team.slug}` : `${team.slug}`) : null; + } + + const eventTypes = await Promise.all(team.eventTypes.map(mapEventType)); + return { + teamId: team.id, + parentId: team.parentId, + bookerUrl: getBookerBaseUrlSync(team.parent?.slug ?? null), + membershipRole: + orgMembership && compareMembership(orgMembership, membership.role) + ? orgMembership + : membership.role, + profile: { + image: team.parentId + ? getOrgAvatarUrl({ + slug: team.parent?.slug || null, + logoUrl: team.parent?.logoUrl, + requestedSlug: team.slug, + }) + : getTeamAvatarUrl({ + slug: team.slug, + logoUrl: team.logoUrl, + requestedSlug: team.metadata?.requestedSlug ?? null, + organizationId: team.parentId, + }), + name: team.name, + slug, + }, + metadata: { + membershipCount: team.members.length, + readOnly: + membership.role === + (team.parentId + ? orgMembership && compareMembership(orgMembership, membership.role) + ? orgMembership + : MembershipRole.MEMBER + : MembershipRole.MEMBER), + }, + eventTypes: eventTypes + .filter(filterTeamsEventTypesBasedOnInput) + .filter((evType) => { + const res = evType.userId === null || evType.userId === user.id; + return res; + }) + .filter((evType) => + membership.role === MembershipRole.MEMBER + ? evType.schedulingType !== SchedulingType.MANAGED + : true + ), + }; + }) + ) + ); + + const denormalizedPayload = { + eventTypeGroups, + // so we can show a dropdown when the user has teams + profiles: eventTypeGroups.map((group) => ({ + ...group.profile, + ...group.metadata, + teamId: group.teamId, + membershipRole: group.membershipRole, + })), + }; + + return normalizePayload(denormalizedPayload); + + /** + * Reduces the size of payload + */ + function normalizePayload(payload: typeof denormalizedPayload) { + const allUsersAcrossAllEventTypes = new Map< + number, + EventTypeGroup["eventTypes"][number]["users"][number] + >(); + const eventTypeGroups = payload.eventTypeGroups.map((group) => { + return { + ...group, + eventTypes: group.eventTypes.map((eventType) => { + const { users, ...rest } = eventType; + return { + ...rest, + // Send userIds per event and keep the actual users object outside + userIds: users.map((user) => { + allUsersAcrossAllEventTypes.set(user.id, user); + return user.id; + }), + }; + }), + }; + }); + + return { + ...payload, + allUsersAcrossAllEventTypes, + eventTypeGroups, + }; + } +}; + +export function compareMembership(mship1: MembershipRole, mship2: MembershipRole) { + const mshipToNumber = (mship: MembershipRole) => + Object.keys(MembershipRole).findIndex((mmship) => mmship === mship); + return mshipToNumber(mship1) > mshipToNumber(mship2); +} diff --git a/packages/lib/event-types/getEventTypesPublic.ts b/packages/lib/event-types/getEventTypesPublic.ts new file mode 100644 index 0000000000..628de9e6b9 --- /dev/null +++ b/packages/lib/event-types/getEventTypesPublic.ts @@ -0,0 +1,74 @@ +import logger from "@calcom/lib/logger"; +import prisma from "@calcom/prisma"; +import { baseEventTypeSelect } from "@calcom/prisma/selects"; +import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; + +import { markdownToSafeHTML } from "../markdownToSafeHTML"; + +const log = logger.getSubLogger({ prefix: ["getEventTypesPublic"] }); + +export type EventTypesPublic = Awaited>; + +export async function getEventTypesPublic(userId: number) { + const eventTypesWithHidden = await getEventTypesWithHiddenFromDB(userId); + + const eventTypesRaw = eventTypesWithHidden.filter((evt) => !evt.hidden); + + return eventTypesRaw.map((eventType) => ({ + ...eventType, + metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}), + descriptionAsSafeHTML: markdownToSafeHTML(eventType.description), + })); +} + +const getEventTypesWithHiddenFromDB = async (userId: number) => { + const eventTypes = await prisma.eventType.findMany({ + where: { + AND: [ + { + teamId: null, + }, + { + OR: [ + { + userId, + }, + { + users: { + some: { + id: userId, + }, + }, + }, + ], + }, + ], + }, + orderBy: [ + { + position: "desc", + }, + { + id: "asc", + }, + ], + select: { + ...baseEventTypeSelect, + metadata: true, + }, + }); + // map and filter metadata, exclude eventType entirely when faulty metadata is found. + // report error to exception so we don't lose the error. + return eventTypes.reduce((eventTypes, eventType) => { + const parsedMetadata = EventTypeMetaDataSchema.safeParse(eventType.metadata); + if (!parsedMetadata.success) { + log.error(parsedMetadata.error); + return eventTypes; + } + eventTypes.push({ + ...eventType, + metadata: parsedMetadata.data, + }); + return eventTypes; + }, []); +}; diff --git a/packages/lib/index.ts b/packages/lib/index.ts index 6943b61262..8746790c23 100644 --- a/packages/lib/index.ts +++ b/packages/lib/index.ts @@ -5,3 +5,5 @@ export * from "./isBookingLimits"; export * from "./isDurationLimits"; export * from "./validateIntervalLimitOrder"; export * from "./schedules"; +export * from "./event-types/getEventTypesByViewer"; +export * from "./event-types/getEventTypesPublic"; diff --git a/packages/platform/atoms/hooks/event-types/useEventTypesPrivate.ts b/packages/platform/atoms/hooks/event-types/useEventTypesPrivate.ts new file mode 100644 index 0000000000..2aaca81594 --- /dev/null +++ b/packages/platform/atoms/hooks/event-types/useEventTypesPrivate.ts @@ -0,0 +1,26 @@ +import { useQuery } from "@tanstack/react-query"; + +import { BASE_URL, API_VERSION, V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants"; +import type { EventTypesByViewer } from "@calcom/platform-libraries"; +import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types"; + +import http from "../../lib/http"; + +export const QUERY_KEY = "get-private-events"; +export const useEventTypesPrivate = () => { + const endpoint = new URL(BASE_URL); + + endpoint.pathname = `api/${API_VERSION}/${V2_ENDPOINTS.eventTypes}`; + + return useQuery({ + queryKey: [QUERY_KEY], + queryFn: () => { + return http?.get>(endpoint.toString()).then((res) => { + if (res.data.status === SUCCESS_STATUS) { + return (res.data as ApiSuccessResponse).data; + } + throw new Error(res.data.error.message); + }); + }, + }); +}; diff --git a/packages/platform/atoms/hooks/event-types/useEventTypesPublic.ts b/packages/platform/atoms/hooks/event-types/useEventTypesPublic.ts new file mode 100644 index 0000000000..358a2d717d --- /dev/null +++ b/packages/platform/atoms/hooks/event-types/useEventTypesPublic.ts @@ -0,0 +1,26 @@ +import { useQuery } from "@tanstack/react-query"; + +import { BASE_URL, API_VERSION, V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants"; +import type { EventTypesPublic } from "@calcom/platform-libraries"; +import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types"; + +import http from "../../lib/http"; + +export const QUERY_KEY = "get-public-events"; +export const useEventTypesPublic = (username: string) => { + const endpoint = new URL(BASE_URL); + + endpoint.pathname = `api/${API_VERSION}/${V2_ENDPOINTS.eventTypes}/${username}/public`; + + return useQuery({ + queryKey: [QUERY_KEY, username], + queryFn: () => { + return http?.get>(endpoint.toString()).then((res) => { + if (res.data.status === SUCCESS_STATUS) { + return (res.data as ApiSuccessResponse).data; + } + throw new Error(res.data.error.message); + }); + }, + }); +}; diff --git a/packages/platform/atoms/hooks/useConnectedCalendars.tsx b/packages/platform/atoms/hooks/useConnectedCalendars.tsx index 48e436f218..33be4ec20b 100644 --- a/packages/platform/atoms/hooks/useConnectedCalendars.tsx +++ b/packages/platform/atoms/hooks/useConnectedCalendars.tsx @@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { SUCCESS_STATUS } from "@calcom/platform-constants"; import type { ConnectedDestinationCalendars } from "@calcom/platform-libraries"; -import type { ApiResponse } from "@calcom/platform-types"; +import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types"; import http from "../lib/http"; @@ -13,7 +13,7 @@ export const useConnectedCalendars = (props: { enabled?: boolean }) => { queryFn: () => { return http.get>("/ee/calendars").then((res) => { if (res.data.status === SUCCESS_STATUS) { - return res.data?.data; + return (res.data as ApiSuccessResponse)?.data; } throw new Error(res.data.error.message); }); diff --git a/packages/platform/atoms/index.ts b/packages/platform/atoms/index.ts index db27ccd37b..5d732c90e1 100644 --- a/packages/platform/atoms/index.ts +++ b/packages/platform/atoms/index.ts @@ -5,3 +5,4 @@ export { BookerPlatformWrapper as Booker } from "./booker"; export { useIsPlatform } from "./hooks/useIsPlatform"; export { useAtomsContext } from "./hooks/useAtomsContext"; export { useConnectedCalendars } from "./hooks/useConnectedCalendars"; +export { useEventTypesPublic } from "./hooks/event-types/useEventTypesPublic"; diff --git a/packages/platform/examples/base/src/pages/booking.tsx b/packages/platform/examples/base/src/pages/booking.tsx index 3b25477968..21a0427c09 100644 --- a/packages/platform/examples/base/src/pages/booking.tsx +++ b/packages/platform/examples/base/src/pages/booking.tsx @@ -2,19 +2,39 @@ import { Navbar } from "@/components/Navbar"; import { Inter } from "next/font/google"; import { useState } from "react"; -import { Booker } from "@calcom/atoms"; +import { Booker, useEventTypesPublic } from "@calcom/atoms"; const inter = Inter({ subsets: ["latin"] }); export default function Bookings(props: { calUsername: string; calEmail: string }) { const [bookingTitle, setBookingTitle] = useState(null); + const [eventTypeSlug, setEventTypeSlug] = useState(null); + + const { isLoading: isLoadingEvents, data: eventTypes } = useEventTypesPublic(props.calUsername); + console.log(isLoadingEvents, eventTypes); return (

{props.calUsername} Public Booking Page

- {!bookingTitle && ( + + {isLoadingEvents && !eventTypeSlug &&

Loading...

} + + {!isLoadingEvents && !eventTypeSlug && Boolean(eventTypes?.length) && ( +
+ {eventTypes?.map((event: { id: number; slug: string; title: string }) => ( + + ))} +
+ )} + + {!bookingTitle && eventTypeSlug && ( >; -export { getEventTypeById } from "@calcom/lib/getEventTypeById"; +export { getEventTypeById } from "@calcom/lib/event-types/getEventTypeById"; +export { getEventTypesByViewer } from "@calcom/lib/event-types/getEventTypesByViewer"; +export { getEventTypesPublic } from "@calcom/lib/event-types/getEventTypesPublic"; + +export type { EventType } from "@calcom/lib/event-types/getEventTypeById"; +export type { EventTypesByViewer } from "@calcom/lib/event-types/getEventTypesByViewer"; +export type { EventTypesPublic } from "@calcom/lib/event-types/getEventTypesPublic"; export type PublicEventType = Awaited>; export { getPublicEvent }; @@ -26,7 +32,6 @@ export { getAvailableSlots }; export type AvailableSlotsType = Awaited>; export { handleNewRecurringBooking } from "@calcom/features/bookings/lib/handleNewRecurringBooking"; -export type { EventType } from "@calcom/lib/getEventTypeById"; export { getConnectedDestinationCalendars } from "@calcom/lib/getConnectedDestinationCalendars"; export type { ConnectedDestinationCalendars } from "@calcom/lib/getConnectedDestinationCalendars"; export { getBusyCalendarTimes } from "@calcom/core/CalendarManager"; diff --git a/packages/platform/types/events.ts b/packages/platform/types/events.ts index d0ff773ead..084d13639c 100644 --- a/packages/platform/types/events.ts +++ b/packages/platform/types/events.ts @@ -1,19 +1,24 @@ +import { ApiProperty } from "@nestjs/swagger"; import { Transform } from "class-transformer"; import { IsBoolean, IsOptional, IsString } from "class-validator"; export class GetPublicEventInput { @IsString() @Transform(({ value }: { value: string }) => value.toLowerCase()) + @ApiProperty({ required: true }) username!: string; @IsString() + @ApiProperty({ required: true }) eventSlug!: string; @Transform(({ value }: { value: string }) => value === "true") @IsBoolean() @IsOptional() + @ApiProperty({ required: false }) isTeamEvent?: boolean; + @ApiProperty({ required: false }) @IsString() @IsOptional() org?: string; diff --git a/packages/trpc/server/routers/viewer/eventTypes/get.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/get.handler.ts index 0987340771..bf631b66da 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/get.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/get.handler.ts @@ -1,4 +1,4 @@ -import getEventTypeById from "@calcom/lib/getEventTypeById"; +import getEventTypeById from "@calcom/lib/event-types/getEventTypeById"; import type { PrismaClient } from "@calcom/prisma"; import type { TrpcSessionUser } from "../../../trpc"; diff --git a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts index b2f20c0b80..0b8f3fe45f 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts @@ -1,29 +1,10 @@ -// eslint-disable-next-line no-restricted-imports -import { orderBy } from "lodash"; - -import { hasFilter } from "@calcom/features/filters/lib/hasFilter"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; -import { getOrgAvatarUrl, getTeamAvatarUrl, getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; -import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client"; -import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; -import logger from "@calcom/lib/logger"; -import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; -import { safeStringify } from "@calcom/lib/safeStringify"; -import { EventTypeRepository } from "@calcom/lib/server/repository/eventType"; -import { MembershipRepository } from "@calcom/lib/server/repository/membership"; -import { ProfileRepository } from "@calcom/lib/server/repository/profile"; -import { UserRepository } from "@calcom/lib/server/repository/user"; +import { getEventTypesByViewer } from "@calcom/lib/event-types/getEventTypesByViewer"; import type { PrismaClient } from "@calcom/prisma"; -import { MembershipRole, SchedulingType } from "@calcom/prisma/enums"; -import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; -import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; - -import { TRPCError } from "@trpc/server"; import type { TrpcSessionUser } from "../../../trpc"; import type { TEventTypeInputSchema } from "./getByViewer.schema"; -const log = logger.getSubLogger({ prefix: ["viewer.eventTypes.getByViewer"] }); type GetByViewerOptions = { ctx: { user: NonNullable; @@ -32,319 +13,14 @@ type GetByViewerOptions = { input: TEventTypeInputSchema; }; -export const compareMembership = (mship1: MembershipRole, mship2: MembershipRole) => { - const mshipToNumber = (mship: MembershipRole) => - Object.keys(MembershipRole).findIndex((mmship) => mmship === mship); - return mshipToNumber(mship1) > mshipToNumber(mship2); -}; - export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) => { await checkRateLimitAndThrowError({ identifier: `eventTypes:getByViewer:${ctx.user.id}`, rateLimitingType: "common", }); - const lightProfile = ctx.user.profile; - const profile = await ProfileRepository.findByUpId(lightProfile.upId); - const parentOrgHasLockedEventTypes = - profile?.organization?.organizationSettings?.lockEventTypeCreationForUsers; - const isFilterSet = input?.filters && hasFilter(input.filters); - const isUpIdInFilter = input?.filters?.upIds?.includes(lightProfile.upId); + const user = ctx.user; + const filters = input?.filters; + const forRoutingForms = input?.forRoutingForms; - let shouldListUserEvents = !isFilterSet || isUpIdInFilter; - // FIX: Handles the case when an upId != lightProfile - pretend like there is no filter. - // Results in {"eventTypeGroups":[],"profiles":[]} - this crashes all dependencies. - if (isFilterSet && input?.filters?.upIds && !isUpIdInFilter) { - shouldListUserEvents = true; - } - const [profileMemberships, profileEventTypes] = await Promise.all([ - MembershipRepository.findAllByUpIdIncludeTeamWithMembersAndEventTypes( - { - upId: lightProfile.upId, - }, - { - where: { - accepted: true, - }, - } - ), - shouldListUserEvents - ? EventTypeRepository.findAllByUpId( - { - upId: lightProfile.upId, - userId: ctx.user.id, - }, - { - where: { - teamId: null, - }, - orderBy: [ - { - position: "desc", - }, - { - id: "asc", - }, - ], - } - ) - : [], - ]); - - if (!profile) { - throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); - } - - const memberships = profileMemberships.map((membership) => ({ - ...membership, - team: { - ...membership.team, - metadata: teamMetadataSchema.parse(membership.team.metadata), - }, - })); - - log.debug( - safeStringify({ - profileMemberships, - profileEventTypes, - }) - ); - - type UserEventTypes = (typeof profileEventTypes)[number]; - - const mapEventType = async (eventType: UserEventTypes) => ({ - ...eventType, - safeDescription: eventType?.description ? markdownToSafeHTML(eventType.description) : undefined, - users: await Promise.all( - (!!eventType?.hosts?.length ? eventType?.hosts.map((host) => host.user) : eventType.users).map( - async (u) => - await UserRepository.enrichUserWithItsProfile({ - user: u, - }) - ) - ), - metadata: eventType.metadata ? EventTypeMetaDataSchema.parse(eventType.metadata) : null, - children: await Promise.all( - (eventType.children || []).map(async (c) => ({ - ...c, - users: await Promise.all( - c.users.map( - async (u) => - await UserRepository.enrichUserWithItsProfile({ - user: u, - }) - ) - ), - })) - ), - }); - - const userEventTypes = (await Promise.all(profileEventTypes.map(mapEventType))).filter((eventType) => { - const isAChildEvent = eventType.parentId; - // A child event only has one user - const childEventAssignee = eventType.users[0]; - if (isAChildEvent && childEventAssignee.id != ctx.user.id) { - return false; - } - return true; - }); - - type EventTypeGroup = { - teamId?: number | null; - parentId?: number | null; - bookerUrl: string; - membershipRole?: MembershipRole | null; - profile: { - slug: (typeof profile)["username"] | null; - name: (typeof profile)["name"]; - image: string; - eventTypesLockedByOrg?: boolean; - }; - metadata: { - membershipCount: number; - readOnly: boolean; - }; - eventTypes: typeof userEventTypes; - }; - - let eventTypeGroups: EventTypeGroup[] = []; - - const unmanagedEventTypes = userEventTypes.filter( - (evType) => evType.schedulingType !== SchedulingType.MANAGED - ); - - log.debug(safeStringify({ profileMemberships, profileEventTypes, profile })); - - log.debug( - "Filter Settings", - safeStringify({ - isFilterSet, - isProfileIdInFilter: isUpIdInFilter, - }) - ); - - if (shouldListUserEvents) { - const bookerUrl = await getBookerBaseUrl(profile.organizationId ?? null); - eventTypeGroups.push({ - teamId: null, - bookerUrl, - membershipRole: null, - profile: { - slug: profile.username, - name: profile.name, - image: getUserAvatarUrl({ - username: profile.username, - avatarUrl: profile.avatarUrl, - profile: profile, - }), - eventTypesLockedByOrg: parentOrgHasLockedEventTypes, - }, - eventTypes: orderBy(unmanagedEventTypes, ["position", "id"], ["desc", "asc"]), - metadata: { - membershipCount: 1, - readOnly: false, - }, - }); - } - - const teamMemberships = profileMemberships.map((membership) => ({ - teamId: membership.team.id, - membershipRole: membership.role, - })); - - const filterTeamsEventTypesBasedOnInput = async (eventType: Awaited>) => { - if (!input?.filters || !hasFilter(input?.filters)) { - return true; - } - return input?.filters?.teamIds?.includes(eventType?.teamId || 0) ?? false; - }; - eventTypeGroups = ([] as EventTypeGroup[]).concat( - eventTypeGroups, - await Promise.all( - memberships - .filter((mmship) => { - if (mmship.team.isOrganization) { - return false; - } else { - if (!input?.filters || !hasFilter(input?.filters)) { - return true; - } - return input?.filters?.teamIds?.includes(mmship?.team?.id || 0) ?? false; - } - }) - .map(async (membership) => { - const orgMembership = teamMemberships.find( - (teamM) => teamM.teamId === membership.team.parentId - )?.membershipRole; - - const team = { - ...membership.team, - metadata: teamMetadataSchema.parse(membership.team.metadata), - }; - - let slug; - - if (input?.forRoutingForms) { - // For Routing form we want to ensure that after migration of team to an org, the URL remains same for the team - // Once we solve this https://github.com/calcom/cal.com/issues/12399, we can remove this conditional change in slug - slug = `team/${team.slug}`; - } else { - // In an Org, a team can be accessed without /team prefix as well as with /team prefix - slug = team.slug ? (!team.parentId ? `team/${team.slug}` : `${team.slug}`) : null; - } - - const eventTypes = await Promise.all(team.eventTypes.map(mapEventType)); - return { - teamId: team.id, - parentId: team.parentId, - bookerUrl: getBookerBaseUrlSync(team.parent?.slug ?? null), - membershipRole: - orgMembership && compareMembership(orgMembership, membership.role) - ? orgMembership - : membership.role, - profile: { - image: team.parentId - ? getOrgAvatarUrl({ - slug: team.parent?.slug || null, - logoUrl: team.parent?.logoUrl, - requestedSlug: team.slug, - }) - : getTeamAvatarUrl({ - slug: team.slug, - logoUrl: team.logoUrl, - requestedSlug: team.metadata?.requestedSlug ?? null, - organizationId: team.parentId, - }), - name: team.name, - slug, - }, - metadata: { - membershipCount: team.members.length, - readOnly: - membership.role === - (team.parentId - ? orgMembership && compareMembership(orgMembership, membership.role) - ? orgMembership - : MembershipRole.MEMBER - : MembershipRole.MEMBER), - }, - eventTypes: eventTypes - .filter(filterTeamsEventTypesBasedOnInput) - .filter((evType) => { - const res = evType.userId === null || evType.userId === ctx.user.id; - return res; - }) - .filter((evType) => - membership.role === MembershipRole.MEMBER - ? evType.schedulingType !== SchedulingType.MANAGED - : true - ), - }; - }) - ) - ); - - const denormalizedPayload = { - eventTypeGroups, - // so we can show a dropdown when the user has teams - profiles: eventTypeGroups.map((group) => ({ - ...group.profile, - ...group.metadata, - teamId: group.teamId, - membershipRole: group.membershipRole, - })), - }; - - return normalizePayload(denormalizedPayload); - - /** - * Reduces the size of payload - */ - function normalizePayload(payload: typeof denormalizedPayload) { - const allUsersAcrossAllEventTypes = new Map< - number, - EventTypeGroup["eventTypes"][number]["users"][number] - >(); - const eventTypeGroups = payload.eventTypeGroups.map((group) => { - return { - ...group, - eventTypes: group.eventTypes.map((eventType) => { - const { users, ...rest } = eventType; - return { - ...rest, - // Send userIds per event and keep the actual users object outside - userIds: users.map((user) => { - allUsersAcrossAllEventTypes.set(user.id, user); - return user.id; - }), - }; - }), - }; - }); - - return { - ...payload, - allUsersAcrossAllEventTypes, - eventTypeGroups, - }; - } + return await getEventTypesByViewer(user, filters, forRoutingForms); }; diff --git a/packages/trpc/server/routers/viewer/webhook/getByViewer.handler.ts b/packages/trpc/server/routers/viewer/webhook/getByViewer.handler.ts index 37676ced76..6ea1f3e9ba 100644 --- a/packages/trpc/server/routers/viewer/webhook/getByViewer.handler.ts +++ b/packages/trpc/server/routers/viewer/webhook/getByViewer.handler.ts @@ -1,3 +1,4 @@ +import { compareMembership } from "@calcom/lib/event-types/getEventTypesByViewer"; import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; import { prisma } from "@calcom/prisma"; import type { Webhook } from "@calcom/prisma/client"; @@ -6,8 +7,6 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; import { TRPCError } from "@trpc/server"; -import { compareMembership } from "../eventTypes/getByViewer.handler"; - type GetByViewerOptions = { ctx: { user: NonNullable; diff --git a/packages/ui/components/test-setup.ts b/packages/ui/components/test-setup.ts index c553d891be..c238180081 100644 --- a/packages/ui/components/test-setup.ts +++ b/packages/ui/components/test-setup.ts @@ -65,6 +65,9 @@ vi.mock("@calcom/atoms/monorepo", () => ({ }, })); +vi.mock("@calcom/lib/event-types/getEventTypesByViewer", () => ({})); +vi.mock("@calcom/lib/event-types/getEventTypesPublic", () => ({})); + expect.extend({ tabToBeDisabled(received) { const isDisabled = received.classList.contains("pointer-events-none");