@suphark/ui 0.1.1 → 0.3.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.
Files changed (30) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +45 -3
  3. package/package.json +14 -2
  4. package/src/components/app-shell/app-brand.tsx +81 -0
  5. package/src/components/app-shell/app-nav.tsx +260 -0
  6. package/src/components/app-shell/app-shell.tsx +270 -0
  7. package/src/components/app-shell/header-breadcrumbs.tsx +78 -0
  8. package/src/components/app-shell/index.ts +14 -0
  9. package/src/components/app-shell/nav-link.tsx +35 -0
  10. package/src/components/app-shell/types.ts +92 -0
  11. package/src/components/app-shell/user-menu.tsx +133 -0
  12. package/src/components/data-table/data-table-column-header.tsx +71 -0
  13. package/src/components/data-table/data-table-date-range-filter.tsx +25 -0
  14. package/src/components/data-table/data-table-faceted-filter.tsx +181 -0
  15. package/src/components/data-table/data-table-pagination.tsx +114 -0
  16. package/src/components/data-table/data-table-row-actions.tsx +166 -0
  17. package/src/components/data-table/data-table-toolbar.tsx +207 -0
  18. package/src/components/data-table/data-table-view-option.tsx +59 -0
  19. package/src/components/data-table/data-table.tsx +399 -0
  20. package/src/components/data-table/index.ts +9 -0
  21. package/src/components/data-table/types.ts +106 -0
  22. package/src/components/data-table/use-data-table.ts +229 -0
  23. package/src/components/design-system/sections/showcase-data-display.tsx +71 -0
  24. package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +8 -8
  25. package/src/components/shared/button/delete-many-button.tsx +85 -0
  26. package/src/components/shared/resource-property-filter.tsx +399 -0
  27. package/src/data-table.ts +21 -0
  28. package/src/hooks/use-table-search-params.ts +147 -0
  29. package/src/index.ts +2 -1
  30. package/src/next.ts +8 -0
