* feat: add activeFilters validator to DataTableProvider for filter segment validation - Add validateActiveFilters prop to DataTableProvider to filter out inaccessible values - Create useActiveFiltersValidator hook in bookings module to validate filter values - Integrate validator in bookings-view.tsx to validate userId, eventTypeId, teamId filters - Add comprehensive tests for the filter validation logic This provides a scalable solution for handling stale filter segment data by validating filters at the caller level using already-fetched accessible resource IDs. Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: add type guard for eventTypeIds to ensure number[] type Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: delay segment filter application until validator is ready - Change useActiveFiltersValidator to return 'loading' state while data is fetching - Add pending segment handling in DataTableProvider to delay filter application - Expose isValidatorPending in context for consumers to disable queries - Update BookingListContainer to disable bookings query while validator is pending This prevents both errors from invalid filters and flash from fetching without filters. Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * refactor: extract useSegmentManagement hook from DataTableProvider - Extract segment management logic into dedicated useSegmentManagement hook - Reduces DataTableProvider from ~500 lines to ~330 lines - Use NuqsSetter type to match nuqs useQueryState setter signatures - Maintains all existing functionality and type safety Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: export ActiveFiltersValidator types from data-table lib/types Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: import ActiveFiltersValidatorState from lib/types instead of hook Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * Revert "refactor: extract useSegmentManagement hook from DataTableProvider" This reverts commit dd588ac4cfb1d709c71d1f621994a7e8f3118f33. * refactor: split DataTableProvider into fine-grained context providers - Create DataTableStateContext for raw nuqs state management - Create DataTableSegmentContext for segment management - Create DataTableFiltersContext for filter manipulation - DataTableProvider now composes all providers with a bridge component - Maintains backward compatibility via combined DataTableContext Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: apply segment filters when navigating back to page with existing URL state When navigating back to a page with a segment already selected in the URL, the segment was being marked as selected but its filters were not being applied. This caused non-deterministic behavior where the filter segment would sometimes not take effect. The fix ensures that when segmentIdRaw exists in the URL and segments are fetched, we also call applySegmentFilters() (with validator timing support) instead of just setting the selectedSegment state. Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * refactor: simplify filter validator to only handle multi-select These filters are always multi-select, so remove unnecessary single-select handling code and tests. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: cancel debounced search term on unmount Cancel the debounced search term on unmount/dependency change to avoid queued updates firing after the provider unmounts. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
170 lines
4.8 KiB
TypeScript
170 lines
4.8 KiB
TypeScript
"use client";
|
|
|
|
import type { SortingState, OnChangeFn, VisibilityState, ColumnSizingState } from "@tanstack/react-table";
|
|
import { useQueryState } from "nuqs";
|
|
import { createContext, useContext, useRef, useMemo } from "react";
|
|
|
|
import { useElementByClassName } from "@calcom/lib/hooks/useElementByClassName";
|
|
|
|
import {
|
|
activeFiltersParser,
|
|
sortingParser,
|
|
columnVisibilityParser,
|
|
columnSizingParser,
|
|
segmentIdParser,
|
|
pageIndexParser,
|
|
pageSizeParser,
|
|
searchTermParser,
|
|
DEFAULT_PAGE_SIZE,
|
|
} from "../lib/parsers";
|
|
import type { ActiveFilters, SegmentIdentifier } from "../lib/types";
|
|
import { CTA_CONTAINER_CLASS_NAME } from "../lib/utils";
|
|
|
|
export type ActiveFiltersValidator = (filters: ActiveFilters) => ActiveFilters;
|
|
|
|
export type ActiveFiltersValidatorState = ActiveFiltersValidator | "loading" | undefined;
|
|
|
|
export type DataTableStateContextType = {
|
|
tableIdentifier: string;
|
|
ctaContainerRef: React.RefObject<HTMLDivElement>;
|
|
filterToOpen: React.MutableRefObject<string | undefined>;
|
|
|
|
pageIndex: number;
|
|
setPageIndex: (pageIndex: number | null) => void;
|
|
|
|
pageSize: number;
|
|
setPageSize: (pageSize: number | null) => void;
|
|
|
|
searchTerm: string;
|
|
setSearchTerm: (searchTerm: string | null) => void;
|
|
|
|
activeFilters: ActiveFilters;
|
|
setActiveFilters: ReturnType<typeof useQueryState<ActiveFilters>>[1];
|
|
|
|
sorting: SortingState;
|
|
setSorting: OnChangeFn<SortingState>;
|
|
|
|
columnVisibility: VisibilityState;
|
|
setColumnVisibility: OnChangeFn<VisibilityState>;
|
|
|
|
columnSizing: ColumnSizingState;
|
|
setColumnSizing: OnChangeFn<ColumnSizingState>;
|
|
|
|
segmentIdRaw: string | null;
|
|
setSegmentIdRaw: (segmentId: string | null) => void;
|
|
|
|
defaultPageSize: number;
|
|
timeZone?: string;
|
|
|
|
validateActiveFilters?: ActiveFiltersValidatorState;
|
|
};
|
|
|
|
export const DataTableStateContext = createContext<DataTableStateContextType | null>(null);
|
|
|
|
export function useDataTableState() {
|
|
const context = useContext(DataTableStateContext);
|
|
if (!context) {
|
|
throw new Error("useDataTableState must be used within a DataTableStateProvider");
|
|
}
|
|
return context;
|
|
}
|
|
|
|
interface DataTableStateProviderProps {
|
|
tableIdentifier: string;
|
|
children: React.ReactNode;
|
|
defaultPageSize?: number;
|
|
ctaContainerClassName?: string;
|
|
timeZone?: string;
|
|
preferredSegmentId?: SegmentIdentifier | null;
|
|
validateActiveFilters?: ActiveFiltersValidatorState;
|
|
}
|
|
|
|
export function DataTableStateProvider({
|
|
tableIdentifier,
|
|
children,
|
|
defaultPageSize = DEFAULT_PAGE_SIZE,
|
|
ctaContainerClassName = CTA_CONTAINER_CLASS_NAME,
|
|
timeZone,
|
|
preferredSegmentId,
|
|
validateActiveFilters,
|
|
}: DataTableStateProviderProps) {
|
|
if (!tableIdentifier.trim()) {
|
|
throw new Error("tableIdentifier is required and cannot be empty");
|
|
}
|
|
|
|
const filterToOpen = useRef<string | undefined>(undefined);
|
|
const [pageIndex, setPageIndex] = useQueryState("page", pageIndexParser);
|
|
const [pageSize, setPageSize] = useQueryState("size", pageSizeParser);
|
|
const [searchTerm, setSearchTerm] = useQueryState("q", searchTermParser);
|
|
const [activeFilters, setActiveFilters] = useQueryState("activeFilters", activeFiltersParser);
|
|
const [sorting, setSorting] = useQueryState("sorting", sortingParser);
|
|
const [columnVisibility, setColumnVisibility] = useQueryState<VisibilityState>(
|
|
"cols",
|
|
columnVisibilityParser
|
|
);
|
|
const [columnSizing, setColumnSizing] = useQueryState<ColumnSizingState>("widths", columnSizingParser);
|
|
|
|
const initialSegmentId = useMemo(
|
|
() => (preferredSegmentId ? String(preferredSegmentId.id) : null),
|
|
[preferredSegmentId]
|
|
);
|
|
const [segmentIdRaw, setSegmentIdRaw] = useQueryState(
|
|
"segment",
|
|
initialSegmentId ? segmentIdParser.withDefault(initialSegmentId) : segmentIdParser
|
|
);
|
|
|
|
const ctaContainerRef = useElementByClassName<HTMLDivElement>(ctaContainerClassName);
|
|
|
|
const value = useMemo(
|
|
() => ({
|
|
tableIdentifier,
|
|
ctaContainerRef,
|
|
filterToOpen,
|
|
pageIndex,
|
|
setPageIndex,
|
|
pageSize,
|
|
setPageSize,
|
|
searchTerm,
|
|
setSearchTerm,
|
|
activeFilters,
|
|
setActiveFilters,
|
|
sorting,
|
|
setSorting,
|
|
columnVisibility,
|
|
setColumnVisibility,
|
|
columnSizing,
|
|
setColumnSizing,
|
|
segmentIdRaw,
|
|
setSegmentIdRaw,
|
|
defaultPageSize,
|
|
timeZone,
|
|
validateActiveFilters,
|
|
}),
|
|
[
|
|
tableIdentifier,
|
|
ctaContainerRef,
|
|
pageIndex,
|
|
setPageIndex,
|
|
pageSize,
|
|
setPageSize,
|
|
searchTerm,
|
|
setSearchTerm,
|
|
activeFilters,
|
|
setActiveFilters,
|
|
sorting,
|
|
setSorting,
|
|
columnVisibility,
|
|
setColumnVisibility,
|
|
columnSizing,
|
|
setColumnSizing,
|
|
segmentIdRaw,
|
|
setSegmentIdRaw,
|
|
defaultPageSize,
|
|
timeZone,
|
|
validateActiveFilters,
|
|
]
|
|
);
|
|
|
|
return <DataTableStateContext.Provider value={value}>{children}</DataTableStateContext.Provider>;
|
|
}
|