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
@@ -19,11 +19,14 @@ import {
|
||||
ColumnPinningState,
|
||||
DataTable,
|
||||
VisibilityState,
|
||||
flexRender,
|
||||
useColumnManagement,
|
||||
} from '@kit/ui/enhanced-data-table';
|
||||
import { Label } from '@kit/ui/label';
|
||||
import { Separator } from '@kit/ui/separator';
|
||||
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 { ComponentStoryLayout } from './story-layout';
|
||||
@@ -326,6 +329,7 @@ export function DataTableStory() {
|
||||
}{' '}
|
||||
/ {currentPageData.length}
|
||||
</span>
|
||||
|
||||
{Object.keys(rowSelection).length > 0 && (
|
||||
<Button
|
||||
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">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Basic Usage</CardTitle>
|
||||
<CardTitle>Basic Usage with Row Click</CardTitle>
|
||||
<CardDescription>
|
||||
Simple data table with minimal setup
|
||||
Simple data table with row click handlers to navigate or show
|
||||
details
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<DataTable
|
||||
columns={[
|
||||
{ accessorKey: 'name', header: 'Name' },
|
||||
@@ -596,58 +627,129 @@ export function DataTableStory() {
|
||||
pageSize={5}
|
||||
pageCount={1}
|
||||
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>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>With Selection & Pinning</CardTitle>
|
||||
<CardTitle>With Selection, Pinning & Cell Click</CardTitle>
|
||||
<CardDescription>
|
||||
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>
|
||||
</CardHeader>
|
||||
<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
|
||||
columns={[
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
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: '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: 'role',
|
||||
header: 'Role',
|
||||
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',
|
||||
header: 'Actions',
|
||||
cell: () => (
|
||||
<Button size="sm" variant="ghost">
|
||||
cell: ({ row }) => (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Prevent row click
|
||||
alert(`Edit user: ${row.original.name}`);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
size: 80,
|
||||
},
|
||||
]}
|
||||
@@ -655,15 +757,45 @@ export function DataTableStory() {
|
||||
pageSize={5}
|
||||
pageCount={1}
|
||||
getRowId={(row) => row.id}
|
||||
columnPinning={
|
||||
{
|
||||
columnPinning={{
|
||||
left: ['select'],
|
||||
right: ['actions'],
|
||||
} satisfies ColumnPinningState
|
||||
}}
|
||||
columnVisibility={exampleColumnManagement.columnVisibility}
|
||||
onColumnVisibilityChange={
|
||||
exampleColumnManagement.setColumnVisibility
|
||||
}
|
||||
rowSelection={{} satisfies Record<string, boolean>}
|
||||
onRowSelectionChange={(_selection: Record<string, boolean>) => {}}
|
||||
rowSelection={exampleRowSelection1}
|
||||
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>
|
||||
</Card>
|
||||
|
||||
@@ -676,8 +808,9 @@ export function DataTableStory() {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64 rounded-lg border">
|
||||
<div className="h-64 rounded-lg border p-0.5">
|
||||
<DataTable
|
||||
className={''}
|
||||
columns={[
|
||||
{ accessorKey: 'name', header: 'Name' },
|
||||
{ accessorKey: 'email', header: 'Email' },
|
||||
@@ -717,9 +850,10 @@ export function DataTableStory() {
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Full-Screen Layout Pattern</CardTitle>
|
||||
<CardTitle>Full-Screen Layout with Action Handlers</CardTitle>
|
||||
<CardDescription>
|
||||
Simulated full-screen table that stretches to fill available space
|
||||
Simulated full-screen table with toolbar actions and keyboard
|
||||
navigation
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -728,10 +862,33 @@ export function DataTableStory() {
|
||||
<div className="bg-muted/30 flex items-center justify-between border-b p-3">
|
||||
<h3 className="text-sm font-semibold">Dashboard Table</h3>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline">
|
||||
Export
|
||||
<Button
|
||||
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 size="sm">Add User</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -743,16 +900,24 @@ export function DataTableStory() {
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
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)}
|
||||
onCheckedChange={(value) =>
|
||||
row.toggleSelected(!!value)
|
||||
}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
@@ -771,13 +936,34 @@ export function DataTableStory() {
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
cell: () => (
|
||||
<Button size="sm" variant="ghost">
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
alert(`Editing ${row.original.name}`);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</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,
|
||||
size: 80,
|
||||
size: 120,
|
||||
},
|
||||
]}
|
||||
data={data.slice(0, 15)}
|
||||
@@ -786,14 +972,29 @@ export function DataTableStory() {
|
||||
getRowId={(row) => row.id}
|
||||
columnPinning={{ left: ['select'], right: ['actions'] }}
|
||||
sticky={true}
|
||||
rowSelection={{}}
|
||||
onRowSelectionChange={() => {}}
|
||||
rowSelection={exampleRowSelection2}
|
||||
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>
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
💻 Use flex-1 min-h-0 for tables that should fill available space
|
||||
</p>
|
||||
<div className="text-muted-foreground mt-2 space-y-1 text-xs">
|
||||
<p>💻 This example shows common dashboard patterns:</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>
|
||||
</Card>
|
||||
|
||||
@@ -806,8 +1007,11 @@ export function DataTableStory() {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<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
|
||||
tableProps={{
|
||||
className: 'border border-border',
|
||||
}}
|
||||
columns={[
|
||||
{ accessorKey: 'name', header: 'Name' },
|
||||
{ accessorKey: 'email', header: 'Email' },
|
||||
@@ -832,8 +1036,370 @@ export function DataTableStory() {
|
||||
</p>
|
||||
</CardContent>
|
||||
</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>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
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 propsString = generatePropsString(
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
"format": "prettier --check --write \"**/*.{js,cjs,mjs,ts,tsx,md,json}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^2.0.19",
|
||||
"@ai-sdk/openai": "^2.0.20",
|
||||
"@faker-js/faker": "^9.9.0",
|
||||
"@hookform/resolvers": "^5.2.1",
|
||||
"@tanstack/react-query": "5.85.5",
|
||||
"ai": "5.0.21",
|
||||
"lucide-react": "^0.540.0",
|
||||
"ai": "5.0.23",
|
||||
"lucide-react": "^0.541.0",
|
||||
"next": "15.5.0",
|
||||
"nodemailer": "^7.0.5",
|
||||
"react": "19.1.1",
|
||||
@@ -30,7 +30,7 @@
|
||||
"@tailwindcss/postcss": "^4.1.12",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/nodemailer": "7.0.1",
|
||||
"@types/react": "19.1.10",
|
||||
"@types/react": "19.1.11",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"pino-pretty": "13.0.0",
|
||||
|
||||
@@ -62,13 +62,13 @@ export default defineConfig({
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
navigationTimeout: 15_000,
|
||||
navigationTimeout: 15 * 1000,
|
||||
},
|
||||
// test timeout set to 2 minutes
|
||||
timeout: 120 * 1000,
|
||||
expect: {
|
||||
// expect timeout set to 5 seconds
|
||||
timeout: 5 * 1000,
|
||||
// expect timeout set to 10 seconds
|
||||
timeout: 10 * 1000,
|
||||
},
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
|
||||
@@ -388,11 +388,15 @@ async function filterAccounts(page: Page, email: string) {
|
||||
.fill(email);
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(250);
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
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.waitForTimeout(500);
|
||||
}
|
||||
|
||||
@@ -60,15 +60,14 @@ async function PersonalAccountBillingPage() {
|
||||
|
||||
<PageBody>
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<If condition={!hasBillingData}>
|
||||
<If
|
||||
condition={hasBillingData}
|
||||
fallback={
|
||||
<>
|
||||
<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'}>
|
||||
<If condition={subscription}>
|
||||
{(subscription) => {
|
||||
|
||||
@@ -60,14 +60,14 @@
|
||||
"@tanstack/react-query": "5.85.5",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.540.0",
|
||||
"lucide-react": "^0.541.0",
|
||||
"next": "15.5.0",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"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",
|
||||
"recharts": "2.15.3",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zod": "^3.25.74"
|
||||
@@ -79,10 +79,10 @@
|
||||
"@next/bundle-analyzer": "15.5.0",
|
||||
"@tailwindcss/postcss": "^4.1.12",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/react": "19.1.10",
|
||||
"@types/react": "19.1.11",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"cssnano": "^7.1.0",
|
||||
"cssnano": "^7.1.1",
|
||||
"pino-pretty": "13.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"supabase": "2.34.0",
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"skip": "Skip",
|
||||
"signedInAs": "Signed in as",
|
||||
"pageOfPages": "Page {{page}} of {{total}}",
|
||||
"showingRecordCount": "Showing {{pageSize}} of {{totalCount}} rows",
|
||||
"noData": "No data available",
|
||||
"pageNotFoundHeading": "Ouch! :|",
|
||||
"errorPageHeading": "Ouch! :|",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
if (pagination.pageIndex !== pageIndex && pageIndex !== undefined) {
|
||||
setPagination({
|
||||
pageIndex,
|
||||
pageSize: pagination.pageSize,
|
||||
});
|
||||
}
|
||||
}, [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;
|
||||
|
||||
@@ -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 (
|
||||
<TableCell
|
||||
style={{
|
||||
left: left !== undefined ? `${left}px` : undefined,
|
||||
right: right !== undefined ? `${right}px` : undefined,
|
||||
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
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
1488
pnpm-lock.yaml
generated
1488
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@ export default tsEsLint.config(
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/triple-slash-reference': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'import/no-anonymous-default-export': 'off',
|
||||
'import/named': 'off',
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"devDependencies": {
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint": "^9.34.0",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"prettier": "@kit/prettier-config"
|
||||
|
||||
Reference in New Issue
Block a user