@openway/ui 1.0.0 → 1.0.2

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 (58) hide show
  1. package/README.md +203 -181
  2. package/dist/chunk-X4LIYOS5.js +3 -0
  3. package/dist/chunk-X4LIYOS5.js.map +1 -0
  4. package/dist/index.cjs +5 -1
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +2740 -1473
  7. package/dist/index.d.ts +2740 -1473
  8. package/dist/index.js +5 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/query.cjs +3 -0
  11. package/dist/query.cjs.map +1 -0
  12. package/dist/query.d.cts +431 -0
  13. package/dist/query.d.ts +431 -0
  14. package/dist/query.js +3 -0
  15. package/dist/query.js.map +1 -0
  16. package/dist/useInfiniteScroll-D9AW6cQV.d.cts +598 -0
  17. package/dist/useInfiniteScroll-D9AW6cQV.d.ts +598 -0
  18. package/docs/AGENTS.md +144 -0
  19. package/docs/README.md +150 -0
  20. package/docs/SKILL.md +54 -0
  21. package/docs/components/alert.md +246 -0
  22. package/docs/components/badge.md +238 -0
  23. package/docs/components/button.md +262 -0
  24. package/docs/components/carousel.md +354 -0
  25. package/docs/components/checkbox.md +252 -0
  26. package/docs/components/collapse.md +318 -0
  27. package/docs/components/confirm.md +322 -0
  28. package/docs/components/datepicker.md +259 -0
  29. package/docs/components/daterangepicker.md +260 -0
  30. package/docs/components/datetimepicker.md +226 -0
  31. package/docs/components/datetimerangepicker.md +222 -0
  32. package/docs/components/dropdown.md +275 -0
  33. package/docs/components/empty.md +200 -0
  34. package/docs/components/file-preview.md +180 -0
  35. package/docs/components/input.md +559 -0
  36. package/docs/components/modal.md +293 -0
  37. package/docs/components/popover.md +255 -0
  38. package/docs/components/radio.md +245 -0
  39. package/docs/components/select.md +254 -0
  40. package/docs/components/skeleton.md +150 -0
  41. package/docs/components/slider.md +346 -0
  42. package/docs/components/table.md +316 -0
  43. package/docs/components/tabs.md +432 -0
  44. package/docs/components/textarea.md +193 -0
  45. package/docs/components/timepicker.md +242 -0
  46. package/docs/components/timerangepicker.md +210 -0
  47. package/docs/components/toast.md +282 -0
  48. package/docs/components/toggle.md +211 -0
  49. package/docs/components/tooltip.md +213 -0
  50. package/docs/components/upload-avatar.md +318 -0
  51. package/docs/components/upload-file.md +245 -0
  52. package/docs/components/upload-image.md +126 -0
  53. package/docs/hooks/useDebounce.md +92 -0
  54. package/docs/hooks/useInfiniteScroll.md +95 -0
  55. package/docs/hooks/useMutationApp.md +242 -0
  56. package/docs/hooks/useSelectInfiniteQuery.md +123 -0
  57. package/docs/hooks/useTableQuery.md +124 -0
  58. package/package.json +31 -12
