From 0f3c37df47e5785608dc9731ef0ec2398620e540 Mon Sep 17 00:00:00 2001 From: Vijay Date: Wed, 19 Feb 2025 17:24:07 +0530 Subject: [PATCH 1/8] fix for user as attendee in bookings filter --- .../routers/viewer/bookings/get.handler.ts | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index 4a5e41c6d6bd8c..99b9044a76ee2d 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -224,6 +224,13 @@ export async function getBookings({ // On prisma 5.4.2 passing undefined to where "AND" causes an error .filter(Boolean); + const filtersCombinedExceptUserIds: Prisma.BookingWhereInput[] = !filters + ? [] + : Object.keys(filters) + .filter((key) => key !== "userIds") + .map((key) => bookingWhereInputFilters[key]) + .filter(Boolean); + const bookingSelect = { ...bookingMinimalSelect, uid: true, @@ -325,6 +332,21 @@ export async function getBookings({ }, }; + let attendeeEmailIds = [user.email]; + if (filters?.userIds && filters.userIds.length > 0) { + const users = await prisma.user.findMany({ + where: { + id: { + in: filters.userIds, + }, + }, + select: { + email: true, + }, + }); + attendeeEmailIds = users && users.length > 0 ? users.map((user) => user.email) : []; + } + const [ // Quering these in parallel to save time. // Note that because we are applying `take` to individual queries, we will usually get more bookings then we need. It is okay to have more bookings faster than having what we need slower @@ -359,12 +381,12 @@ export async function getBookings({ { attendees: { some: { - email: user.email, + email: { in: attendeeEmailIds }, }, }, }, ], - AND: [passedBookingsStatusFilter, ...filtersCombined], + AND: [passedBookingsStatusFilter, ...filtersCombinedExceptUserIds], }, orderBy, take: take + 1, From 065185026ad87216e4257df2e4c30da2a3ef16d7 Mon Sep 17 00:00:00 2001 From: Vijay Date: Thu, 20 Feb 2025 00:54:33 +0530 Subject: [PATCH 2/8] tested with team eventTypes and hence going with filtersCombined --- .../routers/viewer/bookings/get.handler.ts | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index 99b9044a76ee2d..a01c951a583934 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -78,6 +78,26 @@ export async function getBookings({ }) { const bookingWhereInputFilters: Record = {}; + const membershipIdsWhereUserIsAdminOwner = ( + await prisma.membership.findMany({ + where: { + userId: user.id, + role: { + in: ["ADMIN", "OWNER"], + }, + }, + select: { + id: true, + }, + }) + ).map((membership) => membership.id); + + const membershipConditionWhereUserIsAdminOwner = { + some: { + id: { in: membershipIdsWhereUserIsAdminOwner }, + }, + }; + if (filters?.teamIds && filters.teamIds.length > 0) { bookingWhereInputFilters.teamIds = { AND: [ @@ -134,6 +154,13 @@ export async function getBookings({ }, }, }, + { + eventType: { + team: { + members: membershipConditionWhereUserIsAdminOwner, + }, + }, + }, ], }, ], @@ -224,13 +251,6 @@ export async function getBookings({ // On prisma 5.4.2 passing undefined to where "AND" causes an error .filter(Boolean); - const filtersCombinedExceptUserIds: Prisma.BookingWhereInput[] = !filters - ? [] - : Object.keys(filters) - .filter((key) => key !== "userIds") - .map((key) => bookingWhereInputFilters[key]) - .filter(Boolean); - const bookingSelect = { ...bookingMinimalSelect, uid: true, @@ -312,26 +332,6 @@ export async function getBookings({ }, }; - const membershipIdsWhereUserIsAdminOwner = ( - await prisma.membership.findMany({ - where: { - userId: user.id, - role: { - in: ["ADMIN", "OWNER"], - }, - }, - select: { - id: true, - }, - }) - ).map((membership) => membership.id); - - const membershipConditionWhereUserIsAdminOwner = { - some: { - id: { in: membershipIdsWhereUserIsAdminOwner }, - }, - }; - let attendeeEmailIds = [user.email]; if (filters?.userIds && filters.userIds.length > 0) { const users = await prisma.user.findMany({ @@ -386,7 +386,7 @@ export async function getBookings({ }, }, ], - AND: [passedBookingsStatusFilter, ...filtersCombinedExceptUserIds], + AND: [passedBookingsStatusFilter, ...filtersCombined], }, orderBy, take: take + 1, From fbec6ba920de95d69d165cc13e72c468bb3e96fe Mon Sep 17 00:00:00 2001 From: Vijay Date: Thu, 20 Feb 2025 00:59:42 +0530 Subject: [PATCH 3/8] to show as commented --- .../routers/viewer/bookings/get.handler.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index a01c951a583934..db7865d1fee38d 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -332,6 +332,26 @@ export async function getBookings({ }, }; + // const membershipIdsWhereUserIsAdminOwner = ( + // await prisma.membership.findMany({ + // where: { + // userId: user.id, + // role: { + // in: ["ADMIN", "OWNER"], + // }, + // }, + // select: { + // id: true, + // }, + // }) + // ).map((membership) => membership.id); + + // const membershipConditionWhereUserIsAdminOwner = { + // some: { + // id: { in: membershipIdsWhereUserIsAdminOwner }, + // }, + // }; + let attendeeEmailIds = [user.email]; if (filters?.userIds && filters.userIds.length > 0) { const users = await prisma.user.findMany({ From dbbb2228ce5cdec1b4c3db50793db2269e35c2cb Mon Sep 17 00:00:00 2001 From: Vijay Date: Thu, 20 Feb 2025 01:04:20 +0530 Subject: [PATCH 4/8] nit --- .../routers/viewer/bookings/get.handler.ts | 48 +++++-------------- 1 file changed, 13 insertions(+), 35 deletions(-) diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index db7865d1fee38d..4e2fe2fc1af03d 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -77,6 +77,7 @@ export async function getBookings({ skip: number; }) { const bookingWhereInputFilters: Record = {}; + let attendeeEmailIds = [user.email]; const membershipIdsWhereUserIsAdminOwner = ( await prisma.membership.findMany({ @@ -122,6 +123,18 @@ export async function getBookings({ } if (filters?.userIds && filters.userIds.length > 0) { + const users = await prisma.user.findMany({ + where: { + id: { + in: filters.userIds, + }, + }, + select: { + email: true, + }, + }); + attendeeEmailIds = users && users.length > 0 ? users.map((user) => user.email) : []; + bookingWhereInputFilters.userIds = { AND: [ { @@ -332,41 +345,6 @@ export async function getBookings({ }, }; - // const membershipIdsWhereUserIsAdminOwner = ( - // await prisma.membership.findMany({ - // where: { - // userId: user.id, - // role: { - // in: ["ADMIN", "OWNER"], - // }, - // }, - // select: { - // id: true, - // }, - // }) - // ).map((membership) => membership.id); - - // const membershipConditionWhereUserIsAdminOwner = { - // some: { - // id: { in: membershipIdsWhereUserIsAdminOwner }, - // }, - // }; - - let attendeeEmailIds = [user.email]; - if (filters?.userIds && filters.userIds.length > 0) { - const users = await prisma.user.findMany({ - where: { - id: { - in: filters.userIds, - }, - }, - select: { - email: true, - }, - }); - attendeeEmailIds = users && users.length > 0 ? users.map((user) => user.email) : []; - } - const [ // Quering these in parallel to save time. // Note that because we are applying `take` to individual queries, we will usually get more bookings then we need. It is okay to have more bookings faster than having what we need slower From 1445f5bc63b8eb9a11aa678b5fa8d87bffc56e12 Mon Sep 17 00:00:00 2001 From: Vijay Date: Thu, 20 Feb 2025 03:05:39 +0530 Subject: [PATCH 5/8] simpler solution --- .../routers/viewer/bookings/get.handler.ts | 64 ++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index 4e2fe2fc1af03d..4111457daa7ac5 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -77,27 +77,7 @@ export async function getBookings({ skip: number; }) { const bookingWhereInputFilters: Record = {}; - let attendeeEmailIds = [user.email]; - - const membershipIdsWhereUserIsAdminOwner = ( - await prisma.membership.findMany({ - where: { - userId: user.id, - role: { - in: ["ADMIN", "OWNER"], - }, - }, - select: { - id: true, - }, - }) - ).map((membership) => membership.id); - - const membershipConditionWhereUserIsAdminOwner = { - some: { - id: { in: membershipIdsWhereUserIsAdminOwner }, - }, - }; + // let attendeeEmailIds = [user.email]; if (filters?.teamIds && filters.teamIds.length > 0) { bookingWhereInputFilters.teamIds = { @@ -133,7 +113,7 @@ export async function getBookings({ email: true, }, }); - attendeeEmailIds = users && users.length > 0 ? users.map((user) => user.email) : []; + const attendeeEmailIds = users && users.length > 0 ? users.map((user) => user.email) : []; bookingWhereInputFilters.userIds = { AND: [ @@ -167,13 +147,17 @@ export async function getBookings({ }, }, }, - { - eventType: { - team: { - members: membershipConditionWhereUserIsAdminOwner, - }, - }, - }, + ...(attendeeEmailIds.length > 0 + ? [ + { + attendees: { + some: { + email: { in: attendeeEmailIds }, + }, + }, + }, + ] + : []), ], }, ], @@ -345,6 +329,26 @@ export async function getBookings({ }, }; + const membershipIdsWhereUserIsAdminOwner = ( + await prisma.membership.findMany({ + where: { + userId: user.id, + role: { + in: ["ADMIN", "OWNER"], + }, + }, + select: { + id: true, + }, + }) + ).map((membership) => membership.id); + + const membershipConditionWhereUserIsAdminOwner = { + some: { + id: { in: membershipIdsWhereUserIsAdminOwner }, + }, + }; + const [ // Quering these in parallel to save time. // Note that because we are applying `take` to individual queries, we will usually get more bookings then we need. It is okay to have more bookings faster than having what we need slower @@ -379,7 +383,7 @@ export async function getBookings({ { attendees: { some: { - email: { in: attendeeEmailIds }, + email: user.email, }, }, }, From 00fd4784fbc7c46b00f950c88042478343acba26 Mon Sep 17 00:00:00 2001 From: Vijay Date: Thu, 20 Feb 2025 03:07:28 +0530 Subject: [PATCH 6/8] e2e test --- apps/web/playwright/bookings-list.e2e.ts | 125 +++++++++++++++++++++++ apps/web/playwright/fixtures/users.ts | 2 +- 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/apps/web/playwright/bookings-list.e2e.ts b/apps/web/playwright/bookings-list.e2e.ts index 1b25d303de9538..169fd522a6c52e 100644 --- a/apps/web/playwright/bookings-list.e2e.ts +++ b/apps/web/playwright/bookings-list.e2e.ts @@ -4,6 +4,7 @@ import { prisma } from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/client"; import { MembershipRole } from "@calcom/prisma/enums"; +import { createTeamEventType } from "./fixtures/users"; import type { Fixtures } from "./lib/fixtures"; import { test } from "./lib/fixtures"; import { localize, setupManagedEvent } from "./lib/testUtils"; @@ -298,6 +299,130 @@ test.describe("Bookings", () => { firstUpcomingBooking.locator(`text=${firstUserBooking!.title}`) ).toBeVisible(); }); + + test("People filter includes bookings where filtered person is attendee", async ({ + page, + users, + bookings, + }) => { + const t = await localize("en"); + const firstUser = await users.create( + { name: "First", email: "first@cal.com" }, + { + hasTeam: true, + teamRole: MembershipRole.ADMIN, + } + ); + const teamId = (await firstUser.getFirstTeamMembership()).teamId; + const secondUser = await users.create({ name: "Second", email: "second@cal.com" }); + const thirdUser = await users.create({ name: "Third", email: "third@cal.com" }); + // Add teammates to the team + await prisma.membership.createMany({ + data: [ + { + teamId: teamId, + userId: secondUser.id, + role: MembershipRole.MEMBER, + accepted: true, + }, + { + teamId: teamId, + userId: thirdUser.id, + role: MembershipRole.MEMBER, + accepted: true, + }, + ], + }); + const teamEvent = await createTeamEventType( + { id: firstUser.id }, + { id: teamId }, + { teamEventSlug: "team-event-slug" } + ); + + //Create a Team EventType booking where ThirdUser is attendee + const thirdUserAttendeeTeamEventBookingFixture = await createBooking({ + title: "ThirdUser is Attendee for TeamEvent", + bookingsFixture: bookings, + relativeDate: 6, + organizer: firstUser, + organizerEventType: teamEvent, + attendees: [{ name: "Third", email: thirdUser.email, timeZone: "Europe/Berlin" }], + }); + const thirdUserAttendeeTeamEvent = await thirdUserAttendeeTeamEventBookingFixture.self(); + + //Create a Individual EventType booking where ThirdUser,SecondUser are attendees and ThirdUser is organizer + const thirdUserAttendeeIndividualBookingFixture = await createBooking({ + title: "ThirdUser is Attendee and FirstUser is Organizer", + bookingsFixture: bookings, + relativeDate: 3, + organizer: firstUser, + organizerEventType: firstUser.eventTypes[0], + attendees: [ + { name: "Third", email: thirdUser.email, timeZone: "Europe/Berlin" }, + { name: "Second", email: secondUser.email, timeZone: "Europe/Berlin" }, + ], + }); + const thirdUserAttendeeIndividualBooking = await thirdUserAttendeeIndividualBookingFixture.self(); + + //Create a Individual EventType booking where ThirdUser is organizer and FirstUser,SecondUser are attendees + const thirdUserOrganizerBookingFixture = await createBooking({ + title: "ThirdUser is Organizer and FirstUser is Attendee", + bookingsFixture: bookings, + organizer: thirdUser, + relativeDate: 2, + organizerEventType: thirdUser.eventTypes[0], + attendees: [ + { name: "First", email: firstUser.email, timeZone: "Europe/Berlin" }, + { name: "Second", email: secondUser.email, timeZone: "Europe/Berlin" }, + ], + }); + const thirdUserOrganizerBooking = await thirdUserOrganizerBookingFixture.self(); + + //Create a booking where FirstUser is organizer and SecondUser is attendee + await createBooking({ + title: "FirstUser is Organizer and SecondUser is Attendee", + bookingsFixture: bookings, + organizer: firstUser, + relativeDate: 4, + organizerEventType: firstUser.eventTypes[0], + attendees: [{ name: "Second", email: secondUser.email, timeZone: "Europe/Berlin" }], + }); + + //admin login + //Select 'ThirdUser' in people filter + await firstUser.apiLogin(); + await Promise.all([ + page.waitForResponse((response) => /\/api\/trpc\/bookings\/get.*/.test(response.url())), + page.waitForResponse((response) => /\/api\/trpc\/bookings\/get.*/.test(response.url())), + page.goto(`/bookings/upcoming?status=upcoming&userIds=${thirdUser.id}`), + ]); + + //expect only 3 bookings (out of 4 total) to be shown in list. + //where ThirdUser is either organizer or attendee + const upcomingBookingsTable = page.locator('[data-testid="upcoming-bookings"]'); + const bookingListItems = upcomingBookingsTable.locator('[data-testid="booking-item"]'); + const bookingListCount = await bookingListItems.count(); + expect(bookingListCount).toBe(3); + + //verify with the booking titles + const firstUpcomingBooking = bookingListItems.nth(0); + await expect( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + firstUpcomingBooking.locator(`text=${thirdUserOrganizerBooking!.title}`) + ).toBeVisible(); + + const secondUpcomingBooking = bookingListItems.nth(1); + await expect( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + secondUpcomingBooking.locator(`text=${thirdUserAttendeeIndividualBooking!.title}`) + ).toBeVisible(); + + const thirdUpcomingBooking = bookingListItems.nth(2); + await expect( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + thirdUpcomingBooking.locator(`text=${thirdUserAttendeeTeamEvent!.title}`) + ).toBeVisible(); + }); }); async function createBooking({ diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index b2b7340ea183df..a57d38e7cf0c11 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -86,7 +86,7 @@ const createTeamWorkflow = async (user: { id: number }, team: { id: number }) => }); }; -const createTeamEventType = async ( +export const createTeamEventType = async ( user: { id: number }, team: { id: number }, scenario?: { From 0dda4fdbcf6e879bd917e7c2a3e7147bc25ef91c Mon Sep 17 00:00:00 2001 From: Vijay Date: Thu, 20 Feb 2025 03:08:46 +0530 Subject: [PATCH 7/8] chore --- packages/trpc/server/routers/viewer/bookings/get.handler.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index 4111457daa7ac5..058e5e6236c855 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -77,7 +77,6 @@ export async function getBookings({ skip: number; }) { const bookingWhereInputFilters: Record = {}; - // let attendeeEmailIds = [user.email]; if (filters?.teamIds && filters.teamIds.length > 0) { bookingWhereInputFilters.teamIds = { From f5b6d32b29275350aed00fbe082197654a2f6285 Mon Sep 17 00:00:00 2001 From: Vijay Date: Thu, 20 Feb 2025 03:17:59 +0530 Subject: [PATCH 8/8] nit --- apps/web/playwright/bookings-list.e2e.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/web/playwright/bookings-list.e2e.ts b/apps/web/playwright/bookings-list.e2e.ts index 169fd522a6c52e..8ca5972dc412cd 100644 --- a/apps/web/playwright/bookings-list.e2e.ts +++ b/apps/web/playwright/bookings-list.e2e.ts @@ -305,7 +305,6 @@ test.describe("Bookings", () => { users, bookings, }) => { - const t = await localize("en"); const firstUser = await users.create( { name: "First", email: "first@cal.com" }, { @@ -339,7 +338,7 @@ test.describe("Bookings", () => { { teamEventSlug: "team-event-slug" } ); - //Create a Team EventType booking where ThirdUser is attendee + //Create a TeamEventType booking where ThirdUser is attendee const thirdUserAttendeeTeamEventBookingFixture = await createBooking({ title: "ThirdUser is Attendee for TeamEvent", bookingsFixture: bookings, @@ -350,7 +349,7 @@ test.describe("Bookings", () => { }); const thirdUserAttendeeTeamEvent = await thirdUserAttendeeTeamEventBookingFixture.self(); - //Create a Individual EventType booking where ThirdUser,SecondUser are attendees and ThirdUser is organizer + //Create a IndividualEventType booking where ThirdUser,SecondUser are attendees and FirstUser is organizer const thirdUserAttendeeIndividualBookingFixture = await createBooking({ title: "ThirdUser is Attendee and FirstUser is Organizer", bookingsFixture: bookings, @@ -364,7 +363,7 @@ test.describe("Bookings", () => { }); const thirdUserAttendeeIndividualBooking = await thirdUserAttendeeIndividualBookingFixture.self(); - //Create a Individual EventType booking where ThirdUser is organizer and FirstUser,SecondUser are attendees + //Create a IndividualEventType booking where ThirdUser is organizer and FirstUser,SecondUser are attendees const thirdUserOrganizerBookingFixture = await createBooking({ title: "ThirdUser is Organizer and FirstUser is Attendee", bookingsFixture: bookings,