fix: avoid duplicate billing portal link (#330)

* fix: avoid duplicate billing portal link
* fix: improve DataTable API
This commit is contained in:
Giancarlo Buomprisco
2025-08-26 05:30:18 +07:00
committed by GitHub
parent ad427365c9
commit f9ebe2f927
31 changed files with 1706 additions and 1085 deletions

View File

@@ -27,13 +27,13 @@
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@supabase/supabase-js": "2.55.0",
"@types/react": "19.1.10",
"@types/react": "19.1.11",
"date-fns": "^4.1.0",
"lucide-react": "^0.540.0",
"lucide-react": "^0.541.0",
"next": "15.5.0",
"react": "19.1.1",
"react-hook-form": "^7.62.0",
"react-i18next": "^15.7.1",
"react-i18next": "^15.7.2",
"zod": "^3.25.74"
},
"typesVersions": {

View File

@@ -24,7 +24,7 @@
"@kit/supabase": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@types/react": "19.1.10",
"@types/react": "19.1.11",
"next": "15.5.0",
"react": "19.1.1",
"zod": "^3.25.74"

View File

@@ -27,7 +27,7 @@
"@kit/supabase": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@types/react": "19.1.10",
"@types/react": "19.1.11",
"date-fns": "^4.1.0",
"next": "15.5.0",
"react": "19.1.1",

View File

@@ -27,7 +27,7 @@
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@types/node": "^24.3.0",
"@types/react": "19.1.10",
"@types/react": "19.1.11",
"react": "19.1.1",
"zod": "^3.25.74"
},

View File

@@ -21,7 +21,7 @@
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@types/node": "^24.3.0",
"@types/react": "19.1.10",
"@types/react": "19.1.11",
"wp-types": "^4.68.1"
},
"typesVersions": {

View File

@@ -36,15 +36,15 @@
"@radix-ui/react-icons": "^1.3.2",
"@supabase/supabase-js": "2.55.0",
"@tanstack/react-query": "5.85.5",
"@types/react": "19.1.10",
"@types/react": "19.1.11",
"@types/react-dom": "19.1.7",
"lucide-react": "^0.540.0",
"lucide-react": "^0.541.0",
"next": "15.5.0",
"next-themes": "0.4.6",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-hook-form": "^7.62.0",
"react-i18next": "^15.7.1",
"react-i18next": "^15.7.2",
"zod": "^3.25.74"
},
"prettier": "@kit/prettier-config",

View File

@@ -23,8 +23,8 @@
"@supabase/supabase-js": "2.55.0",
"@tanstack/react-query": "5.85.5",
"@tanstack/react-table": "^8.21.3",
"@types/react": "19.1.10",
"lucide-react": "^0.540.0",
"@types/react": "19.1.11",
"lucide-react": "^0.541.0",
"next": "15.5.0",
"react": "19.1.1",
"react-dom": "19.1.1",

View File

@@ -31,11 +31,11 @@
"@radix-ui/react-icons": "^1.3.2",
"@supabase/supabase-js": "2.55.0",
"@tanstack/react-query": "5.85.5",
"@types/react": "19.1.10",
"lucide-react": "^0.540.0",
"@types/react": "19.1.11",
"lucide-react": "^0.541.0",
"next": "15.5.0",
"react-hook-form": "^7.62.0",
"react-i18next": "^15.7.1",
"react-i18next": "^15.7.2",
"sonner": "^2.0.7",
"zod": "^3.25.74"
},

View File

@@ -21,11 +21,11 @@
"@kit/ui": "workspace:*",
"@supabase/supabase-js": "2.55.0",
"@tanstack/react-query": "5.85.5",
"@types/react": "19.1.10",
"lucide-react": "^0.540.0",
"@types/react": "19.1.11",
"lucide-react": "^0.541.0",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-i18next": "^15.7.1"
"react-i18next": "^15.7.2"
},
"prettier": "@kit/prettier-config",
"typesVersions": {

View File

@@ -35,16 +35,16 @@
"@supabase/supabase-js": "2.55.0",
"@tanstack/react-query": "5.85.5",
"@tanstack/react-table": "^8.21.3",
"@types/react": "19.1.10",
"@types/react": "19.1.11",
"@types/react-dom": "19.1.7",
"class-variance-authority": "^0.7.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.540.0",
"lucide-react": "^0.541.0",
"next": "15.5.0",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-hook-form": "^7.62.0",
"react-i18next": "^15.7.1",
"react-i18next": "^15.7.2",
"zod": "^3.25.74"
},
"prettier": "@kit/prettier-config",

View File

