@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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
|
|
4
4
|
|
|
5
|
+
## [0.2.0] — 2026-09-18
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- entry **`@suphark/ui/data-table`** (ยกจาก supplychain `components/data-table`): `DataTable`, `DataTableColumnHeader`, `DataTableToolbar`, `DataTablePagination`, `DataTableFacetedFilter`, `DataTableDateRangeFilter`, `DataTableViewOptions`, `DataTableRowActions`, `useDataTable`, types (`DataTableProps`, `FacetedFilterConfig`) + `DeleteManyButton`, `ResourcePropertyFilter` (+helpers) — ต้องมี peer `@tanstack/react-table` (optional; แอปที่ไม่ใช้ตารางไม่ต้องติดตั้ง); ลากเรียงแถวใช้ `@dnd-kit/*` เป็น dependency ของแพ็กเกจ
|
|
10
|
+
- `useTableSearchParams` / `TABLE_PARAM` (state ตารางใน URL) ที่ `@suphark/ui/next`
|
|
11
|
+
- tests: data-table, data-table-faceted-filter, resource-property-filter (รวม 34)
|
|
12
|
+
- exports `./package.json`, `./components/data-table`
|
|
13
|
+
- showcase: การ์ด DataTable จริงในแท็บ Data Display; code recipe ใช้ import path ของแพ็กเกจ
|
|
14
|
+
- `apps/showcase/vercel.json` + `docs/ops/deploy-vercel.md`
|
|
15
|
+
|
|
16
|
+
## [0.1.1] — 2026-09-18
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `NativeSelect` / `NativeSelectOption` (shadcn `native-select` — `<select>` ธรรมดาสำหรับฟอร์ม server action/FormData) ตามที่ Pi Auth ใช้ 17 ไฟล์
|
|
21
|
+
- dependency `cn` (^0.3.0) — shadcn CLI รุ่นใหม่สร้างไฟล์ที่ `import { cn } from "cn"` แทน `lib/utils`; ทั้งสองทางใช้ได้ ไฟล์เดิม 59 ตัวยังใช้ `@suphark/ui/lib/utils`
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- ตัด `__tests__`/`*.test.*` ออกจาก tarball (`files` ใน package.json) — test ยังอยู่ใน repo
|
|
26
|
+
|
|
27
|
+
### Notes
|
|
28
|
+
|
|
29
|
+
- `ThemeToggle` props `persistedTheme` / `onThemeChange` เป็น optional อยู่แล้ว — ไม่ส่ง = ใช้ next-themes อย่างเดียว (ใช้แทน ThemeToggle ของ @pi/ui ได้โดยไม่ต้องเปลี่ยน call site)
|
|
30
|
+
|
|
5
31
|
## [0.1.0] — 2026-09-18
|
|
6
32
|
|
|
7
33
|
รุ่นแรก — ยกจาก `supplychainnextjs-1` (source of truth) + ชุด generic จาก Pi Auth `@pi/ui`
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Design system กลางของ Suphark apps (Pi Auth, DueBell, Pi PDF, supp
|
|
|
10
10
|
pnpm add @suphark/ui @base-ui/react lucide-react react-hook-form
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
peer ที่ต้องมีอยู่แล้ว: `react`/`react-dom` ≥19, `tailwindcss` ^4, `next` ≥15 (เฉพาะ `@suphark/ui/next`), `recharts` (เฉพาะ `components/ui/chart`)
|
|
13
|
+
peer ที่ต้องมีอยู่แล้ว: `react`/`react-dom` ≥19, `tailwindcss` ^4, `next` ≥15 (เฉพาะ `@suphark/ui/next`), `@tanstack/react-table` (เฉพาะ `@suphark/ui/data-table`), `recharts` (เฉพาะ `components/ui/chart`)
|
|
14
14
|
|
|
15
15
|
### 1. next.config.ts
|
|
16
16
|
|
|
@@ -58,6 +58,9 @@ import { Button, Card, FormFieldInput, StatusBadge, ThemeToggle, cn } from "@sup
|
|
|
58
58
|
// ของที่ใช้ next/navigation หรือ next/link
|
|
59
59
|
import { FormDialog, RowActions, LinkButton, RefreshButton, PageShell } from "@suphark/ui/next";
|
|
60
60
|
|
|
61
|
+
// ตารางข้อมูล (ต้องติดตั้ง @tanstack/react-table เอง)
|
|
62
|
+
import { DataTable, DataTableColumnHeader, useDataTable } from "@suphark/ui/data-table";
|
|
63
|
+
|
|
61
64
|
// import ตรงรายไฟล์ก็ได้ (เช่น chart ที่ต้องมี recharts)
|
|
62
65
|
import { ChartContainer } from "@suphark/ui/components/ui/chart";
|
|
63
66
|
```
|
|
@@ -68,6 +71,7 @@ import { ChartContainer } from "@suphark/ui/components/ui/chart";
|
|
|
68
71
|
|---|---|---|
|
|
69
72
|
| `src/components/ui/*` | shadcn/ui ต้นฉบับ 59 ตัว (+ `__tests__`) | **ห้ามแก้มือ** — เพิ่ม/อัปเดตด้วย `pnpm dlx shadcn@latest add <name>` ใน `packages/ui` (`components.json` ตั้ง alias ไว้แล้ว) |
|
|
70
73
|
| `src/components/shared/*` | ของที่เราประกอบเอง: ปุ่มมาตรฐาน (`button/`), form-field บน react-hook-form (`form/`), layout, theme, combobox, และชุดจาก Pi Auth (FormDialog, NameIdFields, StatusBadge, RowMenu, …) | แก้ได้ แต่คง props เดิมให้มากที่สุด (minor = เพิ่ม, major = เปลี่ยน) |
|
|
74
|
+
| `src/components/data-table/*` | DataTable บน tanstack (toolbar, faceted filter, pagination, view options, row actions, ลากเรียงแถว) | entry `@suphark/ui/data-table` |
|
|
71
75
|
| `src/components/design-system/*` | หน้า showcase (render ใน `apps/showcase`) | ไม่อยู่ใน barrel |
|
|
72
76
|
| `src/lib/*`, `src/hooks/*` | `cn`, formatter วันที่ไทย, สีสถานะ, permission context, `useIsMobile`, `useCopyToClipboard` | |
|
|
73
77
|
| `src/styles.css` | token (slate + success/warning/sidebar/chart), radius, ฟอนต์, scrollbar, dark variant | |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suphark/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Design system กลางของ Suphark apps: shadcn/ui (base-nova, Base UI, Tailwind v4) + shared components + token/ฟอนต์ Noto Sans Thai Looped — ส่งเป็น source ให้แอป Next transpile เอง",
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
],
|
|
25
25
|
"files": [
|
|
26
26
|
"src",
|
|
27
|
+
"!src/**/__tests__",
|
|
28
|
+
"!src/**/*.test.*",
|
|
27
29
|
"scripts",
|
|
28
30
|
"components.json",
|
|
29
31
|
"README.md",
|
|
@@ -36,18 +38,25 @@
|
|
|
36
38
|
"exports": {
|
|
37
39
|
".": "./src/index.ts",
|
|
38
40
|
"./next": "./src/next.ts",
|
|
41
|
+
"./data-table": "./src/data-table.ts",
|
|
39
42
|
"./styles.css": "./src/styles.css",
|
|
40
43
|
"./components/shared/button": "./src/components/shared/button/index.ts",
|
|
41
44
|
"./components/shared/theme": "./src/components/shared/theme/index.ts",
|
|
45
|
+
"./components/data-table": "./src/components/data-table/index.ts",
|
|
42
46
|
"./components/*": "./src/components/*.tsx",
|
|
43
47
|
"./lib/*": "./src/lib/*.ts",
|
|
44
|
-
"./hooks/*": "./src/hooks/*.ts"
|
|
48
|
+
"./hooks/*": "./src/hooks/*.ts",
|
|
49
|
+
"./package.json": "./package.json"
|
|
45
50
|
},
|
|
46
51
|
"dependencies": {
|
|
52
|
+
"@dnd-kit/core": "^6.3.1",
|
|
53
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
54
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
47
55
|
"@fontsource/noto-sans-thai-looped": "^5.3.0",
|
|
48
56
|
"class-variance-authority": "^0.7.1",
|
|
49
57
|
"clsx": "^2.1.1",
|
|
50
58
|
"cmdk": "^1.1.1",
|
|
59
|
+
"cn": "^0.3.0",
|
|
51
60
|
"date-fns": "^4.4.0",
|
|
52
61
|
"embla-carousel-react": "^8.6.0",
|
|
53
62
|
"input-otp": "^1.5.0",
|
|
@@ -61,6 +70,7 @@
|
|
|
61
70
|
},
|
|
62
71
|
"peerDependencies": {
|
|
63
72
|
"@base-ui/react": ">=1.7.0",
|
|
73
|
+
"@tanstack/react-table": ">=8.20.0",
|
|
64
74
|
"lucide-react": ">=1.30.0",
|
|
65
75
|
"next": ">=15",
|
|
66
76
|
"react": ">=19",
|
|
@@ -75,10 +85,14 @@
|
|
|
75
85
|
},
|
|
76
86
|
"recharts": {
|
|
77
87
|
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"@tanstack/react-table": {
|
|
90
|
+
"optional": true
|
|
78
91
|
}
|
|
79
92
|
},
|
|
80
93
|
"devDependencies": {
|
|
81
94
|
"@base-ui/react": "^1.8.0",
|
|
95
|
+
"@tanstack/react-table": "^8.21.3",
|
|
82
96
|
"@testing-library/jest-dom": "^6.9.1",
|
|
83
97
|
"@testing-library/react": "^16.3.2",
|
|
84
98
|
"@testing-library/user-event": "^14.6.1",
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@suphark/ui/components/ui/button";
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuSeparator,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from "@suphark/ui/components/ui/dropdown-menu";
|
|
11
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
12
|
+
import type { Column } from "@tanstack/react-table";
|
|
13
|
+
import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff, Filter } from "lucide-react";
|
|
14
|
+
|
|
15
|
+
interface DataTableColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
|
|
16
|
+
column: Column<TData, TValue>;
|
|
17
|
+
title: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function DataTableColumnHeader<TData, TValue>({
|
|
21
|
+
column,
|
|
22
|
+
title,
|
|
23
|
+
className,
|
|
24
|
+
}: DataTableColumnHeaderProps<TData, TValue>) {
|
|
25
|
+
if (!column.getCanSort()) {
|
|
26
|
+
return <div className={cn(className)}>{title}</div>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className={cn("flex items-center gap-2", className)}>
|
|
31
|
+
<DropdownMenu>
|
|
32
|
+
<DropdownMenuTrigger
|
|
33
|
+
render={
|
|
34
|
+
<Button variant="ghost" size="sm" className="-ml-3 h-8 data-[state=open]:bg-accent" />
|
|
35
|
+
}
|
|
36
|
+
>
|
|
37
|
+
<span>{title}</span>
|
|
38
|
+
{column.getIsSorted() === "desc" ? (
|
|
39
|
+
<ArrowDown />
|
|
40
|
+
) : column.getIsSorted() === "asc" ? (
|
|
41
|
+
<ArrowUp />
|
|
42
|
+
) : (
|
|
43
|
+
<ChevronsUpDown />
|
|
44
|
+
)}
|
|
45
|
+
{column.getIsFiltered() && (
|
|
46
|
+
<Filter className="ml-1 size-3 fill-primary/10 text-primary" />
|
|
47
|
+
)}
|
|
48
|
+
</DropdownMenuTrigger>
|
|
49
|
+
<DropdownMenuContent align="start">
|
|
50
|
+
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
|
|
51
|
+
<ArrowUp />
|
|
52
|
+
Asc
|
|
53
|
+
</DropdownMenuItem>
|
|
54
|
+
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
|
|
55
|
+
<ArrowDown />
|
|
56
|
+
Desc
|
|
57
|
+
</DropdownMenuItem>
|
|
58
|
+
{column.getCanHide() && column.columnDef.enableHiding !== false && (
|
|
59
|
+
<>
|
|
60
|
+
<DropdownMenuSeparator />
|
|
61
|
+
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
|
|
62
|
+
<EyeOff />
|
|
63
|
+
Hide
|
|
64
|
+
</DropdownMenuItem>
|
|
65
|
+
</>
|
|
66
|
+
)}
|
|
67
|
+
</DropdownMenuContent>
|
|
68
|
+
</DropdownMenu>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { DateRangePicker } from "@suphark/ui/components/shared/date/date-range-picker";
|
|
4
|
+
import type { Column } from "@tanstack/react-table";
|
|
5
|
+
import type { DateRange } from "react-day-picker";
|
|
6
|
+
|
|
7
|
+
interface DataTableDateRangeFilterProps<TData> {
|
|
8
|
+
column?: Column<TData, unknown>;
|
|
9
|
+
title?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function DataTableDateRangeFilter<TData>({
|
|
13
|
+
column,
|
|
14
|
+
title,
|
|
15
|
+
}: DataTableDateRangeFilterProps<TData>) {
|
|
16
|
+
const filterValue = column?.getFilterValue() as DateRange | undefined;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<DateRangePicker
|
|
20
|
+
value={filterValue}
|
|
21
|
+
onValueChange={(range) => column?.setFilterValue(range)}
|
|
22
|
+
title={title}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
Command,
|
|
7
|
+
CommandEmpty,
|
|
8
|
+
CommandGroup,
|
|
9
|
+
CommandInput,
|
|
10
|
+
CommandItem,
|
|
11
|
+
CommandList,
|
|
12
|
+
CommandSeparator,
|
|
13
|
+
} from "@suphark/ui/components/ui/command";
|
|
14
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@suphark/ui/components/ui/popover";
|
|
15
|
+
import { Separator } from "@suphark/ui/components/ui/separator";
|
|
16
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
17
|
+
import type { Column } from "@tanstack/react-table";
|
|
18
|
+
import { Check, CirclePlus } from "lucide-react";
|
|
19
|
+
import * as React from "react";
|
|
20
|
+
|
|
21
|
+
interface DataTableFacetedFilterProps<TData, TValue> {
|
|
22
|
+
column?: Column<TData, TValue>;
|
|
23
|
+
title?: string;
|
|
24
|
+
options: {
|
|
25
|
+
label: string;
|
|
26
|
+
value: string | number | boolean;
|
|
27
|
+
icon?: React.ComponentType<{ className?: string }>;
|
|
28
|
+
}[];
|
|
29
|
+
compact?: boolean;
|
|
30
|
+
fullWidth?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function DataTableFacetedFilter<TData, TValue>({
|
|
34
|
+
column,
|
|
35
|
+
title,
|
|
36
|
+
options,
|
|
37
|
+
compact = false,
|
|
38
|
+
fullWidth = false,
|
|
39
|
+
}: DataTableFacetedFilterProps<TData, TValue>) {
|
|
40
|
+
const facets = column?.getFacetedUniqueValues();
|
|
41
|
+
const filterValue = column?.getFilterValue();
|
|
42
|
+
const selectedValues = new Set(Array.isArray(filterValue) ? filterValue.map(String) : []);
|
|
43
|
+
|
|
44
|
+
const areAllSelected = options.length > 0 && selectedValues.size === options.length;
|
|
45
|
+
|
|
46
|
+
const sortedOptions = React.useMemo(() => {
|
|
47
|
+
return [...options].sort((a, b) => {
|
|
48
|
+
const aSelected = selectedValues.has(String(a.value));
|
|
49
|
+
const bSelected = selectedValues.has(String(b.value));
|
|
50
|
+
if (aSelected && !bSelected) return -1;
|
|
51
|
+
if (!aSelected && bSelected) return 1;
|
|
52
|
+
return 0;
|
|
53
|
+
});
|
|
54
|
+
}, [options, selectedValues]);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Popover>
|
|
58
|
+
<PopoverTrigger
|
|
59
|
+
render={
|
|
60
|
+
<Button
|
|
61
|
+
variant="outline"
|
|
62
|
+
size="sm"
|
|
63
|
+
className={cn(
|
|
64
|
+
"h-8 border-dashed",
|
|
65
|
+
compact && "h-7 px-2 text-xs",
|
|
66
|
+
fullWidth && "w-full justify-start",
|
|
67
|
+
)}
|
|
68
|
+
/>
|
|
69
|
+
}
|
|
70
|
+
>
|
|
71
|
+
<CirclePlus />
|
|
72
|
+
{title}
|
|
73
|
+
{selectedValues?.size > 0 && (
|
|
74
|
+
<>
|
|
75
|
+
<Separator orientation="vertical" className="mx-2 h-4" />
|
|
76
|
+
<Badge
|
|
77
|
+
variant="secondary"
|
|
78
|
+
className={cn("rounded-sm px-1 font-normal", !compact && "lg:hidden")}
|
|
79
|
+
>
|
|
80
|
+
{selectedValues.size}
|
|
81
|
+
</Badge>
|
|
82
|
+
<div className={cn("hidden gap-1 lg:flex", compact && "lg:hidden")}>
|
|
83
|
+
{selectedValues.size > 2 ? (
|
|
84
|
+
<Badge variant="secondary" className="rounded-sm px-1 font-normal">
|
|
85
|
+
เลือก {selectedValues.size} รายการ
|
|
86
|
+
</Badge>
|
|
87
|
+
) : (
|
|
88
|
+
options.reduce((acc: React.ReactNode[], option) => {
|
|
89
|
+
if (selectedValues.has(String(option.value))) {
|
|
90
|
+
acc.push(
|
|
91
|
+
<Badge
|
|
92
|
+
variant="secondary"
|
|
93
|
+
key={String(option.value)}
|
|
94
|
+
className="rounded-sm px-1 font-normal"
|
|
95
|
+
>
|
|
96
|
+
{option.label}
|
|
97
|
+
</Badge>,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return acc;
|
|
101
|
+
}, [])
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
</>
|
|
105
|
+
)}
|
|
106
|
+
</PopoverTrigger>
|
|
107
|
+
<PopoverContent className={cn("w-[200px] p-0", compact && "w-44")} align="start">
|
|
108
|
+
<Command>
|
|
109
|
+
{!compact ? <CommandInput placeholder={title} /> : null}
|
|
110
|
+
<CommandList>
|
|
111
|
+
<CommandEmpty>ไม่พบข้อมูล</CommandEmpty>
|
|
112
|
+
<CommandGroup>
|
|
113
|
+
{sortedOptions.map((option) => {
|
|
114
|
+
const valueAsString = String(option.value);
|
|
115
|
+
const isSelected = selectedValues.has(valueAsString);
|
|
116
|
+
return (
|
|
117
|
+
<CommandItem
|
|
118
|
+
key={valueAsString}
|
|
119
|
+
onSelect={() => {
|
|
120
|
+
if (isSelected) {
|
|
121
|
+
selectedValues.delete(valueAsString);
|
|
122
|
+
} else {
|
|
123
|
+
selectedValues.add(valueAsString);
|
|
124
|
+
}
|
|
125
|
+
const filterValues = Array.from(selectedValues);
|
|
126
|
+
column?.setFilterValue(filterValues.length ? filterValues : undefined);
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
<div
|
|
130
|
+
className={cn(
|
|
131
|
+
"flex size-4 items-center justify-center rounded-[4px] border",
|
|
132
|
+
isSelected
|
|
133
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
134
|
+
: "border-input [&_svg]:invisible",
|
|
135
|
+
)}
|
|
136
|
+
>
|
|
137
|
+
<Check className="size-3.5 text-primary-foreground" />
|
|
138
|
+
</div>
|
|
139
|
+
{option.icon && <option.icon className="size-4 text-muted-foreground" />}
|
|
140
|
+
<span>{option.label}</span>
|
|
141
|
+
{facets?.get(valueAsString) && (
|
|
142
|
+
<span className="ml-auto flex size-4 items-center justify-center font-mono text-muted-foreground text-xs">
|
|
143
|
+
{facets.get(valueAsString)}
|
|
144
|
+
</span>
|
|
145
|
+
)}
|
|
146
|
+
</CommandItem>
|
|
147
|
+
);
|
|
148
|
+
})}
|
|
149
|
+
</CommandGroup>
|
|
150
|
+
{!compact && (selectedValues.size > 0 || !areAllSelected) && (
|
|
151
|
+
<>
|
|
152
|
+
<CommandSeparator />
|
|
153
|
+
<CommandGroup>
|
|
154
|
+
{areAllSelected ? (
|
|
155
|
+
<CommandItem
|
|
156
|
+
onSelect={() => column?.setFilterValue(undefined)}
|
|
157
|
+
className="justify-center text-center"
|
|
158
|
+
>
|
|
159
|
+
ล้างตัวกรอง
|
|
160
|
+
</CommandItem>
|
|
161
|
+
) : (
|
|
162
|
+
<CommandItem
|
|
163
|
+
key="select-all"
|
|
164
|
+
onSelect={() => {
|
|
165
|
+
const allValues = options.map((option) => String(option.value));
|
|
166
|
+
column?.setFilterValue(allValues);
|
|
167
|
+
}}
|
|
168
|
+
className="justify-center text-center"
|
|
169
|
+
>
|
|
170
|
+
เลือกทั้งหมด
|
|
171
|
+
</CommandItem>
|
|
172
|
+
)}
|
|
173
|
+
</CommandGroup>
|
|
174
|
+
</>
|
|
175
|
+
)}
|
|
176
|
+
</CommandList>
|
|
177
|
+
</Command>
|
|
178
|
+
</PopoverContent>
|
|
179
|
+
</Popover>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@suphark/ui/components/ui/button";
|
|
4
|
+
import {
|
|
5
|
+
Select,
|
|
6
|
+
SelectContent,
|
|
7
|
+
SelectItem,
|
|
8
|
+
SelectTrigger,
|
|
9
|
+
SelectValue,
|
|
10
|
+
} from "@suphark/ui/components/ui/select";
|
|
11
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
12
|
+
import type { Table } from "@tanstack/react-table";
|
|
13
|
+
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
|
|
14
|
+
|
|
15
|
+
interface DataTablePaginationProps<TData> {
|
|
16
|
+
table: Table<TData>;
|
|
17
|
+
pageSizeOptions?: number[];
|
|
18
|
+
onPageSizeChange?: (pageSize: number) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function DataTablePagination<TData>({
|
|
22
|
+
table,
|
|
23
|
+
pageSizeOptions = [10, 20, 30, 40, 50],
|
|
24
|
+
onPageSizeChange,
|
|
25
|
+
}: DataTablePaginationProps<TData>) {
|
|
26
|
+
const isRowSelectionEnabled = table.options.enableRowSelection !== false;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className={cn(
|
|
31
|
+
"flex flex-col items-center justify-between gap-4 px-2 sm:flex-row sm:gap-0",
|
|
32
|
+
!isRowSelectionEnabled && "sm:justify-end",
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
{isRowSelectionEnabled && (
|
|
36
|
+
<div className="flex-1 text-muted-foreground text-sm max-sm:text-center">
|
|
37
|
+
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
|
38
|
+
{table.getFilteredRowModel().rows.length} row(s) selected.
|
|
39
|
+
</div>
|
|
40
|
+
)}
|
|
41
|
+
<div className="flex flex-wrap items-center justify-center gap-4 sm:gap-6 lg:gap-8">
|
|
42
|
+
<div className="flex items-center gap-2">
|
|
43
|
+
<p className="font-medium text-sm max-sm:hidden">Rows per page</p>
|
|
44
|
+
<Select
|
|
45
|
+
value={`${table.getState().pagination.pageSize}`}
|
|
46
|
+
onValueChange={(value) => {
|
|
47
|
+
const newSize = Number(value);
|
|
48
|
+
table.setPageSize(newSize);
|
|
49
|
+
if (onPageSizeChange) {
|
|
50
|
+
onPageSizeChange(newSize);
|
|
51
|
+
}
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<SelectTrigger className="h-8 w-[70px]">
|
|
55
|
+
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
|
56
|
+
</SelectTrigger>
|
|
57
|
+
<SelectContent side="top">
|
|
58
|
+
{pageSizeOptions.map((pageSize) => (
|
|
59
|
+
<SelectItem key={pageSize} value={`${pageSize}`}>
|
|
60
|
+
{pageSize}
|
|
61
|
+
</SelectItem>
|
|
62
|
+
))}
|
|
63
|
+
</SelectContent>
|
|
64
|
+
</Select>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="flex w-[100px] items-center justify-center font-medium text-sm">
|
|
67
|
+
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
|
68
|
+
</div>
|
|
69
|
+
<div className="flex items-center gap-2">
|
|
70
|
+
<Button
|
|
71
|
+
variant="outline"
|
|
72
|
+
size="icon"
|
|
73
|
+
className="hidden size-8 lg:flex"
|
|
74
|
+
onClick={() => table.setPageIndex(0)}
|
|
75
|
+
disabled={!table.getCanPreviousPage()}
|
|
76
|
+
>
|
|
77
|
+
<span className="sr-only">Go to first page</span>
|
|
78
|
+
<ChevronsLeft />
|
|
79
|
+
</Button>
|
|
80
|
+
<Button
|
|
81
|
+
variant="outline"
|
|
82
|
+
size="icon"
|
|
83
|
+
className="size-8"
|
|
84
|
+
onClick={() => table.previousPage()}
|
|
85
|
+
disabled={!table.getCanPreviousPage()}
|
|
86
|
+
>
|
|
87
|
+
<span className="sr-only">Go to previous page</span>
|
|
88
|
+
<ChevronLeft />
|
|
89
|
+
</Button>
|
|
90
|
+
<Button
|
|
91
|
+
variant="outline"
|
|
92
|
+
size="icon"
|
|
93
|
+
className="size-8"
|
|
94
|
+
onClick={() => table.nextPage()}
|
|
95
|
+
disabled={!table.getCanNextPage()}
|
|
96
|
+
>
|
|
97
|
+
<span className="sr-only">Go to next page</span>
|
|
98
|
+
<ChevronRight />
|
|
99
|
+
</Button>
|
|
100
|
+
<Button
|
|
101
|
+
variant="outline"
|
|
102
|
+
size="icon"
|
|
103
|
+
className="hidden size-8 lg:flex"
|
|
104
|
+
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
|
105
|
+
disabled={!table.getCanNextPage()}
|
|
106
|
+
>
|
|
107
|
+
<span className="sr-only">Go to last page</span>
|
|
108
|
+
<ChevronsRight />
|
|
109
|
+
</Button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|