feat: v2 api endpoints and hooks to list private and public event types of user (#14192)
* refactor: extract getEventTypeByViewer for private user dashboard event types * refactor: extract getEventTypesPublic for public event types to book * refactor: group event-types lib functions in single folder * refactor: rename getEventTypeByViewer to getEventTypesByViewer * feat: add event type helpers from lib to platform libraries * feat: v2 endpoint for private event types * driveby:fix: fixed date in docs instead of being re-generated * feature: endpoint to fetch public endpoints * fix: getting private event types * tests: fetch private event types * tests: fetch private event types * feat: hooks for public and private events * fix: yarn test * tests: IMO better mocking * refactor: track dynamic prop in react query * fix: distinct query key for public events * fix: username in query key and example app demo * fix: swagger for endpoint get public event input --------- Co-authored-by: Morgan Vernay <morgan@cal.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
24 files changed
+680
-410
No files matched your search
@@ -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<EventTypesByViewer> = 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<EventTypesPublic> = 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`)
|
||||
|
||||
@@ -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<ApiSuccessResponse<EventTypesByViewer>> {
|
||||
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<ApiSuccessResponse<EventTypesPublic>> {
|
||||
const eventTypes = await this.eventTypesService.getEventTypesPublicByUsername(username);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventTypes,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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],
|
||||
|
||||
@@ -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<EventTypesPublic> {
|
||||
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;
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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<typeof eventTypes>((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<UserPageProps> = async (context) => {
|
||||
const ssr = await ssrInit(context);
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||
@@ -220,18 +167,12 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = 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) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
@@ -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<ReturnType<typeof getEventTypesByViewer>>;
|
||||
|
||||
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<ReturnType<typeof mapEventType>>) => {
|
||||
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);
|
||||
}
|
||||
@@ -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<ReturnType<typeof getEventTypesPublic>>;
|
||||
|
||||
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<typeof eventTypes>((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;
|
||||
}, []);
|
||||
};
|
||||
@@ -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";
|
||||
@@ -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<ApiResponse<EventTypesByViewer>>(endpoint.toString()).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return (res.data as ApiSuccessResponse<EventTypesByViewer>).data;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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<ApiResponse<EventTypesPublic>>(endpoint.toString()).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return (res.data as ApiSuccessResponse<EventTypesPublic>).data;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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<ApiResponse<ConnectedDestinationCalendars>>("/ee/calendars").then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return res.data?.data;
|
||||
return (res.data as ApiSuccessResponse<ConnectedDestinationCalendars>)?.data;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
@@ -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<string | null>(null);
|
||||
const [eventTypeSlug, setEventTypeSlug] = useState<string | null>(null);
|
||||
|
||||
const { isLoading: isLoadingEvents, data: eventTypes } = useEventTypesPublic(props.calUsername);
|
||||
console.log(isLoadingEvents, eventTypes);
|
||||
return (
|
||||
<main
|
||||
className={`flex min-h-screen flex-col ${inter.className} main text-default flex min-h-full w-full flex-col items-center overflow-visible`}>
|
||||
<Navbar username={props.calUsername} />
|
||||
<div>
|
||||
<h1 className="my-4 text-2xl font-semibold">{props.calUsername} Public Booking Page</h1>
|
||||
{!bookingTitle && (
|
||||
|
||||
{isLoadingEvents && !eventTypeSlug && <p>Loading...</p>}
|
||||
|
||||
{!isLoadingEvents && !eventTypeSlug && Boolean(eventTypes?.length) && (
|
||||
<div className="flex flex-row gap-4">
|
||||
{eventTypes?.map((event: { id: number; slug: string; title: string }) => (
|
||||
<button
|
||||
key={event.id}
|
||||
className="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
|
||||
onClick={() => setEventTypeSlug(event.slug)}>
|
||||
{event.slug}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!bookingTitle && eventTypeSlug && (
|
||||
<Booker
|
||||
eventSlug="sixty-minutes-video"
|
||||
username={props.calUsername ?? ""}
|
||||
|
||||
@@ -11,7 +11,13 @@ export type UpdateScheduleOutputType = Awaited<
|
||||
typeof import("@calcom/trpc/server/routers/viewer/availability/schedule/update.handler").updateHandler
|
||||
>
|
||||
>;
|
||||
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<ReturnType<typeof getPublicEvent>>;
|
||||
export { getPublicEvent };
|
||||
@@ -26,7 +32,6 @@ export { getAvailableSlots };
|
||||
export type AvailableSlotsType = Awaited<ReturnType<typeof getAvailableSlots>>;
|
||||
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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<TrpcSessionUser>;
|
||||
@@ -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<ReturnType<typeof mapEventType>>) => {
|
||||
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);
|
||||
};
|
||||
@@ -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<TrpcSessionUser>;
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user