@@ -24,10 +24,10 @@
"next": "15.5.0",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-i18next": "^15.7.1"
"react-i18next": "^15.7.2"
},
"dependencies": {
"i18next": "25.4.0",
"i18next": "25.4.2",
"i18next-browser-languagedetector": "8.2.0",
"i18next-resources-to-backend": "^1.2.1"
},

View File

@@ -24,7 +24,7 @@
"@kit/sentry": "workspace:*",
"@kit/shared": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/react": "19.1.10",
"@types/react": "19.1.11",
"react": "19.1.1",
"zod": "^3.25.74"
},

View File

@@ -24,7 +24,7 @@
"@kit/eslint-config": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/react": "19.1.10",
"@types/react": "19.1.11",
"react": "19.1.1",
"zod": "^3.25.74"
},

View File

@@ -17,7 +17,7 @@
"@kit/eslint-config": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/react": "19.1.10",
"@types/react": "19.1.11",
"react": "19.1.1"
},
"typesVersions": {

View File

@@ -24,7 +24,7 @@
"@kit/monitoring-core": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/react": "19.1.10",
"@types/react": "19.1.11",
"react": "19.1.1"
},
"typesVersions": {

View File

@@ -26,7 +26,7 @@
"@kit/ui": "workspace:*",
"@radix-ui/react-icons": "^1.3.2",
"@supabase/supabase-js": "2.55.0",
"@types/react": "19.1.10",
"@types/react": "19.1.11",
"@types/react-dom": "19.1.7",
"react": "19.1.1",
"react-dom": "19.1.1",

View File

@@ -20,7 +20,7 @@
"@kit/eslint-config": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/react": "19.1.10"
"@types/react": "19.1.11"
},
"dependencies": {
"pino": "^9.8.0"

View File

@@ -25,10 +25,10 @@
"@kit/eslint-config": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@supabase/ssr": "^0.6.1",
"@supabase/ssr": "^0.7.0",
"@supabase/supabase-js": "2.55.0",
"@tanstack/react-query": "5.85.5",
"@types/react": "19.1.10",
"@types/react": "19.1.11",
"next": "15.5.0",
"react": "19.1.1",
"server-only": "^0.0.1",

View File

@@ -14,7 +14,7 @@
"clsx": "^2.1.1",
"cmdk": "1.1.1",
"input-otp": "1.4.2",
"lucide-react": "^0.540.0",
"lucide-react": "^0.541.0",
"radix-ui": "1.4.3",
"react-dropzone": "^14.3.8",
"react-top-loading-bar": "3.0.2",
@@ -28,17 +28,17 @@
"@supabase/supabase-js": "2.55.0",
"@tanstack/react-query": "5.85.5",
"@tanstack/react-table": "^8.21.3",
"@types/react": "19.1.10",
"@types/react": "19.1.11",
"@types/react-dom": "19.1.7",
"class-variance-authority": "^0.7.1",
"date-fns": "^4.1.0",
"eslint": "^9.33.0",
"eslint": "^9.34.0",
"next": "15.5.0",
"next-themes": "0.4.6",
"prettier": "^3.6.2",
"react-day-picker": "^9.9.0",
"react-hook-form": "^7.62.0",
"react-i18next": "^15.7.1",
"react-i18next": "^15.7.2",
"sonner": "^2.0.7",
"tailwindcss": "4.1.12",
"tailwindcss-animate": "^1.0.7",

View File

@@ -1,10 +1,11 @@
'use client';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Fragment, useCallback, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import {
Cell,
flexRender,
getCoreRowModel,
useReactTable,
@@ -49,6 +50,7 @@ export {
Row,
SortingState,
VisibilityState,
flexRender,
};
interface ReactTableProps<T extends DataItem> {
@@ -57,6 +59,8 @@ interface ReactTableProps<T extends DataItem> {
renderSubComponent?: (props: { row: Row<T> }) => React.ReactElement;
pageIndex?: number;
className?: string;
headerClassName?: string;
footerClassName?: string;
pageSize?: number;
pageCount?: number;
sorting?: SortingState;
@@ -69,14 +73,17 @@ interface ReactTableProps<T extends DataItem> {
onColumnVisibilityChange?: (visibility: VisibilityState) => void;
onColumnPinningChange?: (pinning: ColumnPinningState) => void;
onRowSelectionChange?: (selection: Record<string, boolean>) => void;
onClick?: (row: Row<T>) => void;
onClick?: (props: { row: Row<T>; cell: Cell<T, unknown> }) => void;
tableProps?: React.ComponentProps<typeof Table> &
Record<`data-${string}`, string>;
sticky?: boolean;
renderCell?: (props: {
cell: Cell<T, unknown>;
style?: React.CSSProperties;
className?: string;
}) => (props: React.PropsWithChildren<object>) => React.ReactNode;
renderRow?: (props: {
row: Row<T>;
onClick?: (row: Row<T>) => void;
className?: string;
}) => (props: React.PropsWithChildren<object>) => React.ReactNode;
noResultsMessage?: React.ReactNode;
forcePagination?: boolean; // Force pagination to show even when pageCount <= 1
@@ -97,7 +104,10 @@ export function DataTable<RecordData extends DataItem>({
onClick,
tableProps,
className,
headerClassName,
footerClassName,
renderRow,
renderCell,
noResultsMessage,
sorting: controlledSorting,
columnVisibility: controlledColumnVisibility,
@@ -106,6 +116,9 @@ export function DataTable<RecordData extends DataItem>({
sticky = false,
forcePagination = false,
}: ReactTableProps<RecordData>) {
// TODO: remove when https://github.com/TanStack/table/issues/5567 gets fixed
'use no memo';
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: pageIndex ?? 0,
pageSize: pageSize ?? 15,
@@ -127,19 +140,7 @@ export function DataTable<RecordData extends DataItem>({
controlledRowSelection ?? {},
);
// Use props if provided (controlled mode), otherwise use internal state (uncontrolled mode)
const columnVisibility =
controlledColumnVisibility ?? internalColumnVisibility;
const columnPinning = controlledColumnPinning ?? internalColumnPinning;
const rowSelection = controlledRowSelection ?? internalRowSelection;
if (pagination.pageIndex !== pageIndex && pageIndex !== undefined) {
setPagination({
pageIndex,
pageSize: pagination.pageSize,
});
}
// Computed values for table state - computed inline in callbacks for fresh values
const navigateToPage = useNavigateToNewPage();
@@ -155,7 +156,9 @@ export function DataTable<RecordData extends DataItem>({
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: (updater) => {
if (typeof updater === 'function') {
const nextState = updater(columnVisibility);
const currentVisibility =
controlledColumnVisibility ?? internalColumnVisibility;
const nextState = updater(currentVisibility);
// If controlled mode (callback provided), call it
if (onColumnVisibilityChange) {
@@ -176,7 +179,8 @@ export function DataTable<RecordData extends DataItem>({
},
onColumnPinningChange: (updater) => {
if (typeof updater === 'function') {
const nextState = updater(columnPinning);
const currentPinning = controlledColumnPinning ?? internalColumnPinning;
const nextState = updater(currentPinning);
// If controlled mode (callback provided), call it
if (onColumnPinningChange) {
@@ -197,7 +201,8 @@ export function DataTable<RecordData extends DataItem>({
},
onRowSelectionChange: (updater) => {
if (typeof updater === 'function') {
const nextState = updater(rowSelection);
const currentSelection = controlledRowSelection ?? internalRowSelection;
const nextState = updater(currentSelection);
// If controlled mode (callback provided), call it
if (onRowSelectionChange) {
@@ -221,9 +226,9 @@ export function DataTable<RecordData extends DataItem>({
pagination,
sorting,
columnFilters,
columnVisibility,
columnPinning,
rowSelection,
columnVisibility: controlledColumnVisibility ?? internalColumnVisibility,
columnPinning: controlledColumnPinning ?? internalColumnPinning,
rowSelection: controlledRowSelection ?? internalRowSelection,
},
onSortingChange: (updater) => {
if (typeof updater === 'function') {
@@ -269,27 +274,12 @@ export function DataTable<RecordData extends DataItem>({
},
});
// Force table to update column pinning when controlled prop changes
useEffect(() => {
if (controlledColumnPinning) {
// Use the table's setColumnPinning method to force an update
table.setColumnPinning(controlledColumnPinning);
}
}, [controlledColumnPinning, table]);
// Force table to update column visibility when controlled prop changes
useEffect(() => {
if (controlledColumnVisibility) {
table.setColumnVisibility(controlledColumnVisibility);
}
}, [controlledColumnVisibility, table]);
// Force table to update row selection when controlled prop changes
useEffect(() => {
if (controlledRowSelection) {
table.setRowSelection(controlledRowSelection);
}
}, [controlledRowSelection, table]);
if (pagination.pageIndex !== pageIndex && pageIndex !== undefined) {
setPagination({
pageIndex,
pageSize: pagination.pageSize,
});
}
const rows = table.getRowModel().rows;
@@ -302,7 +292,7 @@ export function DataTable<RecordData extends DataItem>({
data-testid="data-table"
{...tableProps}
className={cn(
'bg-background border-separate border-spacing-0',
'bg-background border-collapse border-spacing-0',
className,
{
'h-full': data.length === 0,
@@ -310,8 +300,8 @@ export function DataTable<RecordData extends DataItem>({
)}
>
<TableHeader
className={cn('', {
['bg-background/20 outline-border sticky top-[0px] z-10 outline backdrop-blur-sm transition-all duration-300']:
className={cn(headerClassName, {
['bg-background/20 outline-border sticky top-[0px] z-10 outline backdrop-blur-sm']:
sticky,
})}
>
@@ -344,10 +334,10 @@ export function DataTable<RecordData extends DataItem>({
className={cn(
'text-muted-foreground bg-background/80 border-transparent font-sans font-medium',
{
['border-r-background sticky top-0 z-10 border-r opacity-95 backdrop-blur-sm']:
isPinned === 'left',
['border-l-background sticky top-0 z-10 border-l opacity-95 backdrop-blur-sm']:
isPinned === 'right',
['border-r-background border-r']: isPinned === 'left',
['border-l-background border-l']: isPinned === 'right',
['sticky top-0 z-10 opacity-95 backdrop-blur-sm']:
isPinned,
['relative z-0']: !isPinned,
},
)}
@@ -375,9 +365,7 @@ export function DataTable<RecordData extends DataItem>({
<TableBody>
{rows.map((row) => {
const RowWrapper = renderRow
? renderRow({ row, onClick })
: TableRow;
const RowWrapper = renderRow ? renderRow({ row }) : TableRow;
const children = row.getVisibleCells().map((cell, index) => {
const isPinned = cell.column.getIsPinned();
@@ -417,16 +405,23 @@ export function DataTable<RecordData extends DataItem>({
},
);
return (
const style = {
width: `${size}px`,
minWidth: `${size}px`,
left: left !== undefined ? `${left}px` : undefined,
right: right !== undefined ? `${right}px` : undefined,
};
return renderCell ? (
<Fragment key={cell.id}>
{renderCell({ cell, style, className })({})}
</Fragment>
) : (
<TableCell
style={{
left: left !== undefined ? `${left}px` : undefined,
right: right !== undefined ? `${right}px` : undefined,
width: `${size}px`,
minWidth: `${size}px`,
}}
key={cell.id}
style={style}
className={className}
onClick={onClick ? () => onClick({ row, cell }) : undefined}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
@@ -435,11 +430,12 @@ export function DataTable<RecordData extends DataItem>({
return (
<RowWrapper
className={cn('active:bg-accent bg-background/80', {
['hover:bg-accent/60 cursor-pointer']: !row.getIsSelected(),
})}
onClick={() => onClick && onClick(row)}
key={row.id}
className={cn('bg-background/80', {
'hover:bg-accent/60': !row.getIsSelected(),
'active:bg-accent': !!onClick,
'cursor-pointer': !!onClick && !row.getIsSelected(),
})}
data-state={row.getIsSelected() && 'selected'}
>
{children}
@@ -460,15 +456,22 @@ export function DataTable<RecordData extends DataItem>({
<If condition={displayPagination}>
<div
className={cn(
'bg-background/80 outline-border sticky bottom-0 z-10 border-b outline backdrop-blur-sm',
'bg-background/80 sticky bottom-0 z-10 border-t backdrop-blur-sm',
{
['sticky bottom-0 z-10 max-w-full rounded-none']: sticky,
},
footerClassName,
)}
>
<div>
<div className={'px-2.5 py-1.5'}>
<Pagination table={table} />
<Pagination
table={table}
pageSize={pageSize}
totalCount={
pageCount && pageSize ? pageCount * pageSize : undefined
}
/>
</div>
</div>
</div>
@@ -479,8 +482,12 @@ export function DataTable<RecordData extends DataItem>({
function Pagination<T>({
table,
totalCount,
pageSize,
}: React.PropsWithChildren<{
table: ReactTable<T>;
totalCount?: number;
pageSize?: number;
}>) {
return (
<div className="flex items-center space-x-4">
@@ -539,6 +546,15 @@ function Pagination<T>({
<ChevronsRight className={'h-4'} />
</Button>
</div>
<If condition={totalCount}>
<span className="text-muted-foreground flex items-center text-xs">
<Trans
i18nKey={'common:showingRecordCount'}
values={{ totalCount, pageSize }}
/>
</span>
</If>
</div>
);
}

View File

@@ -36,7 +36,7 @@ export function DataTable<TData, TValue>({
});
return (
<div className="rounded-md border">
<div className="rounded-md">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (