This reverts commit 641193bd61.
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user