@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.
Files changed (69) hide show
  1. package/dist/api/client.d.ts.map +1 -1
  2. package/dist/api.cjs +1 -1
  3. package/dist/api.mjs +2 -2
  4. package/dist/attachment-constants-B5jlqoKI.cjs +1 -0
  5. package/dist/attachment-constants-C2UHWxmp.js +63 -0
  6. package/dist/auth.cjs +1 -1
  7. package/dist/auth.mjs +1 -1
  8. package/dist/bdo/core/types.d.ts +4 -0
  9. package/dist/bdo/core/types.d.ts.map +1 -1
  10. package/dist/bdo/fields/NumberField.d.ts.map +1 -1
  11. package/dist/bdo/fields/ReferenceField.d.ts +3 -2
  12. package/dist/bdo/fields/ReferenceField.d.ts.map +1 -1
  13. package/dist/bdo/fields/SelectField.d.ts +1 -1
  14. package/dist/bdo/fields/SelectField.d.ts.map +1 -1
  15. package/dist/bdo/fields/UserField.d.ts +5 -0
  16. package/dist/bdo/fields/UserField.d.ts.map +1 -1
  17. package/dist/bdo.cjs +1 -1
  18. package/dist/bdo.mjs +107 -153
  19. package/dist/client-DnO2KKrw.cjs +1 -0
  20. package/dist/{client-CMERmrC-.js → client-iQTqFDNI.js} +34 -30
  21. package/dist/components/hooks/useForm/createItemProxy.d.ts +4 -0
  22. package/dist/components/hooks/useForm/createItemProxy.d.ts.map +1 -1
  23. package/dist/components/hooks/useForm/createResolver.d.ts.map +1 -1
  24. package/dist/components/hooks/useForm/useForm.d.ts +1 -0
  25. package/dist/components/hooks/useForm/useForm.d.ts.map +1 -1
  26. package/dist/form.cjs +1 -1
  27. package/dist/form.mjs +368 -203
  28. package/dist/{metadata-BfJtHz84.cjs → metadata-DgLSJkF5.cjs} +1 -1
  29. package/dist/{metadata-CwAo6a8e.js → metadata-DpfI3zRN.js} +1 -1
  30. package/dist/table.cjs +1 -1
  31. package/dist/table.mjs +1 -1
  32. package/dist/workflow/types.d.ts +3 -2
  33. package/dist/workflow/types.d.ts.map +1 -1
  34. package/dist/workflow.cjs +1 -1
  35. package/dist/workflow.d.ts +0 -2
  36. package/dist/workflow.d.ts.map +1 -1
  37. package/dist/workflow.mjs +204 -274
  38. package/dist/workflow.types.d.ts +0 -1
  39. package/dist/workflow.types.d.ts.map +1 -1
  40. package/docs/api.md +45 -253
  41. package/docs/bdo.md +130 -711
  42. package/docs/useAuth.md +42 -104
  43. package/docs/useFilter.md +117 -1591
  44. package/docs/useForm.md +263 -861
  45. package/docs/useTable.md +255 -1096
  46. package/docs/workflow.md +10 -155
  47. package/package.json +1 -1
  48. package/sdk/api/client.ts +18 -4
  49. package/sdk/bdo/core/types.ts +1 -0
  50. package/sdk/bdo/fields/NumberField.ts +2 -1
  51. package/sdk/bdo/fields/ReferenceField.ts +4 -3
  52. package/sdk/bdo/fields/SelectField.ts +2 -2
  53. package/sdk/bdo/fields/UserField.ts +14 -0
  54. package/sdk/components/hooks/useForm/createItemProxy.ts +221 -4
  55. package/sdk/components/hooks/useForm/createResolver.ts +16 -1
  56. package/sdk/components/hooks/useForm/useForm.ts +151 -50
  57. package/sdk/workflow/types.ts +3 -2
  58. package/sdk/workflow.ts +0 -7
  59. package/sdk/workflow.types.ts +0 -7
  60. package/dist/client-BnVxSHAm.cjs +0 -1
  61. package/dist/workflow/components/useActivityTable/index.d.ts +0 -4
  62. package/dist/workflow/components/useActivityTable/index.d.ts.map +0 -1
  63. package/dist/workflow/components/useActivityTable/types.d.ts +0 -53
  64. package/dist/workflow/components/useActivityTable/types.d.ts.map +0 -1
  65. package/dist/workflow/components/useActivityTable/useActivityTable.d.ts +0 -4
  66. package/dist/workflow/components/useActivityTable/useActivityTable.d.ts.map +0 -1
  67. package/sdk/workflow/components/useActivityTable/index.ts +0 -8
  68. package/sdk/workflow/components/useActivityTable/types.ts +0 -67
  69. 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
