@openway/ui 1.0.2 → 1.0.3

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.
@@ -1,242 +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`). |
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 tiêu đề lỗi tùy biến (chi tiết lỗi bên dưới vẫn tự động bóc tách từ API 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`). |
@@ -99,6 +99,7 @@ export function UserManagementTable() {
99
99
  | `initialPage` | `number` | `1` | Trang bắt đầu (1-indexed). |
100
100
  | `initialPageSize` | `number` | `10` | Số dòng hiển thị mỗi trang. |
101
101
  | `autoResetPageIndex` | `boolean` | `true` | Tự động quay về trang 1 khi đổi bộ lọc hoặc sắp xếp. |
102
+ | `debounceMs` | `number` | `300` | Thời gian trì hoãn debounce (ms) khi thay đổi bộ lọc ở chế độ server trước khi gọi API. Đặt 0 để tắt. |
102
103
  | `queryOptions` | `Omit<UseQueryOptions, ...>` | `undefined` | Các cấu hình nâng cao của TanStack Query (`staleTime`, `refetchInterval`...). |
103
104
 
104
105
  ---
@@ -114,11 +115,15 @@ export function UserManagementTable() {
114
115
  - `onSortingChange: OnChangeFn<SortingState>`
115
116
  - `columnFilters: ColumnFiltersState`
116
117
  - `onColumnFiltersChange: OnChangeFn<ColumnFiltersState>`
118
+ - `globalFilter: string`
119
+ - `onGlobalFilterChange: (filter: string) => void`
120
+ - `debounceMs: number`
117
121
  - `isLoading: boolean`
118
122
  - `manualPagination: true`
119
123
  - `manualSorting: true`
120
124
  - `manualFiltering: true`
121
125
  - `query`: Query result từ `useQuery`.
126
+ - `queryParams`: Tham số truy vấn chuẩn hóa gửi lên API máy chủ (`page`, `pageSize`, `sortBy`, `sortOrder`, `filters`).
122
127
  - `page`, `setPage`: Xem và thay đổi số trang hiện tại.
123
128
  - `pageSize`, `setPageSize`: Xem và thay đổi số dòng/trang.
124
129
  - `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.2",
3
+ "version": "1.0.3",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -20,7 +20,8 @@
20
20
  "types": "./dist/query.d.ts",
21
21
  "import": "./dist/query.js",
22
22
  "require": "./dist/query.cjs"
23
- }
23
+ },
24
+ "./styles.css": "./dist/styles.css"
24
25
  },
25
26
  "files": [
26
27
  "dist",
@@ -1,3 +0,0 @@
1
- "use client";
2
- import{rankItem as me,compareItems as xe}from"@tanstack/match-sorter-utils";function p(e,r,o){return e!=null&&Object.prototype.hasOwnProperty.call(r,e)?r[e]:r[o]}function ze(e,r,o){let t=r.trim();if(!t||!e.length)return e;let c=o?Array.isArray(o)?o:[o]:[],l=[];if(c.length>0)for(let n of c)typeof n=="function"?l.push(i=>{let a=n(i);return a!=null?String(a):""}):l.push(i=>{let a=i,u=a[n];if(u!=null)return String(u);if(a.data&&typeof a.data=="object"){let d=a.data[n];if(d!=null)return String(d)}return""});else l.push(n=>{if(n&&typeof n=="object"){let i=n;return i.label!=null?String(i.label):i.value!=null?String(i.value):""}return n!=null?String(n):""});let s=[];for(let n of e){let i=me(n,t,{accessors:l});i.passed&&s.push({item:n,info:i})}return s.sort((n,i)=>xe(n.info,i.info)),s.map(n=>n.item)}import{jsx as F,jsxs as ye}from"react/jsx-runtime";function P({width:e=16,height:r=16,className:o="",...t}){return ye("svg",{width:e,height:r,className:`animate-[spin_1s_steps(8)_infinite] ${o}`,viewBox:"0 0 25 24",fill:"none",xmlns:"http://www.w3.org/2000/svg",...t,children:[F("path",{d:"M0 0h25v24H0z",fill:"none"}),F("path",{fill:"currentColor",d:"M4.818 6.664h-.001a1.847 1.847 0 1 1 1.306-.541a1.77 1.77 0 0 1-1.277.541h-.029zm-2.97 7.182h-.001a1.847 1.847 0 1 1 1.306-.541a1.77 1.77 0 0 1-1.278.541h-.031h.002zM12 3.692a1.847 1.847 0 1 1 1.306-.541a1.77 1.77 0 0 1-1.277.541zM4.818 21.029h-.001a1.847 1.847 0 1 1 1.306-.541a1.77 1.77 0 0 1-1.276.541h-.031zM19.182 7.125a2.308 2.308 0 1 1 0-4.615a2.308 2.308 0 0 1 0 4.615M12 24a1.847 1.847 0 1 1 1.306-.541a1.77 1.77 0 0 1-1.277.541zm10.154-9.231h-.048c-.75 0-1.428-.309-1.913-.807l-.001-.001c-.499-.503-.808-1.196-.808-1.961s.308-1.458.808-1.962a2.66 2.66 0 0 1 1.914-.808h.05h-.003h.048c.75 0 1.427.309 1.913.807l.001.001c.499.503.808 1.196.808 1.961s-.308 1.458-.808 1.962a2.66 2.66 0 0 1-1.915.809h-.049zm-2.971 7.643h-.05a3.1 3.1 0 0 1-2.236-.951l-.001-.001c-.584-.584-.945-1.391-.945-2.283s.361-1.698.945-2.283a3.1 3.1 0 0 1 2.234-.945h.054h-.003h.042c.877 0 1.67.362 2.237.944l.001.001c.588.582.952 1.39.952 2.283s-.364 1.7-.952 2.282a3.1 3.1 0 0 1-2.24.953h-.04z"})]})}import{jsx as U}from"react/jsx-runtime";function z({width:e=14,height:r=14,className:o="",...t}){return U("svg",{width:e,height:r,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round",className:o,...t,children:U("path",{d:"M18 6 6 18M6 6l12 12"})})}import{jsx as V,jsxs as he}from"react/jsx-runtime";function R({width:e=20,height:r=20,className:o="",...t}){return he("svg",{width:e,height:r,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",className:o,...t,children:[V("circle",{cx:"12",cy:"12",r:"10"}),V("path",{d:"M12 16v-4"}),V("path",{d:"M12 8h.01"})]})}import{jsx as K,jsxs as we}from"react/jsx-runtime";function E({width:e=20,height:r=20,className:o="",...t}){return we("svg",{width:e,height:r,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",className:o,...t,children:[K("circle",{cx:"12",cy:"12",r:"10"}),K("path",{d:"m9 12 2 2 4-4"})]})}import{jsx as j,jsxs as Te}from"react/jsx-runtime";function G({width:e=20,height:r=20,className:o="",...t}){return Te("svg",{width:e,height:r,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",className:o,...t,children:[j("path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"}),j("path",{d:"M12 9v4"}),j("path",{d:"M12 17h.01"})]})}import{jsx as L,jsxs as ve}from"react/jsx-runtime";function M({width:e=20,height:r=20,className:o="",...t}){return ve("svg",{width:e,height:r,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",className:o,...t,children:[L("circle",{cx:"12",cy:"12",r:"10"}),L("path",{d:"M12 8v4"}),L("path",{d:"M12 16h.01"})]})}var H={xs:{container:"p-2 gap-2 text-xs",title:"text-xs font-semibold leading-tight",description:"text-[11px] leading-tight",icon:"size-3.5 mt-0.5",closeButton:"size-4 p-0.5 -mr-0.5",closeIcon:"size-3",action:"gap-1.5 text-xs"},sm:{container:"p-2.5 gap-2.5 text-xs",title:"text-sm font-semibold leading-tight",description:"text-xs leading-normal",icon:"size-4 mt-0.5",closeButton:"size-5 p-0.5 -mr-0.5",closeIcon:"size-3.5",action:"gap-1.5 text-xs"},md:{container:"p-3.5 gap-3 text-sm",title:"text-sm font-semibold leading-tight",description:"text-sm leading-normal",icon:"size-5 mt-0.5",closeButton:"size-6 p-1 -mr-1",closeIcon:"size-4",action:"gap-2 text-sm"},lg:{container:"p-4 gap-3.5 text-base",title:"text-base font-semibold leading-snug",description:"text-sm leading-normal",icon:"size-6 mt-0.5",closeButton:"size-7 p-1.5 -mr-1.5",closeIcon:"size-4.5",action:"gap-2.5 text-sm"},xl:{container:"p-5 gap-4 text-lg",title:"text-lg font-semibold leading-snug",description:"text-base leading-normal",icon:"size-7 mt-0.5",closeButton:"size-8 p-1.5 -mr-1.5",closeIcon:"size-5",action:"gap-3 text-base"}},W={none:"rounded-none",sm:"rounded-sm",md:"rounded-md",lg:"rounded-lg",xl:"rounded-xl",full:"rounded-2xl"},Z={soft:{primary:"bg-primary-50 text-primary-900 border-2 border-primary-200",secondary:"bg-secondary-50 text-secondary-900 border-2 border-secondary-200",neutral:"bg-neutral-100 text-neutral-900 border-2 border-neutral-200",error:"bg-error-50 text-error-900 border-2 border-error-200",success:"bg-success-50 text-success-900 border-2 border-success-200",warning:"bg-warning-50 text-warning-900 border-2 border-warning-200",info:"bg-info-50 text-info-900 border-2 border-info-200"},filled:{primary:"bg-primary-600 text-white border-2 border-primary-600",secondary:"bg-secondary-600 text-white border-2 border-secondary-600",neutral:"bg-neutral-700 text-white border-2 border-neutral-700",error:"bg-error-600 text-white border-2 border-error-600",success:"bg-success-600 text-white border-2 border-success-600",warning:"bg-warning-500 text-neutral-950 border-2 border-warning-500",info:"bg-info-600 text-white border-2 border-info-600"},outline:{primary:"bg-neutral-white text-primary-700 border-2 border-primary-400",secondary:"bg-neutral-white text-secondary-700 border-2 border-secondary-400",neutral:"bg-neutral-white text-neutral-700 border-2 border-neutral-300",error:"bg-neutral-white text-error-700 border-2 border-error-500",success:"bg-neutral-white text-success-700 border-2 border-success-400",warning:"bg-neutral-white text-warning-800 border-2 border-warning-400",info:"bg-neutral-white text-info-700 border-2 border-info-400"},"accent-left":{primary:"bg-primary-50/70 text-primary-900 border-2 border-primary-200 border-l-4 border-l-primary-500",secondary:"bg-secondary-50/70 text-secondary-900 border-2 border-secondary-200 border-l-4 border-l-secondary-500",neutral:"bg-neutral-50/70 text-neutral-900 border-2 border-neutral-200 border-l-4 border-l-neutral-500",error:"bg-error-50/70 text-error-900 border-2 border-error-200 border-l-4 border-l-error-500",success:"bg-success-50/70 text-success-900 border-2 border-success-200 border-l-4 border-l-success-500",warning:"bg-warning-50/70 text-warning-900 border-2 border-warning-200 border-l-4 border-l-warning-500",info:"bg-info-50/70 text-info-900 border-2 border-info-200 border-l-4 border-l-info-500"},ghost:{primary:"bg-transparent text-primary-800 border-2 border-transparent",secondary:"bg-transparent text-secondary-800 border-2 border-transparent",neutral:"bg-transparent text-neutral-800 border-2 border-transparent",error:"bg-transparent text-error-700 border-2 border-transparent",success:"bg-transparent text-success-700 border-2 border-transparent",warning:"bg-transparent text-warning-800 border-2 border-transparent",info:"bg-transparent text-info-800 border-2 border-transparent"}},q={soft:{primary:"text-primary-600",secondary:"text-secondary-600",neutral:"text-neutral-600",error:"text-error-600",success:"text-success-600",warning:"text-warning-600",info:"text-info-600"},filled:{primary:"text-white",secondary:"text-white",neutral:"text-white",error:"text-white",success:"text-white",warning:"text-neutral-950",info:"text-white"},outline:{primary:"text-primary-600",secondary:"text-secondary-600",neutral:"text-neutral-600",error:"text-error-600",success:"text-success-600",warning:"text-warning-600",info:"text-info-600"},"accent-left":{primary:"text-primary-600",secondary:"text-secondary-600",neutral:"text-neutral-600",error:"text-error-600",success:"text-success-600",warning:"text-warning-600",info:"text-info-600"},ghost:{primary:"text-primary-600",secondary:"text-secondary-600",neutral:"text-neutral-600",error:"text-error-600",success:"text-success-600",warning:"text-warning-600",info:"text-info-600"}},J={primary:R,secondary:R,neutral:R,info:R,success:E,warning:G,error:M};import{jsx as h,jsxs as Q}from"react/jsx-runtime";function I({size:e,variant:r,color:o,radius:t,title:c,description:l,icon:s=!0,action:n,closable:i=!0,onClose:a,closeAriaLabel:u="\u0110\xF3ng c\u1EA3nh b\xE1o",banner:d=!1,titleClassName:m="",descriptionClassName:x="",actionClassName:v="",iconClassName:f="",closeButtonClassName:T="",className:A="",children:N,role:k,ref:ne,...se}){let y=p(e,H,"md"),ie=d?"rounded-none border-x-0 w-full":p(t,W,"lg"),ae=r==="other"?"":p(o,p(r,Z,"soft"),"info"),le=r==="other"?"":p(o,p(r,q,"soft"),"info"),ce=i||!!a,de=O=>{O.stopPropagation(),a?.()},ue=k||(o==="error"||o==="warning"?"alert":"status"),fe=o==="error"||o==="warning"?"assertive":"polite",_=l??N,pe=!!_,ge=()=>{if(s===!1)return null;if(s!==!0&&s!==void 0)return h("span",{className:`inline-flex items-center justify-center shrink-0 leading-none ${y.icon} ${f}`,"aria-hidden":"true",children:s});let O=p(o,J,"info");return h("span",{className:`inline-flex items-center justify-center shrink-0 leading-none ${y.icon} ${le} ${f}`,"aria-hidden":"true",children:h(O,{className:"size-full"})})},be=["relative flex w-full",y.container,ie,ae,A].filter(Boolean).join(" ");return Q("div",{ref:ne,role:ue,"aria-live":fe,className:be,...se,children:[ge(),Q("div",{className:"flex-1 min-w-0 flex flex-col justify-center",children:[c&&h("div",{className:`${y.title} ${m}`,children:c}),pe&&h("div",{className:`${y.description} ${c?"mt-0.5":""} ${x}`,children:_})]}),n&&h("div",{className:`inline-flex items-center shrink-0 ${y.action} ${v}`,children:n}),ce&&h("button",{type:"button",onClick:de,"aria-label":u,className:`inline-flex items-center justify-center shrink-0 rounded-md transition-colors cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-current opacity-70 hover:opacity-100 ${r==="filled"?"hover:bg-white/20 active:bg-white/30":"hover:bg-black/5 active:bg-black/10"} ${y.closeButton} ${T}`,children:h(z,{className:y.closeIcon})})]})}var b=4e3;import{Toaster as Re}from"sonner";import{jsx as Se}from"react/jsx-runtime";function X({position:e="top-right",visibleToasts:r=3,expand:o=!1,duration:t=4e3,closeButton:c=!1,className:l="",toastOptions:s,...n}){return Se(Re,{position:e,visibleToasts:r,expand:o,duration:t,closeButton:c,className:`toaster group ${l}`,toastOptions:{unstyled:!0,className:"w-full max-w-md pointer-events-auto",...s},...n})}import{isValidElement as Ce}from"react";import{toast as S}from"sonner";import{jsx as $}from"react/jsx-runtime";function w(e,r="info",o="soft",t,c={},l){let{duration:s,position:n,onDismiss:i,onAutoClose:a,...u}=c;return S.custom(d=>$(I,{color:r,variant:o,title:e,description:t,closable:u.closable??!0,onClose:()=>{u.onClose?.(),S.dismiss(d)},...u}),{id:l,duration:s??4e3,position:n,onDismiss:i,onAutoClose:a})}function Ae(e){return typeof e=="object"&&e!==null&&!Ce(e)&&!Array.isArray(e)&&("title"in e||"description"in e||"color"in e||"variant"in e)}function B(e,r,o){return Ae(e)?{title:e.title??"",description:e.description,color:e.color??r,variant:e.variant??o}:{title:e,description:void 0,color:r,variant:o}}var g=((e,r,o)=>{let t=o||{};return w(e,t.color||"info",t.variant||"soft",r,t)});g.custom=(e,r={})=>{if(typeof e=="function")return S.custom(n=>e(n),{duration:r.duration??4e3,position:r.position,onDismiss:r.onDismiss,onAutoClose:r.onAutoClose});let{duration:o,position:t,onDismiss:c,onAutoClose:l,...s}=e;return S.custom(n=>$(I,{closable:s.closable??!0,onClose:()=>{s.onClose?.(),S.dismiss(n)},...s}),{duration:o??4e3,position:t,onDismiss:c,onAutoClose:l})};g.success=(e,r,o)=>{let t=o||{};return w(e,t.color||"success",t.variant||"soft",r,t)};g.error=(e,r,o)=>{let t=o||{};return w(e,t.color||"error",t.variant||"soft",r,t)};g.warning=(e,r,o)=>{let t=o||{};return w(e,t.color||"warning",t.variant||"soft",r,t)};g.info=(e,r,o)=>{let t=o||{};return w(e,t.color||"info",t.variant||"soft",r,t)};g.loading=(e,r,o)=>{let t=o||{};return w(e,t.color||"info",t.variant||"soft",r,{...t,icon:t.icon??$(P,{className:"size-full animate-spin"}),duration:t.duration??1/0,closable:t.closable??!1})};g.promise=async(e,r)=>{let o=typeof e=="function"?e():e,{title:t,description:c}=B(r.loading,"info",r.variant||"soft"),l=g.loading(t,c,{size:r.size,variant:r.variant,radius:r.radius});try{let s=await o,n=typeof r.success=="function"?r.success(s):r.success,{title:i,description:a,color:u,variant:d}=B(n,"success",r.variant||"soft");return w(i,u,d,a,{duration:r.duration??4e3,size:r.size,radius:r.radius},l),s}catch(s){let n=typeof r.error=="function"?r.error(s):r.error,{title:i,description:a,color:u,variant:d}=B(n,"error",r.variant||"soft");throw w(i,u,d,a,{duration:r.duration??4e3,size:r.size,radius:r.radius},l),s}finally{r.finally&&await r.finally()}};g.dismiss=e=>S.dismiss(e);var Ne=g;import{useCallback as Y,useEffect as ee,useRef as ke,useState as re}from"react";function Tr({onLoadMore:e,hasMore:r=!0,isLoading:o=!1,disabled:t=!1,rootMargin:c="100px",threshold:l=0,root:s=null}){let[n,i]=re(!1),[a,u]=re(null),d=ke(!1),m=r&&!o&&!t,x=Y(()=>{if(!(!m||d.current)){d.current=!0;try{let f=e();f&&typeof f.then=="function"?f.finally(()=>{d.current=!1}):d.current=!1}catch{d.current=!1}}},[m,e]),v=Y(f=>{u(f)},[]);return ee(()=>{if(!a||t||!r||typeof window>"u"||!("IntersectionObserver"in window))return;let f=s&&typeof s=="object"&&"current"in s?s.current:s,T=new IntersectionObserver(A=>{let[N]=A;if(!N)return;let k=N.isIntersecting;i(k),k&&x()},{root:f,rootMargin:c,threshold:l});return T.observe(a),()=>{T.disconnect(),i(!1)}},[a,s,c,l,t,r,x]),ee(()=>{m&&n&&x()},[m,n,x]),{sentinelRef:v,isIntersecting:n}}var te={pulse:"animate-pulse",wave:"skeleton-wave",none:""},oe={none:"rounded-none",sm:"rounded-sm",md:"rounded-md",lg:"rounded-lg",xl:"rounded-xl",full:"rounded-full"},Rr={cover:"object-cover",contain:"object-contain",fill:"object-fill",none:"object-none","scale-down":"object-scale-down"};var C=e=>e,Cr=e=>e?typeof e=="string"?e:typeof File<"u"&&e instanceof File?`${e.name}-${e.size}-${e.lastModified}`:typeof Blob<"u"&&e instanceof Blob?`${e.size}-${e.type}`:typeof e=="object"&&"src"in e&&typeof e.src=="string"?e.src:e:"";import{jsx as D}from"react/jsx-runtime";function Ie({ref:e,variant:r="pulse",shape:o="rectangle",radius:t="md",width:c,height:l="1rem",lines:s=1,gap:n="0.5rem",className:i="",style:a,...u}){let d=p(r,te,"pulse"),m=o==="circle"?"rounded-full":p(t,oe,"md"),x={width:C(c),height:C(l),...a},v=["bg-neutral-200",d,m].filter(Boolean).join(" ");return s>1?D("div",{ref:e,role:"status","aria-label":"Loading...",className:["flex flex-col",i].filter(Boolean).join(" "),style:{gap:C(n),...a},...u,children:Array.from({length:s}).map((f,T)=>{let A=T===s-1;return D("div",{className:v,style:{width:A?"60%":C(c)??"100%",height:C(l)}},T)})}):D("div",{ref:e,role:"status","aria-label":"Loading...",className:[v,i].filter(Boolean).join(" "),style:x,...u})}var zr={sm:{head:"px-3 py-2 text-xs font-semibold",cell:"px-3 py-2 text-xs",text:"text-xs",icon:"w-3.5 h-3.5"},md:{head:"px-4 py-3 text-sm font-semibold",cell:"px-4 py-3 text-sm",text:"text-sm",icon:"w-4 h-4"},lg:{head:"px-5 py-4 text-base font-semibold",cell:"px-5 py-4 text-base",text:"text-base",icon:"w-5 h-5"}},Vr={default:{container:"border border-neutral-200 shadow-xs",table:"border-collapse",head:"bg-neutral-50/80 border-b border-neutral-200 text-neutral-600",row:"border-b border-neutral-200/80 hover:bg-neutral-50/50 transition-colors",cell:"text-neutral-700"},striped:{container:"border border-neutral-200 shadow-xs",table:"border-collapse",head:"bg-neutral-100/80 border-b border-neutral-200 text-neutral-700",row:"border-b border-neutral-200/80 even:bg-neutral-50/60 hover:bg-neutral-100/40 transition-colors",cell:"text-neutral-700"},bordered:{container:"border border-neutral-200 shadow-xs",table:"border-collapse border border-neutral-200",head:"bg-neutral-50 border border-neutral-200 text-neutral-700",row:"border-b border-neutral-200 hover:bg-neutral-50/50 transition-colors",cell:"border border-neutral-200 text-neutral-700"}},Er={left:"text-left justify-start",center:"text-center justify-center",right:"text-right justify-end"},jr=[10,20,50,100],Gr=10;export{p as a,ze as b,P as c,z as d,R as e,E as f,G as g,M as h,I as i,b as j,X as k,Ne as l,Tr as m,oe as n,Rr as o,Cr as p,Ie as q,zr as r,Vr as s,Er as t,jr as u,Gr as v};
3
- //# sourceMappingURL=chunk-X4LIYOS5.js.map