create-app-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/boilerplate/README.md +18 -0
- package/boilerplate/react-base/.env.example +1 -0
- package/boilerplate/react-base/README.md +3 -0
- package/boilerplate/react-base/components.json +19 -0
- package/boilerplate/react-base/eslint.config.js +32 -0
- package/boilerplate/react-base/index.html +12 -0
- package/boilerplate/react-base/package.json +71 -0
- package/boilerplate/react-base/postcss.config.js +6 -0
- package/boilerplate/react-base/prettier.config.js +6 -0
- package/boilerplate/react-base/src/api/axios.ts +20 -0
- package/boilerplate/react-base/src/app/store.ts +13 -0
- package/boilerplate/react-base/src/components/data-table.tsx +919 -0
- package/boilerplate/react-base/src/components/ui/accordion.tsx +44 -0
- package/boilerplate/react-base/src/components/ui/alert-dialog.tsx +105 -0
- package/boilerplate/react-base/src/components/ui/alert.tsx +40 -0
- package/boilerplate/react-base/src/components/ui/avatar.tsx +30 -0
- package/boilerplate/react-base/src/components/ui/badge.tsx +27 -0
- package/boilerplate/react-base/src/components/ui/bar-chart.tsx +76 -0
- package/boilerplate/react-base/src/components/ui/breadcrumb.tsx +87 -0
- package/boilerplate/react-base/src/components/ui/button.tsx +34 -0
- package/boilerplate/react-base/src/components/ui/calendar.tsx +63 -0
- package/boilerplate/react-base/src/components/ui/card.tsx +36 -0
- package/boilerplate/react-base/src/components/ui/chart.tsx +280 -0
- package/boilerplate/react-base/src/components/ui/checkbox.tsx +51 -0
- package/boilerplate/react-base/src/components/ui/context-menu.tsx +173 -0
- package/boilerplate/react-base/src/components/ui/date-picker.tsx +42 -0
- package/boilerplate/react-base/src/components/ui/dialog.tsx +87 -0
- package/boilerplate/react-base/src/components/ui/drawer.tsx +81 -0
- package/boilerplate/react-base/src/components/ui/dropdown-menu.tsx +81 -0
- package/boilerplate/react-base/src/components/ui/dropdown-types.ts +28 -0
- package/boilerplate/react-base/src/components/ui/field.tsx +194 -0
- package/boilerplate/react-base/src/components/ui/hover-card.tsx +26 -0
- package/boilerplate/react-base/src/components/ui/input-group.tsx +98 -0
- package/boilerplate/react-base/src/components/ui/input-otp.tsx +63 -0
- package/boilerplate/react-base/src/components/ui/input.tsx +12 -0
- package/boilerplate/react-base/src/components/ui/item.tsx +152 -0
- package/boilerplate/react-base/src/components/ui/kbd.tsx +13 -0
- package/boilerplate/react-base/src/components/ui/label.tsx +14 -0
- package/boilerplate/react-base/src/components/ui/line-chart.tsx +65 -0
- package/boilerplate/react-base/src/components/ui/menubar.tsx +217 -0
- package/boilerplate/react-base/src/components/ui/multi-select-dropdown.tsx +200 -0
- package/boilerplate/react-base/src/components/ui/navigation-menu.tsx +120 -0
- package/boilerplate/react-base/src/components/ui/pie-chart.tsx +87 -0
- package/boilerplate/react-base/src/components/ui/popover.tsx +29 -0
- package/boilerplate/react-base/src/components/ui/progress.tsx +19 -0
- package/boilerplate/react-base/src/components/ui/radio-group.tsx +36 -0
- package/boilerplate/react-base/src/components/ui/scroll-area.tsx +38 -0
- package/boilerplate/react-base/src/components/ui/searchable-dropdown.tsx +118 -0
- package/boilerplate/react-base/src/components/ui/select.tsx +140 -0
- package/boilerplate/react-base/src/components/ui/separator.tsx +20 -0
- package/boilerplate/react-base/src/components/ui/sheet.tsx +70 -0
- package/boilerplate/react-base/src/components/ui/sidebar.tsx +470 -0
- package/boilerplate/react-base/src/components/ui/skeleton.tsx +11 -0
- package/boilerplate/react-base/src/components/ui/slider.tsx +23 -0
- package/boilerplate/react-base/src/components/ui/sonner.tsx +21 -0
- package/boilerplate/react-base/src/components/ui/sparkline.tsx +38 -0
- package/boilerplate/react-base/src/components/ui/spinner.tsx +10 -0
- package/boilerplate/react-base/src/components/ui/switch.tsx +16 -0
- package/boilerplate/react-base/src/components/ui/table.tsx +80 -0
- package/boilerplate/react-base/src/components/ui/tabs.tsx +32 -0
- package/boilerplate/react-base/src/components/ui/textarea.tsx +12 -0
- package/boilerplate/react-base/src/components/ui/toggle-group.tsx +49 -0
- package/boilerplate/react-base/src/components/ui/toggle.tsx +33 -0
- package/boilerplate/react-base/src/components/ui/tooltip.tsx +23 -0
- package/boilerplate/react-base/src/components/ui/typography.tsx +76 -0
- package/boilerplate/react-base/src/config/constants.ts +3 -0
- package/boilerplate/react-base/src/config/theme.ts +432 -0
- package/boilerplate/react-base/src/config/user.ts +52 -0
- package/boilerplate/react-base/src/context/theme-provider.tsx +12 -0
- package/boilerplate/react-base/src/features/auth/authSlice.ts +19 -0
- package/boilerplate/react-base/src/hooks/index.ts +1 -0
- package/boilerplate/react-base/src/hooks/use-mobile.ts +17 -0
- package/boilerplate/react-base/src/lib/utils.ts +6 -0
- package/boilerplate/react-base/src/routes/index.tsx +7 -0
- package/boilerplate/react-base/src/styles/globals.css +15 -0
- package/boilerplate/react-base/src/vite-env.d.ts +31 -0
- package/boilerplate/react-base/tailwind.config.ts +75 -0
- package/boilerplate/react-base/tsconfig.app.json +20 -0
- package/boilerplate/react-base/tsconfig.json +7 -0
- package/boilerplate/react-base/tsconfig.node.json +16 -0
- package/boilerplate/react-base/vite.config.ts +12 -0
- package/dist/bin/index.js +8 -0
- package/dist/src/cli-args.js +52 -0
- package/dist/src/generator.js +85 -0
- package/dist/src/installer.js +7 -0
- package/dist/src/paths.js +61 -0
- package/dist/src/prompts.js +79 -0
- package/dist/src/replace-placeholders.js +22 -0
- package/dist/src/utils.js +16 -0
- package/package.json +63 -0
- package/templates/admin-portal/README.md +26 -0
- package/templates/admin-portal/src/App.tsx +85 -0
- package/templates/admin-portal/src/assets/auth-hero.jpg +0 -0
- package/templates/admin-portal/src/assets/brand-logo.png +0 -0
- package/templates/admin-portal/src/components/app-breadcrumb.tsx +41 -0
- package/templates/admin-portal/src/components/app-header.tsx +20 -0
- package/templates/admin-portal/src/components/app-sidebar.tsx +78 -0
- package/templates/admin-portal/src/components/auth-layout.tsx +66 -0
- package/templates/admin-portal/src/components/dashboard-metric-card.tsx +105 -0
- package/templates/admin-portal/src/components/data-table.tsx +919 -0
- package/templates/admin-portal/src/components/layout-shell.tsx +23 -0
- package/templates/admin-portal/src/components/notifications-sheet.tsx +91 -0
- package/templates/admin-portal/src/components/sidebar-nav.tsx +164 -0
- package/templates/admin-portal/src/components/user-avatar.tsx +26 -0
- package/templates/admin-portal/src/components/user-menu.tsx +163 -0
- package/templates/admin-portal/src/config/branding.ts +17 -0
- package/templates/admin-portal/src/config/chart-data.ts +44 -0
- package/templates/admin-portal/src/config/navigation.ts +42 -0
- package/templates/admin-portal/src/context/auth-context.tsx +32 -0
- package/templates/admin-portal/src/lib/breadcrumbs.ts +58 -0
- package/templates/admin-portal/src/main.tsx +18 -0
- package/templates/admin-portal/src/pages/components/demo-columns.tsx +170 -0
- package/templates/admin-portal/src/pages/components.tsx +1368 -0
- package/templates/admin-portal/src/pages/dashboard.tsx +143 -0
- package/templates/admin-portal/src/pages/login.tsx +81 -0
- package/templates/admin-portal/src/pages/settings/notifications.tsx +31 -0
- package/templates/admin-portal/src/pages/settings/profile.tsx +26 -0
- package/templates/admin-portal/src/pages/signup.tsx +81 -0
- package/templates/admin-portal/src/pages/users.tsx +12 -0
- package/templates/admin-portal/tsconfig.json +10 -0
- package/templates/blank/README.md +15 -0
- package/templates/blank/src/App.tsx +5 -0
- package/templates/blank/src/main.tsx +15 -0
- package/templates/blank/src/pages/home.tsx +20 -0
- package/templates/blank/tsconfig.json +10 -0
- package/templates/tsconfig.overlay.base.json +7 -0
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Column,
|
|
3
|
+
type ColumnDef,
|
|
4
|
+
type ColumnFiltersState,
|
|
5
|
+
type Row,
|
|
6
|
+
type SortingState,
|
|
7
|
+
type Table as TanstackTable,
|
|
8
|
+
type VisibilityState,
|
|
9
|
+
flexRender,
|
|
10
|
+
getCoreRowModel,
|
|
11
|
+
getFacetedRowModel,
|
|
12
|
+
getFacetedUniqueValues,
|
|
13
|
+
getFilteredRowModel,
|
|
14
|
+
getPaginationRowModel,
|
|
15
|
+
getSortedRowModel,
|
|
16
|
+
useReactTable,
|
|
17
|
+
} from "@tanstack/react-table";
|
|
18
|
+
import {
|
|
19
|
+
ArrowDown,
|
|
20
|
+
ArrowUp,
|
|
21
|
+
ArrowUpDown,
|
|
22
|
+
ChevronLeft,
|
|
23
|
+
ChevronRight,
|
|
24
|
+
ChevronsLeft,
|
|
25
|
+
ChevronsRight,
|
|
26
|
+
Download,
|
|
27
|
+
ListFilter,
|
|
28
|
+
RefreshCw,
|
|
29
|
+
Search,
|
|
30
|
+
SlidersHorizontal,
|
|
31
|
+
UserPlus,
|
|
32
|
+
X,
|
|
33
|
+
} from "lucide-react";
|
|
34
|
+
import * as React from "react";
|
|
35
|
+
import { Badge } from "@/components/ui/badge";
|
|
36
|
+
import { Button } from "@/components/ui/button";
|
|
37
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
38
|
+
import {
|
|
39
|
+
DropdownMenu,
|
|
40
|
+
DropdownMenuCheckboxItem,
|
|
41
|
+
DropdownMenuContent,
|
|
42
|
+
DropdownMenuItem,
|
|
43
|
+
DropdownMenuLabel,
|
|
44
|
+
DropdownMenuSeparator,
|
|
45
|
+
DropdownMenuTrigger,
|
|
46
|
+
} from "@/components/ui/dropdown-menu";
|
|
47
|
+
import { Input } from "@/components/ui/input";
|
|
48
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
49
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
50
|
+
import {
|
|
51
|
+
Select,
|
|
52
|
+
SelectContent,
|
|
53
|
+
SelectItem,
|
|
54
|
+
SelectTrigger,
|
|
55
|
+
SelectValue,
|
|
56
|
+
} from "@/components/ui/select";
|
|
57
|
+
import { Separator } from "@/components/ui/separator";
|
|
58
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
59
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
60
|
+
import { cn } from "@/lib/utils";
|
|
61
|
+
|
|
62
|
+
declare module "@tanstack/react-table" {
|
|
63
|
+
interface ColumnMeta<TData, TValue> {
|
|
64
|
+
title?: string;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type DataTableFilterOption = {
|
|
69
|
+
label: string;
|
|
70
|
+
value: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type DataTableFilterableColumn = {
|
|
74
|
+
id: string;
|
|
75
|
+
title: string;
|
|
76
|
+
options: DataTableFilterOption[];
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type DataTableSearchAction = {
|
|
80
|
+
label: string;
|
|
81
|
+
onClick: () => void;
|
|
82
|
+
backgroundColor?: string;
|
|
83
|
+
hoverBackgroundColor?: string;
|
|
84
|
+
textColor?: string;
|
|
85
|
+
icon?: React.ReactNode;
|
|
86
|
+
ariaLabel?: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type DataTableProps<TData, TValue> = {
|
|
90
|
+
columns: ColumnDef<TData, TValue>[];
|
|
91
|
+
data: TData[];
|
|
92
|
+
pageSize?: number;
|
|
93
|
+
pageSizeOptions?: number[];
|
|
94
|
+
searchPlaceholder?: string;
|
|
95
|
+
filterableColumns?: DataTableFilterableColumn[];
|
|
96
|
+
onRefresh?: () => void | Promise<void>;
|
|
97
|
+
isRefreshing?: boolean;
|
|
98
|
+
onExport?: () => void;
|
|
99
|
+
enableSearch?: boolean;
|
|
100
|
+
enableRowSelection?: boolean;
|
|
101
|
+
enableColumnVisibility?: boolean;
|
|
102
|
+
searchAction?: DataTableSearchAction;
|
|
103
|
+
searchActions?: React.ReactNode;
|
|
104
|
+
toolbarActions?: React.ReactNode;
|
|
105
|
+
bulkActions?: React.ReactNode;
|
|
106
|
+
onSelectionChange?: (rows: TData[]) => void;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
function getColumnLabel<TData>(column: Column<TData, unknown>) {
|
|
110
|
+
return column.columnDef.meta?.title ?? column.id;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const DataTableIconButton = React.forwardRef<
|
|
114
|
+
HTMLButtonElement,
|
|
115
|
+
React.ComponentProps<typeof Button> & { tooltip: string; badge?: number | string }
|
|
116
|
+
>(({ tooltip, badge, children, className, ...props }, ref) => (
|
|
117
|
+
<Tooltip>
|
|
118
|
+
<TooltipTrigger asChild>
|
|
119
|
+
<Button
|
|
120
|
+
ref={ref}
|
|
121
|
+
type="button"
|
|
122
|
+
variant="outline"
|
|
123
|
+
size="sm"
|
|
124
|
+
className={cn("relative h-8 w-8 shrink-0 border-dashed bg-background p-0 shadow-sm", className)}
|
|
125
|
+
aria-label={tooltip}
|
|
126
|
+
{...props}
|
|
127
|
+
>
|
|
128
|
+
{children}
|
|
129
|
+
{badge !== undefined && Number(badge) > 0 ? (
|
|
130
|
+
<Badge
|
|
131
|
+
variant="secondary"
|
|
132
|
+
className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-normal"
|
|
133
|
+
>
|
|
134
|
+
{badge}
|
|
135
|
+
</Badge>
|
|
136
|
+
) : null}
|
|
137
|
+
</Button>
|
|
138
|
+
</TooltipTrigger>
|
|
139
|
+
<TooltipContent side="bottom">{tooltip}</TooltipContent>
|
|
140
|
+
</Tooltip>
|
|
141
|
+
));
|
|
142
|
+
DataTableIconButton.displayName = "DataTableIconButton";
|
|
143
|
+
|
|
144
|
+
export type DataTableRowAction = {
|
|
145
|
+
tooltip: string;
|
|
146
|
+
icon: React.ReactNode;
|
|
147
|
+
onClick: () => void;
|
|
148
|
+
variant?: React.ComponentProps<typeof Button>["variant"];
|
|
149
|
+
disabled?: boolean;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export function DataTableRowActions({
|
|
153
|
+
actions,
|
|
154
|
+
className,
|
|
155
|
+
align = "center",
|
|
156
|
+
}: {
|
|
157
|
+
actions: DataTableRowAction[];
|
|
158
|
+
className?: string;
|
|
159
|
+
align?: "center" | "end";
|
|
160
|
+
}) {
|
|
161
|
+
return (
|
|
162
|
+
<div
|
|
163
|
+
className={cn(
|
|
164
|
+
"flex w-full items-center gap-1",
|
|
165
|
+
align === "center" ? "justify-center" : "justify-end",
|
|
166
|
+
className,
|
|
167
|
+
)}
|
|
168
|
+
>
|
|
169
|
+
{actions.map((action, index) => (
|
|
170
|
+
<Tooltip key={`${action.tooltip}-${index}`}>
|
|
171
|
+
<TooltipTrigger asChild>
|
|
172
|
+
<Button
|
|
173
|
+
type="button"
|
|
174
|
+
variant={action.variant ?? "outline"}
|
|
175
|
+
size="sm"
|
|
176
|
+
className="h-8 w-8 shrink-0 p-0"
|
|
177
|
+
aria-label={action.tooltip}
|
|
178
|
+
onClick={action.onClick}
|
|
179
|
+
disabled={action.disabled}
|
|
180
|
+
>
|
|
181
|
+
{action.icon}
|
|
182
|
+
</Button>
|
|
183
|
+
</TooltipTrigger>
|
|
184
|
+
<TooltipContent side="bottom">{action.tooltip}</TooltipContent>
|
|
185
|
+
</Tooltip>
|
|
186
|
+
))}
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Standard actions column with icon-only update/delete (extend via getActions). */
|
|
192
|
+
export function dataTableActionsColumn<TData>({
|
|
193
|
+
getActions,
|
|
194
|
+
}: {
|
|
195
|
+
getActions: (row: TData) => DataTableRowAction[];
|
|
196
|
+
}): ColumnDef<TData> {
|
|
197
|
+
return {
|
|
198
|
+
id: "actions",
|
|
199
|
+
meta: { title: "Actions" },
|
|
200
|
+
enableSorting: false,
|
|
201
|
+
enableHiding: false,
|
|
202
|
+
header: () => (
|
|
203
|
+
<div className="flex w-full justify-center">
|
|
204
|
+
<span className="text-sm font-medium">Actions</span>
|
|
205
|
+
</div>
|
|
206
|
+
),
|
|
207
|
+
cell: ({ row }) => <DataTableRowActions actions={getActions(row.original)} align="center" />,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function dataTableSelectColumn<TData>({
|
|
212
|
+
shape = "square",
|
|
213
|
+
}: {
|
|
214
|
+
shape?: "rounded" | "square";
|
|
215
|
+
} = {}): ColumnDef<TData> {
|
|
216
|
+
return {
|
|
217
|
+
id: "select",
|
|
218
|
+
header: ({ table }) => (
|
|
219
|
+
<Checkbox
|
|
220
|
+
shape={shape}
|
|
221
|
+
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
|
|
222
|
+
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
|
223
|
+
aria-label="Select all rows on this page"
|
|
224
|
+
/>
|
|
225
|
+
),
|
|
226
|
+
cell: ({ row }) => (
|
|
227
|
+
<Checkbox
|
|
228
|
+
shape={shape}
|
|
229
|
+
checked={row.getIsSelected()}
|
|
230
|
+
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
231
|
+
aria-label="Select row"
|
|
232
|
+
/>
|
|
233
|
+
),
|
|
234
|
+
enableSorting: false,
|
|
235
|
+
enableHiding: false,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function DataTableColumnHeader<TData, TValue>({
|
|
240
|
+
column,
|
|
241
|
+
title,
|
|
242
|
+
className,
|
|
243
|
+
}: {
|
|
244
|
+
column: {
|
|
245
|
+
getCanSort: () => boolean;
|
|
246
|
+
getIsSorted: () => false | "asc" | "desc";
|
|
247
|
+
toggleSorting: (desc?: boolean) => void;
|
|
248
|
+
};
|
|
249
|
+
title: string;
|
|
250
|
+
className?: string;
|
|
251
|
+
}) {
|
|
252
|
+
if (!column.getCanSort()) {
|
|
253
|
+
return <span className={cn("text-sm font-medium", className)}>{title}</span>;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const sorted = column.getIsSorted();
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
className={cn(
|
|
262
|
+
"-ml-2 inline-flex h-8 items-center gap-1.5 rounded-md px-2 text-sm font-medium transition-colors",
|
|
263
|
+
"hover:bg-muted/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
264
|
+
sorted ? "text-foreground" : "text-muted-foreground",
|
|
265
|
+
className,
|
|
266
|
+
)}
|
|
267
|
+
onClick={() => column.toggleSorting(sorted === "asc")}
|
|
268
|
+
>
|
|
269
|
+
<span>{title}</span>
|
|
270
|
+
{sorted === "desc" ? (
|
|
271
|
+
<ArrowDown className="h-3.5 w-3.5" />
|
|
272
|
+
) : sorted === "asc" ? (
|
|
273
|
+
<ArrowUp className="h-3.5 w-3.5" />
|
|
274
|
+
) : (
|
|
275
|
+
<ArrowUpDown className="h-3.5 w-3.5 opacity-50" />
|
|
276
|
+
)}
|
|
277
|
+
</button>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function DataTableFacetedFilterGroup<TData>({
|
|
282
|
+
column,
|
|
283
|
+
title,
|
|
284
|
+
options,
|
|
285
|
+
}: {
|
|
286
|
+
column: Column<TData, unknown> | undefined;
|
|
287
|
+
title: string;
|
|
288
|
+
options: DataTableFilterOption[];
|
|
289
|
+
}) {
|
|
290
|
+
const filterValues = (column?.getFilterValue() as string[] | undefined) ?? [];
|
|
291
|
+
|
|
292
|
+
const toggleOption = (optionValue: string, checked: boolean | "indeterminate") => {
|
|
293
|
+
if (!column) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const next = new Set(filterValues);
|
|
298
|
+
if (checked === true) {
|
|
299
|
+
next.add(optionValue);
|
|
300
|
+
} else {
|
|
301
|
+
next.delete(optionValue);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const values = Array.from(next);
|
|
305
|
+
column.setFilterValue(values.length ? values : undefined);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<div className="space-y-2">
|
|
310
|
+
<p className="text-sm font-medium">{title}</p>
|
|
311
|
+
<div className="space-y-1">
|
|
312
|
+
{options.map((option) => {
|
|
313
|
+
const isSelected = filterValues.includes(option.value);
|
|
314
|
+
return (
|
|
315
|
+
<div
|
|
316
|
+
key={option.value}
|
|
317
|
+
className="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-muted/60"
|
|
318
|
+
onClick={() => toggleOption(option.value, !isSelected)}
|
|
319
|
+
onKeyDown={(event) => {
|
|
320
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
321
|
+
event.preventDefault();
|
|
322
|
+
toggleOption(option.value, !isSelected);
|
|
323
|
+
}
|
|
324
|
+
}}
|
|
325
|
+
role="button"
|
|
326
|
+
tabIndex={0}
|
|
327
|
+
>
|
|
328
|
+
<Checkbox
|
|
329
|
+
shape="square"
|
|
330
|
+
checked={isSelected}
|
|
331
|
+
onCheckedChange={(checked) => toggleOption(option.value, checked)}
|
|
332
|
+
onClick={(event) => event.stopPropagation()}
|
|
333
|
+
/>
|
|
334
|
+
<span>{option.label}</span>
|
|
335
|
+
</div>
|
|
336
|
+
);
|
|
337
|
+
})}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function DataTableSearch<TData>({
|
|
344
|
+
table,
|
|
345
|
+
searchPlaceholder,
|
|
346
|
+
className,
|
|
347
|
+
}: {
|
|
348
|
+
table: TanstackTable<TData>;
|
|
349
|
+
searchPlaceholder: string;
|
|
350
|
+
className?: string;
|
|
351
|
+
}) {
|
|
352
|
+
const query = (table.getState().globalFilter as string) ?? "";
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<div className={cn("relative min-w-0 flex-1", className)}>
|
|
356
|
+
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
357
|
+
<Input
|
|
358
|
+
placeholder={searchPlaceholder}
|
|
359
|
+
value={query}
|
|
360
|
+
onChange={(event) => table.setGlobalFilter(event.target.value)}
|
|
361
|
+
className="h-9 bg-background pl-8 pr-8 shadow-sm"
|
|
362
|
+
/>
|
|
363
|
+
{query ? (
|
|
364
|
+
<button
|
|
365
|
+
type="button"
|
|
366
|
+
aria-label="Clear search"
|
|
367
|
+
className="absolute right-2.5 top-2.5 rounded-sm text-muted-foreground hover:text-foreground"
|
|
368
|
+
onClick={() => table.setGlobalFilter("")}
|
|
369
|
+
>
|
|
370
|
+
<X className="h-4 w-4" />
|
|
371
|
+
</button>
|
|
372
|
+
) : null}
|
|
373
|
+
</div>
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function DataTableSearchActionButton({ action }: { action: DataTableSearchAction }) {
|
|
378
|
+
const { label, onClick, backgroundColor, hoverBackgroundColor, textColor, icon, ariaLabel } = action;
|
|
379
|
+
const tooltip = ariaLabel ?? label;
|
|
380
|
+
const hasCustomColors = Boolean(backgroundColor || textColor || hoverBackgroundColor);
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
<Tooltip>
|
|
384
|
+
<TooltipTrigger asChild>
|
|
385
|
+
<Button
|
|
386
|
+
variant={hasCustomColors ? "default" : "brand"}
|
|
387
|
+
size="sm"
|
|
388
|
+
className={cn(
|
|
389
|
+
"h-8 w-8 shrink-0 border-0 p-0 shadow-sm",
|
|
390
|
+
hasCustomColors && "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
391
|
+
)}
|
|
392
|
+
style={
|
|
393
|
+
hasCustomColors
|
|
394
|
+
? {
|
|
395
|
+
backgroundColor,
|
|
396
|
+
color: textColor,
|
|
397
|
+
}
|
|
398
|
+
: undefined
|
|
399
|
+
}
|
|
400
|
+
aria-label={tooltip}
|
|
401
|
+
onClick={onClick}
|
|
402
|
+
onMouseEnter={(event) => {
|
|
403
|
+
if (hoverBackgroundColor) {
|
|
404
|
+
event.currentTarget.style.backgroundColor = hoverBackgroundColor;
|
|
405
|
+
}
|
|
406
|
+
}}
|
|
407
|
+
onMouseLeave={(event) => {
|
|
408
|
+
if (backgroundColor) {
|
|
409
|
+
event.currentTarget.style.backgroundColor = backgroundColor;
|
|
410
|
+
}
|
|
411
|
+
}}
|
|
412
|
+
>
|
|
413
|
+
{icon ?? <UserPlus className="h-4 w-4" />}
|
|
414
|
+
</Button>
|
|
415
|
+
</TooltipTrigger>
|
|
416
|
+
<TooltipContent side="bottom">{tooltip}</TooltipContent>
|
|
417
|
+
</Tooltip>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function DataTableFiltersPopover<TData>({
|
|
422
|
+
table,
|
|
423
|
+
filterableColumns,
|
|
424
|
+
}: {
|
|
425
|
+
table: TanstackTable<TData>;
|
|
426
|
+
filterableColumns?: DataTableFilterableColumn[];
|
|
427
|
+
}) {
|
|
428
|
+
const activeFilterCount = table.getState().columnFilters.length;
|
|
429
|
+
|
|
430
|
+
if (!filterableColumns?.length) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return (
|
|
435
|
+
<Popover modal>
|
|
436
|
+
<PopoverTrigger asChild>
|
|
437
|
+
<DataTableIconButton tooltip="Filters" badge={activeFilterCount || undefined}>
|
|
438
|
+
<ListFilter className="h-4 w-4" />
|
|
439
|
+
</DataTableIconButton>
|
|
440
|
+
</PopoverTrigger>
|
|
441
|
+
<PopoverContent align="start" className="z-[100] w-80 p-0">
|
|
442
|
+
<div className="border-b px-4 py-3">
|
|
443
|
+
<p className="text-sm font-semibold">Filters</p>
|
|
444
|
+
<p className="text-xs text-muted-foreground">Filter rows by column values.</p>
|
|
445
|
+
</div>
|
|
446
|
+
<ScrollArea className="max-h-80">
|
|
447
|
+
<div className="space-y-4 p-4">
|
|
448
|
+
{filterableColumns.map((filterColumn) => (
|
|
449
|
+
<DataTableFacetedFilterGroup
|
|
450
|
+
key={filterColumn.id}
|
|
451
|
+
column={table.getColumn(filterColumn.id)}
|
|
452
|
+
title={filterColumn.title}
|
|
453
|
+
options={filterColumn.options}
|
|
454
|
+
/>
|
|
455
|
+
))}
|
|
456
|
+
</div>
|
|
457
|
+
</ScrollArea>
|
|
458
|
+
{activeFilterCount > 0 && (
|
|
459
|
+
<div className="border-t p-2">
|
|
460
|
+
<Button
|
|
461
|
+
variant="outline"
|
|
462
|
+
size="sm"
|
|
463
|
+
className="h-8 w-full"
|
|
464
|
+
onClick={() => table.resetColumnFilters()}
|
|
465
|
+
>
|
|
466
|
+
Reset filters
|
|
467
|
+
</Button>
|
|
468
|
+
</div>
|
|
469
|
+
)}
|
|
470
|
+
</PopoverContent>
|
|
471
|
+
</Popover>
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function DataTableSortDropdown<TData>({ table }: { table: TanstackTable<TData> }) {
|
|
476
|
+
const sortableColumns = table.getAllLeafColumns().filter((column) => column.getCanSort());
|
|
477
|
+
const sorting = table.getState().sorting;
|
|
478
|
+
|
|
479
|
+
return (
|
|
480
|
+
<DropdownMenu modal={false}>
|
|
481
|
+
<DropdownMenuTrigger asChild>
|
|
482
|
+
<DataTableIconButton tooltip="Sort" badge={sorting.length || undefined}>
|
|
483
|
+
<ArrowUpDown className="h-4 w-4" />
|
|
484
|
+
</DataTableIconButton>
|
|
485
|
+
</DropdownMenuTrigger>
|
|
486
|
+
<DropdownMenuContent align="start" className="z-[100] w-52">
|
|
487
|
+
<DropdownMenuLabel>Sort by</DropdownMenuLabel>
|
|
488
|
+
<DropdownMenuSeparator />
|
|
489
|
+
{sortableColumns.length === 0 ? (
|
|
490
|
+
<p className="px-2 py-3 text-sm text-muted-foreground">No sortable columns.</p>
|
|
491
|
+
) : (
|
|
492
|
+
sortableColumns.map((column) => {
|
|
493
|
+
const sorted = column.getIsSorted();
|
|
494
|
+
return (
|
|
495
|
+
<DropdownMenuItem
|
|
496
|
+
key={column.id}
|
|
497
|
+
onSelect={(event) => {
|
|
498
|
+
event.preventDefault();
|
|
499
|
+
column.toggleSorting(sorted === "asc");
|
|
500
|
+
}}
|
|
501
|
+
>
|
|
502
|
+
<span className="flex-1">{getColumnLabel(column)}</span>
|
|
503
|
+
{sorted === "asc" ? (
|
|
504
|
+
<ArrowUp className="h-4 w-4 text-muted-foreground" />
|
|
505
|
+
) : sorted === "desc" ? (
|
|
506
|
+
<ArrowDown className="h-4 w-4 text-muted-foreground" />
|
|
507
|
+
) : (
|
|
508
|
+
<ArrowUpDown className="h-4 w-4 opacity-30" />
|
|
509
|
+
)}
|
|
510
|
+
</DropdownMenuItem>
|
|
511
|
+
);
|
|
512
|
+
})
|
|
513
|
+
)}
|
|
514
|
+
{sorting.length > 0 && (
|
|
515
|
+
<>
|
|
516
|
+
<DropdownMenuSeparator />
|
|
517
|
+
<DropdownMenuItem
|
|
518
|
+
onSelect={(event) => {
|
|
519
|
+
event.preventDefault();
|
|
520
|
+
table.resetSorting();
|
|
521
|
+
}}
|
|
522
|
+
className="text-muted-foreground"
|
|
523
|
+
>
|
|
524
|
+
Clear sorting
|
|
525
|
+
</DropdownMenuItem>
|
|
526
|
+
</>
|
|
527
|
+
)}
|
|
528
|
+
</DropdownMenuContent>
|
|
529
|
+
</DropdownMenu>
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function DataTableViewDropdown<TData>({ table }: { table: TanstackTable<TData> }) {
|
|
534
|
+
const hideableColumns = table.getAllLeafColumns().filter((column) => column.getCanHide());
|
|
535
|
+
const hiddenCount = hideableColumns.filter((column) => !column.getIsVisible()).length;
|
|
536
|
+
|
|
537
|
+
return (
|
|
538
|
+
<DropdownMenu modal={false}>
|
|
539
|
+
<DropdownMenuTrigger asChild>
|
|
540
|
+
<DataTableIconButton tooltip="View columns" badge={hiddenCount || undefined}>
|
|
541
|
+
<SlidersHorizontal className="h-4 w-4" />
|
|
542
|
+
</DataTableIconButton>
|
|
543
|
+
</DropdownMenuTrigger>
|
|
544
|
+
<DropdownMenuContent align="end" className="z-[100] w-52">
|
|
545
|
+
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
|
|
546
|
+
<DropdownMenuSeparator />
|
|
547
|
+
{hideableColumns.length === 0 ? (
|
|
548
|
+
<p className="px-2 py-3 text-sm text-muted-foreground">No hideable columns.</p>
|
|
549
|
+
) : (
|
|
550
|
+
hideableColumns.map((column) => (
|
|
551
|
+
<DropdownMenuCheckboxItem
|
|
552
|
+
key={column.id}
|
|
553
|
+
checked={column.getIsVisible()}
|
|
554
|
+
onCheckedChange={(checked) => column.toggleVisibility(checked === true)}
|
|
555
|
+
onSelect={(event) => event.preventDefault()}
|
|
556
|
+
>
|
|
557
|
+
{getColumnLabel(column)}
|
|
558
|
+
</DropdownMenuCheckboxItem>
|
|
559
|
+
))
|
|
560
|
+
)}
|
|
561
|
+
{hiddenCount > 0 && (
|
|
562
|
+
<>
|
|
563
|
+
<DropdownMenuSeparator />
|
|
564
|
+
<DropdownMenuItem
|
|
565
|
+
onSelect={(event) => {
|
|
566
|
+
event.preventDefault();
|
|
567
|
+
table.resetColumnVisibility();
|
|
568
|
+
}}
|
|
569
|
+
>
|
|
570
|
+
Show all columns
|
|
571
|
+
</DropdownMenuItem>
|
|
572
|
+
</>
|
|
573
|
+
)}
|
|
574
|
+
</DropdownMenuContent>
|
|
575
|
+
</DropdownMenu>
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
type DataTableToolbarProps<TData> = {
|
|
580
|
+
table: TanstackTable<TData>;
|
|
581
|
+
searchPlaceholder: string;
|
|
582
|
+
filterableColumns?: DataTableFilterableColumn[];
|
|
583
|
+
onRefresh?: () => void | Promise<void>;
|
|
584
|
+
isRefreshing?: boolean;
|
|
585
|
+
onExport?: () => void;
|
|
586
|
+
enableSearch?: boolean;
|
|
587
|
+
enableColumnVisibility?: boolean;
|
|
588
|
+
searchAction?: DataTableSearchAction;
|
|
589
|
+
searchActions?: React.ReactNode;
|
|
590
|
+
toolbarActions?: React.ReactNode;
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
function DataTableToolbar<TData>({
|
|
594
|
+
table,
|
|
595
|
+
searchPlaceholder,
|
|
596
|
+
filterableColumns,
|
|
597
|
+
onRefresh,
|
|
598
|
+
isRefreshing,
|
|
599
|
+
onExport,
|
|
600
|
+
enableSearch = true,
|
|
601
|
+
enableColumnVisibility = true,
|
|
602
|
+
searchAction,
|
|
603
|
+
searchActions,
|
|
604
|
+
toolbarActions,
|
|
605
|
+
}: DataTableToolbarProps<TData>) {
|
|
606
|
+
const searchRowActions = (
|
|
607
|
+
<>
|
|
608
|
+
{searchAction ? <DataTableSearchActionButton action={searchAction} /> : null}
|
|
609
|
+
{searchActions}
|
|
610
|
+
</>
|
|
611
|
+
);
|
|
612
|
+
const hasSearchRowActions = Boolean(searchAction || searchActions);
|
|
613
|
+
|
|
614
|
+
return (
|
|
615
|
+
<div className="flex flex-col gap-2 border-b bg-muted/20 px-3 py-2">
|
|
616
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
617
|
+
{enableSearch ? <DataTableSearch table={table} searchPlaceholder={searchPlaceholder} /> : null}
|
|
618
|
+
<div className="ml-auto flex shrink-0 flex-wrap items-center gap-1">
|
|
619
|
+
<DataTableFiltersPopover table={table} filterableColumns={filterableColumns} />
|
|
620
|
+
<DataTableSortDropdown table={table} />
|
|
621
|
+
{onExport ? (
|
|
622
|
+
<DataTableIconButton tooltip="Export" onClick={onExport}>
|
|
623
|
+
<Download className="h-4 w-4" />
|
|
624
|
+
</DataTableIconButton>
|
|
625
|
+
) : null}
|
|
626
|
+
{enableColumnVisibility ? <DataTableViewDropdown table={table} /> : null}
|
|
627
|
+
{toolbarActions}
|
|
628
|
+
{onRefresh ? (
|
|
629
|
+
<DataTableIconButton
|
|
630
|
+
tooltip="Refresh"
|
|
631
|
+
onClick={() => onRefresh()}
|
|
632
|
+
disabled={isRefreshing}
|
|
633
|
+
>
|
|
634
|
+
<RefreshCw className={cn("h-4 w-4", isRefreshing && "animate-spin")} />
|
|
635
|
+
</DataTableIconButton>
|
|
636
|
+
) : null}
|
|
637
|
+
{hasSearchRowActions ? searchRowActions : null}
|
|
638
|
+
</div>
|
|
639
|
+
</div>
|
|
640
|
+
</div>
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function DataTableBulkBar<TData>({
|
|
645
|
+
table,
|
|
646
|
+
bulkActions,
|
|
647
|
+
}: {
|
|
648
|
+
table: TanstackTable<TData>;
|
|
649
|
+
bulkActions?: React.ReactNode;
|
|
650
|
+
}) {
|
|
651
|
+
const selectedCount = table.getFilteredSelectedRowModel().rows.length;
|
|
652
|
+
|
|
653
|
+
if (selectedCount === 0) {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return (
|
|
658
|
+
<div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-dashed bg-muted/30 px-3 py-2">
|
|
659
|
+
<p className="text-sm text-muted-foreground">
|
|
660
|
+
{selectedCount} row{selectedCount === 1 ? "" : "s"} selected
|
|
661
|
+
</p>
|
|
662
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
663
|
+
{bulkActions}
|
|
664
|
+
<Button variant="outline" size="sm" className="h-8" onClick={() => table.resetRowSelection()}>
|
|
665
|
+
Clear selection
|
|
666
|
+
</Button>
|
|
667
|
+
</div>
|
|
668
|
+
</div>
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function DataTablePagination<TData>({
|
|
673
|
+
table,
|
|
674
|
+
pageSizeOptions,
|
|
675
|
+
enableRowSelection,
|
|
676
|
+
}: {
|
|
677
|
+
table: TanstackTable<TData>;
|
|
678
|
+
pageSizeOptions: number[];
|
|
679
|
+
enableRowSelection: boolean;
|
|
680
|
+
}) {
|
|
681
|
+
const selectedCount = table.getFilteredSelectedRowModel().rows.length;
|
|
682
|
+
const totalCount = table.getFilteredRowModel().rows.length;
|
|
683
|
+
|
|
684
|
+
return (
|
|
685
|
+
<div className="flex flex-col gap-4 px-1 sm:flex-row sm:items-center sm:justify-between">
|
|
686
|
+
<p className="text-sm text-muted-foreground">
|
|
687
|
+
{enableRowSelection ? (
|
|
688
|
+
<>
|
|
689
|
+
{selectedCount} of {totalCount} row(s) selected.
|
|
690
|
+
</>
|
|
691
|
+
) : (
|
|
692
|
+
<>{totalCount} row(s) total.</>
|
|
693
|
+
)}
|
|
694
|
+
</p>
|
|
695
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:gap-6">
|
|
696
|
+
<div className="flex items-center gap-2">
|
|
697
|
+
<span className="whitespace-nowrap text-sm text-muted-foreground">Rows per page</span>
|
|
698
|
+
<Select
|
|
699
|
+
value={`${table.getState().pagination.pageSize}`}
|
|
700
|
+
onValueChange={(value) => table.setPageSize(Number(value))}
|
|
701
|
+
>
|
|
702
|
+
<SelectTrigger className="h-8 w-[4.5rem]">
|
|
703
|
+
<SelectValue />
|
|
704
|
+
</SelectTrigger>
|
|
705
|
+
<SelectContent side="top">
|
|
706
|
+
{pageSizeOptions.map((size) => (
|
|
707
|
+
<SelectItem key={size} value={`${size}`}>
|
|
708
|
+
{size}
|
|
709
|
+
</SelectItem>
|
|
710
|
+
))}
|
|
711
|
+
</SelectContent>
|
|
712
|
+
</Select>
|
|
713
|
+
</div>
|
|
714
|
+
<div className="flex items-center justify-between gap-4 sm:justify-end">
|
|
715
|
+
<span className="whitespace-nowrap text-sm font-medium">
|
|
716
|
+
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount() || 1}
|
|
717
|
+
</span>
|
|
718
|
+
<div className="flex items-center gap-1">
|
|
719
|
+
<Button
|
|
720
|
+
variant="outline"
|
|
721
|
+
size="sm"
|
|
722
|
+
className="h-8 w-8 p-0"
|
|
723
|
+
onClick={() => table.setPageIndex(0)}
|
|
724
|
+
disabled={!table.getCanPreviousPage()}
|
|
725
|
+
aria-label="First page"
|
|
726
|
+
>
|
|
727
|
+
<ChevronsLeft className="h-4 w-4" />
|
|
728
|
+
</Button>
|
|
729
|
+
<Button
|
|
730
|
+
variant="outline"
|
|
731
|
+
size="sm"
|
|
732
|
+
className="h-8 w-8 p-0"
|
|
733
|
+
onClick={() => table.previousPage()}
|
|
734
|
+
disabled={!table.getCanPreviousPage()}
|
|
735
|
+
aria-label="Previous page"
|
|
736
|
+
>
|
|
737
|
+
<ChevronLeft className="h-4 w-4" />
|
|
738
|
+
</Button>
|
|
739
|
+
<Button
|
|
740
|
+
variant="outline"
|
|
741
|
+
size="sm"
|
|
742
|
+
className="h-8 w-8 p-0"
|
|
743
|
+
onClick={() => table.nextPage()}
|
|
744
|
+
disabled={!table.getCanNextPage()}
|
|
745
|
+
aria-label="Next page"
|
|
746
|
+
>
|
|
747
|
+
<ChevronRight className="h-4 w-4" />
|
|
748
|
+
</Button>
|
|
749
|
+
<Button
|
|
750
|
+
variant="outline"
|
|
751
|
+
size="sm"
|
|
752
|
+
className="h-8 w-8 p-0"
|
|
753
|
+
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
|
754
|
+
disabled={!table.getCanNextPage()}
|
|
755
|
+
aria-label="Last page"
|
|
756
|
+
>
|
|
757
|
+
<ChevronsRight className="h-4 w-4" />
|
|
758
|
+
</Button>
|
|
759
|
+
</div>
|
|
760
|
+
</div>
|
|
761
|
+
</div>
|
|
762
|
+
</div>
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function globalUserSearchFilter<TData>(row: Row<TData>, _columnId: string, filterValue: string) {
|
|
767
|
+
const query = filterValue.trim().toLowerCase();
|
|
768
|
+
if (!query) {
|
|
769
|
+
return true;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const record = row.original as Record<string, unknown>;
|
|
773
|
+
const name = String(record.name ?? "").toLowerCase();
|
|
774
|
+
const email = String(record.email ?? "").toLowerCase();
|
|
775
|
+
const role = String(record.role ?? "").toLowerCase();
|
|
776
|
+
|
|
777
|
+
return name.includes(query) || email.includes(query) || role.includes(query);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
export function DataTable<TData, TValue>({
|
|
781
|
+
columns,
|
|
782
|
+
data,
|
|
783
|
+
pageSize = 10,
|
|
784
|
+
pageSizeOptions = [5, 10, 20, 30, 50],
|
|
785
|
+
searchPlaceholder = "Search rows...",
|
|
786
|
+
filterableColumns,
|
|
787
|
+
onRefresh,
|
|
788
|
+
isRefreshing = false,
|
|
789
|
+
onExport,
|
|
790
|
+
enableSearch = true,
|
|
791
|
+
enableRowSelection = false,
|
|
792
|
+
enableColumnVisibility = true,
|
|
793
|
+
searchAction,
|
|
794
|
+
searchActions,
|
|
795
|
+
toolbarActions,
|
|
796
|
+
bulkActions,
|
|
797
|
+
onSelectionChange,
|
|
798
|
+
}: DataTableProps<TData, TValue>) {
|
|
799
|
+
const [sorting, setSorting] = React.useState<SortingState>([]);
|
|
800
|
+
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
|
801
|
+
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
|
|
802
|
+
const [rowSelection, setRowSelection] = React.useState({});
|
|
803
|
+
const [globalFilter, setGlobalFilter] = React.useState("");
|
|
804
|
+
|
|
805
|
+
const tableColumns = React.useMemo(
|
|
806
|
+
() => (enableRowSelection ? [dataTableSelectColumn<TData>(), ...columns] : columns),
|
|
807
|
+
[columns, enableRowSelection],
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
const table = useReactTable({
|
|
811
|
+
data,
|
|
812
|
+
columns: tableColumns,
|
|
813
|
+
state: {
|
|
814
|
+
sorting,
|
|
815
|
+
columnFilters,
|
|
816
|
+
columnVisibility,
|
|
817
|
+
rowSelection,
|
|
818
|
+
globalFilter,
|
|
819
|
+
},
|
|
820
|
+
enableRowSelection,
|
|
821
|
+
enableSorting: true,
|
|
822
|
+
enableColumnFilters: true,
|
|
823
|
+
enableHiding: true,
|
|
824
|
+
onSortingChange: setSorting,
|
|
825
|
+
onColumnFiltersChange: setColumnFilters,
|
|
826
|
+
onColumnVisibilityChange: setColumnVisibility,
|
|
827
|
+
onRowSelectionChange: setRowSelection,
|
|
828
|
+
onGlobalFilterChange: setGlobalFilter,
|
|
829
|
+
globalFilterFn: globalUserSearchFilter,
|
|
830
|
+
getCoreRowModel: getCoreRowModel(),
|
|
831
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
832
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
833
|
+
getSortedRowModel: getSortedRowModel(),
|
|
834
|
+
getFacetedRowModel: getFacetedRowModel(),
|
|
835
|
+
getFacetedUniqueValues: getFacetedUniqueValues(),
|
|
836
|
+
initialState: {
|
|
837
|
+
pagination: { pageSize },
|
|
838
|
+
},
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
React.useEffect(() => {
|
|
842
|
+
if (!onSelectionChange) {
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
const selected = table.getFilteredSelectedRowModel().rows.map((row) => row.original);
|
|
846
|
+
onSelectionChange(selected);
|
|
847
|
+
}, [rowSelection, data, onSelectionChange, table]);
|
|
848
|
+
|
|
849
|
+
return (
|
|
850
|
+
<div className="space-y-4">
|
|
851
|
+
<div className="overflow-hidden rounded-lg border bg-card shadow-sm">
|
|
852
|
+
<DataTableToolbar
|
|
853
|
+
table={table}
|
|
854
|
+
searchPlaceholder={searchPlaceholder}
|
|
855
|
+
filterableColumns={filterableColumns}
|
|
856
|
+
onRefresh={onRefresh}
|
|
857
|
+
isRefreshing={isRefreshing}
|
|
858
|
+
onExport={onExport}
|
|
859
|
+
enableSearch={enableSearch}
|
|
860
|
+
enableColumnVisibility={enableColumnVisibility}
|
|
861
|
+
searchAction={searchAction}
|
|
862
|
+
searchActions={searchActions}
|
|
863
|
+
toolbarActions={toolbarActions}
|
|
864
|
+
/>
|
|
865
|
+
|
|
866
|
+
{enableRowSelection ? (
|
|
867
|
+
<div className="border-b px-3 py-2">
|
|
868
|
+
<DataTableBulkBar table={table} bulkActions={bulkActions} />
|
|
869
|
+
</div>
|
|
870
|
+
) : null}
|
|
871
|
+
|
|
872
|
+
<Table>
|
|
873
|
+
<TableHeader>
|
|
874
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
875
|
+
<TableRow key={headerGroup.id} className="border-b bg-muted/30 hover:bg-muted/30">
|
|
876
|
+
{headerGroup.headers.map((header) => (
|
|
877
|
+
<TableHead
|
|
878
|
+
key={header.id}
|
|
879
|
+
className={cn("h-11", header.column.id === "actions" && "text-center")}
|
|
880
|
+
>
|
|
881
|
+
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
|
882
|
+
</TableHead>
|
|
883
|
+
))}
|
|
884
|
+
</TableRow>
|
|
885
|
+
))}
|
|
886
|
+
</TableHeader>
|
|
887
|
+
<TableBody>
|
|
888
|
+
{table.getRowModel().rows?.length ? (
|
|
889
|
+
table.getRowModel().rows.map((row) => (
|
|
890
|
+
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
|
891
|
+
{row.getVisibleCells().map((cell) => (
|
|
892
|
+
<TableCell
|
|
893
|
+
key={cell.id}
|
|
894
|
+
className={cn("h-12", cell.column.id === "actions" && "text-center")}
|
|
895
|
+
>
|
|
896
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
897
|
+
</TableCell>
|
|
898
|
+
))}
|
|
899
|
+
</TableRow>
|
|
900
|
+
))
|
|
901
|
+
) : (
|
|
902
|
+
<TableRow>
|
|
903
|
+
<TableCell colSpan={table.getVisibleLeafColumns().length} className="h-24 text-center">
|
|
904
|
+
No results.
|
|
905
|
+
</TableCell>
|
|
906
|
+
</TableRow>
|
|
907
|
+
)}
|
|
908
|
+
</TableBody>
|
|
909
|
+
</Table>
|
|
910
|
+
</div>
|
|
911
|
+
|
|
912
|
+
<DataTablePagination
|
|
913
|
+
table={table}
|
|
914
|
+
pageSizeOptions={pageSizeOptions}
|
|
915
|
+
enableRowSelection={enableRowSelection}
|
|
916
|
+
/>
|
|
917
|
+
</div>
|
|
918
|
+
);
|
|
919
|
+
}
|