Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
dba0df0
fix: Admin/Owner not able to see attendees info in team seated events
asadath1395 Aug 20, 2025
017777d
Merge branch 'main' into fix/admin-owner-attendees
kart1ka Aug 22, 2025
5a74205
Merge branch 'main' of https://github.com/calcom/cal.com into fix/adm…
asadath1395 Aug 27, 2025
75626ac
Address review comments
asadath1395 Aug 28, 2025
23c15bd
Merge branch 'main' into fix/admin-owner-attendees
asadath1395 Aug 28, 2025
fd0773c
Merge branch 'main' into fix/admin-owner-attendees
asadath1395 Aug 29, 2025
b5b17d6
made few changes
anikdhabal Sep 1, 2025
e66bfee
Merge branch 'main' into fix/admin-owner-attendees
anikdhabal Sep 1, 2025
74edc23
update
anikdhabal Sep 1, 2025
c26ec42
Update get.handler.ts
anikdhabal Sep 1, 2025
4c7b5c8
remove comments
anikdhabal Sep 1, 2025
2df6ca7
addresed coderrabit commnet
anikdhabal Sep 1, 2025
0cabcea
Merge branch 'main' of https://github.com/calcom/cal.com into fix/adm…
asadath1395 Sep 4, 2025
348c407
Address review comments
asadath1395 Sep 4, 2025
6ad702c
Merge branch 'main' into fix/admin-owner-attendees
anikdhabal Sep 4, 2025
b2dbe1f
Merge branch 'main' of https://github.com/calcom/cal.com into fix/adm…
asadath1395 Sep 15, 2025
82a32a9
Merge branch 'main' of https://github.com/calcom/cal.com into fix/adm…
asadath1395 Sep 16, 2025
7c29132
Merge branch 'main' into fix/admin-owner-attendees
kart1ka Sep 16, 2025
31f1ae9
fix: org admins able to see seat info
kart1ka Sep 16, 2025
68c475b
fix
kart1ka Sep 16, 2025
a0f9428
Merge branch 'main' into fix/admin-owner-attendees
asadath1395 Sep 18, 2025
39d7841
fix
kart1ka Sep 19, 2025
fdbca8f
Merge branch 'main' into fix/admin-owner-attendees
kart1ka Sep 20, 2025
1599cad
Merge upstream/main into fix/admin-owner-attendees
devin-ai-integration[bot] Jan 15, 2026
da3d419
fix: Update imports to use isTeamOwner and correct isOrganisationAdmi…
devin-ai-integration[bot] Jan 15, 2026
948f721
Update apps/web/playwright/bookings-list.e2e.ts
keithwillcode Jan 15, 2026
316d96a
fix: Replace brittle text locators with data-testid in E2E tests
devin-ai-integration[bot] Jan 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/components/booking/BookingListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,7 @@ const Attendee = (
<Dropdown open={openDropdown} onOpenChange={setOpenDropdown}>
<DropdownMenuTrigger asChild>
<button
data-testid="guest"
data-testid={`attendee-name-${email}`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Changed attendee test id to dynamic value breaks existing tests still targeting [data-testid="guest"]

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/components/booking/BookingListItem.tsx, line 768:

<comment>Changed attendee test id to dynamic value breaks existing tests still targeting [data-testid="guest"]</comment>

<file context>
@@ -765,7 +765,7 @@ const Attendee = (
       <DropdownMenuTrigger asChild>
         <button
-          data-testid="guest"
+          data-testid={`attendee-name-${email}`}
           onClick={(e) => e.stopPropagation()}
           className="radix-state-open:text-blue-500 transition hover:text-blue-500">
</file context>

onClick={(e) => e.stopPropagation()}
className="radix-state-open:text-blue-500 transition hover:text-blue-500">
{noShow ? (
Expand Down
1 change: 1 addition & 0 deletions apps/web/lib/booking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export const getEventTypesFromDB = async (id: number) => {
theme: true,
},
},
parentId: true,
createdByOAuthClientId: true,
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import getBookingInfo from "@calcom/features/bookings/lib/getBookingInfo";
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
import { isTeamMember } from "@calcom/features/ee/teams/lib/queries";
import { isTeamOwner, isTeamMember } from "@calcom/features/ee/teams/lib/queries";
import { isOrganisationAdmin } from "@calcom/features/pbac/utils/isOrganisationAdmin";
import { getDefaultEvent } from "@calcom/features/eventtypes/lib/defaultEvents";
import { getBrandingForEventType } from "@calcom/features/profile/lib/getBranding";
import { shouldHideBrandingForEvent } from "@calcom/features/profile/lib/hideBranding";
Expand Down Expand Up @@ -167,11 +168,17 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {

const userId = session?.user?.id;

const checkIfUserIsHost = (userId?: number | null) => {
const checkIfUserIsHostOrTeamAdmin = async (userId?: number | null) => {
if (!userId) return false;

if (bookingInfo?.user?.id === userId) return true;

const isTeamAdminOrOwner = !!(await isTeamOwner(userId, eventType?.teamId ?? 0));
const isOrgAdminOrOwner = !!(await isOrganisationAdmin(userId, eventType?.team?.parentId ?? 0));

if (isTeamAdminOrOwner || isOrgAdminOrOwner) return true;

return (
bookingInfo?.user?.id === userId ||
eventType.users.some(
(user) =>
user.id === userId && bookingInfo.attendees.some((attendee) => attendee.email === user.email)
Expand All @@ -183,7 +190,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
);
};

const isLoggedInUserHost = checkIfUserIsHost(userId);
const isLoggedInUserHost = await checkIfUserIsHostOrTeamAdmin(userId);
const eventTeamId = eventType.team?.id ?? eventType.parent?.teamId;
const isLoggedInUserTeamMember = !!(userId && eventTeamId && (await isTeamMember(userId, eventTeamId)));

Expand Down
124 changes: 124 additions & 0 deletions apps/web/playwright/booking-seats.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";

import { test } from "./lib/fixtures";
import { setupTeamAndBookingSeats } from "./lib/test-helpers/teamHelpers";
import {
confirmReschedule,
createNewSeatedEventType,
Expand Down Expand Up @@ -457,6 +458,129 @@ test.describe("Reschedule for booking with seats", () => {
await expect(page.locator('[data-testid="confirm-reschedule-button"]')).toHaveCount(1);
});

test("Team Owner should see all attendees when seatsShowAttendees is false", async ({
page,
users,
bookings,
}) => {
const { user, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
]);

const teamOwner = await users.create({
name: "Team Owner",
email: "teamowner@example.com",
});

await setupTeamAndBookingSeats(user, booking, teamOwner, "OWNER");

await teamOwner.apiLogin();
await page.goto(`/booking/${booking.uid}`);

const foundFirstAttendeeAsOwner = page.locator('p[data-testid="attendee-email-first+seats@cal.com"]');
await expect(foundFirstAttendeeAsOwner).toHaveCount(1);

const foundSecondAttendeeAsOwner = page.locator('p[data-testid="attendee-email-second+seats@cal.com"]');
await expect(foundSecondAttendeeAsOwner).toHaveCount(1);
});

test("Team Admin should see all attendees when seatsShowAttendees is false", async ({
page,
users,
bookings,
}) => {
const { user, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
]);

const teamAdmin = await users.create({
name: "Team Admin",
email: "teamadmin@example.com",
});

await setupTeamAndBookingSeats(user, booking, teamAdmin, "ADMIN");

await teamAdmin.apiLogin();
await page.goto(`/booking/${booking.uid}`);

const foundFirstAttendeeAsAdmin = page.locator('p[data-testid="attendee-email-first+seats@cal.com"]');
await expect(foundFirstAttendeeAsAdmin).toHaveCount(1);

const foundSecondAttendeeAsAdmin = page.locator('p[data-testid="attendee-email-second+seats@cal.com"]');
await expect(foundSecondAttendeeAsAdmin).toHaveCount(1);
});

test("Event Host should see all attendees when seatsShowAttendees is false", async ({
page,
users,
bookings,
}) => {
const { user, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
]);

await setupTeamAndBookingSeats(user, booking, user, "MEMBER");

await user.apiLogin();
await page.goto(`/booking/${booking.uid}`);

const foundFirstAttendeeAsHost = page.locator('p[data-testid="attendee-email-first+seats@cal.com"]');
await expect(foundFirstAttendeeAsHost).toHaveCount(1);

const foundSecondAttendeeAsHost = page.locator('p[data-testid="attendee-email-second+seats@cal.com"]');
await expect(foundSecondAttendeeAsHost).toHaveCount(1);
});

test("Regular Team Member should NOT see attendees when seatsShowAttendees is false", async ({
page,
users,
bookings,
}) => {
const { user, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
]);

const teamMember = await users.create({
name: "Team Member",
email: "teammember@example.com",
});

await setupTeamAndBookingSeats(user, booking, teamMember, "MEMBER");

await teamMember.apiLogin();
await page.goto(`/booking/${booking.uid}`);

// Regular team member should not see any attendees when seatsShowAttendees is false
const foundFirstAttendeeAsMember = page.locator('p[data-testid="attendee-email-first+seats@cal.com"]');
await expect(foundFirstAttendeeAsMember).toHaveCount(0);

const foundSecondAttendeeAsMember = page.locator('p[data-testid="attendee-email-second+seats@cal.com"]');
await expect(foundSecondAttendeeAsMember).toHaveCount(0);
});

test("Attendee can only see themselves when using seatReferenceUid", async ({ page, users, bookings }) => {
const { user, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
]);

const { bookingSeats } = await setupTeamAndBookingSeats(user, booking, user, "MEMBER");

await page.goto(`/booking/${booking.uid}?seatReferenceUid=${bookingSeats[0].referenceUid}`);

// Should only see the first attendee (themselves)
const foundFirstAttendee = page.locator('p[data-testid="attendee-email-first+seats@cal.com"]');
await expect(foundFirstAttendee).toHaveCount(1);

// Should not see the second attendee
const notFoundSecondAttendee = page.locator('p[data-testid="attendee-email-second+seats@cal.com"]');
await expect(notFoundSecondAttendee).toHaveCount(0);
});

test("Host reschedule from /upcoming page should have rescheduleUid parameter set to bookingUid", async ({
page,
users,
Expand Down
88 changes: 87 additions & 1 deletion apps/web/playwright/bookings-list.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { addFilter } from "./filter-helpers";
import { createTeamEventType } from "./fixtures/users";
import type { Fixtures } from "./lib/fixtures";
import { test } from "./lib/fixtures";
import { setupManagedEvent } from "./lib/testUtils";
import { setupTeamAndBookingSeats } from "./lib/test-helpers/teamHelpers";
import { setupManagedEvent, createUserWithSeatedEventAndAttendees } from "./lib/testUtils";

test.afterEach(({ users }) => users.deleteAll());

Expand Down Expand Up @@ -617,3 +618,88 @@ async function createBooking({
},
});
}

test.describe("Attendee Visibility in Booking List", () => {
test("Team Owner should see all attendees in booking list when seatsShowAttendees is false", async ({
page,
users,
bookings,
}) => {
const { user, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
]);

const teamOwner = await users.create({
name: "Team Owner",
email: "teamowner@example.com",
});

await setupTeamAndBookingSeats(user, booking, teamOwner, "OWNER");

await teamOwner.apiLogin();
await page.goto("/bookings/upcoming");
await expect(page).toHaveURL("/bookings/upcoming");

// Should see the booking with all attendees visible
const bookingItem = page.locator('[data-testid="booking-item"]').first();
await expect(bookingItem).toBeVisible();

// Should see both attendees
await expect(page.getByTestId("attendee-name-first+seats@cal.com")).toBeVisible();
await expect(page.getByTestId("attendee-name-second+seats@cal.com")).toBeVisible();
});

test("Team Admin should see all attendees in booking list when seatsShowAttendees is false", async ({
page,
users,
bookings,
}) => {
const { user, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
]);

const teamAdmin = await users.create({
name: "Team Admin",
email: "teamadmin@example.com",
});

await setupTeamAndBookingSeats(user, booking, teamAdmin, "ADMIN");

await teamAdmin.apiLogin();
await page.goto("/bookings/upcoming");

// Should see the booking with all attendees visible
const bookingItem = page.locator('[data-testid="booking-item"]').first();
await expect(bookingItem).toBeVisible();

// Should see both attendees
await expect(page.getByTestId("attendee-name-first+seats@cal.com")).toBeVisible();
await expect(page.getByTestId("attendee-name-second+seats@cal.com")).toBeVisible();
});

test("Event Host should see all attendees in booking list when seatsShowAttendees is false", async ({
page,
users,
bookings,
}) => {
const { user, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
]);

await setupTeamAndBookingSeats(user, booking, user, "MEMBER");

await user.apiLogin();
await page.goto("/bookings/upcoming");

// Should see the booking with all attendees visible
const bookingItem = page.locator('[data-testid="booking-item"]').first();
await expect(bookingItem).toBeVisible();

// Should see both attendees
await expect(page.getByTestId("attendee-name-first+seats@cal.com")).toBeVisible();
await expect(page.getByTestId("attendee-name-second+seats@cal.com")).toBeVisible();
});
});
83 changes: 83 additions & 0 deletions apps/web/playwright/lib/test-helpers/teamHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { v4 as uuidv4 } from "uuid";

import { randomString } from "@calcom/lib/random";
import { prisma } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";

Expand Down Expand Up @@ -47,3 +50,83 @@ export async function createRoundRobinTeamEventType({

return createdEventType;
}

export async function setupTeamAndBookingSeats(
user: { id: number },
booking: { uid: string; id: number },
teamUser: { id: number },
role: "ADMIN" | "MEMBER" | "OWNER"
) {
const bookingWithEventType = await prisma.booking.findUnique({
where: { uid: booking.uid },
select: {
id: true,
eventTypeId: true,
},
});

// Create a team and assign the event type to it
const team = await prisma.team.create({
data: {
name: "Test Team",
slug: `test-team-${randomString(10)}`,
},
});

await prisma.eventType.update({
where: { id: bookingWithEventType?.eventTypeId || -1 },
data: {
seatsShowAttendees: false,
teamId: team.id,
},
});

// Add team user with specified role
await prisma.membership.create({
data: {
userId: teamUser.id,
teamId: team.id,
role: role,
accepted: true,
},
});

// Add original user as MEMBER only if they're different from teamUser
if (user.id !== teamUser.id) {
await prisma.membership.create({
data: {
userId: user.id,
teamId: team.id,
role: "MEMBER",
accepted: true,
},
});
}

const bookingAttendees = await prisma.attendee.findMany({
where: { bookingId: booking.id },
select: {
id: true,
name: true,
email: true,
},
});

const bookingSeats = bookingAttendees.map((attendee) => ({
bookingId: booking.id,
attendeeId: attendee.id,
referenceUid: uuidv4(),
data: {
responses: {
name: attendee.name,
email: attendee.email,
},
},
}));

await prisma.bookingSeat.createMany({
data: bookingSeats,
});

return { team, bookingSeats };
}
Loading
Loading