fix: avoid duplicate billing portal link (#330)
* fix: avoid duplicate billing portal link * fix: improve DataTable API
This commit is contained in:
committed by
GitHub
parent
ad427365c9
commit
f9ebe2f927
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user