fix: apply standard pagination to /bookings (#19973)
* fix: apply standard pagination to /bookings * rename params * fix type error --------- Co-authored-by: Benny Joo <sldisek783@gmail.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
This commit is contained in:
co-authored by
Benny Joo
Udit Takkar
parent
209a401113
commit
367e2666c6
@@ -1,42 +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 useStretchedHeightToBottom(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) return;
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
if (rect.top !== lastOffsetY.current || window.innerHeight !== lastWindowHeight.current) {
|
||||
lastOffsetY.current = rect.top;
|
||||
lastWindowHeight.current = window.innerHeight;
|
||||
let height = window.innerHeight - lastOffsetY.current - BUFFER;
|
||||
if (window.innerWidth < 640) {
|
||||
height = height - BOTTOM_NAV_HEIGHT;
|
||||
}
|
||||
ref.current.style.height = `${height}px`;
|
||||
}
|
||||
}, 200),
|
||||
[ref.current]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
updateHeight();
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, [updateHeight]);
|
||||
|
||||
updateHeight();
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { useMemo, useRef } from "react";
|
||||
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import {
|
||||
useDataTable,
|
||||
DataTableProvider,
|
||||
DataTableWrapper,
|
||||
DataTableFilters,
|
||||
@@ -33,7 +34,6 @@ import BookingListItem from "@components/booking/BookingListItem";
|
||||
import SkeletonLoader from "@components/booking/SkeletonLoader";
|
||||
|
||||
import { useFacetedUniqueValues } from "~/bookings/hooks/useFacetedUniqueValues";
|
||||
import { useStretchedHeightToBottom } from "~/bookings/hooks/useStretchedHeightToBottom";
|
||||
import type { validStatuses } from "~/bookings/lib/validStatuses";
|
||||
|
||||
type BookingListingStatus = (typeof validStatuses)[number];
|
||||
@@ -109,7 +109,6 @@ function BookingsContent({ status }: BookingsProps) {
|
||||
const { t } = useLocale();
|
||||
const user = useMeQuery().data;
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
useStretchedHeightToBottom(tableContainerRef);
|
||||
|
||||
const eventTypeIds = useFilterValue("eventTypeId", ZMultiSelectFilterValue)?.data as number[] | undefined;
|
||||
const teamIds = useFilterValue("teamId", ZMultiSelectFilterValue)?.data as number[] | undefined;
|
||||
@@ -118,26 +117,24 @@ function BookingsContent({ status }: BookingsProps) {
|
||||
const attendeeName = useFilterValue("attendeeName", ZTextFilterValue);
|
||||
const attendeeEmail = useFilterValue("attendeeEmail", ZTextFilterValue);
|
||||
|
||||
const query = trpc.viewer.bookings.get.useInfiniteQuery(
|
||||
{
|
||||
limit: 10,
|
||||
filters: {
|
||||
status,
|
||||
eventTypeIds,
|
||||
teamIds,
|
||||
userIds,
|
||||
attendeeName,
|
||||
attendeeEmail,
|
||||
afterStartDate: dateRange?.startDate
|
||||
? dayjs(dateRange?.startDate).startOf("day").toISOString()
|
||||
: undefined,
|
||||
beforeEndDate: dateRange?.endDate ? dayjs(dateRange?.endDate).endOf("day").toISOString() : undefined,
|
||||
},
|
||||
const { limit, offset } = useDataTable();
|
||||
|
||||
const query = trpc.viewer.bookings.get.useQuery({
|
||||
limit,
|
||||
offset,
|
||||
filters: {
|
||||
status,
|
||||
eventTypeIds,
|
||||
teamIds,
|
||||
userIds,
|
||||
attendeeName,
|
||||
attendeeEmail,
|
||||
afterStartDate: dateRange?.startDate
|
||||
? dayjs(dateRange?.startDate).startOf("day").toISOString()
|
||||
: undefined,
|
||||
beforeEndDate: dateRange?.endDate ? dayjs(dateRange?.endDate).endOf("day").toISOString() : undefined,
|
||||
},
|
||||
{
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const columnHelper = createColumnHelper<RowData>();
|
||||
@@ -262,7 +259,7 @@ function BookingsContent({ status }: BookingsProps) {
|
||||
];
|
||||
}, [user, status, t]);
|
||||
|
||||
const isEmpty = useMemo(() => !query.data?.pages[0]?.bookings.length, [query.data]);
|
||||
const isEmpty = useMemo(() => !query.data?.bookings.length, [query.data]);
|
||||
|
||||
const flatData = useMemo<RowData[]>(() => {
|
||||
const shownBookings: Record<string, BookingOutput[]> = {};
|
||||
@@ -289,37 +286,33 @@ function BookingsContent({ status }: BookingsProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
query.data?.pages.flatMap((page) =>
|
||||
page.bookings.filter(filterBookings).map((booking) => ({
|
||||
type: "data",
|
||||
booking,
|
||||
recurringInfo: page.recurringInfo.find(
|
||||
(info) => info.recurringEventId === booking.recurringEventId
|
||||
),
|
||||
isToday: false,
|
||||
}))
|
||||
) || []
|
||||
query.data?.bookings.filter(filterBookings).map((booking) => ({
|
||||
type: "data",
|
||||
booking,
|
||||
recurringInfo: query.data?.recurringInfo.find(
|
||||
(info) => info.recurringEventId === booking.recurringEventId
|
||||
),
|
||||
isToday: false,
|
||||
})) || []
|
||||
);
|
||||
}, [query.data]);
|
||||
|
||||
const bookingsToday = useMemo<RowData[]>(() => {
|
||||
return (
|
||||
query.data?.pages.flatMap((page) =>
|
||||
page.bookings
|
||||
.filter(
|
||||
(booking: BookingOutput) =>
|
||||
dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") ===
|
||||
dayjs().tz(user?.timeZone).format("YYYY-MM-DD")
|
||||
)
|
||||
.map((booking) => ({
|
||||
type: "data" as const,
|
||||
booking,
|
||||
recurringInfo: page.recurringInfo.find(
|
||||
(info) => info.recurringEventId === booking.recurringEventId
|
||||
),
|
||||
isToday: true,
|
||||
}))
|
||||
) || []
|
||||
query.data?.bookings
|
||||
.filter(
|
||||
(booking: BookingOutput) =>
|
||||
dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") ===
|
||||
dayjs().tz(user?.timeZone).format("YYYY-MM-DD")
|
||||
)
|
||||
.map((booking) => ({
|
||||
type: "data" as const,
|
||||
booking,
|
||||
recurringInfo: query.data?.recurringInfo.find(
|
||||
(info) => info.recurringEventId === booking.recurringEventId
|
||||
),
|
||||
isToday: true,
|
||||
})) ?? []
|
||||
);
|
||||
}, [query.data]);
|
||||
|
||||
@@ -379,17 +372,16 @@ function BookingsContent({ status }: BookingsProps) {
|
||||
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
|
||||
)}
|
||||
<DataTableWrapper
|
||||
className="mb-6"
|
||||
tableContainerRef={tableContainerRef}
|
||||
table={table}
|
||||
testId={`${status}-bookings`}
|
||||
bodyTestId="bookings"
|
||||
hideHeader={true}
|
||||
isPending={query.isPending}
|
||||
hasNextPage={query.hasNextPage}
|
||||
fetchNextPage={query.fetchNextPage}
|
||||
isFetching={query.isFetching}
|
||||
totalRowCount={query.data?.totalCount}
|
||||
variant="compact"
|
||||
paginationMode="infinite"
|
||||
paginationMode="standard"
|
||||
ToolbarLeft={
|
||||
<>
|
||||
<DataTableFilters.AddFilterButton table={table} hideWhenFilterApplied />
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
useDataTable,
|
||||
useColumnFilters,
|
||||
} from "@calcom/features/data-table";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
|
||||
type BaseDataTableWrapperProps<TData> = {
|
||||
testId?: string;
|
||||
@@ -110,7 +109,7 @@ export function DataTableWrapper<TData>({
|
||||
return (
|
||||
<>
|
||||
{(ToolbarLeft || ToolbarRight || children) && (
|
||||
<div className={classNames("grid w-full items-center gap-2 py-4", className)}>
|
||||
<div className="grid w-full items-center gap-2 pb-4">
|
||||
<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>
|
||||
|
||||
@@ -77,7 +77,7 @@ const getAllUserBookings = async ({ ctx, filters, bookingListingByStatus, take,
|
||||
|
||||
const combinedFilters = bookingListingByStatus.map((status) => bookingListingFilters[status]);
|
||||
|
||||
const { bookings, recurringInfo } = await getBookings({
|
||||
const { bookings, recurringInfo, totalCount } = await getBookings({
|
||||
user,
|
||||
prisma,
|
||||
passedBookingsStatusFilter: {
|
||||
@@ -89,18 +89,10 @@ const getAllUserBookings = async ({ ctx, filters, bookingListingByStatus, take,
|
||||
skip,
|
||||
});
|
||||
|
||||
const bookingsFetched = bookings.length;
|
||||
let nextCursor: typeof skip | null = skip;
|
||||
if (bookingsFetched > take) {
|
||||
nextCursor += bookingsFetched;
|
||||
} else {
|
||||
nextCursor = null;
|
||||
}
|
||||
|
||||
return {
|
||||
bookings,
|
||||
recurringInfo,
|
||||
nextCursor,
|
||||
totalCount,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -29,24 +29,24 @@ const log = logger.getSubLogger({ prefix: ["bookings.get"] });
|
||||
export const getHandler = async ({ ctx, input }: GetOptions) => {
|
||||
// using offset actually because cursor pagination requires a unique column
|
||||
// for orderBy, but we don't use a unique column in our orderBy
|
||||
const take = input.limit ?? 10;
|
||||
const skip = input.cursor ?? 0;
|
||||
const take = input.limit;
|
||||
const skip = input.offset;
|
||||
const { prisma, user } = ctx;
|
||||
const defaultStatus = "upcoming";
|
||||
const bookingListingByStatus = [input.filters.status || defaultStatus];
|
||||
|
||||
const { bookings, recurringInfo, nextCursor } = await getAllUserBookings({
|
||||
const { bookings, recurringInfo, totalCount } = await getAllUserBookings({
|
||||
ctx: { user: { id: user.id, email: user.email }, prisma: prisma },
|
||||
bookingListingByStatus: bookingListingByStatus,
|
||||
take: take,
|
||||
skip: skip,
|
||||
take,
|
||||
skip,
|
||||
filters: input.filters,
|
||||
});
|
||||
|
||||
return {
|
||||
bookings,
|
||||
recurringInfo,
|
||||
nextCursor,
|
||||
totalCount,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -183,167 +183,174 @@ export async function getBookings({
|
||||
getUserIdsWhereUserIsOrgAdminOrOwner(prisma, membershipConditionWhereUserIsAdminOwner),
|
||||
]);
|
||||
|
||||
const plainBookings = await prisma.booking.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
userId: user.id,
|
||||
const whereClause = {
|
||||
OR: [
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
{
|
||||
attendees: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
{
|
||||
attendees: {
|
||||
some: {
|
||||
},
|
||||
{
|
||||
eventTypeId: {
|
||||
in: eventTypeIdsWhereUserIsAdminOrOwener,
|
||||
},
|
||||
},
|
||||
{
|
||||
userId: {
|
||||
in: userIdsWhereUserIsOrgAdminOrOwener,
|
||||
},
|
||||
},
|
||||
{
|
||||
seatsReferences: {
|
||||
some: {
|
||||
attendee: {
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
eventTypeId: {
|
||||
in: eventTypeIdsWhereUserIsAdminOrOwener,
|
||||
},
|
||||
},
|
||||
{
|
||||
userId: {
|
||||
in: userIdsWhereUserIsOrgAdminOrOwener,
|
||||
},
|
||||
},
|
||||
{
|
||||
seatsReferences: {
|
||||
some: {
|
||||
attendee: {
|
||||
email: user.email,
|
||||
},
|
||||
],
|
||||
AND: [
|
||||
passedBookingsStatusFilter,
|
||||
...(eventTypeIdsFromTeamIdsFilter
|
||||
? [
|
||||
{
|
||||
eventTypeId: {
|
||||
in: eventTypeIdsFromTeamIdsFilter,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
AND: [
|
||||
passedBookingsStatusFilter,
|
||||
...(eventTypeIdsFromTeamIdsFilter
|
||||
? [
|
||||
{
|
||||
eventTypeId: {
|
||||
in: eventTypeIdsFromTeamIdsFilter,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(filters?.userIds && filters.userIds.length > 0
|
||||
? [
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
userId: {
|
||||
in: filters.userIds,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(filters?.userIds && filters.userIds.length > 0
|
||||
? [
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
userId: {
|
||||
in: filters.userIds,
|
||||
},
|
||||
...(attendeeEmailsFromUserIdsFilter?.length
|
||||
? [
|
||||
{
|
||||
attendees: {
|
||||
some: {
|
||||
email: {
|
||||
in: attendeeEmailsFromUserIdsFilter,
|
||||
},
|
||||
},
|
||||
...(attendeeEmailsFromUserIdsFilter?.length
|
||||
? [
|
||||
{
|
||||
attendees: {
|
||||
some: {
|
||||
email: {
|
||||
in: attendeeEmailsFromUserIdsFilter,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(eventTypeIdsFromEventTypeIdsFilter
|
||||
? [
|
||||
{
|
||||
eventTypeId: { in: eventTypeIdsFromEventTypeIdsFilter },
|
||||
},
|
||||
]
|
||||
: []),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(eventTypeIdsFromEventTypeIdsFilter
|
||||
? [
|
||||
{
|
||||
eventTypeId: { in: eventTypeIdsFromEventTypeIdsFilter },
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
...(typeof filters?.attendeeEmail === "string"
|
||||
? [
|
||||
{
|
||||
attendees: { some: { email: filters.attendeeEmail.trim() } },
|
||||
...(typeof filters?.attendeeEmail === "string"
|
||||
? [
|
||||
{
|
||||
attendees: { some: { email: filters.attendeeEmail.trim() } },
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(isTextFilterValue(filters?.attendeeEmail)
|
||||
? [
|
||||
{
|
||||
attendees: {
|
||||
some: makeWhereClause({
|
||||
columnName: "email",
|
||||
filterValue: filters.attendeeEmail,
|
||||
}),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(isTextFilterValue(filters?.attendeeEmail)
|
||||
? [
|
||||
{
|
||||
attendees: {
|
||||
some: makeWhereClause({
|
||||
columnName: "email",
|
||||
filterValue: filters.attendeeEmail,
|
||||
}),
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
...(typeof filters?.attendeeName === "string"
|
||||
? [
|
||||
{
|
||||
attendees: { some: { name: filters.attendeeName.trim() } },
|
||||
...(typeof filters?.attendeeName === "string"
|
||||
? [
|
||||
{
|
||||
attendees: { some: { name: filters.attendeeName.trim() } },
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(isTextFilterValue(filters?.attendeeName)
|
||||
? [
|
||||
{
|
||||
attendees: {
|
||||
some: makeWhereClause({
|
||||
columnName: "name",
|
||||
filterValue: filters.attendeeName,
|
||||
}),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(isTextFilterValue(filters?.attendeeName)
|
||||
? [
|
||||
{
|
||||
attendees: {
|
||||
some: makeWhereClause({
|
||||
columnName: "name",
|
||||
filterValue: filters.attendeeName,
|
||||
}),
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
...(filters?.afterStartDate
|
||||
? [
|
||||
{
|
||||
startTime: {
|
||||
gte: dayjs.utc(filters.afterStartDate).toDate(),
|
||||
},
|
||||
...(filters?.afterStartDate
|
||||
? [
|
||||
{
|
||||
startTime: {
|
||||
gte: dayjs.utc(filters.afterStartDate).toDate(),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(filters?.beforeEndDate
|
||||
? [
|
||||
{
|
||||
endTime: {
|
||||
lte: dayjs.utc(filters.beforeEndDate).toDate(),
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(filters?.beforeEndDate
|
||||
? [
|
||||
{
|
||||
endTime: {
|
||||
lte: dayjs.utc(filters.beforeEndDate).toDate(),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(filters?.afterUpdatedDate
|
||||
? [
|
||||
{
|
||||
updatedAt: {
|
||||
gte: dayjs.utc(filters.afterUpdatedDate).toDate(),
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(filters?.afterUpdatedDate
|
||||
? [
|
||||
{
|
||||
updatedAt: {
|
||||
gte: dayjs.utc(filters.afterUpdatedDate).toDate(),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(filters?.beforeUpdatedDate
|
||||
? [
|
||||
{
|
||||
updatedAt: {
|
||||
lte: dayjs.utc(filters.beforeUpdatedDate).toDate(),
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(filters?.beforeUpdatedDate
|
||||
? [
|
||||
{
|
||||
updatedAt: {
|
||||
lte: dayjs.utc(filters.beforeUpdatedDate).toDate(),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
select: bookingSelect,
|
||||
orderBy,
|
||||
take: take + 1,
|
||||
skip,
|
||||
});
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
};
|
||||
|
||||
const [plainBookings, totalCount] = await Promise.all([
|
||||
prisma.booking.findMany({
|
||||
where: whereClause,
|
||||
select: bookingSelect,
|
||||
orderBy,
|
||||
take,
|
||||
skip,
|
||||
}),
|
||||
prisma.booking.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
const [
|
||||
recurringInfoBasic,
|
||||
@@ -446,7 +453,7 @@ export async function getBookings({
|
||||
};
|
||||
})
|
||||
);
|
||||
return { bookings, recurringInfo };
|
||||
return { bookings, recurringInfo, totalCount };
|
||||
}
|
||||
|
||||
async function getEventTypeIdsFromTeamIdsFilter(prisma: PrismaClient, teamIds?: number[]) {
|
||||
|
||||
@@ -15,8 +15,8 @@ export const ZGetInputSchema = z.object({
|
||||
afterUpdatedDate: z.string().optional(),
|
||||
beforeUpdatedDate: z.string().optional(),
|
||||
}),
|
||||
limit: z.number().min(1).max(100).nullish(),
|
||||
cursor: z.number().nullish(), // <-- "cursor" needs to exist when using useInfiniteQuery, but can be any type
|
||||
limit: z.number().min(1).max(100),
|
||||
offset: z.number(),
|
||||
});
|
||||
|
||||
export type TGetInputSchema = z.infer<typeof ZGetInputSchema>;
|
||||
|
||||
Reference in New Issue
Block a user