@@ -0,0 +1,106 @@
1
+ import type {
2
+ ColumnDef,
3
+ ColumnFiltersState,
4
+ FilterFn,
5
+ OnChangeFn,
6
+ PaginationState,
7
+ Row,
8
+ SortingState,
9
+ Table,
10
+ TableState,
11
+ VisibilityState,
12
+ } from "@tanstack/react-table";
13
+ import type React from "react";
14
+
15
+ export type FacetedFilterConfig = {
16
+ columnId: string;
17
+ title: string;
18
+ compact?: boolean;
19
+ options: {
20
+ label: string;
21
+ value: string | number | boolean;
22
+ icon?: React.ComponentType<{ className?: string }>;
23
+ }[];
24
+ };
25
+
26
+ export interface DataTableProps<TData, TValue> {
27
+ columns: ColumnDef<TData, TValue>[];
28
+ data: TData[];
29
+
30
+ // Styling Hooks
31
+ className?: string; // Appended to table wrap
32
+ containerClassName?: string; // Appended to outer wrapper
33
+ /**
34
+ * Appended to the scrollable box that wraps the table itself.
35
+ *
36
+ * Needed for `stickyHeader`: the box already scrolls horizontally, and CSS
37
+ * forces the vertical axis to `auto` alongside it, so it — not the page — is
38
+ * what a sticky header anchors to. Give it a height (e.g. `max-h-[70vh]`) or
39
+ * the header has nothing to stick within and scrolls away with the page.
40
+ */
41
+ tableContainerClassName?: string;
42
+ /** ตรึงแถบเปลี่ยนหน้าไว้ล่างสุดของจอเสมอ ใช้คู่กับ `stickyHeader` */
43
+ stickyPagination?: boolean;
44
+
45
+ // Accessibility & UI
46
+ stickyHeader?: boolean;
47
+ stickyEdgeColumns?: boolean;
48
+ emptyStateComponent?: React.ReactNode;
49
+
50
+ // Filters & Toolbar
51
+ facetedFilters?: FacetedFilterConfig[];
52
+ actionsComponent?: (table: Table<TData>) => React.ReactNode;
53
+ globalFilterKeys?: string[];
54
+ globalFilterFn?: FilterFn<TData>;
55
+ globalFilterPlaceholder?: string;
56
+ toolbarFilters?: (table: Table<TData>) => React.ReactNode;
57
+ preservedFilterIds?: string[];
58
+
59
+ // State & Visibility
60
+ initialVisibility?: VisibilityState;
61
+ initialColumnFilters?: ColumnFiltersState;
62
+ columnFilters?: ColumnFiltersState;
63
+ initialState?: Partial<TableState>;
64
+
65
+ // Pagination & Sorting (Server-side support)
66
+ pageCount?: number;
67
+ rowCount?: number;
68
+ /**
69
+ * ควบคุม pagination จากภายนอก (ใช้คู่กับ manualPagination)
70
+ * จำเป็นเมื่อ state จริงอยู่ที่อื่น เช่นใน URL — ไม่งั้นเลขหน้าใน UI จะไม่ตรงกับข้อมูลที่ server ส่งมา
71
+ */
72
+ pagination?: PaginationState;
73
+ /** ควบคุม sorting จากภายนอก (ใช้คู่กับ manualSorting) */
74
+ sorting?: SortingState;
75
+ /** ควบคุมคำค้นจากภายนอก (ใช้คู่กับ manualFiltering) */
76
+ globalFilter?: string;
77
+ /**
78
+ * เรียกเมื่อผู้ใช้พิมพ์ในช่องค้นหา
79
+ * ต้องส่งมาด้วยเมื่อเปิด manualFiltering ไม่งั้นช่องค้นหาจะไม่ทำอะไรเลย
80
+ * เพราะ TanStack ถูกสั่งไม่ให้กรองเองแล้ว
81
+ */
82
+ onGlobalFilterChange?: (value: string) => void;
83
+ manualPagination?: boolean;
84
+ manualSorting?: boolean;
85
+ manualFiltering?: boolean;
86
+ onPaginationChange?: OnChangeFn<PaginationState>;
87
+ onPageSizeChange?: (pageSize: number) => void;
88
+ onSortingChange?: OnChangeFn<SortingState>;
89
+ onColumnFiltersChange?: OnChangeFn<ColumnFiltersState>;
90
+
91
+ // Customization
92
+ getRowClassName?: (row: Row<TData>) => string;
93
+ getSubRows?: (originalRow: TData) => TData[] | undefined;
94
+ getRowId?: (originalRow: TData) => string;
95
+ initialPageSize?: number;
96
+ meta?: Record<string, unknown>;
97
+ isLoading?: boolean;
98
+
99
+ // Advanced
100
+ enableColumnResizing?: boolean;
101
+ enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
102
+ getFacetedUniqueValues?: (table: Table<TData>, columnId: string) => () => Map<unknown, number>;
103
+ pageSizeOptions?: number[];
104
+ onReorder?: (newData: TData[]) => void;
105
+ renderCustomView?: (table: Table<TData>) => React.ReactNode;
106
+ }
@@ -0,0 +1,229 @@
1
+ "use client";
2
+
3
+ import {
4
+ type ColumnFiltersState,
5
+ type FilterFn,
6
+ getCoreRowModel,
7
+ getExpandedRowModel,
8
+ getFacetedRowModel,
9
+ getFilteredRowModel,
10
+ getPaginationRowModel,
11
+ getSortedRowModel,
12
+ type PaginationState,
13
+ type SortingState,
14
+ useReactTable,
15
+ type VisibilityState,
16
+ } from "@tanstack/react-table";
17
+ import * as React from "react";
18
+
19
+ import type { DataTableProps } from "./types";
20
+
21
+ function getNestedValue<TData>(obj: TData, path: string): unknown {
22
+ let current: unknown = obj;
23
+ for (const key of path.split(".")) {
24
+ if (current && typeof current === "object" && key in current) {
25
+ current = (current as Record<string, unknown>)[key];
26
+ } else {
27
+ return undefined;
28
+ }
29
+ }
30
+ return current;
31
+ }
32
+
33
+ export function useDataTable<TData, TValue>({
34
+ columns,
35
+ data,
36
+ initialVisibility = {},
37
+ globalFilterFn,
38
+ globalFilterKeys,
39
+ getFacetedUniqueValues: getFacetedUniqueValuesProp,
40
+ initialColumnFilters = [],
41
+ columnFilters: columnFiltersProp,
42
+ getSubRows,
43
+ getRowId,
44
+ initialPageSize = 25,
45
+ meta,
46
+ initialState,
47
+ pageCount,
48
+ rowCount,
49
+ pagination: paginationProp,
50
+ sorting: sortingProp,
51
+ globalFilter: globalFilterProp,
52
+ onGlobalFilterChange,
53
+ manualPagination,
54
+ manualSorting,
55
+ manualFiltering,
56
+ onPaginationChange,
57
+ onSortingChange,
58
+ onColumnFiltersChange,
59
+ enableColumnResizing = false,
60
+ enableRowSelection = true,
61
+ }: DataTableProps<TData, TValue>) {
62
+ const [rowSelection, setRowSelection] = React.useState({});
63
+ const [columnVisibility, setColumnVisibility] =
64
+ React.useState<VisibilityState>(initialVisibility);
65
+ const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
66
+ columnFiltersProp ?? initialColumnFilters,
67
+ );
68
+
69
+ // Sync with prop if provided
70
+ React.useEffect(() => {
71
+ if (columnFiltersProp !== undefined) {
72
+ setColumnFilters(columnFiltersProp);
73
+ }
74
+ }, [columnFiltersProp]);
75
+ const [sorting, setSorting] = React.useState<SortingState>(
76
+ () => sortingProp ?? (initialState?.sorting as SortingState | undefined) ?? [],
77
+ );
78
+
79
+ // Sync with prop if provided — เทียบด้วยค่าเหมือน pagination ด้วยเหตุผลเดียวกัน
80
+ const sortingKey = sortingProp ? JSON.stringify(sortingProp) : undefined;
81
+ React.useEffect(() => {
82
+ if (sortingKey === undefined) return;
83
+ setSorting((current) =>
84
+ JSON.stringify(current) === sortingKey ? current : JSON.parse(sortingKey),
85
+ );
86
+ }, [sortingKey]);
87
+ const [globalFilter, setGlobalFilter] = React.useState(
88
+ globalFilterProp ?? (initialState?.globalFilter as string) ?? "",
89
+ );
90
+
91
+ // Sync with prop if provided
92
+ React.useEffect(() => {
93
+ if (globalFilterProp !== undefined) {
94
+ setGlobalFilter(globalFilterProp);
95
+ }
96
+ }, [globalFilterProp]);
97
+ const [pagination, setPagination] = React.useState<PaginationState>(
98
+ paginationProp ?? { pageIndex: 0, pageSize: initialPageSize },
99
+ );
100
+
101
+ // Sync with prop if provided — กันเลขหน้าใน UI หลุดจาก state จริงที่อยู่ใน URL
102
+ //
103
+ // ต้องเทียบด้วยค่า ไม่ใช่ตัวอ้างอิง เพราะผู้เรียกมักส่ง object literal ใหม่ทุก render
104
+ // ถ้าผูก effect ไว้กับตัว object มันจะรันทุกครั้งแล้วรีเซ็ต state ทับ
105
+ // การกดเปลี่ยนหน้าที่เพิ่งเกิดขึ้นทิ้งไปก่อนที่ค่าใหม่จะกลับมาจาก server
106
+ const pageIndexProp = paginationProp?.pageIndex;
107
+ const pageSizeProp = paginationProp?.pageSize;
108
+ React.useEffect(() => {
109
+ if (pageIndexProp === undefined || pageSizeProp === undefined) return;
110
+ setPagination((current) =>
111
+ current.pageIndex === pageIndexProp && current.pageSize === pageSizeProp
112
+ ? current
113
+ : { pageIndex: pageIndexProp, pageSize: pageSizeProp },
114
+ );
115
+ }, [pageIndexProp, pageSizeProp]);
116
+
117
+ // Sync initialPageSize when it loads asynchronously (e.g. from user preferences)
118
+ React.useEffect(() => {
119
+ setPagination((prev) => {
120
+ // Only update if the size actually changed
121
+ if (prev.pageSize !== initialPageSize) {
122
+ return { ...prev, pageSize: initialPageSize };
123
+ }
124
+ return prev;
125
+ });
126
+ }, [initialPageSize]);
127
+
128
+ const finalGlobalFilterFn = React.useMemo(() => {
129
+ if (globalFilterFn) {
130
+ return globalFilterFn;
131
+ }
132
+
133
+ if (globalFilterKeys && globalFilterKeys.length > 0) {
134
+ const generatedFn: FilterFn<TData> = (row, _columnId, filterValue) => {
135
+ const search = String(filterValue).toLowerCase();
136
+ for (const key of globalFilterKeys) {
137
+ const value = getNestedValue(row.original, key as string);
138
+
139
+ if (value !== null && value !== undefined) {
140
+ const valueAsString = String(value).toLowerCase();
141
+ if (valueAsString.includes(search)) {
142
+ return true;
143
+ }
144
+ }
145
+ }
146
+ return false;
147
+ };
148
+ return generatedFn;
149
+ }
150
+
151
+ return undefined;
152
+ }, [globalFilterFn, globalFilterKeys]);
153
+
154
+ const memoizedData = React.useMemo(() => data, [data]);
155
+ const memoizedColumns = React.useMemo(() => columns, [columns]);
156
+
157
+ const table = useReactTable({
158
+ data: memoizedData,
159
+ columns: memoizedColumns,
160
+ pageCount: pageCount ?? (manualPagination ? -1 : undefined),
161
+ rowCount,
162
+ manualPagination,
163
+ manualSorting,
164
+ manualFiltering,
165
+ state: {
166
+ sorting,
167
+ columnVisibility,
168
+ rowSelection,
169
+ columnFilters,
170
+ globalFilter,
171
+ pagination,
172
+ },
173
+ enableColumnResizing,
174
+ columnResizeMode: "onChange",
175
+ defaultColumn: {
176
+ minSize: 50,
177
+ maxSize: 800,
178
+ },
179
+ initialState: {
180
+ pagination: {
181
+ pageSize: initialPageSize,
182
+ },
183
+ columnFilters: initialColumnFilters,
184
+ ...initialState,
185
+ },
186
+ meta,
187
+ getSubRows: getSubRows,
188
+ getRowId: getRowId,
189
+ getExpandedRowModel: getExpandedRowModel(),
190
+ filterFromLeafRows: true,
191
+ enableRowSelection,
192
+ onRowSelectionChange: setRowSelection,
193
+ onSortingChange: (updater) => {
194
+ setSorting(updater);
195
+ if (onSortingChange) onSortingChange(updater);
196
+ },
197
+ onColumnFiltersChange: (updater) => {
198
+ setColumnFilters(updater);
199
+ if (onColumnFiltersChange) onColumnFiltersChange(updater);
200
+ },
201
+ onColumnVisibilityChange: setColumnVisibility,
202
+ onGlobalFilterChange: (updater) => {
203
+ setGlobalFilter(updater);
204
+ if (onGlobalFilterChange) {
205
+ const next = typeof updater === "function" ? updater(globalFilter) : updater;
206
+ onGlobalFilterChange((next as string) ?? "");
207
+ }
208
+ },
209
+ onPaginationChange: (updater) => {
210
+ setPagination(updater);
211
+ if (onPaginationChange) onPaginationChange(updater);
212
+ },
213
+ // Correction: To use `onPaginationChange`, we usually need to control state.
214
+ // Let's create local pagination state to be robust.
215
+ globalFilterFn: finalGlobalFilterFn,
216
+ getCoreRowModel: getCoreRowModel(),
217
+ getFilteredRowModel: getFilteredRowModel(),
218
+ getPaginationRowModel: getPaginationRowModel(),
219
+ getSortedRowModel: getSortedRowModel(),
220
+ getFacetedRowModel: getFacetedRowModel(),
221
+ getFacetedUniqueValues: getFacetedUniqueValuesProp,
222
+ });
223
+
224
+ // NOTE: If manualPagination is true, getPaginationRowModel might not be needed?
225
+ // TanStack docs say: if manualPagination is true, you don't need getPaginationRowModel IF you only provide the current page data.
226
+ // But usage of getPaginationRowModel is safe even with manualPagination as it just slices.
227
+
228
+ return { table, initialColumnFilters };
229
+ }
@@ -57,6 +57,8 @@ import {
57
57
  TableHeader,
58
58
  TableRow,
59
59
  } from "@suphark/ui/components/ui/table";
