Files
calendar/apps/web/modules/data-table/components/DataTableWrapper.tsx
T
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
404d46c991 refactor: Move trpc-dependent components from features to web [2] (#26420)
* fix

* move event types components to web

* update import paths

* mv apps components

* migrate form builder

* fix

* mv sso

* fix

* mv

* update import paths

* update import paths

* mv

* mv

* mv

* fix

* update Booker

* fix

* fix

* fix

* fix

* mv video

* mv embed components to web

* update import paths

* mv calendar weekly view components

* update import paths

* fix

* fixp

* fix

* fix

* fix

* fix: update FormBuilder imports to use @calcom/features/form-builder

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: update broken import paths after file migrations

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: correct import paths for platform atoms and moved components

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: apply CSS type fixes and add missing atoms exports

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: resolve type errors in test files after component migrations

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: resolve remaining type errors in test files

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix

* migrate

* fix: resolve type errors in test and mock files

- Add missing bookingForm, bookerFormErrorRef, instantConnectCooldownMs to Booker.test.tsx bookings prop
- Add all required BookerEvent properties to event.mock.ts
- Add vi import from vitest to all mock files
- Fix date parameter types in packages/dayjs/__mocks__/index.ts
- Add verificationCode and setVerificationCode to test-utils.tsx mock store
- Remove children.type access in Section.tsx mock to fix type error
- Fix lint issues: remove unused React imports, use import type where needed, add return types
- Add biome-ignore comments for pre-existing lint warnings in test files

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* migrate

* migrate

* migrate

* update import paths

* update import paths

* update import paths

* fix

* migrate data table

* migrate data table

* fix

* fix

* fix

* migrate insights components

* migrate insights components

* fix

* mv

* update import paths

* fix

* fix

* fix

* fix

* fix

* fix: resolve type errors in test mocks

- Booker.test.tsx: Add all required UseFormReturn methods to bookingForm mock
- event.mock.ts: Fix entity, subsetOfHosts, instantMeetingParameters, fieldTranslations, image types
- dayjs/__mocks__/index.ts: Use Object.assign for proper typing of mock properties
- Section.tsx: Change 'class' to 'className' in JSX with biome-ignore comment

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add missing hasDataErrors and dataErrors to bookings.errors mock

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add missing loadingStates properties to bookings mock

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add missing slots properties (setTentativeSelectedTimeslots, tentativeSelectedTimeslots, slotReservationId)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: update quickAvailabilityChecks to include utcEndIso and use valid status type

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add missing bookerForm properties (formName, beforeVerifyEmail, formErrors)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add missing UseFormReturn properties to bookerForm.bookingForm mock

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add hasFormErrors and formErrors to bookerForm.formErrors mock

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add hasFormErrors and formErrors to bookerForm.errors mock

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add missing isError property to mockEvent

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: use complete BookerEvent mock in Booker.test.tsx

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: use branded bookingFields type in event mock

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add missing schedule mock properties (isError, isSuccess, isLoading, dataUpdatedAt)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* revert

* fix

* fix

* fix

* fix build

* fix

* fix

* fix

* fix: correct AddMembersWithSwitch test wrapper to use initial assignAllTeamMembers value

- Fixed test wrapper to initialize useState with componentProps.assignAllTeamMembers
  instead of hardcoded false, allowing tests to properly test different states
- Updated test expectations for ALL_TEAM_MEMBERS_ENABLED_AND_SEGMENT_NOT_APPLICABLE
  state to match actual component behavior (toggle should be present and checked)
- Fixed 'should show Segment when toggled on' test to start with assignAllTeamMembers: false
  to properly test the flow of enabling it
- Added explicit types to satisfy biome lint requirements

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: use JSX.Element instead of React.JSX.Element for type compatibility

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-05 12:53:51 -03:00

209 lines
6.3 KiB
TypeScript

"use client";
import type { Row, VisibilityState } from "@tanstack/react-table";
import { noop } from "lodash";
import { useEffect, useRef } from "react";
import { useColumnFilters } from "@calcom/features/data-table/hooks/useColumnFilters";
import { useDataTable } from "@calcom/features/data-table/hooks/useDataTable";
import { useFetchMoreOnBottomReached } from "@calcom/features/data-table/hooks/useFetchMoreOnBottomReached";
import type { DataTablePropsFromWrapper } from "./DataTable";
import { DataTable } from "./DataTable";
import { DataTablePagination } from "./DataTablePagination";
type BaseDataTableWrapperProps<TData> = Omit<
DataTablePropsFromWrapper<TData>,
"paginationMode" | "tableContainerRef"
> & {
totalRowCount?: number;
ToolbarLeft?: React.ReactNode;
ToolbarRight?: React.ReactNode;
EmptyView?: React.ReactNode;
LoaderView?: React.ReactNode;
ErrorView?: React.ReactNode;
hasError?: boolean;
tableContainerRef?: React.RefObject<HTMLDivElement>;
onRowMouseclick?: (row: Row<TData>) => void;
rowTestId?: string | ((row: Row<TData>) => string | undefined);
rowDataAttributes?: (row: Row<TData>) => Record<string, string> | undefined;
};
type InfinitePaginationProps<TData> = BaseDataTableWrapperProps<TData> & {
paginationMode: "infinite";
hasNextPage: boolean;
fetchNextPage: () => void;
isFetching: boolean;
};
type StandardPaginationProps<TData> = BaseDataTableWrapperProps<TData> & {
paginationMode: "standard";
hasNextPage?: never;
fetchNextPage?: never;
isFetching?: never;
};
export type DataTableWrapperProps<TData> = InfinitePaginationProps<TData> | StandardPaginationProps<TData>;
export function DataTableWrapper<TData>({
testId,
bodyTestId,
table,
isPending,
hasNextPage,
fetchNextPage,
isFetching,
totalRowCount,
variant,
ToolbarLeft,
ToolbarRight,
EmptyView,
LoaderView,
ErrorView,
hasError,
className,
containerClassName,
headerClassName,
rowClassName,
rowTestId,
rowDataAttributes,
children,
tableContainerRef: externalRef,
paginationMode,
onRowMouseclick,
hideSeparatorsOnSort,
hideSeparatorsOnFilter,
separatorClassName,
}: DataTableWrapperProps<TData>) {
const internalRef = useRef<HTMLDivElement>(null);
const tableContainerRef = externalRef || internalRef;
const fetchMoreOnBottomReached = useFetchMoreOnBottomReached({
tableContainerRef,
hasNextPage: paginationMode === "infinite" ? hasNextPage : false,
fetchNextPage: paginationMode === "infinite" ? fetchNextPage : noop,
isFetching: paginationMode === "infinite" ? isFetching : false,
enabled: paginationMode === "infinite",
});
const { sorting, setSorting, columnVisibility, setColumnVisibility } = useDataTable();
const columnFilters = useColumnFilters();
useEffect(() => {
const mergedColumnVisibility = {
...table.initialState?.columnVisibility,
...columnVisibility,
} satisfies VisibilityState;
table.setState((prev) => ({
...prev,
sorting,
columnFilters,
columnVisibility: mergedColumnVisibility,
}));
table.setOptions((prev) => ({
...prev,
onSortingChange: setSorting,
onColumnVisibilityChange: setColumnVisibility,
}));
}, [table, sorting, columnFilters, columnVisibility, setSorting, setColumnVisibility]);
// Scroll to top when table data changes
useScrollToTopOnDataChange({
tableData: table.options.data,
paginationMode,
});
let view: "loader" | "empty" | "error" | "table" = "table";
if (hasError && ErrorView) {
view = "error";
} else if (isPending && LoaderView) {
view = "loader";
} else if (table.getRowCount() === 0 && EmptyView) {
view = "empty";
}
return (
<>
{(ToolbarLeft || ToolbarRight || children) && (
<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>
<div className="flex flex-wrap items-center gap-2">{ToolbarRight}</div>
</div>
</div>
{children}
</div>
)}
{view === "error" && ErrorView}
{view === "loader" && LoaderView}
{view === "empty" && EmptyView}
{view === "table" && (
<DataTable
testId={testId}
bodyTestId={bodyTestId}
table={table}
tableContainerRef={tableContainerRef}
isPending={isPending}
enableColumnResizing={true}
variant={variant}
className={className}
containerClassName={containerClassName}
headerClassName={headerClassName}
rowClassName={rowClassName}
rowTestId={rowTestId}
rowDataAttributes={rowDataAttributes}
paginationMode={paginationMode}
onRowMouseclick={onRowMouseclick}
hasWrapperContext={true}
hideSeparatorsOnSort={hideSeparatorsOnSort}
hideSeparatorsOnFilter={hideSeparatorsOnFilter}
separatorClassName={separatorClassName}
onScroll={
paginationMode === "infinite"
? (e: Pick<React.UIEvent<HTMLDivElement, UIEvent>, "target">) =>
fetchMoreOnBottomReached(e.target as HTMLDivElement)
: undefined
}>
<div style={{ gridArea: "footer" }} className="px-3 py-2">
<DataTablePagination<TData>
table={table}
totalRowCount={totalRowCount}
paginationMode={paginationMode}
/>
</div>
</DataTable>
)}
</>
);
}
/**
* Scrolls to top when table data changes (pagination, filters, etc.)
* Only active for standard pagination mode where we show discrete pages
*/
function useScrollToTopOnDataChange<TData>({
tableData,
paginationMode,
}: {
tableData: TData[];
paginationMode: "standard" | "infinite";
}) {
const previousDataRef = useRef(tableData);
const isInitialMount = useRef(true);
useEffect(() => {
// Skip on initial mount
if (isInitialMount.current) {
isInitialMount.current = false;
return;
}
const hasDataChanged = previousDataRef.current !== tableData;
previousDataRef.current = tableData;
if (paginationMode === "standard" && hasDataChanged) {
window.scrollTo(0, 0);
}
}, [tableData, paginationMode]);
}