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:
co-authored by
eunjae@cal.com <hey@eunjae.dev>
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
4ac8a51bc2
commit
09ddbe886f
@@ -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 />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user