diff --git a/apps/web/modules/bookings/columns/filterColumns.ts b/apps/web/modules/bookings/columns/filterColumns.ts index 1c74c9c6ef..bcb805e018 100644 --- a/apps/web/modules/bookings/columns/filterColumns.ts +++ b/apps/web/modules/bookings/columns/filterColumns.ts @@ -1,8 +1,7 @@ +import { ColumnFilterType } from "@calcom/features/data-table"; import type { VisibilityState } from "@tanstack/react-table"; import { createColumnHelper } from "@tanstack/react-table"; -import { ColumnFilterType } from "@calcom/features/data-table"; - import type { RowData } from "../types"; interface BuildFilterColumnsParams { @@ -51,7 +50,7 @@ export function buildFilterColumns({ t, permissions, status }: BuildFilterColumn }, }, }), - columnHelper.accessor((row) => (row.type === "data" ? row.booking.eventType.team?.id ?? null : null), { + columnHelper.accessor((row) => (row.type === "data" ? (row.booking.eventType.team?.id ?? null) : null), { id: "teamId", header: t("team"), enableColumnFilter: true, @@ -63,10 +62,10 @@ export function buildFilterColumns({ t, permissions, status }: BuildFilterColumn }, }, }), - columnHelper.accessor((row) => (row.type === "data" ? row.booking.user?.id ?? null : null), { + columnHelper.accessor((row) => (row.type === "data" ? (row.booking.user?.id ?? null) : null), { id: "userId", header: t("member"), - enableColumnFilter: permissions.canReadOthersBookings, + enableColumnFilter: true, enableSorting: false, cell: () => null, meta: { diff --git a/apps/web/modules/bookings/components/BookingCalendarContainer.tsx b/apps/web/modules/bookings/components/BookingCalendarContainer.tsx index 29fbe4d4bc..21ca6fa3dc 100644 --- a/apps/web/modules/bookings/components/BookingCalendarContainer.tsx +++ b/apps/web/modules/bookings/components/BookingCalendarContainer.tsx @@ -1,10 +1,6 @@ "use client"; -import { useReactTable, getCoreRowModel, getSortedRowModel } from "@tanstack/react-table"; -import React, { useMemo, useEffect } from "react"; - import dayjs from "@calcom/dayjs"; -import { DataTableFilters } from "~/data-table/components/filters"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; @@ -12,7 +8,8 @@ import { Alert } from "@calcom/ui/components/alert"; import { Button } from "@calcom/ui/components/button"; import { ButtonGroup } from "@calcom/ui/components/buttonGroup"; import { Icon } from "@calcom/ui/components/icon"; - +import { getCoreRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table"; +import React, { useEffect, useMemo } from "react"; import { useBookingCalendarData } from "~/bookings/hooks/useBookingCalendarData"; import { useBookingFilters } from "~/bookings/hooks/useBookingFilters"; import { useCalendarAllowedFilters } from "~/bookings/hooks/useCalendarAllowedFilters"; @@ -20,11 +17,12 @@ import { useCalendarAutoSelector } from "~/bookings/hooks/useCalendarAutoSelecto import { useCalendarNavigationCapabilities } from "~/bookings/hooks/useCalendarNavigationCapabilities"; import { useCurrentWeekStart } from "~/bookings/hooks/useCurrentWeekStart"; import { useFacetedUniqueValues } from "~/bookings/hooks/useFacetedUniqueValues"; +import { DataTableFilters } from "~/data-table/components/filters"; import { buildFilterColumns, getFilterColumnVisibility } from "../columns/filterColumns"; import { getWeekStart } from "../lib/weekUtils"; import { BookingDetailsSheetStoreProvider } from "../store/bookingDetailsSheetStore"; -import type { RowData, BookingListingStatus, BookingsGetOutput } from "../types"; +import type { BookingListingStatus, BookingsGetOutput, RowData } from "../types"; import { BookingCalendarView } from "./BookingCalendarView"; import { BookingDetailsSheet } from "./BookingDetailsSheet"; import { ViewToggleButton } from "./ViewToggleButton"; @@ -106,7 +104,9 @@ function BookingCalendarInner({ ); }, [allowedFilterIds, t, permissions, status]); - const getFacetedUniqueValues = useFacetedUniqueValues(); + const getFacetedUniqueValues = useFacetedUniqueValues({ + canReadOthersBookings: permissions.canReadOthersBookings, + }); const table = useReactTable({ data: rowData, diff --git a/apps/web/modules/bookings/components/BookingListContainer.tsx b/apps/web/modules/bookings/components/BookingListContainer.tsx index cf1bdf2738..a17c43cc06 100644 --- a/apps/web/modules/bookings/components/BookingListContainer.tsx +++ b/apps/web/modules/bookings/components/BookingListContainer.tsx @@ -123,7 +123,9 @@ function BookingListInner({ userTimeZone: user?.timeZone, }); - const getFacetedUniqueValues = useFacetedUniqueValues(); + const getFacetedUniqueValues = useFacetedUniqueValues({ + canReadOthersBookings: permissions.canReadOthersBookings, + }); const displayedFilterCount = useDisplayedFilterCount(); const { currentTab, tabOptions } = useBookingStatusTab(); diff --git a/apps/web/modules/bookings/hooks/useBookingListColumns.tsx b/apps/web/modules/bookings/hooks/useBookingListColumns.tsx index dd0cfbccdf..537458a150 100644 --- a/apps/web/modules/bookings/hooks/useBookingListColumns.tsx +++ b/apps/web/modules/bookings/hooks/useBookingListColumns.tsx @@ -1,13 +1,12 @@ -import { createColumnHelper } from "@tanstack/react-table"; -import { useMemo } from "react"; - import { ColumnFilterType } from "@calcom/features/data-table"; import { isSeparatorRow } from "@calcom/features/data-table/lib/separator"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; +import type useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import BookingListItem from "@calcom/web/components/booking/BookingListItem"; +import { createColumnHelper } from "@tanstack/react-table"; +import { useMemo } from "react"; -import type { RowData, BookingListingStatus } from "../types"; +import type { BookingListingStatus, RowData } from "../types"; export function useBookingListColumns({ user, @@ -55,7 +54,7 @@ export function useBookingListColumns({ columnHelper.accessor((row) => !isSeparatorRow(row) && row.booking.user?.id, { id: "userId", header: t("member"), - enableColumnFilter: canReadOthersBookings, + enableColumnFilter: true, enableSorting: false, cell: () => null, meta: { @@ -148,5 +147,5 @@ export function useBookingListColumns({ }, }), ]; - }, [user, status, t, canReadOthersBookings, bookingsV3Enabled, handleBookingClick]); + }, [user, status, t, bookingsV3Enabled, handleBookingClick]); } diff --git a/apps/web/modules/bookings/hooks/useFacetedUniqueValues.ts b/apps/web/modules/bookings/hooks/useFacetedUniqueValues.ts index c490ec1521..34812515ad 100644 --- a/apps/web/modules/bookings/hooks/useFacetedUniqueValues.ts +++ b/apps/web/modules/bookings/hooks/useFacetedUniqueValues.ts @@ -1,39 +1,63 @@ -import type { Table } from "@tanstack/react-table"; -import { useCallback } from "react"; - import { convertFacetedValuesToMap, type FacetedValue } from "@calcom/features/data-table"; import { trpc } from "@calcom/trpc/react"; +import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; +import type { RowData, Table } from "@tanstack/react-table"; +import { useCallback } from "react"; import { useEventTypes } from "./useEventTypes"; -export function useFacetedUniqueValues() { +interface UseFacetedUniqueValuesOptions { + canReadOthersBookings: boolean; +} + +export function useFacetedUniqueValues({ + canReadOthersBookings, +}: UseFacetedUniqueValuesOptions): ( + table: Table, + columnId: string +) => () => Map { const eventTypes = useEventTypes(); const { data: teams } = trpc.viewer.teams.list.useQuery(); - const { data: members } = trpc.viewer.teams.listSimpleMembers.useQuery(); + const { data: members } = trpc.viewer.teams.listSimpleMembers.useQuery(undefined, { + enabled: canReadOthersBookings, + }); + const { data: currentUser } = useMeQuery(); return useCallback( - (_: Table, columnId: string) => (): Map => { - if (columnId === "eventTypeId") { - return convertFacetedValuesToMap(eventTypes || []); - } else if (columnId === "teamId") { - return convertFacetedValuesToMap( - (teams || []).map((team) => ({ - label: team.name, - value: team.id, - })) - ); - } else if (columnId === "userId") { - return convertFacetedValuesToMap( - (members || []) - .map((member) => ({ - label: member.name, - value: member.id, + (_: Table, columnId: string) => + (): Map => { + if (columnId === "eventTypeId") { + return convertFacetedValuesToMap(eventTypes || []); + } else if (columnId === "teamId") { + return convertFacetedValuesToMap( + (teams || []).map((team) => ({ + label: team.name, + value: team.id, })) - .filter((option): option is { label: string; value: number } => Boolean(option.label)) - ); - } - return new Map(); - }, - [eventTypes, teams, members] + ); + } else if (columnId === "userId") { + if (!canReadOthersBookings) { + if (!currentUser) { + return new Map(); + } + return convertFacetedValuesToMap([ + { + label: currentUser.name || currentUser.email, + value: currentUser.id, + }, + ]); + } + return convertFacetedValuesToMap( + (members || []) + .map((member) => ({ + label: member.name, + value: member.id, + })) + .filter((option): option is { label: string; value: number } => Boolean(option.label)) + ); + } + return new Map(); + }, + [eventTypes, teams, members, canReadOthersBookings, currentUser] ); } diff --git a/apps/web/playwright/booking-filters.e2e.ts b/apps/web/playwright/booking-filters.e2e.ts index 7e00803267..4896e98378 100644 --- a/apps/web/playwright/booking-filters.e2e.ts +++ b/apps/web/playwright/booking-filters.e2e.ts @@ -1,7 +1,5 @@ -import { expect } from "@playwright/test"; - import { MembershipRole } from "@calcom/prisma/enums"; - +import { expect } from "@playwright/test"; import { test } from "./lib/fixtures"; test.describe.configure({ mode: "parallel" }); @@ -9,13 +7,17 @@ test.describe.configure({ mode: "parallel" }); test.afterEach(({ users }) => users.deleteAll()); test.describe("Booking Filters", () => { - test("Member role should not see the member filter", async ({ page, users }) => { + test("Member role should see the member filter with only themselves as option", async ({ page, users }) => { const teamMateName = "team mate 1"; - await users.create(undefined, { - hasTeam: true, - isOrg: true, - teammates: [{ name: teamMateName }], - }); + const ownerName = "team owner"; + await users.create( + { name: ownerName }, + { + hasTeam: true, + isOrg: true, + teammates: [{ name: teamMateName }], + } + ); const allUsers = users.get(); const memberUser = allUsers.find((user) => user.name === teamMateName); @@ -31,7 +33,15 @@ test.describe("Booking Filters", () => { await page.goto(`/bookings/upcoming`, { waitUntil: "domcontentloaded" }); await bookingsGetResponse; await page.locator('[data-testid="add-filter-button"]').click(); - await expect(page.locator('[data-testid="add-filter-item-userId"]')).toBeHidden(); + await expect(page.locator('[data-testid="add-filter-item-userId"]')).toBeVisible(); + + // Click on the member filter to open options + await page.locator('[data-testid="add-filter-item-userId"]').click(); + + // Verify only the current user (member) is shown as an option, not the owner + const filterOptions = page.getByTestId("select-filter-options-userId"); + await expect(filterOptions.getByRole("option", { name: teamMateName })).toBeVisible(); + await expect(filterOptions.getByRole("option", { name: ownerName })).toBeHidden(); }); test("Admin role should see the member filter", async ({ page, users }) => {