@suphark/ui 0.1.0 → 0.2.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/CHANGELOG.md +26 -0
- package/README.md +5 -1
- package/package.json +16 -2
- package/src/components/data-table/data-table-column-header.tsx +71 -0
- package/src/components/data-table/data-table-date-range-filter.tsx +25 -0
- package/src/components/data-table/data-table-faceted-filter.tsx +181 -0
- package/src/components/data-table/data-table-pagination.tsx +114 -0
- package/src/components/data-table/data-table-row-actions.tsx +166 -0
- package/src/components/data-table/data-table-toolbar.tsx +207 -0
- package/src/components/data-table/data-table-view-option.tsx +59 -0
- package/src/components/data-table/data-table.tsx +399 -0
- package/src/components/data-table/index.ts +9 -0
- package/src/components/data-table/types.ts +106 -0
- package/src/components/data-table/use-data-table.ts +229 -0
- package/src/components/design-system/sections/showcase-data-display.tsx +71 -0
- package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +8 -8
- package/src/components/shared/button/delete-many-button.tsx +85 -0
- package/src/components/shared/resource-property-filter.tsx +399 -0
- package/src/components/ui/native-select.tsx +54 -0
- package/src/data-table.ts +21 -0
- package/src/hooks/use-table-search-params.ts +147 -0
- package/src/index.ts +1 -0
- package/src/next.ts +5 -0
- package/src/components/shared/__tests__/multi-select-combobox.test.tsx +0 -180
- package/src/components/shared/__tests__/name-id-fields.test.ts +0 -24
- package/src/components/shared/form/__tests__/simple-select.test.tsx +0 -72
- package/src/components/ui/__tests__/combobox.test.tsx +0 -40
- package/src/components/ui/__tests__/date-picker.test.tsx +0 -53
- package/src/components/ui/__tests__/slider.test.tsx +0 -18
- package/src/components/ui/__tests__/tabs.test.tsx +0 -33
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { DeleteButton } from "@suphark/ui/components/shared/button/delete-button";
|
|
4
|
+
import {
|
|
5
|
+
AlertDialog,
|
|
6
|
+
AlertDialogCancel,
|
|
7
|
+
AlertDialogContent,
|
|
8
|
+
AlertDialogDescription,
|
|
9
|
+
AlertDialogFooter,
|
|
10
|
+
AlertDialogHeader,
|
|
11
|
+
AlertDialogTitle,
|
|
12
|
+
} from "@suphark/ui/components/ui/alert-dialog";
|
|
13
|
+
import { Button } from "@suphark/ui/components/ui/button";
|
|
14
|
+
import {
|
|
15
|
+
DropdownMenu,
|
|
16
|
+
DropdownMenuContent,
|
|
17
|
+
DropdownMenuGroup,
|
|
18
|
+
DropdownMenuItem,
|
|
19
|
+
DropdownMenuSeparator,
|
|
20
|
+
DropdownMenuTrigger,
|
|
21
|
+
} from "@suphark/ui/components/ui/dropdown-menu";
|
|
22
|
+
import type { Row } from "@tanstack/react-table";
|
|
23
|
+
import { Ellipsis, Pen, Trash } from "lucide-react";
|
|
24
|
+
import { useState, useTransition } from "react";
|
|
25
|
+
import { toast } from "sonner";
|
|
26
|
+
|
|
27
|
+
interface ActionState<T> {
|
|
28
|
+
error?: string;
|
|
29
|
+
data?: T;
|
|
30
|
+
success?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface DataTableRowActionsProps<TData> {
|
|
34
|
+
row: Row<TData>;
|
|
35
|
+
/**
|
|
36
|
+
* Optional custom menu items to display before the standard Edit/Delete actions.
|
|
37
|
+
*/
|
|
38
|
+
children?: React.ReactNode;
|
|
39
|
+
/**
|
|
40
|
+
* Optional component to render for the edit dialog.
|
|
41
|
+
* If not provided, the "Edit" action will not be shown.
|
|
42
|
+
*/
|
|
43
|
+
EditDialog?: React.ComponentType<{
|
|
44
|
+
initialData?: TData;
|
|
45
|
+
open?: boolean;
|
|
46
|
+
onOpenChange?: (open: boolean) => void;
|
|
47
|
+
children?: React.ReactNode;
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Optional server action to call for deletion.
|
|
51
|
+
* If not provided, the "Delete" action will not be shown.
|
|
52
|
+
*/
|
|
53
|
+
deleteAction?: (id: string) => Promise<ActionState<unknown>>;
|
|
54
|
+
/**
|
|
55
|
+
* The name of the resource to display in toast messages (e.g. "User Status").
|
|
56
|
+
*/
|
|
57
|
+
resourceName: string;
|
|
58
|
+
/**
|
|
59
|
+
* The key to access the name/title of the item for display. Defaults to "name".
|
|
60
|
+
*/
|
|
61
|
+
nameKey?: keyof TData;
|
|
62
|
+
/**
|
|
63
|
+
* The key to access the unique ID of the item. Defaults to "id".
|
|
64
|
+
*/
|
|
65
|
+
idKey?: keyof TData;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function DataTableRowActions<TData>({
|
|
69
|
+
row,
|
|
70
|
+
children,
|
|
71
|
+
EditDialog,
|
|
72
|
+
deleteAction,
|
|
73
|
+
resourceName,
|
|
74
|
+
nameKey = "name" as keyof TData,
|
|
75
|
+
idKey = "id" as keyof TData,
|
|
76
|
+
}: DataTableRowActionsProps<TData>) {
|
|
77
|
+
const item = row.original;
|
|
78
|
+
const itemRecord = item as Record<string, unknown>;
|
|
79
|
+
const itemName = String(
|
|
80
|
+
itemRecord[nameKey as string] ?? itemRecord.nameThai ?? itemRecord.name ?? "",
|
|
81
|
+
);
|
|
82
|
+
const [isPending, startTransition] = useTransition();
|
|
83
|
+
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
|
84
|
+
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
|
85
|
+
|
|
86
|
+
const handleDelete = () => {
|
|
87
|
+
if (!deleteAction) return;
|
|
88
|
+
|
|
89
|
+
startTransition(() => {
|
|
90
|
+
const promise = deleteAction(String(itemRecord[idKey as string])).then((result) => {
|
|
91
|
+
if (result.error) throw new Error(result.error);
|
|
92
|
+
setIsDeleteDialogOpen(false);
|
|
93
|
+
return result;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
toast.promise(promise, {
|
|
97
|
+
loading: `Deleting ${resourceName.toLowerCase()} "${itemName}"...`,
|
|
98
|
+
success: `${resourceName} "${itemName}" deleted successfully.`,
|
|
99
|
+
error: (err: Error) => err.message,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="flex justify-end">
|
|
106
|
+
<DropdownMenu>
|
|
107
|
+
<DropdownMenuTrigger
|
|
108
|
+
render={
|
|
109
|
+
<Button variant="ghost" className="flex h-8 w-8 p-0 data-[state=open]:bg-muted" />
|
|
110
|
+
}
|
|
111
|
+
>
|
|
112
|
+
<Ellipsis className="h-4 w-4" />
|
|
113
|
+
<span className="sr-only">Open menu</span>
|
|
114
|
+
</DropdownMenuTrigger>
|
|
115
|
+
<DropdownMenuContent align="end" className="w-[160px]">
|
|
116
|
+
{children}
|
|
117
|
+
{children && (EditDialog || deleteAction) && <DropdownMenuSeparator />}
|
|
118
|
+
|
|
119
|
+
<DropdownMenuGroup>
|
|
120
|
+
{EditDialog && (
|
|
121
|
+
<DropdownMenuItem onClick={() => setIsEditDialogOpen(true)}>
|
|
122
|
+
<Pen className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
|
|
123
|
+
Edit
|
|
124
|
+
</DropdownMenuItem>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
{EditDialog && deleteAction && <DropdownMenuSeparator />}
|
|
128
|
+
|
|
129
|
+
{deleteAction && (
|
|
130
|
+
<DropdownMenuItem
|
|
131
|
+
className="text-destructive focus:text-destructive"
|
|
132
|
+
onClick={() => setIsDeleteDialogOpen(true)}
|
|
133
|
+
>
|
|
134
|
+
<Trash className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
|
|
135
|
+
Delete
|
|
136
|
+
</DropdownMenuItem>
|
|
137
|
+
)}
|
|
138
|
+
</DropdownMenuGroup>
|
|
139
|
+
</DropdownMenuContent>
|
|
140
|
+
</DropdownMenu>
|
|
141
|
+
|
|
142
|
+
{EditDialog && (
|
|
143
|
+
<EditDialog initialData={item} open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen} />
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{deleteAction && (
|
|
147
|
+
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
|
148
|
+
<AlertDialogContent>
|
|
149
|
+
<AlertDialogHeader>
|
|
150
|
+
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
|
151
|
+
<AlertDialogDescription>
|
|
152
|
+
This action cannot be undone. This will permanently delete the{" "}
|
|
153
|
+
{resourceName.toLowerCase()} “{itemName}” and remove it from our
|
|
154
|
+
servers.
|
|
155
|
+
</AlertDialogDescription>
|
|
156
|
+
</AlertDialogHeader>
|
|
157
|
+
<AlertDialogFooter>
|
|
158
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
159
|
+
<DeleteButton onClick={handleDelete} isLoading={isPending} />
|
|
160
|
+
</AlertDialogFooter>
|
|
161
|
+
</AlertDialogContent>
|
|
162
|
+
</AlertDialog>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Badge } from "@suphark/ui/components/ui/badge";
|
|
4
|
+
import { Button } from "@suphark/ui/components/ui/button";
|
|
5
|
+
import {
|
|
6
|
+
InputGroup,
|
|
7
|
+
InputGroupAddon,
|
|
8
|
+
InputGroupButton,
|
|
9
|
+
InputGroupInput,
|
|
10
|
+
} from "@suphark/ui/components/ui/input-group";
|
|
11
|
+
import {
|
|
12
|
+
Sheet,
|
|
13
|
+
SheetClose,
|
|
14
|
+
SheetContent,
|
|
15
|
+
SheetDescription,
|
|
16
|
+
SheetFooter,
|
|
17
|
+
SheetHeader,
|
|
18
|
+
SheetTitle,
|
|
19
|
+
SheetTrigger,
|
|
20
|
+
} from "@suphark/ui/components/ui/sheet";
|
|
21
|
+
import type { Table } from "@tanstack/react-table";
|
|
22
|
+
import { Search, SlidersHorizontal, X } from "lucide-react";
|
|
23
|
+
import type React from "react";
|
|
24
|
+
import type { FacetedFilterConfig } from ".";
|
|
25
|
+
import { DataTableFacetedFilter } from "./data-table-faceted-filter";
|
|
26
|
+
import { DataTableViewOptions } from "./data-table-view-option";
|
|
27
|
+
|
|
28
|
+
interface DataTableToolbarProps<TData> {
|
|
29
|
+
table: Table<TData>;
|
|
30
|
+
facetedFilters?: FacetedFilterConfig[];
|
|
31
|
+
actionsComponent?: React.ReactNode;
|
|
32
|
+
globalFilterPlaceholder?: string;
|
|
33
|
+
toolbarFilters?: React.ReactNode;
|
|
34
|
+
preservedFilterIds?: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function DataTableToolbar<TData>({
|
|
38
|
+
table,
|
|
39
|
+
facetedFilters,
|
|
40
|
+
actionsComponent,
|
|
41
|
+
globalFilterPlaceholder,
|
|
42
|
+
toolbarFilters,
|
|
43
|
+
preservedFilterIds = [],
|
|
44
|
+
}: DataTableToolbarProps<TData>) {
|
|
45
|
+
const globalFilter = table.getState().globalFilter;
|
|
46
|
+
const preservedFilterIdSet = new Set(preservedFilterIds);
|
|
47
|
+
const userColumnFilters = table
|
|
48
|
+
.getState()
|
|
49
|
+
.columnFilters.filter((filter) => !preservedFilterIdSet.has(filter.id));
|
|
50
|
+
const activeFilterCount = userColumnFilters.length + (globalFilter ? 1 : 0);
|
|
51
|
+
|
|
52
|
+
const isFiltered = activeFilterCount > 0;
|
|
53
|
+
|
|
54
|
+
const resetUserFilters = () => {
|
|
55
|
+
table.setColumnFilters((filters) =>
|
|
56
|
+
filters.filter((filter) => preservedFilterIdSet.has(filter.id)),
|
|
57
|
+
);
|
|
58
|
+
table.setGlobalFilter("");
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const renderFilterControls = (fullWidth = false) =>
|
|
62
|
+
facetedFilters?.map((filter: FacetedFilterConfig) => {
|
|
63
|
+
const column = table.getColumn(filter.columnId);
|
|
64
|
+
if (!column) return null;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<DataTableFacetedFilter
|
|
68
|
+
key={filter.columnId}
|
|
69
|
+
column={column}
|
|
70
|
+
title={filter.title}
|
|
71
|
+
options={filter.options}
|
|
72
|
+
compact={filter.compact}
|
|
73
|
+
fullWidth={fullWidth}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
80
|
+
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
|
|
81
|
+
{globalFilterPlaceholder && (
|
|
82
|
+
<InputGroup className="h-8 w-full sm:w-[240px] lg:w-[320px]">
|
|
83
|
+
<InputGroupAddon align="inline-start">
|
|
84
|
+
<Search aria-hidden="true" className="size-4" />
|
|
85
|
+
</InputGroupAddon>
|
|
86
|
+
<InputGroupInput
|
|
87
|
+
aria-label="ค้นหาข้อมูลในตาราง"
|
|
88
|
+
name="table-search"
|
|
89
|
+
autoComplete="off"
|
|
90
|
+
placeholder={globalFilterPlaceholder}
|
|
91
|
+
value={(globalFilter as string) ?? ""}
|
|
92
|
+
onChange={(event) => table.setGlobalFilter(String(event.target.value))}
|
|
93
|
+
/>
|
|
94
|
+
{!!globalFilter && (
|
|
95
|
+
<InputGroupAddon align="inline-end">
|
|
96
|
+
<InputGroupButton
|
|
97
|
+
variant="ghost"
|
|
98
|
+
aria-label="ล้างคำค้นหา"
|
|
99
|
+
onClick={() => table.setGlobalFilter("")}
|
|
100
|
+
>
|
|
101
|
+
<X aria-hidden="true" className="size-4" />
|
|
102
|
+
</InputGroupButton>
|
|
103
|
+
</InputGroupAddon>
|
|
104
|
+
)}
|
|
105
|
+
</InputGroup>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
<div className="hidden xl:contents">
|
|
109
|
+
{renderFilterControls()}
|
|
110
|
+
{toolbarFilters}
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<Sheet>
|
|
114
|
+
<SheetTrigger render={<Button variant="outline" size="sm" className="xl:hidden" />}>
|
|
115
|
+
<SlidersHorizontal />
|
|
116
|
+
ตัวกรอง
|
|
117
|
+
{activeFilterCount > 0 && (
|
|
118
|
+
<Badge variant="secondary" className="ml-1 px-1.5 tabular-nums">
|
|
119
|
+
{activeFilterCount}
|
|
120
|
+
</Badge>
|
|
121
|
+
)}
|
|
122
|
+
</SheetTrigger>
|
|
123
|
+
<SheetContent
|
|
124
|
+
side="right"
|
|
125
|
+
showCloseButton={false}
|
|
126
|
+
className="w-[min(24rem,calc(100vw-1rem))] sm:max-w-md"
|
|
127
|
+
>
|
|
128
|
+
<SheetHeader className="border-b pr-12">
|
|
129
|
+
<SheetTitle>ตัวกรองข้อมูล</SheetTitle>
|
|
130
|
+
<SheetDescription>เลือกได้หลายเงื่อนไข ผลลัพธ์จะอัปเดตทันที</SheetDescription>
|
|
131
|
+
<SheetClose
|
|
132
|
+
render={
|
|
133
|
+
<Button variant="ghost" size="icon-sm" className="absolute top-3 right-3" />
|
|
134
|
+
}
|
|
135
|
+
>
|
|
136
|
+
<X />
|
|
137
|
+
<span className="sr-only">ปิดตัวกรอง</span>
|
|
138
|
+
</SheetClose>
|
|
139
|
+
</SheetHeader>
|
|
140
|
+
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto px-4">
|
|
141
|
+
{renderFilterControls(true)}
|
|
142
|
+
{toolbarFilters}
|
|
143
|
+
</div>
|
|
144
|
+
{isFiltered && (
|
|
145
|
+
<SheetFooter className="border-t">
|
|
146
|
+
<Button variant="secondary" onClick={resetUserFilters}>
|
|
147
|
+
<X />
|
|
148
|
+
ล้างตัวกรองทั้งหมด
|
|
149
|
+
</Button>
|
|
150
|
+
</SheetFooter>
|
|
151
|
+
)}
|
|
152
|
+
</SheetContent>
|
|
153
|
+
</Sheet>
|
|
154
|
+
|
|
155
|
+
{isFiltered && (
|
|
156
|
+
<Button variant="secondary" size="sm" onClick={resetUserFilters}>
|
|
157
|
+
ล้างตัวกรอง
|
|
158
|
+
<X />
|
|
159
|
+
</Button>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div className="ml-auto flex flex-wrap items-center justify-end gap-2 self-start">
|
|
164
|
+
{actionsComponent}
|
|
165
|
+
<DataTableViewOptions table={table} />
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
{isFiltered && (
|
|
169
|
+
<div className="flex w-full flex-wrap gap-2 pt-1">
|
|
170
|
+
{userColumnFilters.map((filter) => {
|
|
171
|
+
const config = facetedFilters?.find((f) => f.columnId === filter.id);
|
|
172
|
+
if (!config) return null;
|
|
173
|
+
|
|
174
|
+
const values = Array.isArray(filter.value) ? filter.value : [filter.value];
|
|
175
|
+
const labels = values
|
|
176
|
+
.map((val) => {
|
|
177
|
+
const option = config.options.find((opt) => String(opt.value) === String(val));
|
|
178
|
+
return option ? option.label : String(val);
|
|
179
|
+
})
|
|
180
|
+
.join(", ");
|
|
181
|
+
const displayedLabels =
|
|
182
|
+
config.compact && values.length > 1 ? `${values.length} รายการ` : labels;
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<Badge
|
|
186
|
+
key={filter.id}
|
|
187
|
+
variant="secondary"
|
|
188
|
+
className="flex items-center gap-1.5 rounded-md px-2 py-1 font-normal"
|
|
189
|
+
>
|
|
190
|
+
<span className="text-muted-foreground">{config.title}:</span>
|
|
191
|
+
<span className="font-medium">{displayedLabels}</span>
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
aria-label={`ล้างตัวกรอง ${config.title}: ${displayedLabels}`}
|
|
195
|
+
onClick={() => table.getColumn(filter.id)?.setFilterValue(undefined)}
|
|
196
|
+
className="ml-1 rounded-full outline-none hover:bg-muted-foreground/20"
|
|
197
|
+
>
|
|
198
|
+
<X className="size-3" />
|
|
199
|
+
</button>
|
|
200
|
+
</Badge>
|
|
201
|
+
);
|
|
202
|
+
})}
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@suphark/ui/components/ui/button";
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuCheckboxItem,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuGroup,
|
|
9
|
+
DropdownMenuLabel,
|
|
10
|
+
DropdownMenuSeparator,
|
|
11
|
+
DropdownMenuTrigger,
|
|
12
|
+
} from "@suphark/ui/components/ui/dropdown-menu";
|
|
13
|
+
import type { Table } from "@tanstack/react-table";
|
|
14
|
+
import { Settings2 } from "lucide-react";
|
|
15
|
+
|
|
16
|
+
interface DataTableViewOptionsProps<TData> {
|
|
17
|
+
table: Table<TData>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function DataTableViewOptions<TData>({ table }: DataTableViewOptionsProps<TData>) {
|
|
21
|
+
return (
|
|
22
|
+
<DropdownMenu>
|
|
23
|
+
<DropdownMenuTrigger
|
|
24
|
+
render={<Button variant="outline" size="sm" className="ml-auto hidden h-8 lg:flex" />}
|
|
25
|
+
>
|
|
26
|
+
<Settings2 className="mr-2 h-4 w-4" />
|
|
27
|
+
View
|
|
28
|
+
</DropdownMenuTrigger>
|
|
29
|
+
<DropdownMenuContent align="end" className="w-[150px]">
|
|
30
|
+
<DropdownMenuGroup>
|
|
31
|
+
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
|
|
32
|
+
</DropdownMenuGroup>
|
|
33
|
+
<DropdownMenuSeparator />
|
|
34
|
+
{table
|
|
35
|
+
.getAllColumns()
|
|
36
|
+
.filter(
|
|
37
|
+
(column) =>
|
|
38
|
+
typeof column.accessorFn !== "undefined" &&
|
|
39
|
+
column.getCanHide() &&
|
|
40
|
+
column.columnDef.enableHiding !== false,
|
|
41
|
+
)
|
|
42
|
+
.map((column) => {
|
|
43
|
+
return (
|
|
44
|
+
<DropdownMenuCheckboxItem
|
|
45
|
+
key={column.id}
|
|
46
|
+
className="capitalize"
|
|
47
|
+
checked={column.getIsVisible()}
|
|
48
|
+
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
|
49
|
+
// Base UI ปิดเมนูหลังคลิกเป็นค่าเริ่มต้น — ติ๊กเลือกคอลัมน์ต้องเปิดค้างไว้
|
|
50
|
+
closeOnClick={false}
|
|
51
|
+
>
|
|
52
|
+
{column.id.replace(/([A-Z])/g, " $1")}
|
|
53
|
+
</DropdownMenuCheckboxItem>
|
|
54
|
+
);
|
|
55
|
+
})}
|
|
56
|
+
</DropdownMenuContent>
|
|
57
|
+
</DropdownMenu>
|
|
58
|
+
);
|
|
59
|
+
}
|