@@ -0,0 +1,242 @@
1
+ # ⚡ Hook `useMutationApp` (`@openway/ui/query`)
2
+
3
+ Hook adapter chuyên dụng bọc quanh `useMutation` của **TanStack Query v5**, được thiết kế tối ưu cho các ứng dụng sử dụng hệ sinh thái **`@openway/ui`**. Hook giúp loại bỏ boilerplate code khi thao tác tạo, sửa, xóa (CUD), tự động hóa toàn diện quy trình hiển thị Toast thông báo trạng thái và làm mới cache dữ liệu (Query Invalidation).
4
+
5
+ ---
6
+
7
+ ## 🌟 Điểm nổi bật
8
+
9
+ - **Tự động hóa Toast Thông minh**:
10
+ - Tự động hiển thị `toast.loading` khi bắt đầu thực thi mutation.
11
+ - Tự động chuyển đổi mượt mà sang `toast.success` hoặc `toast.error` khi hoàn tất mà không bị nhảy popup thừa.
12
+ - **Trích xuất Lỗi Tự động (`extractErrorMessage`)**:
13
+ - Tự động bóc tách thông điệp lỗi từ cấu trúc `error.response?.data?.message`, `error.response?.data?.error`, NestJS/Laravel validation array, HTTP status codes hoặc standard `Error.message`.
14
+ - Không cần phải thủ công `catch (err) { toast.error(err.response.data.message) }` ở từng component.
15
+ - **Tự động Invalidate Cache Query**:
16
+ - Hỗ trợ option `invalidateQueries` nhận vào một hoặc nhiều `QueryKey` (ví dụ `["teachers"]`, `["classes"]`) hoặc hàm tính toán động theo `(data, variables)`.
17
+ - Khi mutation thành công, tự động gọi `queryClient.invalidateQueries` để các bảng `<Table />` (`useTableQuery`) hoặc `<Select />` (`useSelectInfiniteQuery`) lập tức hiển thị dữ liệu mới nhất.
18
+ - **Bổ sung `isLoading` (alias `isPending`)**:
19
+ - Cung cấp `isLoading: boolean` tương thích với thói quen sử dụng của TanStack Query v4 và code giao diện thân thuộc.
20
+ - **Zero `any` & Chuẩn Generic Type**:
21
+ - Hỗ trợ đầy đủ 4 tham số generic type chuẩn của TanStack Query: `<TData, TError, TVariables, TContext>`.
22
+ - Giữ nguyên toàn bộ options và callback lifecycle (`onMutate`, `onSuccess`, `onError`, `onSettled`).
23
+
24
+ ---
25
+
26
+ ## 🚀 Import
27
+
28
+ ```tsx
29
+ import { useMutationApp, extractErrorMessage } from "@openway/ui/query";
30
+ import type {
31
+ UseMutationAppOptions,
32
+ UseMutationAppReturn,
33
+ UseMutationAppToastOptions,
34
+ InvalidateQueryTarget,
35
+ } from "@openway/ui/query";
36
+ ```
37
+
38
+ ---
39
+
40
+ ## 📖 Hướng dẫn sử dụng
41
+
42
+ ### 1. Thêm mới bản ghi (Create) với Shortcut Message
43
+
44
+ Cách đơn giản nhất để tạo một mutation có thông báo thành công và tự động refresh dữ liệu bảng:
45
+
46
+ ```tsx
47
+ import { Button, Input, Modal } from "@openway/ui";
48
+ import { useMutationApp } from "@openway/ui/query";
49
+ import { useState } from "react";
50
+
51
+ interface CreateTeacherDto {
52
+ name: string;
53
+ email: string;
54
+ subjectId: string;
55
+ }
56
+
57
+ export function CreateTeacherModal({ open, onClose }: { open: boolean; onClose: () => void }) {
58
+ const [name, setName] = useState("");
59
+ const [email, setEmail] = useState("");
60
+
61
+ const { mutate, isLoading } = useMutationApp({
62
+ mutationFn: async (dto: CreateTeacherDto) => {
63
+ const res = await fetch("/api/teachers", {
64
+ method: "POST",
65
+ headers: { "Content-Type": "application/json" },
66
+ body: JSON.stringify(dto),
67
+ });
68
+ if (!res.ok) throw await res.json();
69
+ return res.json();
70
+ },
71
+ // Hiển thị toast thành công & tự động báo lỗi nếu server trả về mã lỗi
72
+ loadingMessage: "Đang lưu thông tin giáo viên...",
73
+ successMessage: "Thêm mới giáo viên thành công!",
74
+ // Tự động làm mới cache của bảng danh sách giáo viên
75
+ invalidateQueries: [["teachers"]],
76
+ onSuccess: () => {
77
+ onClose();
78
+ setName("");
79
+ setEmail("");
80
+ },
81
+ });
82
+
83
+ const handleSubmit = (e: React.FormEvent) => {
84
+ e.preventDefault();
85
+ mutate({ name, email, subjectId: "math" });
86
+ };
87
+
88
+ return (
89
+ <Modal open={open} onClose={onClose} title="Thêm mới Giáo viên">
90
+ <form onSubmit={handleSubmit} className="space-y-4">
91
+ <Input label="Họ và tên" value={name} onChange={(e) => setName(e.target.value)} required />
92
+ <Input label="Email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
93
+ <div className="flex justify-end gap-2 pt-4">
94
+ <Button variant="outline" onClick={onClose} disabled={isLoading}>
95
+ Hủy
96
+ </Button>
97
+ <Button type="submit" loading={isLoading}>
98
+ Lưu giáo viên
99
+ </Button>
100
+ </div>
101
+ </form>
102
+ </Modal>
103
+ );
104
+ }
105
+ ```
106
+
107
+ ---
108
+
109
+ ### 2. Cập nhật bản ghi với Toast động (`(data, variables)`)
110
+
111
+ Có thể truyền function để tạo thông điệp Toast chứa tên hoặc thông tin động từ dữ liệu:
112
+
113
+ ```tsx
114
+ import { useMutationApp } from "@openway/ui/query";
115
+
116
+ interface UpdateUserDto {
117
+ id: string;
118
+ name: string;
119
+ }
120
+
121
+ export function useUpdateUser() {
122
+ return useMutationApp({
123
+ mutationFn: async ({ id, name }: UpdateUserDto) => {
124
+ const res = await fetch(`/api/users/${id}`, {
125
+ method: "PUT",
126
+ body: JSON.stringify({ name }),
127
+ });
128
+ return res.json();
129
+ },
130
+ toast: {
131
+ loading: (vars) => `Đang cập nhật thông tin người dùng #${vars.id}...`,
132
+ success: (data, vars) => `Cập nhật người dùng "${vars.name}" thành công!`,
133
+ error: (err) => `Không thể cập nhật: ${extractErrorMessage(err)}`,
134
+ },
135
+ // Làm mới cả danh sách chung và chi tiết user
136
+ invalidateQueries: (data, vars) => [
137
+ ["users"],
138
+ ["user-detail", vars.id],
139
+ ],
140
+ });
141
+ }
142
+ ```
143
+
144
+ ---
145
+
146
+ ### 3. Xóa dữ liệu (Delete) & Invalidate nhiều Query
147
+
148
+ ```tsx
149
+ import { Button } from "@openway/ui";
150
+ import { useMutationApp } from "@openway/ui/query";
151
+
152
+ export function DeleteClassButton({ classId, className }: { classId: string; className: string }) {
153
+ const { mutate, isLoading } = useMutationApp({
154
+ mutationFn: async (id: string) => {
155
+ await fetch(`/api/classes/${id}`, { method: "DELETE" });
156
+ },
157
+ successMessage: `Đã xóa lớp ${className} khỏi hệ thống!`,
158
+ // Invalidate cả bảng lớp học và số liệu thống kê ở dashboard
159
+ invalidateQueries: [
160
+ ["classes"],
161
+ ["dashboard-stats"],
162
+ ],
163
+ });
164
+
165
+ return (
166
+ <Button
167
+ variant="soft"
168
+ color="error"
169
+ loading={isLoading}
170
+ onClick={() => {
171
+ if (confirm(`Bạn có chắc chắn muốn xóa lớp ${className}?`)) {
172
+ mutate(classId);
173
+ }
174
+ }}
175
+ >
176
+ Xóa lớp
177
+ </Button>
178
+ );
179
+ }
180
+ ```
181
+
182
+ ---
183
+
184
+ ### 4. Tắt Toast hoặc Tùy biến Giao diện Toast
185
+
186
+ ```tsx
187
+ // Tắt hoàn toàn toast (nếu muốn tự xử lý UI riêng)
188
+ const mutation1 = useMutationApp({
189
+ mutationFn: trackUserActivity,
190
+ toast: false,
191
+ });
192
+
193
+ // Tùy biến vị trí và kiểu hiển thị của Toast
194
+ const mutation2 = useMutationApp({
195
+ mutationFn: updateSettings,
196
+ toast: {
197
+ variant: "solid",
198
+ success: "Đã lưu cài đặt!",
199
+ options: {
200
+ position: "bottom-center",
201
+ duration: 3000,
202
+ },
203
+ },
204
+ });
205
+ ```
206
+
207
+ ---
208
+
209
+ ## 🎛️ Bảng Options (`UseMutationAppOptions`)
210
+
211
+ Kế thừa toàn bộ options chuẩn của `UseMutationOptions` từ TanStack Query v5, bổ sung thêm:
212
+
213
+ | Tên Option | Kiểu dữ liệu | Mặc định | Mô tả |
214
+ | :--- | :--- | :--- | :--- |
215
+ | `mutationFn` | `(variables: TVariables) => Promise<TData>` | `undefined` | Hàm bất đồng bộ gọi API thực thi tác vụ mutation. |
216
+ | `invalidateQueries` | `QueryKey \| QueryKey[] \| InvalidateQueryFilters \| InvalidateQueryFilters[] \| ((data, vars) => ...)` | `undefined` | Khóa truy vấn hoặc danh sách khóa truy vấn cần tự động làm mới khi mutation thành công. |
217
+ | `invalidateOptions` | `InvalidateOptions` | `undefined` | Tùy chọn nâng cao khi invalidate (ví dụ: `throwOnError`, `cancelRefetch`). |
218
+ | `toast` | `boolean \| UseMutationAppToastOptions` | `true` | Cấu hình Toast thông báo. Truyền `false` để tắt toàn bộ toast. |
219
+ | `loadingMessage` | `ReactNode \| ((vars) => ReactNode)` | `undefined` | Shortcut đặt thông báo loading khi đang chạy. |
220
+ | `successMessage` | `ReactNode \| ((data, vars) => ReactNode)` | `undefined` | Shortcut đặt thông báo khi thành công. |
221
+ | `errorMessage` | `ReactNode \| ((err, vars) => ReactNode)` | `undefined` | Shortcut đặt thông báo lỗi tùy biến (mặc định tự bóc tách lỗi qua `extractErrorMessage`). |
222
+ | `onSuccess` | `(data, variables, context) => Promise<unknown> \| unknown` | `undefined` | Callback chạy sau khi mutation thành công và sau khi đã refresh cache. |
223
+ | `onError` | `(error, variables, context) => Promise<unknown> \| unknown` | `undefined` | Callback chạy khi mutation gặp lỗi. |
224
+ | `onSettled` | `(data, error, variables, context) => Promise<unknown> \| unknown` | `undefined` | Callback chạy khi mutation kết thúc (dù thành công hay thất bại). |
225
+
226
+ ---
227
+
228
+ ## 📦 Bảng Return (`UseMutationAppReturn`)
229
+
230
+ Kế thừa toàn bộ kết quả trả về của `UseMutationResult` từ TanStack Query v5:
231
+
232
+ | Thuộc tính | Kiểu dữ liệu | Mô tả |
233
+ | :--- | :--- | :--- |
234
+ | `mutate` | `(variables: TVariables, options?) => void` | Kích hoạt mutation theo cơ chế fire-and-forget. |
235
+ | `mutateAsync` | `(variables: TVariables, options?) => Promise<TData>` | Kích hoạt mutation và trả về Promise để có thể `await`. |
236
+ | `isLoading` | `boolean` | **Alias tiện ích của `isPending`**, là `true` khi mutation đang chạy. |
237
+ | `isPending` | `boolean` | Trạng thái đang chạy của TanStack Query v5. |
238
+ | `isSuccess` | `boolean` | Là `true` khi mutation đã hoàn tất thành công. |
239
+ | `isError` | `boolean` | Là `true` khi mutation thất bại. |
240
+ | `data` | `TData \| undefined` | Dữ liệu trả về từ `mutationFn` khi thành công. |
241
+ | `error` | `TError \| null` | Đối tượng lỗi trả về từ `mutationFn` khi thất bại. |
242
+ | `reset` | `() => void` | Đặt lại trạng thái mutation về ban đầu (`idle`). |
@@ -0,0 +1,123 @@
1
+ # 🔽 Hook `useSelectInfiniteQuery` (`@openway/ui/query`)
2
+
3
+ Hook adapter chuyên dụng kết hợp **TanStack Query v5** (`useInfiniteQuery`) với **`useInfiniteScroll`** và **`Skeleton`**, dành riêng cho các thành phần `<Select />` và `<MultiSelect />` ở chế độ máy chủ (`searchMode="server"`).
4
+
5
+ ---
6
+
7
+ ## 🌟 Điểm nổi bật
8
+
9
+ - **Tự động hóa hoàn toàn**: Trả về trọn gói `selectProps` sẵn sàng spread thẳng vào `<Select {...selectProps} />` hoặc `<MultiSelect {...selectProps} />`.
10
+ - **Phân trang Vô tận (Infinite Scroll)**: Tự động quan sát khi người dùng cuộn đến đáy danh sách qua `useInfiniteScroll` (native `IntersectionObserver`), kích hoạt `fetchNextPage()`.
11
+ - **Hiệu ứng Skeleton Loading**: Tự động hiển thị 2 dòng `Skeleton` nhấp nháy mô phỏng option item tại `listFooter` khi đang tải thêm trang (`isFetchingNextPage`).
12
+ - **Gộp & Khử Trùng lặp Options**: Tự động gom tụ các trang dữ liệu (`pages`) thành danh sách phẳng và khử trùng lặp theo `value`.
13
+ - **Chống Double Debounce**: `<Select />` tự chịu trách nhiệm debounce từ khóa người dùng gõ theo `debounceMs` rồi mới gọi `onSearch`, hook nhận sự kiện và kích hoạt query ngay lập tức, không tạo độ trễ thừa thãi.
14
+ - **Zero `any`**: Type-safe 100% với Generic type `TData`, `TResponse`, `TPageParam`.
15
+
16
+ ---
17
+
18
+ ## 🚀 Import
19
+
20
+ ```tsx
21
+ import { useSelectInfiniteQuery } from "@openway/ui/query";
22
+ import type {
23
+ SelectQueryParams,
24
+ UseSelectInfiniteQueryOptions,
25
+ UseSelectInfiniteQueryReturn,
26
+ } from "@openway/ui/query";
27
+ ```
28
+
29
+ ---
30
+
31
+ ## 📖 Hướng dẫn sử dụng
32
+
33
+ ### 1. Phân trang Server cơ bản với Cursor / NextPage
34
+
35
+ ```tsx
36
+ import { Select } from "@openway/ui";
37
+ import { useSelectInfiniteQuery } from "@openway/ui/query";
38
+
39
+ interface Product {
40
+ id: number;
41
+ title: string;
42
+ price: number;
43
+ }
44
+
45
+ interface ProductPageResponse {
46
+ items: Product[];
47
+ nextPage?: number;
48
+ }
49
+
50
+ export function InfiniteProductSelect() {
51
+ const { selectProps, query } = useSelectInfiniteQuery<
52
+ Product,
53
+ ProductPageResponse,
54
+ number
55
+ >({
56
+ queryKey: ["products-infinite"],
57
+ queryFn: async ({ pageParam, search }) => {
58
+ const res = await fetch(
59
+ `/api/products?page=${pageParam}&search=${encodeURIComponent(search)}`
60
+ );
61
+ return res.json();
62
+ },
63
+ initialPageParam: 1,
64
+ getNextPageParam: (lastPage) => lastPage.nextPage,
65
+ mapOption: (item) => {
66
+ const product = item as Product;
67
+ return {
68
+ value: product.id,
69
+ label: product.title,
70
+ description: `${product.price.toLocaleString()} đ`,
71
+ data: product,
72
+ };
73
+ },
74
+ debounceMs: 300,
75
+ endMessage: "Đã hiển thị toàn bộ sản phẩm",
76
+ });
77
+
78
+ return (
79
+ <div className="max-w-md">
80
+ <Select
81
+ {...selectProps}
82
+ label="Chọn sản phẩm"
83
+ placeholder="Tìm kiếm theo tên sản phẩm..."
84
+ searchable
85
+ clearable
86
+ />
87
+ </div>
88
+ );
89
+ }
90
+ ```
91
+
92
+ ---
93
+
94
+ ## 🎛️ Bảng Options (`UseSelectInfiniteQueryOptions`)
95
+
96
+ | Tên Option | Kiểu dữ liệu | Mặc định | Mô tả |
97
+ | :--- | :--- | :--- | :--- |
98
+ | `queryKey` | `readonly unknown[]` | **Bắt buộc** | Query key gốc. `[{ search, filters }]` tự động được thêm vào cuối queryKey. |
99
+ | `queryFn` | `(params, context) => Promise<TResponse>` | **Bắt buộc** | Hàm fetch API theo từng trang, nhận `pageParam`, `search`, `filters`. |
100
+ | `initialPageParam` | `TPageParam` | `1` | Giá trị tham số trang ban đầu (page hoặc cursor). |
101
+ | `getNextPageParam` | `(lastPage, allPages, ...) => TPageParam` | Tự động đoán (`nextPage`, `nextCursor`...) | Xác định tham số trang kế tiếp. Trả về `undefined` khi hết dữ liệu. |
102
+ | `selectOptions` | `(response) => SelectOptionItem<TData>[]` | Tự động bóc tách | Hàm trích xuất mảng option từ response mỗi trang. |
103
+ | `mapOption` | `(item, index) => SelectOptionItem<TData>` | `undefined` | Hàm biến đổi phần tử thô thành `SelectOptionItem`. |
104
+ | `debounceMs` | `number` | `300` | Thời gian hoãn tìm kiếm truyền xuống cho Select. |
105
+ | `skeletonLines` | `number` | `2` | Số dòng Skeleton hiển thị ở đáy danh sách khi tải thêm trang. |
106
+ | `endMessage` | `ReactNode` | `undefined` | Văn bản hiển thị dưới đáy danh sách khi đã tải hết tất cả trang. |
107
+
108
+ ---
109
+
110
+ ## 📦 Giá trị trả về (`UseSelectInfiniteQueryReturn`)
111
+
112
+ - `selectProps`: Gói props truyền thẳng vào `<Select />` hoặc `<MultiSelect />`:
113
+ - `options`: Mảng options gom tụ từ tất cả các trang đã tải.
114
+ - `isLoading`: Trạng thái đang tải trang đầu tiên.
115
+ - `searchMode: "server"`
116
+ - `debounceMs`: Thời gian debounce.
117
+ - `onSearch`: Handler nhận từ khóa và filters từ Select.
118
+ - `listFooter`: Sentinel kèm Skeleton loading khi đang cuộn tải trang tiếp.
119
+ - `query`: Đối tượng `UseInfiniteQueryResult` từ TanStack Query.
120
+ - `options`: Danh sách options đầy đủ.
121
+ - `search`, `setSearch`: Từ khóa tìm kiếm hiện tại.
122
+ - `filters`, `setFilters`: Bộ lọc menu hiện tại.
123
+ - `reset`: Đặt lại toàn bộ tìm kiếm và bộ lọc về giá trị ban đầu.
@@ -0,0 +1,124 @@
1
+ # 📊 Hook `useTableQuery` (`@openway/ui/query`)
2
+
3
+ Hook adapter chuyên dụng kết nối **TanStack Query v5** (`useQuery`) với component `<Table />`, tự động hóa toàn bộ quy trình phân trang máy chủ (Server-side pagination), sắp xếp nhiều cột (Sorting), lọc dữ liệu (Filtering), và tối ưu UX qua `keepPreviousData`.
4
+
5
+ ---
6
+
7
+ ## 🌟 Điểm nổi bật
8
+
9
+ - **Tự động đồng bộ với Table**: Trả về trọn gói `tableProps` sẵn sàng spread thẳng vào `<Table {...tableProps} />`.
10
+ - **Server Pagination**: Quản lý `page` (1-indexed), `pageSize`, tự động chuyển trang và tính toán tổng số trang (`pageCount`).
11
+ - **Server Sorting**: Đồng bộ `sortBy` và `sortOrder` ("asc" | "desc") từ trạng thái click header của Table gửi lên server.
12
+ - **Server Filtering & Auto-Reset**: Tự động chuyển đổi mảng filter của TanStack Table thành dictionary phẳng `Record<string, unknown>`, tự động đưa trang về trang 1 khi người dùng thay đổi bộ lọc (`autoResetPageIndex`).
13
+ - **Giữ dữ liệu mượt mà (`keepPreviousData`)**: Dữ liệu trang cũ vẫn hiển thị trong lúc trang mới đang tải ngầm, loại bỏ hiện tượng nhấp nháy trắng bảng.
14
+ - **Zero `any`**: Type-safe 100% với Generic type `TData` và `TResponse`.
15
+
16
+ ---
17
+
18
+ ## 🚀 Import
19
+
20
+ ```tsx
21
+ import { useTableQuery } from "@openway/ui/query";
22
+ import type {
23
+ TableQueryParams,
24
+ UseTableQueryOptions,
25
+ UseTableQueryReturn,
26
+ } from "@openway/ui/query";
27
+ ```
28
+
29
+ ---
30
+
31
+ ## 📖 Hướng dẫn sử dụng
32
+
33
+ ### Phân trang, Sắp xếp & Lọc phía Máy chủ
34
+
35
+ ```tsx
36
+ import { Table, type ColumnDef } from "@openway/ui";
37
+ import { useTableQuery } from "@openway/ui/query";
38
+
39
+ interface User {
40
+ id: string;
41
+ name: string;
42
+ email: string;
43
+ role: string;
44
+ }
45
+
46
+ interface UserApiResponse {
47
+ data: User[];
48
+ total: number;
49
+ }
50
+
51
+ const columns: ColumnDef<User>[] = [
52
+ { accessorKey: "name", header: "Họ và tên", enableSorting: true },
53
+ { accessorKey: "email", header: "Email" },
54
+ { accessorKey: "role", header: "Vai trò" },
55
+ ];
56
+
57
+ export function UserManagementTable() {
58
+ const { tableProps, query, page, setPage } = useTableQuery<User, UserApiResponse>({
59
+ queryKey: ["users-table"],
60
+ queryFn: async (params) => {
61
+ const searchParams = new URLSearchParams({
62
+ page: String(params.page),
63
+ pageSize: String(params.pageSize),
64
+ ...(params.sortBy && { sortBy: params.sortBy, sortOrder: params.sortOrder ?? "asc" }),
65
+ });
66
+ const res = await fetch(`/api/users?${searchParams}`);
67
+ return res.json();
68
+ },
69
+ // Tự động bóc tách mảng dữ liệu và tổng số dòng từ response
70
+ selectData: (res) => res.data,
71
+ selectTotal: (res) => res.total,
72
+ initialPageSize: 10,
73
+ });
74
+
75
+ return (
76
+ <div className="space-y-4">
77
+ <Table
78
+ {...tableProps}
79
+ columns={columns}
80
+ border
81
+ striped
82
+ hoverable
83
+ />
84
+ </div>
85
+ );
86
+ }
87
+ ```
88
+
89
+ ---
90
+
91
+ ## 🎛️ Bảng Options (`UseTableQueryOptions`)
92
+
93
+ | Tên Option | Kiểu dữ liệu | Mặc định | Mô tả |
94
+ | :--- | :--- | :--- | :--- |
95
+ | `queryKey` | `readonly unknown[]` | **Bắt buộc** | Query key gốc của TanStack Query. |
96
+ | `queryFn` | `(params, context) => Promise<TResponse>` | **Bắt buộc** | Hàm fetch dữ liệu từ API nhận `TableQueryParams`. |
97
+ | `selectData` | `(res) => TData[]` | Tự trích xuất `data/items/results/rows` | Hàm lấy mảng dữ liệu từ API response. |
98
+ | `selectTotal` | `(res) => number` | Tự trích xuất `total/totalCount/count` | Hàm lấy tổng số bản ghi từ API response. |
99
+ | `initialPage` | `number` | `1` | Trang bắt đầu (1-indexed). |
100
+ | `initialPageSize` | `number` | `10` | Số dòng hiển thị mỗi trang. |
101
+ | `autoResetPageIndex` | `boolean` | `true` | Tự động quay về trang 1 khi đổi bộ lọc hoặc sắp xếp. |
102
+ | `queryOptions` | `Omit<UseQueryOptions, ...>` | `undefined` | Các cấu hình nâng cao của TanStack Query (`staleTime`, `refetchInterval`...). |
103
+
104
+ ---
105
+
106
+ ## 📦 Giá trị trả về (`UseTableQueryReturn`)
107
+
108
+ - `tableProps`: Gói props truyền thẳng vào `<Table />`:
109
+ - `data: TData[]`
110
+ - `pageCount: number`
111
+ - `pagination: { pageIndex, pageSize }`
112
+ - `onPaginationChange: OnChangeFn<PaginationState>`
113
+ - `sorting: SortingState`
114
+ - `onSortingChange: OnChangeFn<SortingState>`
115
+ - `columnFilters: ColumnFiltersState`
116
+ - `onColumnFiltersChange: OnChangeFn<ColumnFiltersState>`
117
+ - `isLoading: boolean`
118
+ - `manualPagination: true`
119
+ - `manualSorting: true`
120
+ - `manualFiltering: true`
121
+ - `query`: Query result từ `useQuery`.
122
+ - `page`, `setPage`: Xem và thay đổi số trang hiện tại.
123
+ - `pageSize`, `setPageSize`: Xem và thay đổi số dòng/trang.
124
+ - `resetFilters`: Đặt lại toàn bộ bộ lọc.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openway/ui",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -15,49 +15,68 @@
15
15
  "types": "./dist/index.d.ts",
16
16
  "import": "./dist/index.js",
17
17
  "require": "./dist/index.cjs"
18
+ },
19
+ "./query": {
20
+ "types": "./dist/query.d.ts",
21
+ "import": "./dist/query.js",
22
+ "require": "./dist/query.cjs"
18
23
  }
19
24
  },
20
25
  "files": [
21
26
  "dist",
27
+ "docs",
22
28
  "README.md",
23
29
  "LICENSE"
24
30
  ],
25
- "scripts": {
26
- "build": "tsup",
27
- "prepublishOnly": "pnpm build",
28
- "lint": "eslint",
29
- "check-types": "tsc --noEmit",
30
- "doctor": "pnpm dlx react-doctor@latest",
31
- "cypress:open": "cypress open --component",
32
- "cypress:run": "cypress run --component"
33
- },
34
31
  "peerDependencies": {
32
+ "@tanstack/react-query": ">=5.0.0",
35
33
  "next": ">=16.0.0",
36
34
  "react": ">=19.0.0",
37
35
  "react-dom": ">=19.0.0",
38
36
  "tailwindcss": ">=4.0.0"
39
37
  },
38
+ "peerDependenciesMeta": {
39
+ "@tanstack/react-query": {
40
+ "optional": true
41
+ }
42
+ },
40
43
  "dependencies": {
44
+ "@dnd-kit/core": "^6.3.1",
45
+ "@dnd-kit/modifiers": "^9.0.0",
46
+ "@dnd-kit/sortable": "^10.0.0",
47
+ "@dnd-kit/utilities": "^3.2.2",
41
48
  "@floating-ui/react": "^0.27.20",
42
49
  "@radix-ui/react-slider": "^1.4.7",
43
50
  "@tanstack/match-sorter-utils": "^9.1.2",
51
+ "@tanstack/react-table": "^9.2.4",
52
+ "react-dropzone": "^20.1.1",
44
53
  "react-easy-crop": "^6.2.3",
45
54
  "react-textarea-autosize": "^8.5.9",
46
55
  "sonner": "^2.0.8"
47
56
  },
48
57
  "devDependencies": {
49
- "eslint-config-next": "16.3.4",
50
58
  "@tailwindcss/postcss": "^4.3.3",
59
+ "@tanstack/react-query": "^5.102.8",
51
60
  "@types/node": "^22.15.3",
52
61
  "@types/react": "19.2.2",
53
62
  "@types/react-dom": "19.2.2",
54
63
  "cypress": "^15.21.0",
55
64
  "eslint": "^9.39.1",
65
+ "eslint-config-next": "16.3.4",
66
+ "eslint-config-prettier": "^10.1.8",
56
67
  "next": "16.3.0",
57
68
  "react": "^19.2.0",
58
69
  "react-dom": "^19.2.0",
59
70
  "tailwindcss": "^4.3.3",
60
71
  "tsup": "^8.5.1",
61
72
  "typescript": "^5.9.2"
73
+ },
74
+ "scripts": {
75
+ "build": "tsup",
76
+ "lint": "eslint",
77
+ "check-types": "tsc --noEmit",
78
+ "doctor": "pnpm dlx react-doctor@latest",
79
+ "cypress:open": "cypress open --component",
80
+ "cypress:run": "cypress run --component"
62
81
  }
63
- }
82
+ }