Revert "fix: replace filter implementations on bookings (#19445)" (#19506)

This reverts commit 641193bd61.
This commit is contained in:
Eunjae Lee
2025-02-24 17:14:06 +00:00
committed by GitHub
parent 5f5cfaf7c7
commit f6af6e34bd
16 changed files with 147 additions and 478 deletions
@@ -1,74 +0,0 @@
import { useSession } from "next-auth/react";
import { useMemo } from "react";
import type { FacetedValue } from "@calcom/features/data-table";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
export function useEventTypes() {
const { data: user } = useSession();
const { t } = useLocale();
const eventTypesQuery = trpc.viewer.eventTypes.listWithTeam.useQuery(undefined, {
enabled: !!user,
});
return useMemo(() => {
const eventTypes = eventTypesQuery.data || [];
// Separate individual and team event types
const individualEvents = eventTypes.filter((el) => !el.team);
// Group team events by team
const teamEventsMap = eventTypes.reduce((acc, event) => {
if (!event.team) return acc;
const teamId = event.team.id;
if (!acc[teamId]) {
acc[teamId] = {
teamName: event.team.name,
events: [],
};
}
acc[teamId].events.push(event);
return acc;
}, {} as Record<number, { teamName: string; events: typeof eventTypes }>);
// Create flat array with section markers
const flatArray: FacetedValue[] = [];
// Add individual events section if exists
if (individualEvents.length > 0) {
flatArray.push({
section: t("individual"),
label: individualEvents[0].title,
value: individualEvents[0].id,
});
flatArray.push(
...individualEvents.slice(1).map((event) => ({
label: event.title,
value: event.id,
}))
);
}
// Add each team's events as a separate section
Object.values(teamEventsMap).forEach(({ teamName, events }) => {
if (events.length > 0) {
flatArray.push({
section: `${teamName} Events`,
label: events[0].title,
value: events[0].id,
});
flatArray.push(
...events.slice(1).map((event) => ({
label: event.title,
value: event.id,
}))
);
}
});
return flatArray;
}, [eventTypesQuery, t]);
}
@@ -1,39 +0,0 @@
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 { useEventTypes } from "./useEventTypes";
export function useFacetedUniqueValues() {
const eventTypes = useEventTypes();
const { data: teams } = trpc.viewer.teams.list.useQuery();
const { data: members } = trpc.viewer.teams.listSimpleMembers.useQuery();
return useCallback(
(_: Table<any>, columnId: string) => (): Map<FacetedValue, number> => {
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,
}))
.filter((option): option is { label: string; value: number } => Boolean(option.label))
);
}
return new Map<FacetedValue, number>();
},
[eventTypes, teams, members]
);
}
@@ -1,39 +0,0 @@
// eslint-disable-next-line no-restricted-imports
import { debounce } from "lodash";
import { useCallback, useEffect, useRef } from "react";
// Dynamically adjusts DataTable height on mobile to prevent nested scrolling
// and ensure the table fits within the viewport without overflowing (hacky)
export function useProperHeightForMobile(ref: React.RefObject<HTMLDivElement>) {
const lastOffsetY = useRef<number>();
const lastWindowHeight = useRef<number>();
const BOTTOM_NAV_HEIGHT = 64;
const BUFFER = 32;
const updateHeight = useCallback(
debounce(() => {
if (!ref.current || window.innerWidth >= 640) return;
const rect = ref.current.getBoundingClientRect();
if (rect.top !== lastOffsetY.current || window.innerHeight !== lastWindowHeight.current) {
lastOffsetY.current = rect.top;
lastWindowHeight.current = window.innerHeight;
const height = window.innerHeight - lastOffsetY.current - BOTTOM_NAV_HEIGHT - BUFFER;
ref.current.style.height = `${height}px`;
}
}, 200),
[ref.current]
);
useEffect(() => {
const handleResize = () => {
updateHeight();
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, [updateHeight]);
updateHeight();
}
@@ -7,19 +7,18 @@ import {
getSortedRowModel,
createColumnHelper,
} from "@tanstack/react-table";
import { useMemo, useRef } from "react";
// eslint-disable-next-line no-restricted-imports
import { debounce } from "lodash";
import { useMemo, useState, useRef, useEffect, useCallback } from "react";
import type { z } from "zod";
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
import dayjs from "@calcom/dayjs";
import {
DataTableProvider,
DataTableWrapper,
DataTableFilters,
ColumnFilterType,
useFilterValue,
ZMultiSelectFilterValue,
ZDateRangeFilterValue,
} from "@calcom/features/data-table";
import { FilterToggle } from "@calcom/features/bookings/components/FilterToggle";
import { FiltersContainer } from "@calcom/features/bookings/components/FiltersContainer";
import type { filterQuerySchema } from "@calcom/features/bookings/lib/useFilterQuery";
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
import { DataTableProvider, DataTableWrapper } from "@calcom/features/data-table";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
@@ -31,11 +30,9 @@ import useMeQuery from "@lib/hooks/useMeQuery";
import BookingListItem from "@components/booking/BookingListItem";
import SkeletonLoader from "@components/booking/SkeletonLoader";
import { useFacetedUniqueValues } from "~/bookings/hooks/useFacetedUniqueValues";
import { useProperHeightForMobile } from "~/bookings/hooks/useProperHeightForMobile";
import type { validStatuses } from "~/bookings/lib/validStatuses";
type BookingListingStatus = (typeof validStatuses)[number];
type BookingListingStatus = z.infer<NonNullable<typeof filterQuerySchema>>["status"];
type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
type RecurringInfo = {
@@ -73,7 +70,7 @@ const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
},
];
const descriptionByStatus: Record<BookingListingStatus, string> = {
const descriptionByStatus: Record<NonNullable<BookingListingStatus>, string> = {
upcoming: "upcoming_bookings",
recurring: "recurring_bookings",
past: "past_bookings",
@@ -105,29 +102,24 @@ type RowData =
};
function BookingsContent({ status }: BookingsProps) {
const { data: filterQuery } = useFilterQuery();
const { t } = useLocale();
const user = useMeQuery().data;
const [isFiltersVisible, setIsFiltersVisible] = useState<boolean>(false);
const tableContainerRef = useRef<HTMLDivElement>(null);
useProperHeightForMobile(tableContainerRef);
const eventTypeIds = useFilterValue("eventTypeId", ZMultiSelectFilterValue)?.data as number[] | undefined;
const teamIds = useFilterValue("teamId", ZMultiSelectFilterValue)?.data as number[] | undefined;
const userIds = useFilterValue("userId", ZMultiSelectFilterValue)?.data as number[] | undefined;
const dateRange = useFilterValue("time", ZDateRangeFilterValue)?.data;
const query = trpc.viewer.bookings.get.useInfiniteQuery(
{
limit: 10,
filters: {
status,
eventTypeIds,
teamIds,
userIds,
afterStartDate: dateRange?.startDate ?? undefined,
beforeEndDate: dateRange?.endDate ?? undefined,
...filterQuery,
status: filterQuery.status ?? status,
},
},
{
enabled: true,
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);
@@ -136,60 +128,8 @@ function BookingsContent({ status }: BookingsProps) {
const columnHelper = createColumnHelper<RowData>();
return [
columnHelper.accessor((row) => row.type === "data" && row.booking.eventType.id, {
id: "eventTypeId",
header: t("event_type"),
enableColumnFilter: true,
enableSorting: false,
cell: () => null,
filterFn: () => true,
meta: {
filter: {
type: ColumnFilterType.MULTI_SELECT,
},
},
}),
columnHelper.accessor((row) => row.type === "data" && row.booking.eventType.team?.id, {
id: "teamId",
header: t("team"),
enableColumnFilter: true,
enableSorting: false,
cell: () => null,
filterFn: () => true,
meta: {
filter: {
type: ColumnFilterType.MULTI_SELECT,
},
},
}),
columnHelper.accessor((row) => row.type === "data" && row.booking.user?.id, {
id: "userId",
header: t("people"),
enableColumnFilter: true,
enableSorting: false,
cell: () => null,
filterFn: () => true,
meta: {
filter: {
type: ColumnFilterType.MULTI_SELECT,
},
},
}),
columnHelper.accessor((row) => row, {
id: "time",
header: t("date_range"),
enableColumnFilter: true,
enableSorting: false,
cell: () => null,
filterFn: () => true,
meta: {
filter: {
type: ColumnFilterType.DATE_RANGE,
},
},
}),
columnHelper.display({
id: "customView",
id: "custom-view",
cell: (props) => {
if (props.row.original.type === "data") {
const { booking, recurringInfo, isToday } = props.row.original;
@@ -224,7 +164,7 @@ function BookingsContent({ status }: BookingsProps) {
},
}),
];
}, [user, status, t]);
}, [user, status]);
const isEmpty = useMemo(() => !query.data?.pages[0]?.bookings.length, [query.data]);
@@ -301,23 +241,12 @@ function BookingsContent({ status }: BookingsProps) {
return merged;
}, [bookingsToday, flatData, status]);
const getFacetedUniqueValues = useFacetedUniqueValues();
const table = useReactTable<RowData>({
data: finalData,
columns,
initialState: {
columnVisibility: {
eventTypeId: false,
teamId: false,
userId: false,
time: false,
},
},
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedUniqueValues,
});
return (
@@ -329,14 +258,16 @@ function BookingsContent({ status }: BookingsProps) {
name: t(tab.name),
}))}
/>
<FilterToggle setIsFiltersVisible={setIsFiltersVisible} />
</div>
<FiltersContainer isFiltersVisible={isFiltersVisible} />
<main className="w-full">
<div className="flex w-full flex-col">
{query.status === "error" && (
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
)}
{(query.status === "pending" || query.isPaused) && <SkeletonLoader />}
{query.status === "success" && (
{query.status === "success" && !isEmpty && (
<>
{!!bookingsToday.length && status === "upcoming" && (
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
@@ -352,30 +283,59 @@ function BookingsContent({ status }: BookingsProps) {
fetchNextPage={query.fetchNextPage}
isFetching={query.isFetching}
variant="compact"
ToolbarLeft={
<>
<DataTableFilters.AddFilterButton table={table} />
<DataTableFilters.ActiveFilters table={table} />
<DataTableFilters.ClearFiltersButton />
</>
}
EmptyView={
<div className="flex items-center justify-center pt-2 xl:pt-0">
<EmptyScreen
Icon="calendar"
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
description={t("no_status_bookings_yet_description", {
status: t(status).toLowerCase(),
description: t(descriptionByStatus[status]),
})}
/>
</div>
}
/>
</>
)}
{query.status === "success" && isEmpty && (
<div className="flex items-center justify-center pt-2 xl:pt-0">
<EmptyScreen
Icon="calendar"
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
description={t("no_status_bookings_yet_description", {
status: t(status).toLowerCase(),
description: t(descriptionByStatus[status]),
})}
/>
</div>
)}
</div>
</main>
</div>
);
}
// Dynamically adjusts DataTable height on mobile to prevent nested scrolling
// and ensure the table fits within the viewport without overflowing (hacky)
function useProperHeightForMobile(ref: React.RefObject<HTMLDivElement>) {
const lastOffsetY = useRef<number>();
const lastWindowHeight = useRef<number>();
const BOTTOM_NAV_HEIGHT = 64;
const BUFFER = 32;
const updateHeight = useCallback(
debounce(() => {
if (!ref.current || window.innerWidth >= 640) return;
const rect = ref.current.getBoundingClientRect();
if (rect.top !== lastOffsetY.current || window.innerHeight !== lastWindowHeight.current) {
lastOffsetY.current = rect.top;
lastWindowHeight.current = window.innerHeight;
const height = window.innerHeight - lastOffsetY.current - BOTTOM_NAV_HEIGHT - BUFFER;
ref.current.style.height = `${height}px`;
}
}, 200),
[ref.current]
);
useEffect(() => {
const handleResize = () => {
updateHeight();
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, [updateHeight]);
updateHeight();
}
@@ -265,6 +265,7 @@ function DataTableBody<TData>({
className={classNames(onRowMouseclick && "hover:cursor-pointer", "group")}>
{row.getVisibleCells().map((cell) => {
const column = cell.column;
const meta = column?.columnDef.meta;
return (
<TableCell
key={cell.id}
@@ -5,12 +5,12 @@ import { useEffect, useRef } from "react";
import {
DataTable,
DataTableToolbar,
DataTablePagination,
useFetchMoreOnBottomReached,
useDataTable,
useColumnFilters,
} from "@calcom/features/data-table";
import { classNames } from "@calcom/lib";
export type DataTableWrapperProps<TData, TValue> = {
testId?: string;
@@ -25,7 +25,6 @@ export type DataTableWrapperProps<TData, TValue> = {
totalDBRowCount?: number;
ToolbarLeft?: React.ReactNode;
ToolbarRight?: React.ReactNode;
EmptyView?: React.ReactNode;
className?: string;
containerClassName?: string;
children?: React.ReactNode;
@@ -45,7 +44,6 @@ export function DataTableWrapper<TData, TValue>({
hideHeader,
ToolbarLeft,
ToolbarRight,
EmptyView,
className,
containerClassName,
children,
@@ -81,12 +79,21 @@ export function DataTableWrapper<TData, TValue>({
}));
}, [table, sorting, columnFilters, columnVisibility]);
const showEmptyView = table.getRowCount() === 0 && EmptyView;
return (
<>
<DataTable
testId={testId}
bodyTestId={bodyTestId}
table={table}
tableContainerRef={tableContainerRef}
isPending={isPending}
enableColumnResizing={true}
hideHeader={hideHeader}
variant={variant}
className={className}
containerClassName={containerClassName}
onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}>
{(ToolbarLeft || ToolbarRight || children) && (
<div className={classNames("grid w-full items-center gap-2 py-4", className)}>
<DataTableToolbar.Root>
<div className="flex w-full flex-col gap-2">
<div className="flex w-full flex-wrap justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">{ToolbarLeft}</div>
@@ -95,29 +102,14 @@ export function DataTableWrapper<TData, TValue>({
</div>
{children}
</DataTableToolbar.Root>
)}
{totalDBRowCount && (
<div style={{ gridArea: "footer", marginTop: "1rem" }}>
<DataTablePagination table={table} totalDbDataCount={totalDBRowCount} />
</div>
)}
{showEmptyView && EmptyView}
{!showEmptyView && (
<DataTable
testId={testId}
bodyTestId={bodyTestId}
table={table}
tableContainerRef={tableContainerRef}
isPending={isPending}
enableColumnResizing={true}
hideHeader={hideHeader}
variant={variant}
className={className}
containerClassName={containerClassName}
onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}>
{totalDBRowCount && (
<div style={{ gridArea: "footer", marginTop: "1rem" }}>
<DataTablePagination table={table} totalDbDataCount={totalDBRowCount} />
</div>
)}
</DataTable>
)}
</>
</DataTable>
);
}
@@ -5,8 +5,6 @@ import { type Table } from "@tanstack/react-table";
import { Fragment } from "react";
import { useDataTable, useFilterableColumns } from "../../hooks";
import { ColumnFilterType } from "../../lib/types";
import { DateRangeFilter } from "./DateRangeFilter";
import { FilterPopover } from "./FilterPopover";
// Add the new ActiveFilters component
@@ -23,12 +21,7 @@ export function ActiveFilters<TData>({ table }: ActiveFiltersProps<TData>) {
{activeFilters.map((filter) => {
const column = filterableColumns.find((col) => col.id === filter.f);
if (!column) return null;
if (column.type === ColumnFilterType.DATE_RANGE) {
return <DateRangeFilter key={column.id} column={column} showClearButton />;
} else {
return <FilterPopover key={column.id} column={column} />;
}
return <FilterPopover key={column.id} column={column} />;
})}
</>
);
@@ -62,10 +62,7 @@ function AddFilterButtonComponent<TData>(
<CommandList>
<CommandEmpty>{t("no_columns_found")}</CommandEmpty>
{filterableColumns.map((column) => {
const showHiddenIndicator =
!table.getColumn(column.id)?.getIsVisible() &&
table.initialState.columnVisibility?.[column.id] !== false;
const isVisible = table.getColumn(column.id)?.getIsVisible();
if (activeFilters?.some((filter) => filter.f === column.id)) return null;
return (
<CommandItem
@@ -74,7 +71,7 @@ function AddFilterButtonComponent<TData>(
className="flex items-center justify-between px-4 py-2"
data-testid={`add-filter-item-${column.id}`}>
<span>{startCase(column.title)}</span>
{showHiddenIndicator && <Icon name="eye-off" className="h-4 w-4 opacity-50" />}
{!isVisible && <Icon name="eye-off" className="h-4 w-4 opacity-50" />}
</CommandItem>
);
})}
@@ -15,9 +15,6 @@ import {
Command,
CommandList,
CommandItem,
CommandGroup,
CommandSeparator,
buttonClasses,
} from "@calcom/ui";
import { useDataTable, useFilterValue } from "../../hooks";
@@ -35,7 +32,6 @@ import { ZDateRangeFilterValue, ColumnFilterType } from "../../lib/types";
type DateRangeFilterProps = {
column: Extract<FilterableColumn, { type: ColumnFilterType.DATE_RANGE }>;
showClearButton?: boolean;
};
const getDateRangeFromPreset = (val: string | null) => {
@@ -76,9 +72,9 @@ const getDateRangeFromPreset = (val: string | null) => {
return { startDate, endDate, preset };
};
export const DateRangeFilter = ({ column, showClearButton = false }: DateRangeFilterProps) => {
export const DateRangeFilter = ({ column }: DateRangeFilterProps) => {
const filterValue = useFilterValue(column.id, ZDateRangeFilterValue);
const { updateFilter, removeFilter } = useDataTable();
const { updateFilter } = useDataTable();
const { t } = useLocale();
const currentDate = dayjs();
@@ -88,11 +84,7 @@ export const DateRangeFilter = ({ column, showClearButton = false }: DateRangeFi
const [endDate, setEndDate] = useState<Dayjs | undefined>(
filterValue?.data.endDate ? dayjs(filterValue.data.endDate) : getDefaultEndDate()
);
const [selectedPreset, setSelectedPreset] = useState<PresetOption>(
filterValue?.data.preset
? PRESET_OPTIONS.find((o) => o.value === filterValue.data.preset) ?? DEFAULT_PRESET
: DEFAULT_PRESET
);
const [selectedPreset, setSelectedPreset] = useState<PresetOption>(DEFAULT_PRESET);
const updateValues = ({
preset,
@@ -200,23 +192,6 @@ export const DateRangeFilter = ({ column, showClearButton = false }: DateRangeFi
</CommandItem>
))}
</CommandList>
{showClearButton && (
<>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => {
removeFilter(column.id);
}}
className={classNames(
"w-full justify-center text-center",
buttonClasses({ color: "secondary" })
)}>
{t("clear")}
</CommandItem>
</CommandGroup>
</>
)}
</Command>
</PopoverContent>
</Popover>
@@ -2,6 +2,7 @@
import type { FilterableColumn } from "../../lib/types";
import { ColumnFilterType } from "../../lib/types";
import { DateRangeFilter } from "./DateRangeFilter";
import { MultiSelectFilterOptions } from "./MultiSelectFilterOptions";
import { NumberFilterOptions } from "./NumberFilterOptions";
import { SingleSelectFilterOptions } from "./SingleSelectFilterOptions";
@@ -20,6 +21,8 @@ export function FilterOptions({ column }: FilterOptionsProps) {
return <SingleSelectFilterOptions column={column} />;
} else if (column.type === ColumnFilterType.NUMBER) {
return <NumberFilterOptions column={column} />;
} else if (column.type === ColumnFilterType.DATE_RANGE) {
return <DateRangeFilter column={column} />;
} else {
return null;
}
@@ -32,42 +32,31 @@ export function MultiSelectFilterOptions({ column }: MultiSelectFilterOptionsPro
<CommandInput placeholder={t("search")} />
<CommandList>
<CommandEmpty>{t("no_options_found")}</CommandEmpty>
{column.options.map((option, index) => {
{column.options.map((option) => {
if (!option) return null;
const {
label: optionLabel,
value: optionValue,
section,
} = typeof option === "string" ? { label: option, value: option, section: undefined } : option;
const { label: optionLabel, value: optionValue } =
typeof option === "string" ? { label: option, value: option } : option;
return (
<>
{section && index !== 0 && <hr className="border-subtle my-1" />}
{section && (
<div className="text-subtle px-4 py-2 text-xs font-medium uppercase leading-none">
{section}
</div>
)}
<CommandItem
key={optionValue}
onSelect={() => {
const newFilterValue = filterValue?.data.includes(optionValue)
? filterValue?.data.filter((value) => value !== optionValue)
: [...(filterValue?.data || []), optionValue];
updateFilter(column.id, { type: ColumnFilterType.MULTI_SELECT, data: newFilterValue });
}}>
<div
className={classNames(
"border-subtle mr-2 flex h-4 w-4 items-center justify-center rounded-sm border",
filterValue?.data.includes(optionValue) ? "bg-primary" : "opacity-50"
)}>
{filterValue?.data.includes(optionValue) && (
<Icon name="check" className="text-primary-foreground h-4 w-4" />
)}
</div>
{optionLabel}
</CommandItem>
</>
<CommandItem
key={optionValue}
onSelect={() => {
const newFilterValue = filterValue?.data.includes(optionValue)
? filterValue?.data.filter((value) => value !== optionValue)
: [...(filterValue?.data || []), optionValue];
updateFilter(column.id, { type: ColumnFilterType.MULTI_SELECT, data: newFilterValue });
}}>
<div
className={classNames(
"border-subtle mr-2 flex h-4 w-4 items-center justify-center rounded-sm border",
filterValue?.data.includes(optionValue) ? "bg-primary" : "opacity-50"
)}>
{filterValue?.data.includes(optionValue) && (
<Icon name="check" className="text-primary-foreground h-4 w-4" />
)}
</div>
{optionLabel}
</CommandItem>
);
})}
</CommandList>
@@ -32,39 +32,28 @@ export function SingleSelectFilterOptions({ column }: SingleSelectFilterOptionsP
<CommandInput placeholder={t("search")} />
<CommandList>
<CommandEmpty>{t("no_options_found")}</CommandEmpty>
{column.options.map((option, index) => {
{column.options.map((option) => {
if (!option) return null;
const {
label: optionLabel,
value: optionValue,
section,
} = typeof option === "string" ? { label: option, value: option, section: undefined } : option;
const { label: optionLabel, value: optionValue } =
typeof option === "string" ? { label: option, value: option } : option;
return (
<>
{section && index !== 0 && <hr className="border-subtle my-1" />}
{section && (
<div className="text-subtle px-4 py-2 text-xs font-medium uppercase leading-none">
{section}
</div>
)}
<CommandItem
key={optionValue}
onSelect={() => {
updateFilter(column.id, { type: ColumnFilterType.SINGLE_SELECT, data: optionValue });
}}>
<div
className={classNames(
"border-subtle mr-2 flex h-4 w-4 items-center justify-center rounded-sm border",
filterValue?.data === optionValue ? "bg-primary" : "opacity-50"
)}>
{filterValue?.data === optionValue && (
<Icon name="check" className="text-primary-foreground h-4 w-4" />
)}
</div>
{optionLabel}
</CommandItem>
</>
<CommandItem
key={optionValue}
onSelect={() => {
updateFilter(column.id, { type: ColumnFilterType.SINGLE_SELECT, data: optionValue });
}}>
<div
className={classNames(
"border-subtle mr-2 flex h-4 w-4 items-center justify-center rounded-sm border",
filterValue?.data === optionValue ? "bg-primary" : "opacity-50"
)}>
{filterValue?.data === optionValue && (
<Icon name="check" className="text-primary-foreground h-4 w-4" />
)}
</div>
{optionLabel}
</CommandItem>
);
})}
</CommandList>
+2 -3
View File
@@ -128,11 +128,11 @@ export type FilterableColumn = {
} & (
| {
type: ColumnFilterType.SINGLE_SELECT;
options: FacetedValue[];
options: Array<{ label: string; value: string | number }>;
}
| {
type: ColumnFilterType.MULTI_SELECT;
options: FacetedValue[];
options: Array<{ label: string; value: string | number }>;
}
| {
type: ColumnFilterType.TEXT;
@@ -186,5 +186,4 @@ export const ZColumnVisibility = z.record(z.string(), z.boolean());
export type FacetedValue = {
label: string;
value: string | number;
section?: string;
};
+3 -3
View File
@@ -151,7 +151,7 @@ export const dataTableFilter = (cellValue: unknown, filterValue: FilterValue) =>
export const convertFacetedValuesToMap = (array: FacetedValue[]) => {
return new Map<FacetedValue, number>(
array.map((option) => [{ label: option.label, value: option.value, section: option.section }, 1])
array.map((option) => [{ label: option.label, value: option.value }, 1])
);
};
@@ -161,8 +161,8 @@ export const convertMapToFacetedValues = (map: Map<FacetedValue, number> | undef
}
return Array.from(map.keys()).map((option) => {
if (typeof option === "string") {
return { label: option, value: option, section: undefined };
return { label: option, value: option };
}
return { label: option.label as string, value: option.value as string | number, section: option.section };
return { label: option.label as string, value: option.value as string | number };
});
};
@@ -123,13 +123,6 @@ export const viewerTeamsRouter = router({
const handler = await importHandler(namespaced("listMembers"), () => import("./listMembers.handler"));
return handler(opts);
}),
listSimpleMembers: authedProcedure.query(async (opts) => {
const handler = await importHandler(
namespaced("listSimpleMembers"),
() => import("./listSimpleMembers.handler")
);
return handler(opts);
}),
legacyListMembers: authedProcedure.input(ZLegacyListMembersInputSchema).query(async (opts) => {
const handler = await importHandler(
namespaced("legacyListMembers"),
@@ -1,70 +0,0 @@
/**
* Simplified version of legacyListMembers.handler.ts that returns basic member info.
* Used for filtering people on /bookings.
*/
import type { PrismaClient } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
type ListSimpleMembersOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
prisma: PrismaClient;
};
};
export const listSimpleMembers = async ({ ctx }: ListSimpleMembersOptions) => {
const { prisma } = ctx;
const { isOrgAdmin } = ctx.user.organization;
const hasPermsToView = !ctx.user.organization.isPrivate || isOrgAdmin;
if (!hasPermsToView) {
return [];
}
// query all teams the user is a member of
const teamsToQuery = (
await prisma.membership.findMany({
where: {
userId: ctx.user.id,
accepted: true,
},
select: { teamId: true },
})
).map((membership) => membership.teamId);
if (!teamsToQuery.length) {
return [];
}
// Fetch unique users through memberships
const members = (
await prisma.membership.findMany({
where: {
accepted: true,
teamId: { in: teamsToQuery },
},
select: {
id: true,
accepted: true,
user: {
select: {
id: true,
name: true,
username: true,
avatarUrl: true,
email: true,
},
},
},
distinct: ["userId"],
orderBy: [
{ userId: "asc" }, // First order by userId to ensure consistent ordering
{ id: "asc" }, // Then by id as secondary sort
],
})
).map((membership) => membership.user);
return members;
};
export default listSimpleMembers;