- This Table SDK API provides the React-hooks and apis to build Table and any List like component when building the Pages.
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
- UseTableOptionsType,
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
- ## Type Definitions
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
- // Called when data fetch fails
52
- onError?: (error: Error) => void;
15
+ ## Common Mistakes (READ FIRST)
53
16
 
54
- // Called with fetched data after successful load
55
- onSuccess?: (data: T[]) => void;
56
- }
57
- ```
17
+ ### 1. Missing `as keyof FieldType` cast on column fieldId (TS2322)
58
18
 
59
- ### ColumnDefinitionType
19
+ `ColumnDefinitionType<T>.fieldId` is typed as `keyof T`, but `bdo.field.id` returns `string`. MUST cast.
60
20
 
61
21
  ```typescript
62
- // Column configuration for table display
63
- interface ColumnDefinitionType<T> {
64
- // Field name from the data type (required)
65
- fieldId: keyof T;
66
-
67
- // Display label for column header
68
- // Defaults to fieldId if not provided
69
- label?: string;
70
-
71
- // Enable sorting for this column
72
- // When true, column header is clickable to toggle sort
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
- ### UseTableReturnType
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
- // Field being searched, null if no search active
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
- // Pagination state for initial configuration
216
- interface PaginationStateType {
217
- // Page number (1-indexed)
218
- pageNo: number;
40
+ // WRONG
41
+ useTable({ bdo: product, columns });
219
42
 
220
- // Number of items per page
221
- pageSize: number;
222
- }
43
+ // CORRECT
44
+ useTable({ source: product.meta._id, columns });
223
45
  ```
224
46
 
225
- ## Basic Example
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
- const table = useTable<BuyerProductFieldType>({
246
- source: product.meta._id,
247
- columns,
248
- });
49
+ Table `rows` are **plain objects**, NOT `ItemType`. Access fields directly.
249
50
 
250
- if (table.isLoading) return <div>Loading...</div>;
251
- if (table.error) return <div>Error: {table.error.message}</div>;
51
+ ```typescript
52
+ // WRONG rows are plain objects
53
+ table.rows.map(row => row.product_name.get());
252
54
 
253
- return (
254
- <table>
255
- <thead>
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
- if (table.isLoading) return <div>Loading...</div>;
62
+ Entity types exclude `SystemFieldsType` — `row._id` won't work. Use role-specific type.
332
63
 
333
- return (
334
- <div>
335
- <table>
336
- <thead>
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
- <div className="pagination">
367
- <button
368
- onClick={table.pagination.goToPrevious}
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
- const table = useTable<BuyerProductFieldType>({
406
- source: product.meta._id,
407
- columns,
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
- return (
425
- <div>
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
- ### Multiple Filters Combined
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
- const table = useTable<BuyerProductFieldType>({
576
- source: product.meta._id,
577
- columns,
578
- });
86
+ ```typescript
87
+ // ❌ WRONG
88
+ { fieldId: bdo.product_name.id as keyof T, header: "Product" }
579
89
 
580
- return (
581
- <table>
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
- ### Sort Dropdown
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
- import { useMemo, useState } from "react";
620
- import { BuyerProduct } from "../bdo/buyer/Product";
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
- return (
655
- <div>
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
- Navigate between pages with previous/next buttons.
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
- import { useMemo } from "react";
681
- import { BuyerProduct } from "../bdo/buyer/Product";
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
- ### Page Size Selector
112
+ // WRONG — renders [object Object] for image field
113
+ <td>{row.product_image}</td>
726
114
 
727
- Allow users to change the number of items per page.
115
+ // CORRECT reference: access _name
116
+ <td>{(row.category as any)?._name ?? "—"}</td>
728
117
 
729
- ```tsx
730
- import { useMemo } from "react";
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
- return (
743
- <div>
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
- ### Jump to Page
125
+ ### 9. Displaying images from ImageField in tables/cards/grids (NEVER use src="#")
781
126
 
782
- Allow users to navigate directly to a specific page.
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 { useMemo, useState } from "react";
786
- import { BuyerProduct } from "../bdo/buyer/Product";
787
- import type { BuyerProductFieldType } from "../bdo/buyer/Product";
788
-
789
- function TableWithPageJump() {
790
- const product = useMemo(() => new BuyerProduct(), []);
791
- const [pageInput, setPageInput] = useState("");
792
-
793
- const table = useTable<BuyerProductFieldType>({
794
- source: product.meta._id,
795
- columns,
796
- });
797
-
798
- const handlePageJump = (e: React.KeyboardEvent<HTMLInputElement>) => {
799
- if (e.key === "Enter") {
800
- const page = parseInt(pageInput, 10);
801
- if (page >= 1 && page <= table.pagination.totalPages) {
802
- table.pagination.goToPage(page);
803
- setPageInput("");
804
- }
805
- }
806
- };
807
-
808
- return (
809
- <div>
810
- {/* table rendering */}
811
-
812
- <div className="pagination">
813
- <button
814
- onClick={table.pagination.goToPrevious}
815
- disabled={!table.pagination.canGoPrevious}
816
- >
817
- Previous
818
- </button>
819
-
820
- <span>
821
- Page {table.pagination.pageNo} of {table.pagination.totalPages}
822
- </span>
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
- ## Search
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
- Search by field with filter-based implementation. The search internally creates a `Contains` filter condition for the specified field.
177
+ ### 10. Displaying files from FileField in tables/cards/grids
851
178
 
852
- ### API
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 { useMemo } from "react";
865
- import { BuyerProduct } from "../bdo/buyer/Product";
866
- import type { BuyerProductFieldType } from "../bdo/buyer/Product";
867
-
868
- function SearchableTable() {
869
- const product = useMemo(() => new BuyerProduct(), []);
870
-
871
- const table = useTable<BuyerProductFieldType>({
872
- source: product.meta._id,
873
- columns,
874
- });
875
-
876
- return (
877
- <div>
878
- <div className="search-bar">
879
- <input
880
- type="text"
881
- placeholder="Search by name..."
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
- ### Search with Field Selector
898
-
899
- Allow users to choose which field to search:
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 { ConditionOperator, RHSType } from "@ram_28/kf-ai-sdk/filter";
959
- import type {
960
- UseTableOptionsType,
961
- UseTableReturnType,
962
- ColumnDefinitionType,
963
- } from "@ram_28/kf-ai-sdk/table/types";
964
- import { BuyerProduct } from "../bdo/buyer/Product";
965
- import type { BuyerProductFieldType } from "../bdo/buyer/Product";
966
-
967
- function ProductListPage() {
968
- const product = useMemo(() => new BuyerProduct(), []);
969
- const [selectedCategory, setSelectedCategory] = useState("all");
970
- const [selectedSort, setSelectedSort] = useState("featured");
971
-
972
- const columns: ColumnDefinitionType<BuyerProductFieldType>[] = [
973
- { fieldId: product.Title.id, label: product.Title.label, enableSorting: true },
974
- { fieldId: product.Price.id, label: product.Price.label, enableSorting: true },
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 tableOptions: UseTableOptionsType<BuyerProductFieldType> = {
980
- source: product.meta._id,
234
+ const table = useTable<AdminProductFieldType>({
235
+ source: bdo.meta._id,
981
236
  columns,
982
237
  initialState: {
983
- sort: [{ [product.Title.id]: "ASC" }],
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
- const handleCategoryChange = (category: string) => {
992
- setSelectedCategory(category);
243
+ // Status filter useEffect for side effects, NOT useMemo
244
+ const [statusFilter, setStatusFilter] = useState("all");
245
+ useEffect(() => {
993
246
  table.filter.clearAllConditions();
994
- if (category !== "all") {
247
+ if (statusFilter !== "all") {
995
248
  table.filter.addCondition({
996
- LHSField: product.Category.id,
997
249
  Operator: ConditionOperator.EQ,
998
- RHSValue: category,
999
- RHSType: RHSType.Constant,
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.error) {
1023
- return (
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
- if (table.isLoading) {
1032
- return <div>Loading...</div>;
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
- {/* Controls */}
1038
- <div className="controls">
1039
- <input
1040
- type="text"
1041
- placeholder="Search by name..."
1042
- value={table.search.query}
1043
- onChange={(e) => table.search.set(product.Title.id, e.target.value)}
1044
- />
1045
-
1046
- <select
1047
- value={selectedCategory}
1048
- onChange={(e) => handleCategoryChange(e.target.value)}
1049
- >
1050
- <option value="all">All Categories</option>
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
- {/* Results count */}
1067
- <p>{table.totalItems} results found</p>
1068
-
1069
- {/* Product grid */}
1070
- {table.rows.length === 0 ? (
1071
- <div>
1072
- <p>No products found</p>
1073
- <button
1074
- onClick={() => {
1075
- table.search.clear();
1076
- table.filter.clearAllConditions();
1077
- setSelectedCategory("all");
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
- <div key={row._id} className="product-card">
1087
- <h3>{row.Title}</h3>
1088
- <p>${row.Price}</p>
1089
- <p>{row.Category}</p>
1090
- <p>{row.Stock > 0 ? "In Stock" : "Out of Stock"}</p>
1091
- </div>
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
- </div>
1094
- )}
307
+ </tbody>
308
+ </table>
1095
309
 
1096
310
  {/* Pagination */}
1097
- <div className="pagination">
1098
- <button
1099
- onClick={table.pagination.goToPrevious}
1100
- disabled={!table.pagination.canGoPrevious}
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
- ## Common Mistakes
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
- Entity-level types (from `@/bdo/entities/`) exclude `SystemFieldsType`, so `row._id` won't work.
326
+ ### ColumnDefinitionType
1179
327
 
1180
328
  ```typescript
1181
- // WRONG — ProductFieldType excludes _id, _created_at, etc.
1182
- import type { ProductFieldType } from "@/bdo/entities/Product";
1183
- useTable<ProductFieldType>({ ... });
1184
-
1185
- // ✅ CORRECT — role-specific type includes SystemFieldsType
1186
- import type { BuyerProductFieldType } from "@/bdo/buyer/Product";
1187
- useTable<BuyerProductFieldType>({ ... });
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
- ### 6. Using `header` instead of `label` in columns
338
+ ### UseTableReturnType
1191
339
 
1192
340
  ```typescript
1193
- // WRONG
1194
- { fieldId: bdo.Title.id, header: "Title" }
1195
-
1196
- // ✅ CORRECT
1197
- { fieldId: bdo.Title.id, label: "Title" }
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
- ### 7. Accessing `data` or `columns` on the return type
355
+ ### UseTableOptionsType
1201
356
 
1202
357
  ```typescript
1203
- // WRONG — these properties don't exist
1204
- table.data
1205
- table.columns
1206
-
1207
- // CORRECT
1208
- table.rows // current page data
1209
- table.totalItems // total matching records
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
  ```