@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.
- package/README.md +203 -181
- package/dist/chunk-X4LIYOS5.js +3 -0
- package/dist/chunk-X4LIYOS5.js.map +1 -0
- package/dist/index.cjs +5 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2740 -1473
- package/dist/index.d.ts +2740 -1473
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/query.cjs +3 -0
- package/dist/query.cjs.map +1 -0
- package/dist/query.d.cts +431 -0
- package/dist/query.d.ts +431 -0
- package/dist/query.js +3 -0
- package/dist/query.js.map +1 -0
- package/dist/useInfiniteScroll-D9AW6cQV.d.cts +598 -0
- package/dist/useInfiniteScroll-D9AW6cQV.d.ts +598 -0
- package/docs/AGENTS.md +144 -0
- package/docs/README.md +150 -0
- package/docs/SKILL.md +54 -0
- package/docs/components/alert.md +246 -0
- package/docs/components/badge.md +238 -0
- package/docs/components/button.md +262 -0
- package/docs/components/carousel.md +354 -0
- package/docs/components/checkbox.md +252 -0
- package/docs/components/collapse.md +318 -0
- package/docs/components/confirm.md +322 -0
- package/docs/components/datepicker.md +259 -0
- package/docs/components/daterangepicker.md +260 -0
- package/docs/components/datetimepicker.md +226 -0
- package/docs/components/datetimerangepicker.md +222 -0
- package/docs/components/dropdown.md +275 -0
- package/docs/components/empty.md +200 -0
- package/docs/components/file-preview.md +180 -0
- package/docs/components/input.md +559 -0
- package/docs/components/modal.md +293 -0
- package/docs/components/popover.md +255 -0
- package/docs/components/radio.md +245 -0
- package/docs/components/select.md +254 -0
- package/docs/components/skeleton.md +150 -0
- package/docs/components/slider.md +346 -0
- package/docs/components/table.md +316 -0
- package/docs/components/tabs.md +432 -0
- package/docs/components/textarea.md +193 -0
- package/docs/components/timepicker.md +242 -0
- package/docs/components/timerangepicker.md +210 -0
- package/docs/components/toast.md +282 -0
- package/docs/components/toggle.md +211 -0
- package/docs/components/tooltip.md +213 -0
- package/docs/components/upload-avatar.md +318 -0
- package/docs/components/upload-file.md +245 -0
- package/docs/components/upload-image.md +126 -0
- package/docs/hooks/useDebounce.md +92 -0
- package/docs/hooks/useInfiniteScroll.md +95 -0
- package/docs/hooks/useMutationApp.md +242 -0
- package/docs/hooks/useSelectInfiniteQuery.md +123 -0
- package/docs/hooks/useTableQuery.md +124 -0
- 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.
|
|
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
|
+
}
|