refactor: make DataTableProvider framework-agnostic by requiring tableIdentifier (#24513)

* refactor: make DataTableProvider framework-agnostic by requiring tableIdentifier

- Remove Next.js usePathname dependency from DataTableProvider
- Make tableIdentifier a required prop instead of optional
- Update all usages to provide explicit tableIdentifier values
- This makes DataTableProvider usable in non-Next.js contexts

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: use usePathname at usage sites instead of hardcoding tableIdentifier

- Add validation in DataTableProvider for empty/nullish tableIdentifier
- Use usePathname() in Next.js apps to pass pathname as tableIdentifier
- Use descriptive identifiers for non-Next.js package components
- This keeps DataTableProvider framework-agnostic while allowing Next.js apps to use pathname

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* use pathname instead of hard-coded identifiers

* change type of tableIdentifier

* simplify

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Eunjae Lee
2025-10-23 15:32:16 +02:00
committed by GitHub
co-authored by eunjae@cal.com <hey@eunjae.dev> Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 4ac8a51bc2
commit 09ddbe886f
14 changed files with 67 additions and 42 deletions
@@ -74,11 +74,9 @@ function useSystemSegments(userId?: number) {
export default function Bookings(props: BookingsProps) {
const pathname = usePathname();
const systemSegments = useSystemSegments(props.userId);
if (!pathname) return null;
return (
<DataTableProvider
useSegments={useSegments}
systemSegments={systemSegments}
tableIdentifier={pathname || undefined}>
<DataTableProvider tableIdentifier={pathname} useSegments={useSegments} systemSegments={systemSegments}>
<BookingsContent {...props} />
</DataTableProvider>
);
@@ -1,6 +1,7 @@
"use client";
import { getCoreRowModel, getSortedRowModel, useReactTable, type ColumnDef } from "@tanstack/react-table";
import { usePathname } from "next/navigation";
import { useMemo, useState, useReducer } from "react";
import {
@@ -62,8 +63,10 @@ function reducer(state: CallDetailsState, action: CallDetailsAction): CallDetail
}
function CallHistoryTable(props: CallHistoryProps) {
const pathname = usePathname();
if (!pathname) return null;
return (
<DataTableProvider useSegments={useSegments} defaultPageSize={25}>
<DataTableProvider tableIdentifier={pathname} useSegments={useSegments} defaultPageSize={25}>
<CallHistoryContent {...props} />
</DataTableProvider>
);
@@ -1,5 +1,7 @@
"use client";
import { usePathname } from "next/navigation";
import { DataTableProvider } from "@calcom/features/data-table/DataTableProvider";
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
import {
@@ -13,9 +15,12 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
export default function InsightsRoutingFormResponsesPage() {
const { t } = useLocale();
const pathname = usePathname();
if (!pathname) return null;
return (
<DataTableProvider useSegments={useSegments}>
<DataTableProvider tableIdentifier={pathname} useSegments={useSegments}>
<InsightsOrgTeamsProvider>
<div className="mb-4 space-y-4">
<RoutingFormResponsesTable />
+4 -1
View File
@@ -1,5 +1,6 @@
"use client";
import { usePathname } from "next/navigation";
import { useState, useCallback } from "react";
import {
@@ -39,8 +40,10 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
export default function InsightsPage({ timeZone }: { timeZone: string }) {
const pathname = usePathname();
if (!pathname) return null;
return (
<DataTableProvider useSegments={useSegments} timeZone={timeZone}>
<DataTableProvider tableIdentifier={pathname} useSegments={useSegments} timeZone={timeZone}>
<InsightsOrgTeamsProvider>
<InsightsPageContent />
</InsightsOrgTeamsProvider>
@@ -1,11 +1,8 @@
"use client";
import type { SortingState, OnChangeFn, VisibilityState, ColumnSizingState } from "@tanstack/react-table";
// eslint-disable-next-line no-restricted-imports
import debounce from "lodash/debounce";
// eslint-disable-next-line no-restricted-imports
import isEqual from "lodash/isEqual";
import { usePathname } from "next/navigation";
import { useQueryState } from "nuqs";
import { useState, createContext, useCallback, useEffect, useRef, useMemo } from "react";
@@ -79,9 +76,9 @@ export type DataTableContextType = {
export const DataTableContext = createContext<DataTableContextType | null>(null);
interface DataTableProviderProps {
useSegments?: UseSegments;
tableIdentifier?: string;
tableIdentifier: string;
children: React.ReactNode;
useSegments?: UseSegments;
ctaContainerClassName?: string;
defaultPageSize?: number;
segments?: FilterSegmentOutput[];
@@ -91,7 +88,7 @@ interface DataTableProviderProps {
}
export function DataTableProvider({
tableIdentifier: _tableIdentifier,
tableIdentifier,
children,
useSegments = useSegmentsNoop,
defaultPageSize = DEFAULT_PAGE_SIZE,
@@ -101,10 +98,8 @@ export function DataTableProvider({
preferredSegmentId,
systemSegments,
}: DataTableProviderProps) {
const pathname = usePathname() as string | null;
const tableIdentifier = _tableIdentifier ?? pathname ?? undefined;
if (!tableIdentifier) {
throw new Error("tableIdentifier is required");
if (!tableIdentifier.trim()) {
throw new Error("tableIdentifier is required and cannot be empty");
}
const filterToOpen = useRef<string | undefined>(undefined);
+6 -5
View File
@@ -145,8 +145,11 @@ function UserTable() {
// ... other table options
});
const pathname = usePathname();
const tableIdentifier = "hard-coded idenfidier" // or pathname;
return (
<DataTableProvider tableIdentifier="user-table">
<DataTableProvider tableIdentifier={tableIdentifier}>
<DataTableWrapper
table={table}
paginationMode="standard"
@@ -179,7 +182,7 @@ The context provider that manages all table state including filters, sorting, pa
```tsx
interface DataTableProviderProps {
tableIdentifier?: string; // Unique identifier for the table
tableIdentifier: string; // Unique identifier for the table (throws if empty)
children: React.ReactNode;
useSegments?: UseSegments; // Custom segment hook
defaultPageSize?: number; // Default: 10
@@ -530,7 +533,6 @@ Segments allow users to save and share filter configurations. There are two type
```tsx
<DataTableProvider
tableIdentifier="users"
useSegments={useSegments} // Required to enable segments
>
{/* Your table content */}
@@ -564,7 +566,6 @@ const systemSegments: SystemFilterSegment[] = [
<DataTableProvider
systemSegments={systemSegments}
tableIdentifier="user-table"
>
{/* ... */}
</DataTableProvider>
@@ -1210,7 +1211,7 @@ function UserTableContainer() {
const { data, isPending } = useUsers(filters);
return (
<DataTableProvider tableIdentifier="users">
<DataTableProvider>
<UserTable data={data} isPending={isPending} />
</DataTableProvider>
);
@@ -1,5 +1,6 @@
import type { ColumnDef } from "@tanstack/react-table";
import { useReactTable, getCoreRowModel } from "@tanstack/react-table";
import { usePathname } from "next/navigation";
import { useRef, useState } from "react";
import { DataTableProvider } from "@calcom/features/data-table/DataTableProvider";
@@ -18,8 +19,10 @@ interface TeamGroupMapping {
}
const GroupTeamMappingTable = () => {
const pathname = usePathname();
if (!pathname) return null;
return (
<DataTableProvider>
<DataTableProvider tableIdentifier={pathname}>
<GroupTeamMappingTableContent />
</DataTableProvider>
);
@@ -1,5 +1,7 @@
"use client";
import { usePathname } from "next/navigation";
import { DataTableProvider } from "@calcom/features/data-table";
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
@@ -20,14 +22,16 @@ const PrivacyView = ({
canDelete: boolean;
};
}) => {
const pathname = usePathname();
const { t } = useLocale();
const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery();
const isInviteOpen = !currentOrg?.user.accepted;
const isDisabled = !permissions.canEdit || isInviteOpen;
if (!currentOrg) return null;
if (!pathname) return null;
return (
<LicenseRequired>
<div className="space-y-8">
@@ -45,7 +49,7 @@ const PrivacyView = ({
<p className="text-muted text-sm">{t("manage_blocked_emails_and_domains")}</p>
</div>
<div className="mt-2">
<DataTableProvider useSegments={useSegments} defaultPageSize={25}>
<DataTableProvider tableIdentifier={pathname} useSegments={useSegments} defaultPageSize={25}>
<BlocklistTable permissions={watchlistPermissions} />
</DataTableProvider>
</div>
@@ -11,6 +11,7 @@ import {
import classNames from "classnames";
import { useSession } from "next-auth/react";
import { signIn } from "next-auth/react";
import { usePathname } from "next/navigation";
import { useQueryState, parseAsBoolean } from "nuqs";
import { useMemo, useReducer, useRef, useState } from "react";
import type { Dispatch, SetStateAction } from "react";
@@ -166,8 +167,10 @@ interface Props {
}
export default function MemberList(props: Props) {
const pathname = usePathname();
if (!pathname) return null;
return (
<DataTableProvider>
<DataTableProvider tableIdentifier={pathname}>
<MemberListContent {...props} />
</DataTableProvider>
);
@@ -668,7 +671,7 @@ function MemberListContent(props: Props) {
getFacetedUniqueValues: (_, columnId) => () => {
if (facetedTeamValues) {
switch (columnId) {
case "role":
case "role": {
// Include both traditional roles and PBAC custom roles
const allRoles = facetedTeamValues.roles.map((role) => ({
label: role.name,
@@ -676,6 +679,7 @@ function MemberListContent(props: Props) {
}));
return convertFacetedValuesToMap(allRoles);
}
default:
return new Map();
}
@@ -685,7 +689,7 @@ function MemberListContent(props: Props) {
getRowId: (row) => `${row.id}`,
});
const fetchMoreOnBottomReached = useFetchMoreOnBottomReached({
useFetchMoreOnBottomReached({
tableContainerRef,
hasNextPage,
fetchNextPage,
@@ -1,4 +1,5 @@
import { getCoreRowModel, getSortedRowModel, useReactTable, type ColumnDef } from "@tanstack/react-table";
import { usePathname } from "next/navigation";
import { useMemo, useState } from "react";
import { DataTableProvider, DataTableWrapper } from "@calcom/features/data-table";
@@ -36,8 +37,10 @@ function VoiceSelectionTable({
selectedVoiceId?: string;
onVoiceSelect: (voiceId: string) => void;
}) {
const pathname = usePathname();
if (!pathname) return null;
return (
<DataTableProvider useSegments={useSegments} defaultPageSize={1000}>
<DataTableProvider tableIdentifier={pathname} useSegments={useSegments} defaultPageSize={1000}>
<VoiceSelectionContent selectedVoiceId={selectedVoiceId} onVoiceSelect={onVoiceSelect} />
</DataTableProvider>
);
@@ -1,6 +1,7 @@
"use client";
import { getCoreRowModel, getSortedRowModel, useReactTable, type ColumnDef } from "@tanstack/react-table";
import { usePathname } from "next/navigation";
import { useMemo, useState, useReducer } from "react";
import {
@@ -64,8 +65,10 @@ function reducer(state: CallDetailsState, action: CallDetailsAction): CallDetail
}
function CallHistoryTable(props: CallHistoryProps) {
const pathname = usePathname();
if (!pathname) return null;
return (
<DataTableProvider useSegments={useSegments} defaultPageSize={25}>
<DataTableProvider tableIdentifier={pathname} useSegments={useSegments} defaultPageSize={25}>
<CallHistoryContent {...props} />
</DataTableProvider>
);
@@ -7,8 +7,8 @@ import {
getFilteredRowModel,
useReactTable,
} from "@tanstack/react-table";
import { usePathname } from "next/navigation";
import { useEffect, useMemo, useRef, useState } from "react";
import { useFormState } from "react-hook-form";
import dayjs from "@calcom/dayjs";
import {
@@ -65,6 +65,9 @@ interface OutOfOfficeEntry {
export default function OutOfOfficeEntriesList() {
const { t } = useLocale();
const pathname = usePathname();
if (!pathname) return null;
return (
<SettingsHeader
@@ -76,7 +79,7 @@ export default function OutOfOfficeEntriesList() {
<CreateNewOutOfOfficeEntryButton data-testid="add_entry_ooo" />
</div>
}>
<DataTableProvider useSegments={useSegments}>
<DataTableProvider tableIdentifier={pathname} useSegments={useSegments}>
<OutOfOfficeEntriesListContent />
</DataTableProvider>
</SettingsHeader>
@@ -341,7 +344,6 @@ function OutOfOfficeEntriesListContent() {
onSuccess: () => {
showToast(t("success_deleted_entry_out_of_office"), "success");
setDeletedEntry((previousValue) => previousValue + 1);
useFormState;
},
onError: () => {
showToast(`An error occurred`, "error");
@@ -3,6 +3,7 @@
import { keepPreviousData } from "@tanstack/react-query";
import type { ColumnDef } from "@tanstack/react-table";
import { getCoreRowModel, getFilteredRowModel, useReactTable } from "@tanstack/react-table";
import { usePathname } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import dayjs from "@calcom/dayjs";
@@ -68,8 +69,10 @@ function UpgradeTeamTip() {
}
export function AvailabilitySliderTable(props: { isOrg: boolean }) {
const pathname = usePathname();
if (!pathname) return null;
return (
<DataTableProvider>
<DataTableProvider tableIdentifier={pathname}>
<AvailabilitySliderTableContent {...props} />
</DataTableProvider>
);
@@ -82,10 +85,6 @@ function AvailabilitySliderTableContent(props: { isOrg: boolean }) {
const [selectedUser, setSelectedUser] = useState<SliderUser | null>(null);
const { searchTerm } = useDataTable();
const tbStore = createTimezoneBuddyStore({
browsingDate: browsingDate.toDate(),
});
const { data, isPending, fetchNextPage, isFetching } = trpc.viewer.availability.listTeam.useInfiniteQuery(
{
limit: 10,
@@ -3,6 +3,7 @@
import { keepPreviousData } from "@tanstack/react-query";
import { getCoreRowModel, getSortedRowModel, useReactTable, type ColumnDef } from "@tanstack/react-table";
import { useSession } from "next-auth/react";
import { usePathname } from "next/navigation";
import { useQueryState, parseAsBoolean } from "nuqs";
import { useMemo, useReducer, useState } from "react";
import { createPortal } from "react-dom";
@@ -126,8 +127,10 @@ export type UserListTableProps = {
};
export function UserListTable(props: UserListTableProps) {
const pathname = usePathname();
if (!pathname) return null;
return (
<DataTableProvider useSegments={useSegments} defaultPageSize={25}>
<DataTableProvider tableIdentifier={pathname} useSegments={useSegments} defaultPageSize={25}>
<UserListTableContent {...props} />
</DataTableProvider>
);
@@ -168,8 +171,6 @@ function UserListTableContent({
}
);
// TODO (SEAN): Make Column filters a trpc query param so we can fetch serverside even if the data is not loaded
const totalRowCount = data?.meta?.totalRowCount ?? 0;
const adminOrOwner = checkAdminOrOwner(org?.user?.role);
//we must flatten the array of arrays from the useInfiniteQuery hook
@@ -514,7 +515,7 @@ function UserListTableContent({
value: team.name,
}))
);
default:
default: {
const attribute = facetedTeamValues.attributes.find((attr) => attr.id === columnId);
if (attribute) {
return convertFacetedValuesToMap(
@@ -525,6 +526,7 @@ function UserListTableContent({
);
}
return new Map();
}
}
}
return new Map();