60
+ import { DataTable, DataTableColumnHeader } from "@suphark/ui/data-table";
61
+ import type { ColumnDef } from "@tanstack/react-table";
60
62
  import {
61
63
  AlertTriangle,
62
64
  Download,
@@ -125,6 +127,43 @@ export function ShowcaseDataDisplay() {
125
127
  },
126
128
  ];
127
129
 
130
+ type PackageRow = (typeof tableRows)[number];
131
+ const packageColumns: ColumnDef<PackageRow>[] = [
132
+ {
133
+ accessorKey: "code",
134
+ header: ({ column }) => <DataTableColumnHeader column={column} title="รหัส" />,
135
+ cell: ({ row }) => <span className="font-mono text-xs">{row.original.code}</span>,
136
+ },
137
+ {
138
+ accessorKey: "name",
139
+ header: ({ column }) => <DataTableColumnHeader column={column} title="รายการ" />,
140
+ cell: ({ row }) => (
141
+ <PrimarySecondaryText primary={row.original.name} secondary={row.original.category} />
142
+ ),
143
+ },
144
+ {
145
+ accessorKey: "vendor",
146
+ header: ({ column }) => <DataTableColumnHeader column={column} title="คู่ค้า" />,
147
+ },
148
+ {
149
+ accessorKey: "status",
150
+ header: ({ column }) => <DataTableColumnHeader column={column} title="สถานะ" />,
151
+ cell: ({ row }) => <Badge variant="outline">{row.original.status}</Badge>,
152
+ filterFn: (row, id, value: string[]) => value.includes(row.getValue(id)),
153
+ },
154
+ {
155
+ accessorKey: "totalAmount",
156
+ header: ({ column }) => (
157
+ <DataTableColumnHeader column={column} title="มูลค่ารวม (THB)" className="justify-end" />
158
+ ),
159
+ cell: ({ row }) => (
160
+ <div className="text-right">
161
+ <FormattedNumber value={row.original.totalAmount} />
162
+ </div>
163
+ ),
164
+ },
165
+ ];
166
+
128
167
  return (
129
168
  <div className="flex flex-col gap-8">
130
169
  {/* Section Header */}
@@ -310,6 +349,38 @@ export function ShowcaseDataDisplay() {
310
349
  </CardContent>
311
350
  </Card>
312
351
 
352
+ {/* DataTable (@suphark/ui/data-table) */}
353
+ <Card>
354
+ <CardHeader>
355
+ <CardTitle className="text-base">DataTable (tanstack) — @suphark/ui/data-table</CardTitle>
356
+ <CardDescription className="text-xs">
357
+ ตารางมาตรฐานพร้อม global search, faceted filter, เรียงคอลัมน์, ซ่อน/แสดงคอลัมน์ และ pagination
358
+ — import แยกจาก <code>@suphark/ui/data-table</code> (ต้องมี peer{" "}
359
+ <code>@tanstack/react-table</code>)
360
+ </CardDescription>
361
+ </CardHeader>
362
+ <CardContent>
363
+ <DataTable
364
+ columns={packageColumns}
365
+ data={tableRows}
366
+ globalFilterKeys={["code", "name", "vendor"]}
367
+ globalFilterPlaceholder="ค้นหารหัส / รายการ / คู่ค้า…"
368
+ facetedFilters={[
369
+ {
370
+ columnId: "status",
371
+ title: "สถานะ",
372
+ options: [
373
+ { label: "Approved", value: "Approved" },
374
+ { label: "Under Review", value: "Under Review" },
375
+ { label: "Draft", value: "Draft" },
376
+ ],
377
+ },
378
+ ]}
379
+ initialPageSize={5}
380
+ />
381
+ </CardContent>
382
+ </Card>
383
+
313
384
  {/* Item & Attachment Primitives */}
314
385
  <div className="grid gap-6 lg:grid-cols-2">
315
386
  {/* Item Primitive */}
@@ -91,9 +91,9 @@ export function ShowcaseGuidelinesRecipes() {
91
91
  ];
92
92
 
93
93
  const recipe1 = `// 1. PageShell + Toolbar + DataTable Pattern
94
- import { PageShell } from "@suphark/ui/components/shared/layout/page-shell";
95
- import { AddNewButton } from "@suphark/ui/components/shared/button";
96
- import { DataTable } from "@suphark/ui/components/shared/data-table/data-table";
94
+ import { PageShell } from "@suphark/ui/next";
95
+ import { AddNewButton } from "@suphark/ui";
96
+ import { DataTable } from "@suphark/ui/data-table";
97
97
 
98
98
  export default function PackageListPage() {
99
99
  return (
@@ -117,8 +117,8 @@ export default function PackageListPage() {
117
117
  }`;
118
118
 
119
119
  const recipe2 = `// 2. FieldGroup + InputGroup + Server Action Validation
120
- import { FieldGroup, Field, FieldLabel, FieldDescription, FieldError } from "@suphark/ui/components/ui/field";
121
- import { InputGroup, InputGroupAddon, InputGroupInput } from "@suphark/ui/components/ui/input-group";
120
+ import { FieldGroup, Field, FieldLabel, FieldDescription, FieldError } from "@suphark/ui";
121
+ import { InputGroup, InputGroupAddon, InputGroupInput } from "@suphark/ui";
122
122
  import { DollarSign } from "lucide-react";
123
123
 
124
124
  export function BudgetField({ state }: { state?: ActionResponse }) {
@@ -161,7 +161,7 @@ import {
161
161
  AlertDialogTitle,
162
162
  AlertDialogTrigger,
163
163
  } from "@suphark/ui/components/ui/alert-dialog";
164
- import { Button } from "@suphark/ui/components/ui/button";
164
+ import { Button } from "@suphark/ui";
165
165
 
166
166
  export function DeletePackageDialog({ packageId }: { packageId: string }) {
167
167
  return (
@@ -191,8 +191,8 @@ export function DeletePackageDialog({ packageId }: { packageId: string }) {
191
191
  "use client";
192
192
 
193
193
  import { useTransition } from "react";
194
- import { Button } from "@suphark/ui/components/ui/button";
195
- import { Spinner } from "@suphark/ui/components/ui/spinner";
194
+ import { Button } from "@suphark/ui";
195
+ import { Spinner } from "@suphark/ui";
196
196
  import { toast } from "sonner";
197
197
  import { approvePackageAction } from "../actions/approve-package";
198
198
 
@@ -0,0 +1,85 @@
1
+ "use client";
2
+
3
+ import { DeleteButton } from "@suphark/ui/components/shared/button/delete-button";
4
+ import {
5
+ AlertDialog,
6
+ AlertDialogAction,
7
+ AlertDialogCancel,
8
+ AlertDialogContent,
9
+ AlertDialogDescription,
10
+ AlertDialogFooter,
11
+ AlertDialogHeader,
12
+ AlertDialogTitle,
13
+ AlertDialogTrigger,
14
+ } from "@suphark/ui/components/ui/alert-dialog";
15
+ import { buttonVariants } from "@suphark/ui/components/ui/button";
16
+ import type { Table } from "@tanstack/react-table";
17
+ import { Loader2 } from "lucide-react";
18
+ import { useTransition } from "react";
19
+ import { toast } from "sonner";
20
+
21
+ interface DeleteManyButtonProps<TData> {
22
+ table: Table<TData>;
23
+ deleteAction: (data: { ids: string[] }) => Promise<unknown>;
24
+ onSuccess?: () => void;
25
+ itemDescription?: string;
26
+ responsive?: boolean;
27
+ }
28
+
29
+ export function DeleteManyButton<TData>({
30
+ table,
31
+ deleteAction,
32
+ onSuccess,
33
+ itemDescription = "items",
34
+ responsive = false,
35
+ }: DeleteManyButtonProps<TData>) {
36
+ const [isPending, startTransition] = useTransition();
37
+ const selectedRows = table.getFilteredSelectedRowModel().rows;
38
+
39
+ if (selectedRows.length === 0) {
40
+ return null;
41
+ }
42
+
43
+ function handleDelete() {
44
+ startTransition(async () => {
45
+ const ids = selectedRows.map((row) => (row.original as { id: string }).id);
46
+ try {
47
+ const result = (await deleteAction({ ids })) as any;
48
+ if (result?.error) {
49
+ throw new Error(result.error);
50
+ }
51
+ toast.success(`${ids.length} ${itemDescription} deleted successfully.`);
52
+ onSuccess?.();
53
+ } catch (err: any) {
54
+ toast.error(err.message);
55
+ }
56
+ });
57
+ }
58
+
59
+ return (
60
+ <AlertDialog>
61
+ <AlertDialogTrigger render={<DeleteButton isLoading={isPending} responsive={responsive} />}>
62
+ Delete ({selectedRows.length})
63
+ </AlertDialogTrigger>
64
+ <AlertDialogContent>
65
+ <AlertDialogHeader>
66
+ <AlertDialogTitle>Are you sure?</AlertDialogTitle>
67
+ <AlertDialogDescription>
68
+ This will permanently delete the selected {itemDescription}.
69
+ </AlertDialogDescription>
70
+ </AlertDialogHeader>
71
+ <AlertDialogFooter>
72
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
73
+ <AlertDialogAction
74
+ onClick={handleDelete}
75
+ disabled={isPending}
76
+ className={buttonVariants({ variant: "destructive" })}
77
+ >
78
+ {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
79
+ Delete
80
+ </AlertDialogAction>
81
+ </AlertDialogFooter>
82
+ </AlertDialogContent>
83
+ </AlertDialog>
84
+ );
85
+ }