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

@@ -19,11 +19,14 @@ import {
ColumnPinningState, ColumnPinningState,
DataTable, DataTable,
VisibilityState, VisibilityState,
flexRender,
useColumnManagement, useColumnManagement,
} from '@kit/ui/enhanced-data-table'; } from '@kit/ui/enhanced-data-table';
import { Label } from '@kit/ui/label'; import { Label } from '@kit/ui/label';
import { Separator } from '@kit/ui/separator'; import { Separator } from '@kit/ui/separator';
import { Switch } from '@kit/ui/switch'; import { Switch } from '@kit/ui/switch';
import { TableCell } from '@kit/ui/table';
import { cn } from '@kit/ui/utils';
import { generatePropsString, useStoryControls } from '../lib/story-utils'; import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout'; import { ComponentStoryLayout } from './story-layout';
@@ -326,6 +329,7 @@ export function DataTableStory() {
}{' '} }{' '}
/ {currentPageData.length} / {currentPageData.length}
</span> </span>
{Object.keys(rowSelection).length > 0 && ( {Object.keys(rowSelection).length > 0 && (
<Button <Button
onClick={() => setRowSelection({})} onClick={() => setRowSelection({})}
@@ -570,16 +574,43 @@ export function DataTableStory() {
</> </>
); );
const renderExamples = () => ( const renderExamples = () => {
// Example-specific column management state
const exampleColumnManagement = useColumnManagement({
defaultVisibility: {
name: true,
email: true,
status: true,
role: true,
department: true,
salary: true,
},
defaultPinning: { left: [], right: [] },
});
// Example-specific row selection states
const [exampleRowSelection1, setExampleRowSelection1] = useState<
Record<string, boolean>
>({});
const [exampleRowSelection2, setExampleRowSelection2] = useState<
Record<string, boolean>
>({});
const [paginationRowSelection, setPaginationRowSelection] = useState<
Record<string, boolean>
>({});
return (
<div className="space-y-6"> <div className="space-y-6">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Basic Usage</CardTitle> <CardTitle>Basic Usage with Row Click</CardTitle>
<CardDescription> <CardDescription>
Simple data table with minimal setup Simple data table with row click handlers to navigate or show
details
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4">
<DataTable <DataTable
columns={[ columns={[
{ accessorKey: 'name', header: 'Name' }, { accessorKey: 'name', header: 'Name' },
@@ -596,58 +627,129 @@ export function DataTableStory() {
pageSize={5} pageSize={5}
pageCount={1} pageCount={1}
getRowId={(row) => row.id} getRowId={(row) => row.id}
onClick={({ row, cell }) => {
console.log('Row clicked:', row.original);
console.log('Cell clicked:', cell);
}}
/> />
<div className="text-muted-foreground text-xs">
💡 Click on any row to see the onClick handler in action. In a
real application, this might navigate to a user detail page or
open a modal.
</div>
</div>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>With Selection & Pinning</CardTitle> <CardTitle>With Selection, Pinning & Cell Click</CardTitle>
<CardDescription> <CardDescription>
Advanced table with selection (checkbox always pinned left) and Advanced table with selection (checkbox always pinned left) and
action column pinned right action column pinned right. Demonstrates both row and cell click
handlers.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4">
<div className="flex items-center gap-4 text-sm">
<span className="font-medium">Column Visibility:</span>
{['name', 'email', 'role', 'department'].map((columnId) => (
<label key={columnId} className="flex items-center gap-2">
<Checkbox
checked={exampleColumnManagement.isColumnVisible(
columnId,
)}
onCheckedChange={(checked) =>
exampleColumnManagement.setColumnVisible(
columnId,
!!checked,
)
}
/>
<span className="capitalize">{columnId}</span>
</label>
))}
</div>
<DataTable <DataTable
columns={[ columns={[
{ {
id: 'select', id: 'select',
header: ({ table }) => ( header: ({ table }) => (
<Checkbox <Checkbox
checked={table.getIsAllPageRowsSelected()} checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && 'indeterminate')
}
onCheckedChange={(value) => onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value) table.toggleAllPageRowsSelected(!!value)
} }
aria-label="Select all"
/> />
), ),
cell: ({ row }) => ( cell: ({ row }) => (
<Checkbox <Checkbox
checked={row.getIsSelected()} checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)} onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/> />
), ),
enableSorting: false, enableSorting: false,
enableHiding: false,
size: 40, size: 40,
}, },
{ accessorKey: 'name', header: 'Name' }, {
accessorKey: 'name',
header: 'Name',
cell: ({ row }) => (
<button
className="text-left hover:underline focus:underline focus:outline-none"
onClick={(e) => {
e.stopPropagation(); // Prevent row click
alert(
`Cell click: Opening profile for ${row.original.name}`,
);
}}
>
{row.getValue('name')}
</button>
),
},
{ accessorKey: 'email', header: 'Email' }, { accessorKey: 'email', header: 'Email' },
{ {
accessorKey: 'role', accessorKey: 'role',
header: 'Role', header: 'Role',
cell: ({ row }) => ( cell: ({ row }) => (
<Badge variant="outline">{row.getValue('role')}</Badge> <Badge
variant="outline"
className="hover:bg-accent cursor-pointer"
onClick={(e) => {
e.stopPropagation(); // Prevent row click
alert(`Filter by role: ${row.getValue('role')}`);
}}
>
{row.getValue('role')}
</Badge>
), ),
}, },
{ accessorKey: 'department', header: 'Department' },
{ {
id: 'actions', id: 'actions',
header: 'Actions', header: 'Actions',
cell: () => ( cell: ({ row }) => (
<Button size="sm" variant="ghost"> <Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation(); // Prevent row click
alert(`Edit user: ${row.original.name}`);
}}
>
Edit Edit
</Button> </Button>
), ),
enableSorting: false, enableSorting: false,
enableHiding: false,
size: 80, size: 80,
}, },
]} ]}
@@ -655,15 +757,45 @@ export function DataTableStory() {
pageSize={5} pageSize={5}
pageCount={1} pageCount={1}
getRowId={(row) => row.id} getRowId={(row) => row.id}
columnPinning={ columnPinning={{
{
left: ['select'], left: ['select'],
right: ['actions'], right: ['actions'],
} satisfies ColumnPinningState }}
columnVisibility={exampleColumnManagement.columnVisibility}
onColumnVisibilityChange={
exampleColumnManagement.setColumnVisibility
} }
rowSelection={{} satisfies Record<string, boolean>} rowSelection={exampleRowSelection1}
onRowSelectionChange={(_selection: Record<string, boolean>) => {}} onRowSelectionChange={setExampleRowSelection1}
onRowClick={(row) => {
console.log('Row clicked:', row.original);
// In a real app, might navigate to detail view
alert(`Row click: Viewing details for ${row.original.name}`);
}}
/> />
<div className="text-muted-foreground space-y-1 text-xs">
<p>💡 This example demonstrates multiple click handlers:</p>
<ul className="ml-4 list-disc space-y-1">
<li>
<strong>Name cell:</strong> Click to open user profile
</li>
<li>
<strong>Role badge:</strong> Click to filter by role
</li>
<li>
<strong>Edit button:</strong> Click to edit user
</li>
<li>
<strong>Row click:</strong> Click empty space to view
details
</li>
<li>
Cell clicks use <code>e.stopPropagation()</code> to prevent
row click
</li>
</ul>
</div>
</div>
</CardContent> </CardContent>
</Card> </Card>
@@ -676,8 +808,9 @@ export function DataTableStory() {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="h-64 rounded-lg border"> <div className="h-64 rounded-lg border p-0.5">
<DataTable <DataTable
className={''}
columns={[ columns={[
{ accessorKey: 'name', header: 'Name' }, { accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' }, { accessorKey: 'email', header: 'Email' },
@@ -717,9 +850,10 @@ export function DataTableStory() {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Full-Screen Layout Pattern</CardTitle> <CardTitle>Full-Screen Layout with Action Handlers</CardTitle>
<CardDescription> <CardDescription>
Simulated full-screen table that stretches to fill available space Simulated full-screen table with toolbar actions and keyboard
navigation
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -728,10 +862,33 @@ export function DataTableStory() {
<div className="bg-muted/30 flex items-center justify-between border-b p-3"> <div className="bg-muted/30 flex items-center justify-between border-b p-3">
<h3 className="text-sm font-semibold">Dashboard Table</h3> <h3 className="text-sm font-semibold">Dashboard Table</h3>
<div className="flex gap-2"> <div className="flex gap-2">
<Button size="sm" variant="outline"> <Button
Export size="sm"
variant="outline"
onClick={() => {
const selectedRows = Object.keys(
exampleRowSelection2,
).filter((key) => exampleRowSelection2[key]);
const selectedCount = selectedRows.length;
if (selectedCount > 0) {
alert(`Exporting ${selectedCount} selected users`);
} else {
alert('Exporting all users');
}
}}
>
Export (
{Object.keys(exampleRowSelection2).filter(
(key) => exampleRowSelection2[key],
).length || 'All'}
)
</Button>
<Button
size="sm"
onClick={() => alert('Opening add user form')}
>
Add User
</Button> </Button>
<Button size="sm">Add User</Button>
</div> </div>
</div> </div>
@@ -743,16 +900,24 @@ export function DataTableStory() {
id: 'select', id: 'select',
header: ({ table }) => ( header: ({ table }) => (
<Checkbox <Checkbox
checked={table.getIsAllPageRowsSelected()} checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() &&
'indeterminate')
}
onCheckedChange={(value) => onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value) table.toggleAllPageRowsSelected(!!value)
} }
aria-label="Select all"
/> />
), ),
cell: ({ row }) => ( cell: ({ row }) => (
<Checkbox <Checkbox
checked={row.getIsSelected()} checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)} onCheckedChange={(value) =>
row.toggleSelected(!!value)
}
aria-label="Select row"
/> />
), ),
enableSorting: false, enableSorting: false,
@@ -771,13 +936,34 @@ export function DataTableStory() {
{ {
id: 'actions', id: 'actions',
header: 'Actions', header: 'Actions',
cell: () => ( cell: ({ row }) => (
<Button size="sm" variant="ghost"> <div className="flex gap-1">
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation();
alert(`Editing ${row.original.name}`);
}}
>
Edit Edit
</Button> </Button>
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation();
if (confirm(`Delete ${row.original.name}?`)) {
alert('User deleted (simulated)');
}
}}
>
Delete
</Button>
</div>
), ),
enableSorting: false, enableSorting: false,
size: 80, size: 120,
}, },
]} ]}
data={data.slice(0, 15)} data={data.slice(0, 15)}
@@ -786,14 +972,29 @@ export function DataTableStory() {
getRowId={(row) => row.id} getRowId={(row) => row.id}
columnPinning={{ left: ['select'], right: ['actions'] }} columnPinning={{ left: ['select'], right: ['actions'] }}
sticky={true} sticky={true}
rowSelection={{}} rowSelection={exampleRowSelection2}
onRowSelectionChange={() => {}} onRowSelectionChange={setExampleRowSelection2}
onRowClick={(row) => {
console.log(
'Row clicked in full-screen layout:',
row.original,
);
}}
onRowDoubleClick={(row) => {
alert(`Double-click: Quick edit for ${row.original.name}`);
}}
/> />
</div> </div>
</div> </div>
<p className="text-muted-foreground mt-2 text-xs"> <div className="text-muted-foreground mt-2 space-y-1 text-xs">
💻 Use flex-1 min-h-0 for tables that should fill available space <p>💻 This example shows common dashboard patterns:</p>
</p> <ul className="ml-4 list-disc space-y-1">
<li>Export button shows selected count dynamically</li>
<li>Action buttons (Edit/Delete) with confirmation dialogs</li>
<li>Double-click rows for quick actions</li>
<li>Flex layout fills available space</li>
</ul>
</div>
</CardContent> </CardContent>
</Card> </Card>
@@ -806,8 +1007,11 @@ export function DataTableStory() {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="h-48 rounded-lg border sm:h-64 lg:h-80"> <div className="h-48 sm:h-64 lg:h-80">
<DataTable <DataTable
tableProps={{
className: 'border border-border',
}}
columns={[ columns={[
{ accessorKey: 'name', header: 'Name' }, { accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' }, { accessorKey: 'email', header: 'Email' },
@@ -832,8 +1036,370 @@ export function DataTableStory() {
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader>
<CardTitle>Custom Cell Rendering with renderCell</CardTitle>
<CardDescription>
Using the renderCell prop to wrap all cells with custom behavior
and styling
</CardDescription>
</CardHeader>
<CardContent>
<DataTable
columns={[
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
{
accessorKey: 'role',
header: 'Role',
cell: ({ row }) => (
<Badge variant="outline">{row.getValue('role')}</Badge>
),
},
{
accessorKey: 'salary',
header: 'Salary',
cell: ({ row }) => {
const salary = row.getValue('salary') as number;
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
}).format(salary);
},
},
{
accessorKey: 'rating',
header: 'Rating',
cell: ({ row }) => {
const rating = row.getValue('rating') as number;
return (
<div className="flex items-center gap-1">
{Array.from({ length: 5 }).map((_, i) => (
<span
key={i}
className={
i < rating
? 'text-yellow-400'
: 'text-muted-foreground'
}
>
</span>
))}
<span className="text-muted-foreground ml-1 text-xs">
({rating}/5)
</span>
</div> </div>
); );
},
},
]}
data={data.slice(0, 8)}
pageSize={8}
pageCount={1}
getRowId={(row) => row.id}
renderCell={({ cell, style, className }) => {
// Custom cell wrapper that adds hover effects and tooltips
return () => (
<TableCell
key={cell.id}
style={style}
className={cn(
className,
'group hover:bg-accent/30 relative transition-all duration-200',
// Add special styling for salary column
cell.column.id === 'salary' && 'font-mono',
// Add padding for rating column
cell.column.id === 'rating' && 'px-6',
)}
title={`Column: ${cell.column.id} | Value: ${cell.getValue()}`}
>
<div className="relative">
{/* Add a subtle border indicator on hover */}
<div className="bg-primary/20 absolute top-0 -left-2 h-full w-1 scale-y-0 transform rounded transition-transform duration-200 group-hover:scale-y-100" />
<div className="relative z-10">
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</div>
</div>
</TableCell>
);
}}
/>
<div className="text-muted-foreground mt-3 space-y-2 text-xs">
<p>💡 This example shows renderCell usage:</p>
<ul className="ml-4 list-disc space-y-1">
<li>Adds custom hover effects to all cells</li>
<li>Shows tooltips with column and value info</li>
<li>Applies conditional styling (monospace font for salary)</li>
<li>Adds animated border indicators on hover</li>
<li>Maintains all original cell content and behavior</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Pagination Examples</CardTitle>
<CardDescription>
Different pagination scenarios with proper page management
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Small dataset with pagination */}
<div className="space-y-3">
<h4 className="text-sm font-semibold">
Small Dataset (15 items, 5 per page)
</h4>
<DataTable
columns={[
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => (
<Badge variant="outline">
{row.getValue('status')}
</Badge>
),
},
]}
data={data.slice(0, 15)}
pageSize={5}
pageCount={3}
getRowId={(row) => row.id}
/>
</div>
{/* Medium dataset with pagination */}
<div className="space-y-3">
<h4 className="text-sm font-semibold">
Medium Dataset (30 items, 10 per page)
</h4>
<DataTable
columns={[
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'department', header: 'Department' },
{
accessorKey: 'role',
header: 'Role',
cell: ({ row }) => (
<Badge variant="outline">{row.getValue('role')}</Badge>
),
},
{
accessorKey: 'salary',
header: 'Salary',
cell: ({ row }) => {
const salary = row.getValue('salary') as number;
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
}).format(salary);
},
},
]}
data={data.slice(0, 30)}
pageSize={10}
pageCount={3}
getRowId={(row) => row.id}
/>
</div>
{/* Large dataset with selection and pagination */}
<div className="space-y-3">
<h4 className="text-sm font-semibold">
Large Dataset with Selection & Context Menu (50 items, 15 per
page)
</h4>
<DataTable
columns={[
{
id: 'select',
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() &&
'indeterminate')
}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) =>
row.toggleSelected(!!value)
}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
size: 40,
},
{ accessorKey: 'name', header: 'Name' },
{
accessorKey: 'email',
header: 'Email',
cell: ({ row }) => (
<button
className="text-left text-blue-600 hover:underline focus:underline focus:outline-none"
onClick={(e) => {
e.stopPropagation();
window.open(
`mailto:${row.original.email}`,
'_blank',
);
}}
>
{row.getValue('email')}
</button>
),
},
{ accessorKey: 'department', header: 'Department' },
{ accessorKey: 'location', header: 'Location' },
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => (
<Badge
variant={
row.getValue('status') === 'Active'
? 'default'
: 'secondary'
}
className="cursor-pointer"
onClick={(e) => {
e.stopPropagation();
const currentStatus = row.getValue('status');
const newStatus =
currentStatus === 'Active'
? 'Inactive'
: 'Active';
alert(
`Status would change from ${currentStatus} to ${newStatus}`,
);
}}
>
{row.getValue('status') as string}
</Badge>
),
},
{
id: 'actions',
header: 'Actions',
cell: ({ row }) => (
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation();
alert(
`Opening detailed view for ${row.original.name}`,
);
}}
>
View
</Button>
),
enableSorting: false,
enableHiding: false,
size: 80,
},
]}
data={data.slice(0, 50)}
pageSize={15}
pageCount={Math.ceil(50 / 15)}
getRowId={(row) => row.id}
columnPinning={{
left: ['select'],
right: ['actions'],
}}
rowSelection={paginationRowSelection}
onRowSelectionChange={setPaginationRowSelection}
onRowContextMenu={(row, event) => {
event.preventDefault();
const actions = [
`Edit ${row.original.name}`,
`Send message to ${row.original.name}`,
`View ${row.original.name}'s profile`,
'---',
`Delete ${row.original.name}`,
];
alert(
`Right-click context menu for ${row.original.name}:\n\n${actions.join('\n')}`,
);
}}
onRowClick={(row) => {
console.log(
'Row clicked in pagination example:',
row.original,
);
}}
/>
</div>
{/* Force pagination example */}
<div className="space-y-3">
<h4 className="text-sm font-semibold">
Force Pagination (3 items, but pagination shown)
</h4>
<p className="text-muted-foreground text-xs">
Using `forcePagination={true}` to show pagination controls
even with few items
</p>
<DataTable
columns={[
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
{
accessorKey: 'role',
header: 'Role',
cell: ({ row }) => (
<Badge variant="outline">{row.getValue('role')}</Badge>
),
},
]}
data={data.slice(0, 3)}
pageSize={5}
pageCount={1}
getRowId={(row) => row.id}
forcePagination={true}
/>
</div>
</div>
<div className="text-muted-foreground mt-6 space-y-2 text-xs">
<p>💡 Pagination examples demonstrate:</p>
<ul className="ml-4 list-disc space-y-1">
<li>Different page sizes (5, 10, 15 per page)</li>
<li>Proper pageCount calculation based on total items</li>
<li>Selection state preserved across page changes</li>
<li>Force pagination option for consistency</li>
<li>
Real pagination controls (note: URL updates don't work in
stories)
</li>
</ul>
</div>
</CardContent>
</Card>
</div>
);
};
const generateCode = () => { const generateCode = () => {
const propsString = generatePropsString( const propsString = generatePropsString(

View File

@@ -8,12 +8,12 @@
"format": "prettier --check --write \"**/*.{js,cjs,mjs,ts,tsx,md,json}\"" "format": "prettier --check --write \"**/*.{js,cjs,mjs,ts,tsx,md,json}\""
}, },
"dependencies": { "dependencies": {
"@ai-sdk/openai": "^2.0.19", "@ai-sdk/openai": "^2.0.20",
"@faker-js/faker": "^9.9.0", "@faker-js/faker": "^9.9.0",
"@hookform/resolvers": "^5.2.1", "@hookform/resolvers": "^5.2.1",
"@tanstack/react-query": "5.85.5", "@tanstack/react-query": "5.85.5",
"ai": "5.0.21", "ai": "5.0.23",
"lucide-react": "^0.540.0", "lucide-react": "^0.541.0",
"next": "15.5.0", "next": "15.5.0",
"nodemailer": "^7.0.5", "nodemailer": "^7.0.5",
"react": "19.1.1", "react": "19.1.1",
@@ -30,7 +30,7 @@
"@tailwindcss/postcss": "^4.1.12", "@tailwindcss/postcss": "^4.1.12",
"@types/node": "^24.3.0", "@types/node": "^24.3.0",
"@types/nodemailer": "7.0.1", "@types/nodemailer": "7.0.1",
"@types/react": "19.1.10", "@types/react": "19.1.11",
"@types/react-dom": "19.1.7", "@types/react-dom": "19.1.7",
"babel-plugin-react-compiler": "19.1.0-rc.2", "babel-plugin-react-compiler": "19.1.0-rc.2",
"pino-pretty": "13.0.0", "pino-pretty": "13.0.0",

View File

@@ -62,13 +62,13 @@ export default defineConfig({
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry', trace: 'on-first-retry',
navigationTimeout: 15_000, navigationTimeout: 15 * 1000,
}, },
// test timeout set to 2 minutes // test timeout set to 2 minutes
timeout: 120 * 1000, timeout: 120 * 1000,
expect: { expect: {
// expect timeout set to 5 seconds // expect timeout set to 10 seconds
timeout: 5 * 1000, timeout: 10 * 1000,
}, },
/* Configure projects for major browsers */ /* Configure projects for major browsers */
projects: [ projects: [

View File

@@ -388,11 +388,15 @@ async function filterAccounts(page: Page, email: string) {
.fill(email); .fill(email);
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
await page.waitForTimeout(250); await page.waitForTimeout(500);
} }
async function selectAccount(page: Page, email: string) { async function selectAccount(page: Page, email: string) {
await page.getByRole('link', { name: email.split('@')[0] }).click(); await page
.locator('tr', { hasText: email.split('@')[0] })
.locator('a')
.click();
await page.waitForURL(new RegExp(`/admin/accounts/[a-z0-9-]+`)); await page.waitForURL(new RegExp(`/admin/accounts/[a-z0-9-]+`));
await page.waitForTimeout(500); await page.waitForTimeout(500);
} }

View File

@@ -60,15 +60,14 @@ async function PersonalAccountBillingPage() {
<PageBody> <PageBody>
<div className={'flex flex-col space-y-4'}> <div className={'flex flex-col space-y-4'}>
<If condition={!hasBillingData}> <If
condition={hasBillingData}
fallback={
<>
<PersonalAccountCheckoutForm customerId={customerId} /> <PersonalAccountCheckoutForm customerId={customerId} />
</>
<If condition={customerId}> }
<CustomerBillingPortalForm /> >
</If>
</If>
<If condition={hasBillingData}>
<div className={'flex w-full max-w-2xl flex-col space-y-6'}> <div className={'flex w-full max-w-2xl flex-col space-y-6'}>
<If condition={subscription}> <If condition={subscription}>
{(subscription) => { {(subscription) => {

View File

@@ -60,14 +60,14 @@
"@tanstack/react-query": "5.85.5", "@tanstack/react-query": "5.85.5",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"lucide-react": "^0.540.0", "lucide-react": "^0.541.0",
"next": "15.5.0", "next": "15.5.0",
"next-sitemap": "^4.2.3", "next-sitemap": "^4.2.3",
"next-themes": "0.4.6", "next-themes": "0.4.6",
"react": "19.1.1", "react": "19.1.1",
"react-dom": "19.1.1", "react-dom": "19.1.1",
"react-hook-form": "^7.62.0", "react-hook-form": "^7.62.0",
"react-i18next": "^15.7.1", "react-i18next": "^15.7.2",
"recharts": "2.15.3", "recharts": "2.15.3",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"zod": "^3.25.74" "zod": "^3.25.74"
@@ -79,10 +79,10 @@
"@next/bundle-analyzer": "15.5.0", "@next/bundle-analyzer": "15.5.0",
"@tailwindcss/postcss": "^4.1.12", "@tailwindcss/postcss": "^4.1.12",
"@types/node": "^24.3.0", "@types/node": "^24.3.0",
"@types/react": "19.1.10", "@types/react": "19.1.11",
"@types/react-dom": "19.1.7", "@types/react-dom": "19.1.7",
"babel-plugin-react-compiler": "19.1.0-rc.2", "babel-plugin-react-compiler": "19.1.0-rc.2",
"cssnano": "^7.1.0", "cssnano": "^7.1.1",
"pino-pretty": "13.0.0", "pino-pretty": "13.0.0",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"supabase": "2.34.0", "supabase": "2.34.0",

View File

@@ -45,6 +45,7 @@
"skip": "Skip", "skip": "Skip",
"signedInAs": "Signed in as", "signedInAs": "Signed in as",
"pageOfPages": "Page {{page}} of {{total}}", "pageOfPages": "Page {{page}} of {{total}}",
"showingRecordCount": "Showing {{pageSize}} of {{totalCount}} rows",
"noData": "No data available", "noData": "No data available",
"pageNotFoundHeading": "Ouch! :|", "pageNotFoundHeading": "Ouch! :|",
"errorPageHeading": "Ouch! :|", "errorPageHeading": "Ouch! :|",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1488
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,7 @@ export default tsEsLint.config(
}, },
{ {
rules: { rules: {
'@typescript-eslint/triple-slash-reference': 'off',
'react/react-in-jsx-scope': 'off', 'react/react-in-jsx-scope': 'off',
'import/no-anonymous-default-export': 'off', 'import/no-anonymous-default-export': 'off',
'import/named': 'off', 'import/named': 'off',

View File

@@ -22,7 +22,7 @@
"devDependencies": { "devDependencies": {
"@kit/prettier-config": "workspace:*", "@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*", "@kit/tsconfig": "workspace:*",
"eslint": "^9.33.0", "eslint": "^9.34.0",
"typescript": "^5.9.2" "typescript": "^5.9.2"
}, },
"prettier": "@kit/prettier-config" "prettier": "@kit/prettier-config"