@ram_28/kf-ai-sdk 2.0.12 → 2.0.13
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/dist/api/client.d.ts.map +1 -1
- package/dist/api.cjs +1 -1
- package/dist/api.mjs +2 -2
- package/dist/attachment-constants-B5jlqoKI.cjs +1 -0
- package/dist/attachment-constants-C2UHWxmp.js +63 -0
- package/dist/auth.cjs +1 -1
- package/dist/auth.mjs +1 -1
- package/dist/bdo/core/types.d.ts +4 -0
- package/dist/bdo/core/types.d.ts.map +1 -1
- package/dist/bdo/fields/NumberField.d.ts.map +1 -1
- package/dist/bdo/fields/ReferenceField.d.ts +3 -2
- package/dist/bdo/fields/ReferenceField.d.ts.map +1 -1
- package/dist/bdo/fields/SelectField.d.ts +1 -1
- package/dist/bdo/fields/SelectField.d.ts.map +1 -1
- package/dist/bdo/fields/UserField.d.ts +5 -0
- package/dist/bdo/fields/UserField.d.ts.map +1 -1
- package/dist/bdo.cjs +1 -1
- package/dist/bdo.mjs +107 -153
- package/dist/client-DnO2KKrw.cjs +1 -0
- package/dist/{client-CMERmrC-.js → client-iQTqFDNI.js} +34 -30
- package/dist/components/hooks/useForm/createItemProxy.d.ts +4 -0
- package/dist/components/hooks/useForm/createItemProxy.d.ts.map +1 -1
- package/dist/components/hooks/useForm/createResolver.d.ts.map +1 -1
- package/dist/components/hooks/useForm/useForm.d.ts +1 -0
- package/dist/components/hooks/useForm/useForm.d.ts.map +1 -1
- package/dist/form.cjs +1 -1
- package/dist/form.mjs +368 -203
- package/dist/{metadata-BfJtHz84.cjs → metadata-DgLSJkF5.cjs} +1 -1
- package/dist/{metadata-CwAo6a8e.js → metadata-DpfI3zRN.js} +1 -1
- package/dist/table.cjs +1 -1
- package/dist/table.mjs +1 -1
- package/dist/workflow/types.d.ts +3 -2
- package/dist/workflow/types.d.ts.map +1 -1
- package/dist/workflow.cjs +1 -1
- package/dist/workflow.d.ts +0 -2
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.mjs +204 -274
- package/dist/workflow.types.d.ts +0 -1
- package/dist/workflow.types.d.ts.map +1 -1
- package/docs/api.md +45 -253
- package/docs/bdo.md +130 -711
- package/docs/useAuth.md +42 -104
- package/docs/useFilter.md +117 -1591
- package/docs/useForm.md +263 -861
- package/docs/useTable.md +255 -1096
- package/docs/workflow.md +10 -155
- package/package.json +1 -1
- package/sdk/api/client.ts +18 -4
- package/sdk/bdo/core/types.ts +1 -0
- package/sdk/bdo/fields/NumberField.ts +2 -1
- package/sdk/bdo/fields/ReferenceField.ts +4 -3
- package/sdk/bdo/fields/SelectField.ts +2 -2
- package/sdk/bdo/fields/UserField.ts +14 -0
- package/sdk/components/hooks/useForm/createItemProxy.ts +221 -4
- package/sdk/components/hooks/useForm/createResolver.ts +16 -1
- package/sdk/components/hooks/useForm/useForm.ts +151 -50
- package/sdk/workflow/types.ts +3 -2
- package/sdk/workflow.ts +0 -7
- package/sdk/workflow.types.ts +0 -7
- package/dist/client-BnVxSHAm.cjs +0 -1
- package/dist/workflow/components/useActivityTable/index.d.ts +0 -4
- package/dist/workflow/components/useActivityTable/index.d.ts.map +0 -1
- package/dist/workflow/components/useActivityTable/types.d.ts +0 -53
- package/dist/workflow/components/useActivityTable/types.d.ts.map +0 -1
- package/dist/workflow/components/useActivityTable/useActivityTable.d.ts +0 -4
- package/dist/workflow/components/useActivityTable/useActivityTable.d.ts.map +0 -1
- package/sdk/workflow/components/useActivityTable/index.ts +0 -8
- package/sdk/workflow/components/useActivityTable/types.ts +0 -67
- package/sdk/workflow/components/useActivityTable/useActivityTable.ts +0 -145
package/docs/useTable.md
CHANGED
|
@@ -1,1115 +1,318 @@
|
|
|
1
1
|
# Table SDK API
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Here is the complete example of building table with state management with data fetching, sorting, filtering, search, and pagination.
|
|
6
|
-
|
|
7
|
-
You SHOULD only use this API to build Table, List, Gallery, any other list like component.
|
|
3
|
+
React hook for tables, lists, and galleries with sorting, filtering, search, and pagination.
|
|
8
4
|
|
|
9
5
|
## Imports
|
|
10
6
|
|
|
11
7
|
```typescript
|
|
12
8
|
import { useTable } from "@ram_28/kf-ai-sdk/table";
|
|
13
|
-
import type {
|
|
14
|
-
|
|
15
|
-
UseTableReturnType,
|
|
16
|
-
ColumnDefinitionType,
|
|
17
|
-
PaginationStateType,
|
|
18
|
-
} from "@ram_28/kf-ai-sdk/table/types";
|
|
9
|
+
import type { UseTableOptionsType, UseTableReturnType, ColumnDefinitionType, PaginationStateType } from "@ram_28/kf-ai-sdk/table/types";
|
|
10
|
+
import { ConditionOperator, GroupOperator, FilterValueSource } from "@ram_28/kf-ai-sdk/filter";
|
|
19
11
|
```
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
### UseTableOptionsType
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
// Hook options for initializing the table
|
|
27
|
-
interface UseTableOptionsType<T> {
|
|
28
|
-
// Business Object ID (required)
|
|
29
|
-
// Example: "BDO_Product", "BDO_Order"
|
|
30
|
-
source: string;
|
|
31
|
-
|
|
32
|
-
// Column configurations (required)
|
|
33
|
-
// Defines which fields to display and their behavior
|
|
34
|
-
columns: ColumnDefinitionType<T>[];
|
|
35
|
-
|
|
36
|
-
// Initial state for the table (optional)
|
|
37
|
-
initialState?: {
|
|
38
|
-
// Initial sort configuration
|
|
39
|
-
// Format: [{ "fieldName": "ASC" }] or [{ "fieldName": "DESC" }]
|
|
40
|
-
sort?: SortType;
|
|
41
|
-
|
|
42
|
-
// Initial pagination
|
|
43
|
-
// Defaults: { pageNo: 1, pageSize: 10 }
|
|
44
|
-
pagination?: PaginationStateType;
|
|
45
|
-
|
|
46
|
-
// Initial filter conditions
|
|
47
|
-
// See useFilter docs for full options
|
|
48
|
-
filter?: UseFilterOptionsType<T>;
|
|
49
|
-
};
|
|
13
|
+
---
|
|
50
14
|
|
|
51
|
-
|
|
52
|
-
onError?: (error: Error) => void;
|
|
15
|
+
## Common Mistakes (READ FIRST)
|
|
53
16
|
|
|
54
|
-
|
|
55
|
-
onSuccess?: (data: T[]) => void;
|
|
56
|
-
}
|
|
57
|
-
```
|
|
17
|
+
### 1. Missing `as keyof FieldType` cast on column fieldId (TS2322)
|
|
58
18
|
|
|
59
|
-
|
|
19
|
+
`ColumnDefinitionType<T>.fieldId` is typed as `keyof T`, but `bdo.field.id` returns `string`. MUST cast.
|
|
60
20
|
|
|
61
21
|
```typescript
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
label
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
enableSorting?: boolean;
|
|
74
|
-
|
|
75
|
-
// Enable filtering for this column
|
|
76
|
-
// When true, column can be used in filter conditions
|
|
77
|
-
enableFiltering?: boolean;
|
|
78
|
-
|
|
79
|
-
// Custom value transformer for display
|
|
80
|
-
// Receives raw value and full row, returns rendered content
|
|
81
|
-
transform?: (value: any, row: T) => React.ReactNode;
|
|
82
|
-
}
|
|
22
|
+
// ❌ WRONG — string not assignable to keyof (TS2322)
|
|
23
|
+
const columns: ColumnDefinitionType<AdminProductFieldType>[] = [
|
|
24
|
+
{ fieldId: bdo.product_name.id, label: "Product" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// ✅ CORRECT — cast each fieldId
|
|
28
|
+
const columns: ColumnDefinitionType<AdminProductFieldType>[] = [
|
|
29
|
+
{ fieldId: bdo.product_name.id as keyof AdminProductFieldType, label: "Product" },
|
|
30
|
+
{ fieldId: bdo.unit_price.id as keyof AdminProductFieldType, label: "Price" },
|
|
31
|
+
{ fieldId: bdo.status.id as keyof AdminProductFieldType, label: "Status" },
|
|
32
|
+
];
|
|
83
33
|
```
|
|
84
34
|
|
|
85
|
-
###
|
|
86
|
-
|
|
87
|
-
```typescript
|
|
88
|
-
// Hook return type with all table state and methods
|
|
89
|
-
interface UseTableReturnType<T> {
|
|
90
|
-
// ============================================================
|
|
91
|
-
// DATA
|
|
92
|
-
// ============================================================
|
|
93
|
-
|
|
94
|
-
// Current page data (array of records)
|
|
95
|
-
rows: T[];
|
|
96
|
-
|
|
97
|
-
// Total matching records across all pages
|
|
98
|
-
totalItems: number;
|
|
99
|
-
|
|
100
|
-
// ============================================================
|
|
101
|
-
// LOADING STATES
|
|
102
|
-
// ============================================================
|
|
103
|
-
|
|
104
|
-
// True during initial load
|
|
105
|
-
isLoading: boolean;
|
|
106
|
-
|
|
107
|
-
// True during background refetch
|
|
108
|
-
isFetching: boolean;
|
|
109
|
-
|
|
110
|
-
// ============================================================
|
|
111
|
-
// ERROR HANDLING
|
|
112
|
-
// ============================================================
|
|
113
|
-
|
|
114
|
-
// Current error state, null when no error
|
|
115
|
-
error: Error | null;
|
|
116
|
-
|
|
117
|
-
// ============================================================
|
|
118
|
-
// SEARCH (field-based search using filter conditions)
|
|
119
|
-
// ============================================================
|
|
120
|
-
|
|
121
|
-
search: {
|
|
122
|
-
// Current search query string
|
|
123
|
-
query: string;
|
|
35
|
+
### 2. Passing `bdo` instead of `source`
|
|
124
36
|
|
|
125
|
-
|
|
126
|
-
field: keyof T | null;
|
|
127
|
-
|
|
128
|
-
// Set search field and query (triggers API call, 300ms debounced)
|
|
129
|
-
// Internally creates a Contains filter condition
|
|
130
|
-
set: (field: keyof T, query: string) => void;
|
|
131
|
-
|
|
132
|
-
// Clear search and reset to empty string (triggers API call)
|
|
133
|
-
clear: () => void;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
// ============================================================
|
|
137
|
-
// SORT (single column sorting)
|
|
138
|
-
// ============================================================
|
|
139
|
-
|
|
140
|
-
sort: {
|
|
141
|
-
// Currently sorted field, null if no sort active
|
|
142
|
-
field: keyof T | null;
|
|
143
|
-
|
|
144
|
-
// Current sort direction, null if no sort active
|
|
145
|
-
direction: "ASC" | "DESC" | null;
|
|
146
|
-
|
|
147
|
-
// Toggle sort on a field (triggers API call)
|
|
148
|
-
// Cycles: none → ASC → DESC → none
|
|
149
|
-
toggle: (field: keyof T) => void;
|
|
150
|
-
|
|
151
|
-
// Clear sorting (triggers API call)
|
|
152
|
-
clear: () => void;
|
|
153
|
-
|
|
154
|
-
// Set explicit sort field and direction (triggers API call)
|
|
155
|
-
set: (field: keyof T, direction: "ASC" | "DESC") => void;
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// ============================================================
|
|
159
|
-
// FILTER (see useFilter docs for full API)
|
|
160
|
-
// ============================================================
|
|
161
|
-
|
|
162
|
-
// Full useFilter return type
|
|
163
|
-
// Includes addCondition, removeCondition, clearAllConditions, etc.
|
|
164
|
-
filter: UseFilterReturnType<T>;
|
|
165
|
-
|
|
166
|
-
// ============================================================
|
|
167
|
-
// PAGINATION (1-indexed pages)
|
|
168
|
-
// ============================================================
|
|
169
|
-
|
|
170
|
-
pagination: {
|
|
171
|
-
// Current page number (1-indexed, starts at 1)
|
|
172
|
-
pageNo: number;
|
|
173
|
-
|
|
174
|
-
// Number of items per page
|
|
175
|
-
pageSize: number;
|
|
176
|
-
|
|
177
|
-
// Total number of pages
|
|
178
|
-
totalPages: number;
|
|
179
|
-
|
|
180
|
-
// Total matching records (same as top-level totalItems)
|
|
181
|
-
totalItems: number;
|
|
182
|
-
|
|
183
|
-
// True if there's a next page available
|
|
184
|
-
canGoNext: boolean;
|
|
185
|
-
|
|
186
|
-
// True if there's a previous page available
|
|
187
|
-
canGoPrevious: boolean;
|
|
188
|
-
|
|
189
|
-
// Navigate to next page
|
|
190
|
-
goToNext: () => void;
|
|
191
|
-
|
|
192
|
-
// Navigate to previous page
|
|
193
|
-
goToPrevious: () => void;
|
|
194
|
-
|
|
195
|
-
// Navigate to specific page number (1-indexed)
|
|
196
|
-
goToPage: (page: number) => void;
|
|
197
|
-
|
|
198
|
-
// Change items per page (resets to page 1)
|
|
199
|
-
setPageSize: (size: number) => void;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
// ============================================================
|
|
203
|
-
// OPERATIONS
|
|
204
|
-
// ============================================================
|
|
205
|
-
|
|
206
|
-
// Manually trigger data refetch
|
|
207
|
-
// Returns promise with the list response
|
|
208
|
-
refetch: () => Promise<ListResponseType<T>>;
|
|
209
|
-
}
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
### PaginationStateType
|
|
37
|
+
`useTable` takes `source` (a BO_ID string), NOT `bdo` (a BDO instance).
|
|
213
38
|
|
|
214
39
|
```typescript
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
// Page number (1-indexed)
|
|
218
|
-
pageNo: number;
|
|
40
|
+
// ❌ WRONG
|
|
41
|
+
useTable({ bdo: product, columns });
|
|
219
42
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
43
|
+
// ✅ CORRECT
|
|
44
|
+
useTable({ source: product.meta._id, columns });
|
|
223
45
|
```
|
|
224
46
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
A minimal table displaying data with loading and error states.
|
|
228
|
-
|
|
229
|
-
```tsx
|
|
230
|
-
import { useMemo } from "react";
|
|
231
|
-
import { useTable } from "@ram_28/kf-ai-sdk/table";
|
|
232
|
-
import type { ColumnDefinitionType } from "@ram_28/kf-ai-sdk/table/types";
|
|
233
|
-
import { BuyerProduct } from "../bdo/buyer/Product";
|
|
234
|
-
import type { BuyerProductFieldType } from "../bdo/buyer/Product";
|
|
235
|
-
|
|
236
|
-
function ProductsTable() {
|
|
237
|
-
const product = useMemo(() => new BuyerProduct(), []);
|
|
238
|
-
|
|
239
|
-
const columns: ColumnDefinitionType<BuyerProductFieldType>[] = [
|
|
240
|
-
{ fieldId: product.Title.id, label: product.Title.label },
|
|
241
|
-
{ fieldId: product.Price.id, label: product.Price.label },
|
|
242
|
-
{ fieldId: product.Category.id, label: product.Category.label },
|
|
243
|
-
];
|
|
47
|
+
### 3. Calling `.get()` on table rows
|
|
244
48
|
|
|
245
|
-
|
|
246
|
-
source: product.meta._id,
|
|
247
|
-
columns,
|
|
248
|
-
});
|
|
49
|
+
Table `rows` are **plain objects**, NOT `ItemType`. Access fields directly.
|
|
249
50
|
|
|
250
|
-
|
|
251
|
-
|
|
51
|
+
```typescript
|
|
52
|
+
// ❌ WRONG — rows are plain objects
|
|
53
|
+
table.rows.map(row => row.product_name.get());
|
|
252
54
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
<tr>
|
|
257
|
-
{columns.map((col) => (
|
|
258
|
-
<th key={String(col.fieldId)}>{col.label}</th>
|
|
259
|
-
))}
|
|
260
|
-
</tr>
|
|
261
|
-
</thead>
|
|
262
|
-
<tbody>
|
|
263
|
-
{table.rows.map((row) => (
|
|
264
|
-
<tr key={row._id}>
|
|
265
|
-
<td>{row.Title}</td>
|
|
266
|
-
<td>${row.Price}</td>
|
|
267
|
-
<td>{row.Category}</td>
|
|
268
|
-
</tr>
|
|
269
|
-
))}
|
|
270
|
-
</tbody>
|
|
271
|
-
</table>
|
|
272
|
-
);
|
|
273
|
-
}
|
|
55
|
+
// ✅ CORRECT — access directly
|
|
56
|
+
table.rows.map(row => row.product_name);
|
|
57
|
+
table.rows.map(row => String(row.product_name ?? ""));
|
|
274
58
|
```
|
|
275
59
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
## Initial State
|
|
279
|
-
|
|
280
|
-
### Table with Initial Configuration
|
|
281
|
-
|
|
282
|
-
Set default pagination, sorting, and filters when the table loads.
|
|
283
|
-
|
|
284
|
-
```tsx
|
|
285
|
-
import { useMemo } from "react";
|
|
286
|
-
import { useTable } from "@ram_28/kf-ai-sdk/table";
|
|
287
|
-
import { useAuth } from "@ram_28/kf-ai-sdk/auth";
|
|
288
|
-
import { ConditionOperator, RHSType, GroupOperator } from "@ram_28/kf-ai-sdk/filter";
|
|
289
|
-
import type {
|
|
290
|
-
UseTableOptionsType,
|
|
291
|
-
UseTableReturnType,
|
|
292
|
-
ColumnDefinitionType,
|
|
293
|
-
} from "@ram_28/kf-ai-sdk/table/types";
|
|
294
|
-
import { BuyerProduct } from "../bdo/buyer/Product";
|
|
295
|
-
import type { BuyerProductFieldType } from "../bdo/buyer/Product";
|
|
296
|
-
|
|
297
|
-
function MyItemsTable() {
|
|
298
|
-
const product = useMemo(() => new BuyerProduct(), []);
|
|
299
|
-
const { user } = useAuth();
|
|
300
|
-
|
|
301
|
-
const columns: ColumnDefinitionType<BuyerProductFieldType>[] = [
|
|
302
|
-
{ fieldId: product.Title.id, label: product.Title.label, enableSorting: true },
|
|
303
|
-
{ fieldId: product.Price.id, label: product.Price.label, enableSorting: true },
|
|
304
|
-
{ fieldId: product.Category.id, label: product.Category.label, enableSorting: true },
|
|
305
|
-
{ fieldId: product.Stock.id, label: product.Stock.label, enableSorting: true },
|
|
306
|
-
];
|
|
307
|
-
|
|
308
|
-
const tableOptions: UseTableOptionsType<BuyerProductFieldType> = {
|
|
309
|
-
source: product.meta._id,
|
|
310
|
-
columns,
|
|
311
|
-
initialState: {
|
|
312
|
-
sort: [{ [product.Title.id]: "ASC" }],
|
|
313
|
-
pagination: { pageNo: 1, pageSize: 10 },
|
|
314
|
-
filter: {
|
|
315
|
-
conditions: [
|
|
316
|
-
{
|
|
317
|
-
Operator: ConditionOperator.EQ,
|
|
318
|
-
LHSField: "_created_by", // System field
|
|
319
|
-
RHSValue: user._id,
|
|
320
|
-
RHSType: RHSType.Constant,
|
|
321
|
-
},
|
|
322
|
-
],
|
|
323
|
-
operator: GroupOperator.And,
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
const table: UseTableReturnType<BuyerProductFieldType> =
|
|
329
|
-
useTable<BuyerProductFieldType>(tableOptions);
|
|
60
|
+
### 4. Using entity-level FieldType as generic
|
|
330
61
|
|
|
331
|
-
|
|
62
|
+
Entity types exclude `SystemFieldsType` — `row._id` won't work. Use role-specific type.
|
|
332
63
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
<tr>
|
|
338
|
-
{columns.map((col) => (
|
|
339
|
-
<th
|
|
340
|
-
key={String(col.fieldId)}
|
|
341
|
-
onClick={() =>
|
|
342
|
-
col.enableSorting && table.sort.toggle(col.fieldId)
|
|
343
|
-
}
|
|
344
|
-
style={{ cursor: col.enableSorting ? "pointer" : "default" }}
|
|
345
|
-
>
|
|
346
|
-
{col.label}
|
|
347
|
-
{table.sort.field === col.fieldId && (
|
|
348
|
-
<span>{table.sort.direction === "ASC" ? " ↑" : " ↓"}</span>
|
|
349
|
-
)}
|
|
350
|
-
</th>
|
|
351
|
-
))}
|
|
352
|
-
</tr>
|
|
353
|
-
</thead>
|
|
354
|
-
<tbody>
|
|
355
|
-
{table.rows.map((row) => (
|
|
356
|
-
<tr key={row._id}>
|
|
357
|
-
<td>{row.Title}</td>
|
|
358
|
-
<td>${row.Price}</td>
|
|
359
|
-
<td>{row.Category}</td>
|
|
360
|
-
<td>{row.Stock}</td>
|
|
361
|
-
</tr>
|
|
362
|
-
))}
|
|
363
|
-
</tbody>
|
|
364
|
-
</table>
|
|
64
|
+
```typescript
|
|
65
|
+
// ❌ WRONG — ProductType excludes _id
|
|
66
|
+
import type { ProductType } from "@/bdo/entities/Product";
|
|
67
|
+
useTable<ProductType>({ ... });
|
|
365
68
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
disabled={!table.pagination.canGoPrevious}
|
|
370
|
-
>
|
|
371
|
-
Previous
|
|
372
|
-
</button>
|
|
373
|
-
<span>
|
|
374
|
-
Page {table.pagination.pageNo} of {table.pagination.totalPages}
|
|
375
|
-
</span>
|
|
376
|
-
<button
|
|
377
|
-
onClick={table.pagination.goToNext}
|
|
378
|
-
disabled={!table.pagination.canGoNext}
|
|
379
|
-
>
|
|
380
|
-
Next
|
|
381
|
-
</button>
|
|
382
|
-
</div>
|
|
383
|
-
</div>
|
|
384
|
-
);
|
|
385
|
-
}
|
|
69
|
+
// ✅ CORRECT — role type includes SystemFieldsType
|
|
70
|
+
import type { AdminProductFieldType } from "@/bdo/admin/Product";
|
|
71
|
+
useTable<AdminProductFieldType>({ ... });
|
|
386
72
|
```
|
|
387
73
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
## Filter
|
|
391
|
-
|
|
392
|
-
### Status Filter
|
|
393
|
-
|
|
394
|
-
Dropdown-based status filtering.
|
|
395
|
-
|
|
396
|
-
```tsx
|
|
397
|
-
import { useMemo } from "react";
|
|
398
|
-
import { ConditionOperator, RHSType } from "@ram_28/kf-ai-sdk/filter";
|
|
399
|
-
import { BuyerProduct } from "../bdo/buyer/Product";
|
|
400
|
-
import type { BuyerProductFieldType } from "../bdo/buyer/Product";
|
|
401
|
-
|
|
402
|
-
function ProductsWithStatusFilter() {
|
|
403
|
-
const product = useMemo(() => new BuyerProduct(), []);
|
|
74
|
+
### 5. Wrong initialState property names
|
|
404
75
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
const handleStatusChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
411
|
-
const status = e.target.value;
|
|
412
|
-
table.filter.clearAllConditions();
|
|
413
|
-
|
|
414
|
-
if (status !== "all") {
|
|
415
|
-
table.filter.addCondition({
|
|
416
|
-
Operator: ConditionOperator.EQ,
|
|
417
|
-
LHSField: product.IsActive.id,
|
|
418
|
-
RHSValue: status === "active",
|
|
419
|
-
RHSType: RHSType.Constant,
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
};
|
|
76
|
+
```typescript
|
|
77
|
+
// ❌ WRONG — react-table naming
|
|
78
|
+
initialState: { sorting: [...], pagination: { pageIndex: 0, pageSize: 10 } }
|
|
423
79
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
<select onChange={handleStatusChange} defaultValue="all">
|
|
427
|
-
<option value="all">All Products</option>
|
|
428
|
-
<option value="active">Active</option>
|
|
429
|
-
<option value="inactive">Inactive</option>
|
|
430
|
-
</select>
|
|
431
|
-
{/* table rendering */}
|
|
432
|
-
</div>
|
|
433
|
-
);
|
|
434
|
-
}
|
|
80
|
+
// ✅ CORRECT — use sort (not sorting), pageNo (not pageIndex), 1-indexed
|
|
81
|
+
initialState: { sort: [{ [bdo.product_name.id]: "ASC" }], pagination: { pageNo: 1, pageSize: 10 } }
|
|
435
82
|
```
|
|
436
83
|
|
|
437
|
-
###
|
|
438
|
-
|
|
439
|
-
Apply category and price range filters together.
|
|
440
|
-
|
|
441
|
-
```tsx
|
|
442
|
-
import { useMemo, useState } from "react";
|
|
443
|
-
import { ConditionOperator, RHSType } from "@ram_28/kf-ai-sdk/filter";
|
|
444
|
-
import { BuyerProduct } from "../bdo/buyer/Product";
|
|
445
|
-
import type { BuyerProductFieldType } from "../bdo/buyer/Product";
|
|
446
|
-
|
|
447
|
-
const PRICE_RANGES = [
|
|
448
|
-
{ label: "Under $25", min: 0, max: 25 },
|
|
449
|
-
{ label: "$25 to $50", min: 25, max: 50 },
|
|
450
|
-
{ label: "$50 to $100", min: 50, max: 100 },
|
|
451
|
-
{ label: "$100 to $200", min: 100, max: 200 },
|
|
452
|
-
{ label: "$200 & Above", min: 200, max: null },
|
|
453
|
-
] as const;
|
|
454
|
-
|
|
455
|
-
function ProductsWithMultipleFilters() {
|
|
456
|
-
const product = useMemo(() => new BuyerProduct(), []);
|
|
457
|
-
const [selectedCategory, setSelectedCategory] = useState("all");
|
|
458
|
-
const [selectedPriceRange, setSelectedPriceRange] = useState<string | null>(
|
|
459
|
-
null,
|
|
460
|
-
);
|
|
461
|
-
|
|
462
|
-
const table = useTable<BuyerProductFieldType>({
|
|
463
|
-
source: product.meta._id,
|
|
464
|
-
columns,
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
// Helper to apply all active filters
|
|
468
|
-
const applyFilters = (category: string, priceRange: string | null) => {
|
|
469
|
-
table.filter.clearAllConditions();
|
|
470
|
-
|
|
471
|
-
// Apply category filter
|
|
472
|
-
if (category !== "all") {
|
|
473
|
-
table.filter.addCondition({
|
|
474
|
-
LHSField: product.Category.id,
|
|
475
|
-
Operator: ConditionOperator.EQ,
|
|
476
|
-
RHSValue: category,
|
|
477
|
-
RHSType: RHSType.Constant,
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Apply price filter
|
|
482
|
-
if (priceRange) {
|
|
483
|
-
const range = PRICE_RANGES.find((r) => r.label === priceRange);
|
|
484
|
-
if (range) {
|
|
485
|
-
if (range.max === null) {
|
|
486
|
-
// "$200 & Above" - use GTE
|
|
487
|
-
table.filter.addCondition({
|
|
488
|
-
Operator: ConditionOperator.GTE,
|
|
489
|
-
LHSField: product.Price.id,
|
|
490
|
-
RHSValue: range.min,
|
|
491
|
-
RHSType: RHSType.Constant,
|
|
492
|
-
});
|
|
493
|
-
} else if (range.min === 0) {
|
|
494
|
-
// "Under $25" - use LT
|
|
495
|
-
table.filter.addCondition({
|
|
496
|
-
Operator: ConditionOperator.LT,
|
|
497
|
-
LHSField: product.Price.id,
|
|
498
|
-
RHSValue: range.max,
|
|
499
|
-
RHSType: RHSType.Constant,
|
|
500
|
-
});
|
|
501
|
-
} else {
|
|
502
|
-
// Range like "$25 to $50" - use Between
|
|
503
|
-
table.filter.addCondition({
|
|
504
|
-
LHSField: product.Price.id,
|
|
505
|
-
Operator: ConditionOperator.Between,
|
|
506
|
-
RHSValue: [range.min, range.max],
|
|
507
|
-
RHSType: RHSType.Constant,
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
};
|
|
513
|
-
|
|
514
|
-
const handleCategoryChange = (category: string) => {
|
|
515
|
-
setSelectedCategory(category);
|
|
516
|
-
applyFilters(category, selectedPriceRange);
|
|
517
|
-
};
|
|
518
|
-
|
|
519
|
-
const handlePriceChange = (priceRange: string | null) => {
|
|
520
|
-
setSelectedPriceRange(priceRange);
|
|
521
|
-
applyFilters(selectedCategory, priceRange);
|
|
522
|
-
};
|
|
523
|
-
|
|
524
|
-
return (
|
|
525
|
-
<div>
|
|
526
|
-
<select
|
|
527
|
-
value={selectedCategory}
|
|
528
|
-
onChange={(e) => handleCategoryChange(e.target.value)}
|
|
529
|
-
>
|
|
530
|
-
<option value="all">All Categories</option>
|
|
531
|
-
<option value="Electronics">Electronics</option>
|
|
532
|
-
<option value="Books">Books</option>
|
|
533
|
-
</select>
|
|
534
|
-
|
|
535
|
-
<select
|
|
536
|
-
value={selectedPriceRange || ""}
|
|
537
|
-
onChange={(e) => handlePriceChange(e.target.value || null)}
|
|
538
|
-
>
|
|
539
|
-
<option value="">Any Price</option>
|
|
540
|
-
{PRICE_RANGES.map((range) => (
|
|
541
|
-
<option key={range.label} value={range.label}>
|
|
542
|
-
{range.label}
|
|
543
|
-
</option>
|
|
544
|
-
))}
|
|
545
|
-
</select>
|
|
546
|
-
|
|
547
|
-
{/* table rendering */}
|
|
548
|
-
</div>
|
|
549
|
-
);
|
|
550
|
-
}
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
---
|
|
554
|
-
|
|
555
|
-
## Sort
|
|
556
|
-
|
|
557
|
-
### Column Sort Toggle
|
|
558
|
-
|
|
559
|
-
Click column headers to toggle sort direction.
|
|
560
|
-
|
|
561
|
-
```tsx
|
|
562
|
-
import { useMemo } from "react";
|
|
563
|
-
import { BuyerProduct } from "../bdo/buyer/Product";
|
|
564
|
-
import type { BuyerProductFieldType } from "../bdo/buyer/Product";
|
|
565
|
-
|
|
566
|
-
function SortableTable() {
|
|
567
|
-
const product = useMemo(() => new BuyerProduct(), []);
|
|
568
|
-
|
|
569
|
-
const columns: ColumnDefinitionType<BuyerProductFieldType>[] = [
|
|
570
|
-
{ fieldId: product.Title.id, label: product.Title.label, enableSorting: true },
|
|
571
|
-
{ fieldId: product.Price.id, label: product.Price.label, enableSorting: true },
|
|
572
|
-
{ fieldId: product.Category.id, label: product.Category.label, enableSorting: true },
|
|
573
|
-
];
|
|
84
|
+
### 6. Using `header` instead of `label` in columns
|
|
574
85
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
});
|
|
86
|
+
```typescript
|
|
87
|
+
// ❌ WRONG
|
|
88
|
+
{ fieldId: bdo.product_name.id as keyof T, header: "Product" }
|
|
579
89
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
<thead>
|
|
583
|
-
<tr>
|
|
584
|
-
{columns.map((col) => (
|
|
585
|
-
<th
|
|
586
|
-
key={String(col.fieldId)}
|
|
587
|
-
onClick={() =>
|
|
588
|
-
col.enableSorting && table.sort.toggle(col.fieldId)
|
|
589
|
-
}
|
|
590
|
-
style={{ cursor: col.enableSorting ? "pointer" : "default" }}
|
|
591
|
-
>
|
|
592
|
-
{col.label}
|
|
593
|
-
{table.sort.field === col.fieldId && (
|
|
594
|
-
<span>{table.sort.direction === "ASC" ? " ↑" : " ↓"}</span>
|
|
595
|
-
)}
|
|
596
|
-
</th>
|
|
597
|
-
))}
|
|
598
|
-
</tr>
|
|
599
|
-
</thead>
|
|
600
|
-
<tbody>
|
|
601
|
-
{table.rows.map((row) => (
|
|
602
|
-
<tr key={row._id}>
|
|
603
|
-
<td>{row.Title}</td>
|
|
604
|
-
<td>${row.Price}</td>
|
|
605
|
-
<td>{row.Category}</td>
|
|
606
|
-
</tr>
|
|
607
|
-
))}
|
|
608
|
-
</tbody>
|
|
609
|
-
</table>
|
|
610
|
-
);
|
|
611
|
-
}
|
|
90
|
+
// ✅ CORRECT
|
|
91
|
+
{ fieldId: bdo.product_name.id as keyof T, label: "Product" }
|
|
612
92
|
```
|
|
613
93
|
|
|
614
|
-
###
|
|
615
|
-
|
|
616
|
-
Allow users to select sort order from a dropdown.
|
|
94
|
+
### 7. Using `useMemo` for filter side effects instead of `useEffect`
|
|
617
95
|
|
|
618
96
|
```tsx
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
import type { BuyerProductFieldType } from "../bdo/buyer/Product";
|
|
622
|
-
|
|
623
|
-
function TableWithSortDropdown() {
|
|
624
|
-
const product = useMemo(() => new BuyerProduct(), []);
|
|
625
|
-
const [selectedSort, setSelectedSort] = useState("featured");
|
|
626
|
-
|
|
627
|
-
const table = useTable<BuyerProductFieldType>({
|
|
628
|
-
source: product.meta._id,
|
|
629
|
-
columns,
|
|
630
|
-
initialState: {
|
|
631
|
-
sort: [{ [product.Title.id]: "ASC" }],
|
|
632
|
-
},
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
const handleSortChange = (value: string) => {
|
|
636
|
-
setSelectedSort(value);
|
|
637
|
-
switch (value) {
|
|
638
|
-
case "price-asc":
|
|
639
|
-
table.sort.set(product.Price.id, "ASC");
|
|
640
|
-
break;
|
|
641
|
-
case "price-desc":
|
|
642
|
-
table.sort.set(product.Price.id, "DESC");
|
|
643
|
-
break;
|
|
644
|
-
case "newest":
|
|
645
|
-
table.sort.set("_created_at", "DESC"); // System field
|
|
646
|
-
break;
|
|
647
|
-
case "featured":
|
|
648
|
-
default:
|
|
649
|
-
table.sort.set(product.Title.id, "ASC");
|
|
650
|
-
break;
|
|
651
|
-
}
|
|
652
|
-
};
|
|
97
|
+
// ❌ WRONG — side effects in useMemo cause infinite re-render
|
|
98
|
+
useMemo(() => { table.filter.clearAllConditions(); table.filter.addCondition({...}); }, [status]);
|
|
653
99
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
<select
|
|
657
|
-
value={selectedSort}
|
|
658
|
-
onChange={(e) => handleSortChange(e.target.value)}
|
|
659
|
-
>
|
|
660
|
-
<option value="featured">Featured</option>
|
|
661
|
-
<option value="price-asc">Price: Low to High</option>
|
|
662
|
-
<option value="price-desc">Price: High to Low</option>
|
|
663
|
-
<option value="newest">Newest Arrivals</option>
|
|
664
|
-
</select>
|
|
665
|
-
{/* table rendering */}
|
|
666
|
-
</div>
|
|
667
|
-
);
|
|
668
|
-
}
|
|
100
|
+
// ✅ CORRECT — use useEffect
|
|
101
|
+
useEffect(() => { table.filter.clearAllConditions(); table.filter.addCondition({...}); }, [status]);
|
|
669
102
|
```
|
|
670
103
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
## Pagination
|
|
674
|
-
|
|
675
|
-
### Basic Pagination Controls
|
|
104
|
+
### 8. Rendering Reference/Image/File values in table columns
|
|
676
105
|
|
|
677
|
-
|
|
106
|
+
Table rows are plain objects. Reference values are objects `{ _id, _name }` — render `_name`. Image/File values are `FileType | null` / `FileType[]`.
|
|
678
107
|
|
|
679
108
|
```tsx
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
import type { BuyerProductFieldType } from "../bdo/buyer/Product";
|
|
683
|
-
|
|
684
|
-
function PaginatedTable() {
|
|
685
|
-
const product = useMemo(() => new BuyerProduct(), []);
|
|
686
|
-
|
|
687
|
-
const table = useTable<BuyerProductFieldType>({
|
|
688
|
-
source: product.meta._id,
|
|
689
|
-
columns,
|
|
690
|
-
initialState: {
|
|
691
|
-
pagination: { pageNo: 1, pageSize: 10 },
|
|
692
|
-
},
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
return (
|
|
696
|
-
<div>
|
|
697
|
-
{/* table rendering */}
|
|
698
|
-
|
|
699
|
-
<div className="pagination">
|
|
700
|
-
<button
|
|
701
|
-
onClick={table.pagination.goToPrevious}
|
|
702
|
-
disabled={!table.pagination.canGoPrevious}
|
|
703
|
-
>
|
|
704
|
-
Previous
|
|
705
|
-
</button>
|
|
706
|
-
|
|
707
|
-
<span>
|
|
708
|
-
Page {table.pagination.pageNo} of {table.pagination.totalPages}
|
|
709
|
-
</span>
|
|
710
|
-
|
|
711
|
-
<button
|
|
712
|
-
onClick={table.pagination.goToNext}
|
|
713
|
-
disabled={!table.pagination.canGoNext}
|
|
714
|
-
>
|
|
715
|
-
Next
|
|
716
|
-
</button>
|
|
717
|
-
</div>
|
|
718
|
-
|
|
719
|
-
<p>{table.pagination.totalItems} total items</p>
|
|
720
|
-
</div>
|
|
721
|
-
);
|
|
722
|
-
}
|
|
723
|
-
```
|
|
109
|
+
// ❌ WRONG — renders [object Object] for reference field
|
|
110
|
+
<td>{row.category}</td>
|
|
724
111
|
|
|
725
|
-
|
|
112
|
+
// ❌ WRONG — renders [object Object] for image field
|
|
113
|
+
<td>{row.product_image}</td>
|
|
726
114
|
|
|
727
|
-
|
|
115
|
+
// ✅ CORRECT — reference: access _name
|
|
116
|
+
<td>{(row.category as any)?._name ?? "—"}</td>
|
|
728
117
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
import { BuyerProduct } from "../bdo/buyer/Product";
|
|
732
|
-
import type { BuyerProductFieldType } from "../bdo/buyer/Product";
|
|
733
|
-
|
|
734
|
-
function TableWithPageSize() {
|
|
735
|
-
const product = useMemo(() => new BuyerProduct(), []);
|
|
736
|
-
|
|
737
|
-
const table = useTable<BuyerProductFieldType>({
|
|
738
|
-
source: product.meta._id,
|
|
739
|
-
columns,
|
|
740
|
-
});
|
|
118
|
+
// ✅ CORRECT — file array: show count or names
|
|
119
|
+
<td>{Array.isArray(row.attachments) ? `${(row.attachments as any[]).length} files` : "—"}</td>
|
|
741
120
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
{/* table rendering */}
|
|
745
|
-
|
|
746
|
-
<div className="pagination">
|
|
747
|
-
<button
|
|
748
|
-
onClick={table.pagination.goToPrevious}
|
|
749
|
-
disabled={!table.pagination.canGoPrevious}
|
|
750
|
-
>
|
|
751
|
-
Previous
|
|
752
|
-
</button>
|
|
753
|
-
|
|
754
|
-
<span>
|
|
755
|
-
Page {table.pagination.pageNo} of {table.pagination.totalPages}
|
|
756
|
-
</span>
|
|
757
|
-
|
|
758
|
-
<button
|
|
759
|
-
onClick={table.pagination.goToNext}
|
|
760
|
-
disabled={!table.pagination.canGoNext}
|
|
761
|
-
>
|
|
762
|
-
Next
|
|
763
|
-
</button>
|
|
764
|
-
|
|
765
|
-
<select
|
|
766
|
-
value={table.pagination.pageSize}
|
|
767
|
-
onChange={(e) => table.pagination.setPageSize(Number(e.target.value))}
|
|
768
|
-
>
|
|
769
|
-
<option value={10}>10 per page</option>
|
|
770
|
-
<option value={25}>25 per page</option>
|
|
771
|
-
<option value={50}>50 per page</option>
|
|
772
|
-
<option value={100}>100 per page</option>
|
|
773
|
-
</select>
|
|
774
|
-
</div>
|
|
775
|
-
</div>
|
|
776
|
-
);
|
|
777
|
-
}
|
|
121
|
+
// ✅ BEST — use transform in column definition
|
|
122
|
+
{ fieldId: bdo.category.id as keyof T, label: "Category", transform: (val) => (val as any)?._name ?? "—" }
|
|
778
123
|
```
|
|
779
124
|
|
|
780
|
-
###
|
|
125
|
+
### 9. Displaying images from ImageField in tables/cards/grids (NEVER use src="#")
|
|
781
126
|
|
|
782
|
-
|
|
127
|
+
Use the pre-built `ImageThumbnail` component from `@/components/ui/image-thumbnail`. It handles the server proxy URL, error fallback, and placeholder automatically. NEVER build image URLs manually or use `src="#"`.
|
|
783
128
|
|
|
784
129
|
```tsx
|
|
785
|
-
import {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
<button
|
|
825
|
-
onClick={table.pagination.goToNext}
|
|
826
|
-
disabled={!table.pagination.canGoNext}
|
|
827
|
-
>
|
|
828
|
-
Next
|
|
829
|
-
</button>
|
|
830
|
-
|
|
831
|
-
<input
|
|
832
|
-
type="number"
|
|
833
|
-
min={1}
|
|
834
|
-
max={table.pagination.totalPages}
|
|
835
|
-
placeholder="Go to page"
|
|
836
|
-
value={pageInput}
|
|
837
|
-
onChange={(e) => setPageInput(e.target.value)}
|
|
838
|
-
onKeyDown={handlePageJump}
|
|
839
|
-
/>
|
|
840
|
-
</div>
|
|
841
|
-
</div>
|
|
842
|
-
);
|
|
130
|
+
import { ImageThumbnail } from "@/components/ui/image-thumbnail";
|
|
131
|
+
|
|
132
|
+
// ❌ WRONG — src="#" shows broken image, NEVER do this
|
|
133
|
+
<img src="#" alt="product" />
|
|
134
|
+
|
|
135
|
+
// ❌ WRONG — FileName is just the filename string, NOT a URL
|
|
136
|
+
<img src={(row.product_image as any)?.FileName} />
|
|
137
|
+
|
|
138
|
+
// ✅ CORRECT — use ImageThumbnail in table cell
|
|
139
|
+
<td>
|
|
140
|
+
<ImageThumbnail boId={bdo.meta._id} instanceId={row._id} fieldId={bdo.product_image.id} value={row[bdo.product_image.id]} />
|
|
141
|
+
</td>
|
|
142
|
+
|
|
143
|
+
// ✅ CORRECT — use ImageThumbnail in product card/grid
|
|
144
|
+
<ImageThumbnail
|
|
145
|
+
boId={bdo.meta._id}
|
|
146
|
+
instanceId={row._id}
|
|
147
|
+
fieldId={bdo.product_image.id}
|
|
148
|
+
value={row[bdo.product_image.id]}
|
|
149
|
+
imgClassName="w-full h-full object-cover rounded-lg"
|
|
150
|
+
alt={String(row.product_name ?? "")}
|
|
151
|
+
/>
|
|
152
|
+
|
|
153
|
+
// ✅ CORRECT — use ImageThumbnail in detail page (with bdo.get() ItemType)
|
|
154
|
+
<ImageThumbnail
|
|
155
|
+
boId={bdo.meta._id}
|
|
156
|
+
instanceId={item._id}
|
|
157
|
+
fieldId={bdo.product_image.id}
|
|
158
|
+
value={item.product_image.get()}
|
|
159
|
+
imgClassName="w-48 h-48 object-cover rounded-xl"
|
|
160
|
+
/>
|
|
161
|
+
|
|
162
|
+
// ✅ CORRECT — use in column transform
|
|
163
|
+
{
|
|
164
|
+
fieldId: bdo.product_image.id as keyof T,
|
|
165
|
+
label: "Image",
|
|
166
|
+
transform: (val, row) => (
|
|
167
|
+
<ImageThumbnail boId={bdo.meta._id} instanceId={row._id} fieldId={bdo.product_image.id} value={val} />
|
|
168
|
+
),
|
|
843
169
|
}
|
|
844
170
|
```
|
|
845
171
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
172
|
+
**Key rules:**
|
|
173
|
+
- ALWAYS use `<ImageThumbnail>` for displaying images outside forms — it builds the correct server proxy URL internally
|
|
174
|
+
- NEVER hardcode `src="#"` or use `FileName` as a URL
|
|
175
|
+
- For forms (upload/edit), use `<ImageUpload>` instead
|
|
849
176
|
|
|
850
|
-
|
|
177
|
+
### 10. Displaying files from FileField in tables/cards/grids
|
|
851
178
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
- `search.query: string` - Current search query string
|
|
855
|
-
- `search.field: keyof T | null` - Field being searched
|
|
856
|
-
- `search.set(field, query)` - Set search field and query (triggers API call, 300ms debounced)
|
|
857
|
-
- `search.clear()` - Clear search (triggers API call)
|
|
858
|
-
|
|
859
|
-
### Basic Search
|
|
860
|
-
|
|
861
|
-
Add search functionality to filter results by a specific field. The search has built-in debouncing (300ms).
|
|
179
|
+
Use the pre-built `FilePreview` component from `@/components/ui/file-preview`. It shows file names with download links, and inline thumbnails for image files.
|
|
862
180
|
|
|
863
181
|
```tsx
|
|
864
|
-
import {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
value={table.search.query}
|
|
883
|
-
onChange={(e) => table.search.set(product.Title.id, e.target.value)}
|
|
884
|
-
/>
|
|
885
|
-
{table.search.query && (
|
|
886
|
-
<button onClick={table.search.clear}>Clear</button>
|
|
887
|
-
)}
|
|
888
|
-
{table.isFetching && <span>Searching...</span>}
|
|
889
|
-
</div>
|
|
890
|
-
|
|
891
|
-
{/* table rendering */}
|
|
892
|
-
</div>
|
|
893
|
-
);
|
|
182
|
+
import { FilePreview } from "@/components/ui/file-preview";
|
|
183
|
+
|
|
184
|
+
// ❌ WRONG — renders [object Object] or meaningless text
|
|
185
|
+
<td>{row.attachments}</td>
|
|
186
|
+
<td>{JSON.stringify(row.attachments)}</td>
|
|
187
|
+
|
|
188
|
+
// ✅ CORRECT — use FilePreview in table cell
|
|
189
|
+
<td>
|
|
190
|
+
<FilePreview boId={bdo.meta._id} instanceId={row._id} fieldId={bdo.attachments.id} value={row[bdo.attachments.id]} />
|
|
191
|
+
</td>
|
|
192
|
+
|
|
193
|
+
// ✅ CORRECT — use in column transform
|
|
194
|
+
{
|
|
195
|
+
fieldId: bdo.attachments.id as keyof T,
|
|
196
|
+
label: "Files",
|
|
197
|
+
transform: (val, row) => (
|
|
198
|
+
<FilePreview boId={bdo.meta._id} instanceId={row._id} fieldId={bdo.attachments.id} value={val} />
|
|
199
|
+
),
|
|
894
200
|
}
|
|
895
201
|
```
|
|
896
202
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
```tsx
|
|
902
|
-
import { useMemo, useState } from "react";
|
|
903
|
-
import { BuyerProduct } from "../bdo/buyer/Product";
|
|
904
|
-
import type { BuyerProductFieldType } from "../bdo/buyer/Product";
|
|
905
|
-
|
|
906
|
-
function SearchableTableWithFieldSelector() {
|
|
907
|
-
const product = useMemo(() => new BuyerProduct(), []);
|
|
908
|
-
const [searchField, setSearchField] = useState<string>(product.Title.id);
|
|
909
|
-
|
|
910
|
-
const table = useTable<BuyerProductFieldType>({
|
|
911
|
-
source: product.meta._id,
|
|
912
|
-
columns,
|
|
913
|
-
});
|
|
914
|
-
|
|
915
|
-
return (
|
|
916
|
-
<div>
|
|
917
|
-
<div className="search-bar">
|
|
918
|
-
<select
|
|
919
|
-
value={searchField}
|
|
920
|
-
onChange={(e) => {
|
|
921
|
-
setSearchField(e.target.value);
|
|
922
|
-
// Re-apply search with new field if there's an existing query
|
|
923
|
-
if (table.search.query) {
|
|
924
|
-
table.search.set(e.target.value, table.search.query);
|
|
925
|
-
}
|
|
926
|
-
}}
|
|
927
|
-
>
|
|
928
|
-
<option value={product.Title.id}>{product.Title.label}</option>
|
|
929
|
-
<option value={product.Category.id}>{product.Category.label}</option>
|
|
930
|
-
<option value={product.Description.id}>{product.Description.label}</option>
|
|
931
|
-
</select>
|
|
932
|
-
<input
|
|
933
|
-
type="text"
|
|
934
|
-
placeholder={`Search by ${searchField}...`}
|
|
935
|
-
value={table.search.query}
|
|
936
|
-
onChange={(e) => table.search.set(searchField, e.target.value)}
|
|
937
|
-
/>
|
|
938
|
-
{table.search.query && (
|
|
939
|
-
<button onClick={table.search.clear}>Clear</button>
|
|
940
|
-
)}
|
|
941
|
-
</div>
|
|
942
|
-
|
|
943
|
-
{/* table rendering */}
|
|
944
|
-
</div>
|
|
945
|
-
);
|
|
946
|
-
}
|
|
947
|
-
```
|
|
203
|
+
**Key rules:**
|
|
204
|
+
- ALWAYS use `<FilePreview>` for displaying files outside forms
|
|
205
|
+
- For forms (upload/edit), use `<FileUpload>` instead
|
|
948
206
|
|
|
949
207
|
---
|
|
950
208
|
|
|
951
|
-
## Complete Example
|
|
952
|
-
|
|
953
|
-
A full-featured product listing page with filters, search, sort, and pagination.
|
|
209
|
+
## Complete Table Example
|
|
954
210
|
|
|
955
211
|
```tsx
|
|
956
|
-
import { useMemo, useState } from "react";
|
|
212
|
+
import { useMemo, useState, useEffect } from "react";
|
|
213
|
+
import { useNavigate } from "react-router-dom";
|
|
957
214
|
import { useTable } from "@ram_28/kf-ai-sdk/table";
|
|
958
|
-
import {
|
|
959
|
-
import
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
const [
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
{ fieldId:
|
|
974
|
-
|
|
975
|
-
{ fieldId: product.Category.id, label: product.Category.label, enableSorting: true },
|
|
976
|
-
{ fieldId: product.Stock.id, label: product.Stock.label, enableSorting: true },
|
|
215
|
+
import type { ColumnDefinitionType } from "@ram_28/kf-ai-sdk/table/types";
|
|
216
|
+
import { ConditionOperator, FilterValueSource } from "@ram_28/kf-ai-sdk/filter";
|
|
217
|
+
import { AdminProduct } from "@/bdo/admin/Product";
|
|
218
|
+
import type { AdminProductFieldType } from "@/bdo/admin/Product";
|
|
219
|
+
import { toast } from "sonner";
|
|
220
|
+
|
|
221
|
+
export default function ProductList() {
|
|
222
|
+
const navigate = useNavigate();
|
|
223
|
+
const bdo = useMemo(() => new AdminProduct(), []);
|
|
224
|
+
|
|
225
|
+
// Columns — MUST cast fieldId as keyof FieldType
|
|
226
|
+
const columns: ColumnDefinitionType<AdminProductFieldType>[] = [
|
|
227
|
+
{ fieldId: bdo.product_name.id as keyof AdminProductFieldType, label: bdo.product_name.label, enableSorting: true },
|
|
228
|
+
{ fieldId: bdo.unit_price.id as keyof AdminProductFieldType, label: bdo.unit_price.label, enableSorting: true },
|
|
229
|
+
{ fieldId: bdo.status.id as keyof AdminProductFieldType, label: bdo.status.label },
|
|
230
|
+
{ fieldId: bdo.category.id as keyof AdminProductFieldType, label: bdo.category.label,
|
|
231
|
+
transform: (val) => (val as any)?._name ?? "—" },
|
|
977
232
|
];
|
|
978
233
|
|
|
979
|
-
const
|
|
980
|
-
source:
|
|
234
|
+
const table = useTable<AdminProductFieldType>({
|
|
235
|
+
source: bdo.meta._id,
|
|
981
236
|
columns,
|
|
982
237
|
initialState: {
|
|
983
|
-
sort: [{ [
|
|
238
|
+
sort: [{ [bdo.product_name.id]: "ASC" }],
|
|
984
239
|
pagination: { pageNo: 1, pageSize: 10 },
|
|
985
240
|
},
|
|
986
|
-
};
|
|
987
|
-
|
|
988
|
-
const table: UseTableReturnType<BuyerProductFieldType> =
|
|
989
|
-
useTable<BuyerProductFieldType>(tableOptions);
|
|
241
|
+
});
|
|
990
242
|
|
|
991
|
-
|
|
992
|
-
|
|
243
|
+
// Status filter — useEffect for side effects, NOT useMemo
|
|
244
|
+
const [statusFilter, setStatusFilter] = useState("all");
|
|
245
|
+
useEffect(() => {
|
|
993
246
|
table.filter.clearAllConditions();
|
|
994
|
-
if (
|
|
247
|
+
if (statusFilter !== "all") {
|
|
995
248
|
table.filter.addCondition({
|
|
996
|
-
LHSField: product.Category.id,
|
|
997
249
|
Operator: ConditionOperator.EQ,
|
|
998
|
-
|
|
999
|
-
|
|
250
|
+
LHSField: bdo.status.id,
|
|
251
|
+
RHSValue: statusFilter,
|
|
252
|
+
RHSType: FilterValueSource.Constant,
|
|
1000
253
|
});
|
|
1001
254
|
}
|
|
1002
|
-
};
|
|
1003
|
-
|
|
1004
|
-
const handleSortChange = (value: string) => {
|
|
1005
|
-
setSelectedSort(value);
|
|
1006
|
-
switch (value) {
|
|
1007
|
-
case "price-asc":
|
|
1008
|
-
table.sort.set(product.Price.id, "ASC");
|
|
1009
|
-
break;
|
|
1010
|
-
case "price-desc":
|
|
1011
|
-
table.sort.set(product.Price.id, "DESC");
|
|
1012
|
-
break;
|
|
1013
|
-
case "newest":
|
|
1014
|
-
table.sort.set("_created_at", "DESC"); // System field
|
|
1015
|
-
break;
|
|
1016
|
-
default:
|
|
1017
|
-
table.sort.set(product.Title.id, "ASC");
|
|
1018
|
-
break;
|
|
1019
|
-
}
|
|
1020
|
-
};
|
|
255
|
+
}, [statusFilter]);
|
|
1021
256
|
|
|
1022
|
-
if (table.
|
|
1023
|
-
|
|
1024
|
-
<div>
|
|
1025
|
-
<p>Error: {table.error.message}</p>
|
|
1026
|
-
<button onClick={() => table.refetch()}>Try Again</button>
|
|
1027
|
-
</div>
|
|
1028
|
-
);
|
|
1029
|
-
}
|
|
257
|
+
if (table.isLoading) return <div>Loading...</div>;
|
|
258
|
+
if (table.error) return <div>Error: {table.error.message}</div>;
|
|
1030
259
|
|
|
1031
|
-
|
|
1032
|
-
return
|
|
1033
|
-
|
|
260
|
+
const handleDelete = async (id: string) => {
|
|
261
|
+
if (!confirm("Delete this item?")) return;
|
|
262
|
+
try {
|
|
263
|
+
await bdo.delete(id);
|
|
264
|
+
toast.success("Deleted");
|
|
265
|
+
table.refetch();
|
|
266
|
+
} catch (e) { toast.error("Failed to delete"); }
|
|
267
|
+
};
|
|
1034
268
|
|
|
1035
269
|
return (
|
|
1036
|
-
<div>
|
|
1037
|
-
{/*
|
|
1038
|
-
<
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
>
|
|
1050
|
-
|
|
1051
|
-
<option value="Electronics">Electronics</option>
|
|
1052
|
-
<option value="Books">Books</option>
|
|
1053
|
-
</select>
|
|
1054
|
-
|
|
1055
|
-
<select
|
|
1056
|
-
value={selectedSort}
|
|
1057
|
-
onChange={(e) => handleSortChange(e.target.value)}
|
|
1058
|
-
>
|
|
1059
|
-
<option value="featured">Featured</option>
|
|
1060
|
-
<option value="price-asc">Price: Low to High</option>
|
|
1061
|
-
<option value="price-desc">Price: High to Low</option>
|
|
1062
|
-
<option value="newest">Newest</option>
|
|
1063
|
-
</select>
|
|
1064
|
-
</div>
|
|
270
|
+
<div className="space-y-4">
|
|
271
|
+
{/* Search */}
|
|
272
|
+
<input
|
|
273
|
+
type="text"
|
|
274
|
+
placeholder="Search..."
|
|
275
|
+
value={table.search.query}
|
|
276
|
+
onChange={(e) => table.search.set(bdo.product_name.id as keyof AdminProductFieldType, e.target.value)}
|
|
277
|
+
/>
|
|
278
|
+
|
|
279
|
+
{/* Status filter dropdown */}
|
|
280
|
+
<select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}>
|
|
281
|
+
<option value="all">All</option>
|
|
282
|
+
<option value="Active">Active</option>
|
|
283
|
+
<option value="Discontinued">Discontinued</option>
|
|
284
|
+
</select>
|
|
1065
285
|
|
|
1066
|
-
{/*
|
|
1067
|
-
<
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
>
|
|
1080
|
-
Clear Filters
|
|
1081
|
-
</button>
|
|
1082
|
-
</div>
|
|
1083
|
-
) : (
|
|
1084
|
-
<div className="product-grid">
|
|
286
|
+
{/* Table */}
|
|
287
|
+
<table>
|
|
288
|
+
<thead>
|
|
289
|
+
<tr>
|
|
290
|
+
{columns.map((col) => (
|
|
291
|
+
<th key={String(col.fieldId)} onClick={() => col.enableSorting && table.sort.toggle(col.fieldId)}>
|
|
292
|
+
{col.label}
|
|
293
|
+
{table.sort.field === col.fieldId && (table.sort.direction === "ASC" ? " ↑" : " ↓")}
|
|
294
|
+
</th>
|
|
295
|
+
))}
|
|
296
|
+
</tr>
|
|
297
|
+
</thead>
|
|
298
|
+
<tbody>
|
|
1085
299
|
{table.rows.map((row) => (
|
|
1086
|
-
<
|
|
1087
|
-
<
|
|
1088
|
-
<
|
|
1089
|
-
<
|
|
1090
|
-
<
|
|
1091
|
-
</
|
|
300
|
+
<tr key={row._id} onClick={() => navigate(`/products/${row._id}`)}>
|
|
301
|
+
<td>{String(row.product_name ?? "")}</td>
|
|
302
|
+
<td>${Number(row.unit_price ?? 0).toFixed(2)}</td>
|
|
303
|
+
<td>{String(row.status ?? "")}</td>
|
|
304
|
+
<td>{(row.category as any)?._name ?? "—"}</td>
|
|
305
|
+
</tr>
|
|
1092
306
|
))}
|
|
1093
|
-
</
|
|
1094
|
-
|
|
307
|
+
</tbody>
|
|
308
|
+
</table>
|
|
1095
309
|
|
|
1096
310
|
{/* Pagination */}
|
|
1097
|
-
<div className="
|
|
1098
|
-
<button
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
>
|
|
1102
|
-
Previous
|
|
1103
|
-
</button>
|
|
1104
|
-
<span>
|
|
1105
|
-
Page {table.pagination.pageNo} of {table.pagination.totalPages}
|
|
1106
|
-
</span>
|
|
1107
|
-
<button
|
|
1108
|
-
onClick={table.pagination.goToNext}
|
|
1109
|
-
disabled={!table.pagination.canGoNext}
|
|
1110
|
-
>
|
|
1111
|
-
Next
|
|
1112
|
-
</button>
|
|
311
|
+
<div className="flex items-center gap-4">
|
|
312
|
+
<button onClick={table.pagination.goToPrevious} disabled={!table.pagination.canGoPrevious}>Previous</button>
|
|
313
|
+
<span>Page {table.pagination.pageNo} of {table.pagination.totalPages}</span>
|
|
314
|
+
<button onClick={table.pagination.goToNext} disabled={!table.pagination.canGoNext}>Next</button>
|
|
315
|
+
<span>{table.pagination.totalItems} total</span>
|
|
1113
316
|
</div>
|
|
1114
317
|
</div>
|
|
1115
318
|
);
|
|
@@ -1118,93 +321,49 @@ function ProductListPage() {
|
|
|
1118
321
|
|
|
1119
322
|
---
|
|
1120
323
|
|
|
1121
|
-
##
|
|
1122
|
-
|
|
1123
|
-
### 1. Passing `bdo` instead of `source`
|
|
1124
|
-
|
|
1125
|
-
`useTable` takes `source` (a BO_ID string), NOT `bdo` (a BDO instance). Don't confuse with `useForm({ bdo })`.
|
|
1126
|
-
|
|
1127
|
-
```typescript
|
|
1128
|
-
// ❌ WRONG — bdo is NOT a valid property
|
|
1129
|
-
useTable({ bdo, columns });
|
|
1130
|
-
useTable({ bdo: product, columns });
|
|
1131
|
-
|
|
1132
|
-
// ✅ CORRECT — pass the BO_ID string via source
|
|
1133
|
-
useTable({ source: product.meta._id, columns });
|
|
1134
|
-
```
|
|
1135
|
-
|
|
1136
|
-
### 2. Wrong initialState property names
|
|
1137
|
-
|
|
1138
|
-
This is NOT react-table. Don't use react-table naming conventions.
|
|
1139
|
-
|
|
1140
|
-
```typescript
|
|
1141
|
-
// ❌ WRONG — sorting and pageIndex don't exist
|
|
1142
|
-
initialState: { sorting: [...], pagination: { pageIndex: 0, pageSize: 10 } }
|
|
1143
|
-
|
|
1144
|
-
// ✅ CORRECT — use sort and pageNo
|
|
1145
|
-
initialState: { sort: [{ [bdo.Title.id]: "ASC" }], pagination: { pageNo: 1, pageSize: 10 } }
|
|
1146
|
-
```
|
|
1147
|
-
|
|
1148
|
-
### 3. Wrong sort direction type
|
|
1149
|
-
|
|
1150
|
-
Sort direction must be the string literal `"ASC"` or `"DESC"` — nothing else.
|
|
1151
|
-
|
|
1152
|
-
```typescript
|
|
1153
|
-
// ❌ WRONG — booleans, lowercase, or arbitrary strings
|
|
1154
|
-
sort.set(bdo.Title.id, true);
|
|
1155
|
-
sort.set(bdo.Title.id, "asc");
|
|
1156
|
-
sort.set(bdo.Title.id, "ascending");
|
|
1157
|
-
|
|
1158
|
-
// ✅ CORRECT — uppercase string literals only
|
|
1159
|
-
sort.set(bdo.Title.id, "ASC");
|
|
1160
|
-
sort.set(bdo.Title.id, "DESC");
|
|
1161
|
-
```
|
|
1162
|
-
|
|
1163
|
-
### 4. Calling `.get()` on table rows
|
|
1164
|
-
|
|
1165
|
-
Table `rows` are **plain objects**, NOT `ItemType`. `.get()` is only for `ItemType` returned by `bdo.get()`, `bdo.create()`, or `useForm` item proxy.
|
|
1166
|
-
|
|
1167
|
-
```typescript
|
|
1168
|
-
// ❌ WRONG — rows are plain objects, not ItemType
|
|
1169
|
-
table.rows.map(row => row.Title.get());
|
|
1170
|
-
|
|
1171
|
-
// ✅ CORRECT — access properties directly
|
|
1172
|
-
table.rows.map(row => row.Title);
|
|
1173
|
-
table.rows.map(row => row[bdo.Title.id]);
|
|
1174
|
-
```
|
|
1175
|
-
|
|
1176
|
-
### 5. Using entity-level FieldType as generic
|
|
324
|
+
## Type Definitions
|
|
1177
325
|
|
|
1178
|
-
|
|
326
|
+
### ColumnDefinitionType
|
|
1179
327
|
|
|
1180
328
|
```typescript
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
329
|
+
interface ColumnDefinitionType<T> {
|
|
330
|
+
fieldId: keyof T; // MUST cast: bdo.field.id as keyof T
|
|
331
|
+
label?: string;
|
|
332
|
+
enableSorting?: boolean;
|
|
333
|
+
enableFiltering?: boolean;
|
|
334
|
+
transform?: (value: any, row: T) => React.ReactNode; // Custom rendering
|
|
335
|
+
}
|
|
1188
336
|
```
|
|
1189
337
|
|
|
1190
|
-
###
|
|
338
|
+
### UseTableReturnType
|
|
1191
339
|
|
|
1192
340
|
```typescript
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
341
|
+
interface UseTableReturnType<T> {
|
|
342
|
+
rows: T[]; // Plain objects — access row.field directly (NOT .get())
|
|
343
|
+
totalItems: number;
|
|
344
|
+
isLoading: boolean;
|
|
345
|
+
isFetching: boolean;
|
|
346
|
+
error: Error | null;
|
|
347
|
+
search: { query: string; field: keyof T | null; set: (field: keyof T, query: string) => void; clear: () => void; };
|
|
348
|
+
sort: { field: keyof T | null; direction: "ASC" | "DESC" | null; toggle: (field: keyof T) => void; set: (field: keyof T, dir: "ASC" | "DESC") => void; clear: () => void; };
|
|
349
|
+
filter: UseFilterReturnType<T>; // Full useFilter API
|
|
350
|
+
pagination: { pageNo: number; pageSize: number; totalPages: number; totalItems: number; canGoNext: boolean; canGoPrevious: boolean; goToNext: () => void; goToPrevious: () => void; goToPage: (n: number) => void; setPageSize: (n: number) => void; };
|
|
351
|
+
refetch: () => Promise<any>;
|
|
352
|
+
}
|
|
1198
353
|
```
|
|
1199
354
|
|
|
1200
|
-
###
|
|
355
|
+
### UseTableOptionsType
|
|
1201
356
|
|
|
1202
357
|
```typescript
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
358
|
+
interface UseTableOptionsType<T> {
|
|
359
|
+
source: string; // bdo.meta._id (NOT the bdo instance)
|
|
360
|
+
columns: ColumnDefinitionType<T>[];
|
|
361
|
+
initialState?: {
|
|
362
|
+
sort?: Record<string, "ASC" | "DESC">[];
|
|
363
|
+
pagination?: { pageNo: number; pageSize: number; };
|
|
364
|
+
filter?: UseFilterOptionsType<T>;
|
|
365
|
+
};
|
|
366
|
+
onError?: (error: Error) => void;
|
|
367
|
+
onSuccess?: (data: T[]) => void;
|
|
368
|
+
}
|
|
1210
369
|
```
|