@ram_28/kf-ai-sdk 2.0.19 → 2.0.20-beta.0

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 (105) hide show
  1. package/README.md +8 -16
  2. package/dist/{FileField-CZjS2uLh.js → FileField-BWrSHNRq.js} +3 -3
  3. package/dist/{FileField-DU4UWo_t.cjs → FileField-eDeuzln8.cjs} +1 -1
  4. package/dist/api.cjs +1 -1
  5. package/dist/api.mjs +1 -1
  6. package/dist/auth/authConfig.d.ts +1 -1
  7. package/dist/auth/types.d.ts +1 -1
  8. package/dist/auth/types.d.ts.map +1 -1
  9. package/dist/auth.cjs +1 -1
  10. package/dist/auth.mjs +1 -1
  11. package/dist/bdo/core/Item.d.ts +4 -0
  12. package/dist/bdo/core/Item.d.ts.map +1 -1
  13. package/dist/bdo/fields/ReferenceField.d.ts +1 -1
  14. package/dist/bdo/fields/ReferenceField.d.ts.map +1 -1
  15. package/dist/bdo/fields/SelectField.d.ts +1 -1
  16. package/dist/bdo/fields/SelectField.d.ts.map +1 -1
  17. package/dist/bdo/fields/UserField.d.ts +1 -1
  18. package/dist/bdo/fields/UserField.d.ts.map +1 -1
  19. package/dist/bdo.cjs +1 -1
  20. package/dist/bdo.mjs +62 -53
  21. package/dist/components/hooks/useActivityForm/types.d.ts +5 -4
  22. package/dist/components/hooks/useActivityForm/types.d.ts.map +1 -1
  23. package/dist/components/hooks/useActivityForm/useActivityForm.d.ts.map +1 -1
  24. package/dist/components/hooks/useActivityTable/types.d.ts +4 -5
  25. package/dist/components/hooks/useActivityTable/types.d.ts.map +1 -1
  26. package/dist/components/hooks/useActivityTable/useActivityTable.d.ts.map +1 -1
  27. package/dist/components/hooks/useBDOForm/createItemProxy.d.ts +3 -2
  28. package/dist/components/hooks/useBDOForm/createItemProxy.d.ts.map +1 -1
  29. package/dist/components/hooks/useBDOTable/types.d.ts +12 -20
  30. package/dist/components/hooks/useBDOTable/types.d.ts.map +1 -1
  31. package/dist/components/hooks/useBDOTable/useBDOTable.d.ts +2 -2
  32. package/dist/components/hooks/useBDOTable/useBDOTable.d.ts.map +1 -1
  33. package/dist/{constants-Cyi942Yr.js → constants-ConHc1oS.js} +5 -5
  34. package/dist/constants-QX2RX-wu.cjs +1 -0
  35. package/dist/filter.cjs +1 -1
  36. package/dist/filter.mjs +1 -1
  37. package/dist/form.cjs +1 -1
  38. package/dist/form.mjs +243 -226
  39. package/dist/table.cjs +1 -1
  40. package/dist/table.mjs +16 -15
  41. package/dist/table.types.d.ts +1 -1
  42. package/dist/table.types.d.ts.map +1 -1
  43. package/dist/types/constants.d.ts +1 -1
  44. package/dist/workflow/Activity.d.ts +5 -8
  45. package/dist/workflow/Activity.d.ts.map +1 -1
  46. package/dist/workflow.cjs +1 -1
  47. package/dist/workflow.mjs +476 -461
  48. package/docs/api.md +95 -0
  49. package/docs/bdo.md +224 -0
  50. package/docs/gaps.md +360 -0
  51. package/docs/useActivityForm.md +393 -0
  52. package/docs/useActivityTable.md +418 -0
  53. package/docs/useBDOForm.md +498 -0
  54. package/docs/useBDOTable.md +284 -0
  55. package/docs/useFilter.md +188 -0
  56. package/docs/workflow.md +560 -0
  57. package/package.json +14 -15
  58. package/sdk/auth/authConfig.ts +1 -1
  59. package/sdk/auth/types.ts +1 -1
  60. package/sdk/bdo/core/Item.ts +10 -1
  61. package/sdk/bdo/fields/ReferenceField.ts +1 -1
  62. package/sdk/bdo/fields/SelectField.ts +1 -1
  63. package/sdk/bdo/fields/UserField.ts +1 -1
  64. package/sdk/components/hooks/useActivityForm/types.ts +6 -4
  65. package/sdk/components/hooks/useActivityForm/useActivityForm.ts +73 -10
  66. package/sdk/components/hooks/useActivityTable/types.ts +5 -4
  67. package/sdk/components/hooks/useActivityTable/useActivityTable.ts +8 -10
  68. package/sdk/components/hooks/useBDOForm/createItemProxy.ts +58 -17
  69. package/sdk/components/hooks/useBDOTable/types.ts +10 -20
  70. package/sdk/components/hooks/useBDOTable/useBDOTable.ts +8 -12
  71. package/sdk/table.types.ts +0 -2
  72. package/sdk/types/constants.ts +1 -1
  73. package/sdk/workflow/Activity.ts +7 -39
  74. package/dist/constants-DEmYwKfC.cjs +0 -1
  75. package/docs/README.md +0 -57
  76. package/docs/bdo/README.md +0 -161
  77. package/docs/bdo/api_reference.md +0 -281
  78. package/docs/examples/bdo/create-product.md +0 -69
  79. package/docs/examples/bdo/edit-product-dialog.md +0 -95
  80. package/docs/examples/bdo/filtered-product-table.md +0 -100
  81. package/docs/examples/bdo/product-listing.md +0 -73
  82. package/docs/examples/bdo/supplier-dropdown.md +0 -60
  83. package/docs/examples/fields/complex-fields.md +0 -248
  84. package/docs/examples/fields/primitive-fields.md +0 -217
  85. package/docs/examples/workflow/approve-leave-request.md +0 -76
  86. package/docs/examples/workflow/filtered-activity-table.md +0 -101
  87. package/docs/examples/workflow/my-pending-requests.md +0 -90
  88. package/docs/examples/workflow/start-new-workflow.md +0 -47
  89. package/docs/examples/workflow/submit-leave-request.md +0 -72
  90. package/docs/examples/workflow/workflow-progress.md +0 -49
  91. package/docs/fields/README.md +0 -141
  92. package/docs/fields/api_reference.md +0 -134
  93. package/docs/useActivityForm/README.md +0 -244
  94. package/docs/useActivityForm/api_reference.md +0 -279
  95. package/docs/useActivityTable/README.md +0 -263
  96. package/docs/useActivityTable/api_reference.md +0 -294
  97. package/docs/useBDOForm/README.md +0 -175
  98. package/docs/useBDOForm/api_reference.md +0 -244
  99. package/docs/useBDOTable/README.md +0 -242
  100. package/docs/useBDOTable/api_reference.md +0 -253
  101. package/docs/useFilter/README.md +0 -323
  102. package/docs/useFilter/api_reference.md +0 -228
  103. package/docs/workflow/README.md +0 -158
  104. package/docs/workflow/api_reference.md +0 -161
  105. /package/docs/{useAuth/README.md → useAuth.md} +0 -0
@@ -1,100 +0,0 @@
1
- # Filtered Product Table
2
-
3
- > Filter products by category and price range using the integrated `table.filter` API.
4
-
5
- ```tsx
6
- import { useState, useMemo } from "react";
7
- import { useBDOTable } from "@ram_28/kf-ai-sdk/table";
8
- import type { UseBDOTableReturnType } from "@ram_28/kf-ai-sdk/table/types";
9
- import { ConditionOperator, RHSType } from "@ram_28/kf-ai-sdk/filter";
10
- import { BuyerProduct } from "@/bdo/buyer/Product";
11
-
12
- export default function FilteredProductTable() {
13
- const product = useMemo(() => new BuyerProduct(), []);
14
- const table: UseBDOTableReturnType<BuyerProduct> = useBDOTable({ bdo: product });
15
-
16
- // ── Category filter (single EQ condition) ─────────────────────
17
- const [categoryFilter, setCategoryFilter] = useState("");
18
- const [categoryConditionId, setCategoryConditionId] = useState<string | null>(null);
19
-
20
- const handleCategoryFilter = (value: string) => {
21
- if (categoryConditionId) {
22
- table.filter.removeCondition(categoryConditionId);
23
- setCategoryConditionId(null);
24
- }
25
- setCategoryFilter(value);
26
- if (value && value !== "all") {
27
- const id = table.filter.addCondition({
28
- LHSField: "Category",
29
- Operator: ConditionOperator.EQ,
30
- RHSType: RHSType.Constant,
31
- RHSValue: value,
32
- });
33
- setCategoryConditionId(id);
34
- }
35
- };
36
-
37
- // ── Price range filter (GTE + LTE conditions) ─────────────────
38
- const [minPrice, setMinPrice] = useState("");
39
- const [maxPrice, setMaxPrice] = useState("");
40
- const [minPriceConditionId, setMinPriceConditionId] = useState<string | null>(null);
41
- const [maxPriceConditionId, setMaxPriceConditionId] = useState<string | null>(null);
42
-
43
- const applyPriceFilter = () => {
44
- if (minPriceConditionId) { table.filter.removeCondition(minPriceConditionId); setMinPriceConditionId(null); }
45
- if (maxPriceConditionId) { table.filter.removeCondition(maxPriceConditionId); setMaxPriceConditionId(null); }
46
-
47
- if (minPrice !== "") {
48
- const id = table.filter.addCondition({
49
- LHSField: "Price", Operator: ConditionOperator.GTE, RHSType: RHSType.Constant, RHSValue: Number(minPrice),
50
- });
51
- setMinPriceConditionId(id);
52
- }
53
- if (maxPrice !== "") {
54
- const id = table.filter.addCondition({
55
- LHSField: "Price", Operator: ConditionOperator.LTE, RHSType: RHSType.Constant, RHSValue: Number(maxPrice),
56
- });
57
- setMaxPriceConditionId(id);
58
- }
59
- };
60
-
61
- if (table.isLoading) return <p>Loading...</p>;
62
-
63
- return (
64
- <div>
65
- {/* Category dropdown */}
66
- <select value={categoryFilter} onChange={(e) => handleCategoryFilter(e.target.value)}>
67
- <option value="all">All categories</option>
68
- {product.Category.options.map((opt) => (
69
- <option key={opt.value} value={opt.value}>{opt.label}</option>
70
- ))}
71
- </select>
72
-
73
- {/* Price range */}
74
- <input type="number" placeholder="Min" value={minPrice} onChange={(e) => setMinPrice(e.target.value)} />
75
- <input type="number" placeholder="Max" value={maxPrice} onChange={(e) => setMaxPrice(e.target.value)} />
76
- <button onClick={applyPriceFilter}>Apply</button>
77
-
78
- {/* Table */}
79
- <table>
80
- <tbody>
81
- {table.rows.map((row) => (
82
- <tr key={row._id}>
83
- <td>{row.Title.get()}</td>
84
- <td>${row.Price.get()?.toFixed(2)}</td>
85
- <td>{row.Category.get()}</td>
86
- </tr>
87
- ))}
88
- </tbody>
89
- </table>
90
- </div>
91
- );
92
- }
93
- ```
94
-
95
- ## Key Patterns
96
-
97
- - **`addCondition()` returns an ID** -- store it in state to remove the condition later with `removeCondition(id)`
98
- - **Category filter** -- single `ConditionOperator.EQ` condition; remove-then-add on every change
99
- - **Price range** -- two separate conditions (`GTE` and `LTE`), each tracked by its own ID
100
- - **Filter changes auto-reset pagination** -- when conditions change, the table resets to page 1
@@ -1,73 +0,0 @@
1
- # Product Listing
2
-
3
- > Browse products with search, sortable columns, and pagination using `useBDOTable`.
4
-
5
- ```tsx
6
- import { useMemo } from "react";
7
- import { useBDOTable } from "@ram_28/kf-ai-sdk/table";
8
- import type { UseBDOTableReturnType } from "@ram_28/kf-ai-sdk/table/types";
9
- import { BuyerProduct } from "@/bdo/buyer/Product";
10
-
11
- export default function ProductListingPage() {
12
- const product = useMemo(() => new BuyerProduct(), []);
13
- const table: UseBDOTableReturnType<BuyerProduct> = useBDOTable({
14
- bdo: product,
15
- initialState: { sort: [{ _created_at: "DESC" }], pagination: { pageNo: 1, pageSize: 10 } },
16
- });
17
-
18
- if (table.isLoading) return <p>Loading...</p>;
19
- if (table.error) return <p>Error: {table.error.message}</p>;
20
-
21
- return (
22
- <div>
23
- {/* Search */}
24
- <input
25
- placeholder="Search by title..."
26
- value={table.search.query}
27
- onChange={(e) => table.search.set("Title", e.target.value)}
28
- />
29
- {table.search.query && <button onClick={table.search.clear}>Clear</button>}
30
-
31
- {/* Table with sortable headers */}
32
- <table>
33
- <thead>
34
- <tr>
35
- <th onClick={() => table.sort.toggle("Title")} style={{ cursor: "pointer" }}>
36
- {product.Title.label}
37
- {table.sort.field === "Title" && (table.sort.direction === "ASC" ? " ↑" : " ↓")}
38
- </th>
39
- <th onClick={() => table.sort.toggle("Price")} style={{ cursor: "pointer" }}>
40
- {product.Price.label}
41
- {table.sort.field === "Price" && (table.sort.direction === "ASC" ? " ↑" : " ↓")}
42
- </th>
43
- <th>{product.Category.label}</th>
44
- </tr>
45
- </thead>
46
- <tbody>
47
- {table.rows.map((row) => (
48
- <tr key={row._id}>
49
- <td>{row.Title.get()}</td>
50
- <td>${row.Price.get()?.toFixed(2)}</td>
51
- <td>{row.Category.get()}</td>
52
- </tr>
53
- ))}
54
- </tbody>
55
- </table>
56
-
57
- {/* Pagination */}
58
- <div>
59
- <button onClick={table.pagination.goToPrevious} disabled={!table.pagination.canGoPrevious}>Previous</button>
60
- <span>Page {table.pagination.pageNo} of {table.pagination.totalPages}</span>
61
- <button onClick={table.pagination.goToNext} disabled={!table.pagination.canGoNext}>Next</button>
62
- </div>
63
- </div>
64
- );
65
- }
66
- ```
67
-
68
- ## Key Patterns
69
-
70
- - **`row.Field.get()`** -- rows are `ItemType` proxies; always use `.get()` to read values
71
- - **`search.set(field, query)`** -- updates the input instantly, debounces the API call (300ms)
72
- - **`sort.toggle(field)`** -- cycles ASC → DESC → cleared on each click
73
- - **Pagination** -- `canGoNext`/`canGoPrevious` disable buttons at boundaries; `pageNo`, `totalPages` for display
@@ -1,60 +0,0 @@
1
- # Supplier Dropdown
2
-
3
- > Lazy-load ReferenceField options when the dropdown opens using `useQuery`.
4
-
5
- ```tsx
6
- import { useState, useMemo } from "react";
7
- import { useQuery } from "@tanstack/react-query";
8
- import { useBDOForm } from "@ram_28/kf-ai-sdk/form";
9
- import type { UseBDOFormReturnType } from "@ram_28/kf-ai-sdk/form/types";
10
- import { BuyerProduct } from "@/bdo/buyer/Product";
11
- import type { ProductSupplierRefType } from "@/bdo/buyer/Product";
12
-
13
- export default function SupplierDropdown({ recordId }: { recordId?: string }) {
14
- const [dropdownOpen, setDropdownOpen] = useState(false);
15
- const product = useMemo(() => new BuyerProduct(), []);
16
-
17
- const { watch, setValue, item }: UseBDOFormReturnType<BuyerProduct> = useBDOForm({ bdo: product, recordId });
18
-
19
- // Lazy-load options only when dropdown opens and item has an _id
20
- const { data: suppliers = [], isFetching } = useQuery<ProductSupplierRefType[]>({
21
- queryKey: ["supplier-options", item._id],
22
- queryFn: () => product.SupplierInfo.fetchOptions(item._id!),
23
- enabled: dropdownOpen && !!item._id,
24
- staleTime: Infinity,
25
- });
26
-
27
- const currentSupplier = watch(product.SupplierInfo.id);
28
-
29
- return (
30
- <div>
31
- <label>{product.SupplierInfo.label}</label>
32
- <select
33
- value={currentSupplier?._id ?? ""}
34
- onFocus={() => setDropdownOpen(true)}
35
- onChange={(e) => {
36
- const supplier = suppliers.find((s) => s._id === e.target.value);
37
- if (supplier) setValue(product.SupplierInfo.id, supplier);
38
- }}
39
- >
40
- <option value="">{currentSupplier?.SupplierName ?? "Select supplier"}</option>
41
- {isFetching ? (
42
- <option disabled>Loading...</option>
43
- ) : (
44
- suppliers.map((s) => (
45
- <option key={s._id} value={s._id}>{s.SupplierName}</option>
46
- ))
47
- )}
48
- </select>
49
- </div>
50
- );
51
- }
52
- ```
53
-
54
- ## Key Patterns
55
-
56
- - **`enabled: dropdownOpen && !!item._id`** -- options aren't fetched until the dropdown opens and the draft/record has an ID
57
- - **`fetchOptions(instanceId)`** -- loads reference options for the current record context
58
- - **`watch()` + `setValue()`** -- reads/writes the entire reference object (`{ _id, SupplierName }`)
59
- - **`referenceFields`** -- `product.SupplierInfo.referenceFields` lists the fields fetched from the referenced BDO
60
- - **`staleTime: Infinity`** -- options are cached and not refetched on re-focus
@@ -1,248 +0,0 @@
1
- # Complex Fields
2
-
3
- > Complete form demonstrating selection, reference, and attachment fields with async options loading and pre-built UI components: Select, Reference, User, File, and Image.
4
-
5
- ```tsx
6
- import { useState, useMemo } from "react";
7
- import { useQuery } from "@tanstack/react-query";
8
- import { useBDOForm } from "@ram_28/kf-ai-sdk/form";
9
- import type { UseBDOFormReturnType } from "@ram_28/kf-ai-sdk/form/types";
10
- import type { UserFieldType } from "@ram_28/kf-ai-sdk/types";
11
- import { AdminFieldTest } from "@/bdo/admin/FieldTest";
12
- import type { FieldTestSupplierRefType } from "@/bdo/entities/field-test";
13
- import {
14
- Field,
15
- FieldContent,
16
- FieldLabel,
17
- FieldError,
18
- } from "@/components/ui/field";
19
- import {
20
- Select,
21
- SelectContent,
22
- SelectItem,
23
- SelectTrigger,
24
- SelectValue,
25
- } from "@/components/ui/select";
26
- import { ReferenceSelect } from "@/components/ui/reference-select";
27
- import { FileUpload } from "@/components/ui/file-upload";
28
- import { ImageUpload } from "@/components/ui/image-upload";
29
- import { FilePreview } from "@/components/ui/file-preview";
30
- import { ImageThumbnail } from "@/components/ui/image-thumbnail";
31
-
32
- export default function ComplexFieldsForm({ recordId }: { recordId?: string }) {
33
- const fieldTest = useMemo(() => new AdminFieldTest(), []);
34
-
35
- const {
36
- watch,
37
- setValue,
38
- handleSubmit,
39
- errors,
40
- item,
41
- isLoading,
42
- isSubmitting,
43
- }: UseBDOFormReturnType<AdminFieldTest> = useBDOForm({
44
- bdo: fieldTest,
45
- recordId,
46
- });
47
-
48
- // Dropdown open state for lazy-loading
49
- const [userDropdownOpen, setUserDropdownOpen] = useState(false);
50
-
51
- // ─── Async Options ─────────────────────────────────────────
52
-
53
- // User field options — lazy-load when dropdown opens
54
- const { data: users = [], isFetching: isUsersLoading } = useQuery<
55
- UserFieldType[]
56
- >({
57
- queryKey: ["fieldtest-user-options", item._id],
58
- queryFn: () => fieldTest.AssignedUser.fetchOptions(item._id!),
59
- enabled: userDropdownOpen && !!item._id,
60
- staleTime: Infinity,
61
- });
62
-
63
- if (isLoading) return <p>Loading...</p>;
64
-
65
- return (
66
- <form onSubmit={handleSubmit((data) => console.log("Saved", data._id))}>
67
- {/* ───────────────────────────────────────────────── */}
68
- {/* SelectField — static options from Constraint.Enum */}
69
- {/* watch() + setValue(), NOT register() */}
70
- {/* ───────────────────────────────────────────────── */}
71
- <Field>
72
- <FieldLabel>
73
- {fieldTest.Status.label} {fieldTest.Status.required && <span>*</span>}
74
- </FieldLabel>
75
- <FieldContent>
76
- <Select
77
- value={watch(fieldTest.Status.id) ?? ""}
78
- onValueChange={(value) => setValue(fieldTest.Status.id, value)}
79
- >
80
- <SelectTrigger>
81
- <SelectValue placeholder="Select status" />
82
- </SelectTrigger>
83
- <SelectContent>
84
- {fieldTest.Status.options.map((opt) => (
85
- <SelectItem key={opt.value} value={opt.value}>
86
- {opt.label}
87
- </SelectItem>
88
- ))}
89
- </SelectContent>
90
- </Select>
91
- </FieldContent>
92
- {errors.Status && <FieldError>{errors.Status.message}</FieldError>}
93
- </Field>
94
-
95
- {/* ───────────────────────────────────────────────── */}
96
- {/* ReferenceField — pre-built <ReferenceSelect> */}
97
- {/* Abstracts fetchOptions + dropdown UI */}
98
- {/* ───────────────────────────────────────────────── */}
99
- <Field>
100
- <FieldLabel>{fieldTest.SupplierRef.label}</FieldLabel>
101
- <FieldContent>
102
- <ReferenceSelect
103
- bdoField={fieldTest.SupplierRef}
104
- instanceId={item._id}
105
- value={watch(fieldTest.SupplierRef.id)}
106
- onChange={(supplier) =>
107
- setValue(
108
- fieldTest.SupplierRef.id,
109
- supplier as FieldTestSupplierRefType,
110
- )
111
- }
112
- />
113
- </FieldContent>
114
- {errors.SupplierRef && <FieldError>{errors.SupplierRef.message}</FieldError>}
115
- </Field>
116
-
117
- {/* ───────────────────────────────────────────────── */}
118
- {/* UserField — fetchOptions() + manual dropdown */}
119
- {/* Value shape: { _id: string; _name: string } */}
120
- {/* ───────────────────────────────────────────────── */}
121
- <Field>
122
- <FieldLabel>{fieldTest.AssignedUser.label}</FieldLabel>
123
- <FieldContent>
124
- <Select
125
- value={watch(fieldTest.AssignedUser.id)?._id ?? ""}
126
- onValueChange={(value) => {
127
- const user = users.find((u) => u._id === value);
128
- if (user) setValue(fieldTest.AssignedUser.id, user);
129
- }}
130
- open={userDropdownOpen}
131
- onOpenChange={setUserDropdownOpen}
132
- >
133
- <SelectTrigger>
134
- <SelectValue placeholder="Select user">
135
- {watch(fieldTest.AssignedUser.id)?._name ?? "Select user"}
136
- </SelectValue>
137
- </SelectTrigger>
138
- <SelectContent>
139
- {isUsersLoading ? (
140
- <div className="py-4 text-center text-sm text-gray-500">
141
- Loading...
142
- </div>
143
- ) : users.length > 0 ? (
144
- users.map((user) => (
145
- <SelectItem key={user._id} value={user._id}>
146
- {user._name}
147
- </SelectItem>
148
- ))
149
- ) : (
150
- <div className="py-4 text-center text-sm text-gray-500">
151
- No users available
152
- </div>
153
- )}
154
- </SelectContent>
155
- </Select>
156
- </FieldContent>
157
- {errors.AssignedUser && <FieldError>{errors.AssignedUser.message}</FieldError>}
158
- </Field>
159
-
160
- {/* ───────────────────────────────────────────────── */}
161
- {/* FileField — <FileUpload> for edit mode */}
162
- {/* Runtime methods (upload, delete) on item accessor */}
163
- {/* ───────────────────────────────────────────────── */}
164
- <Field>
165
- <FieldLabel>{fieldTest.Documents.label}</FieldLabel>
166
- <FieldContent>
167
- <FileUpload
168
- field={fieldTest.Documents}
169
- value={watch(fieldTest.Documents.id)}
170
- boId={fieldTest.meta._id}
171
- instanceId={item._id}
172
- fieldId={fieldTest.Documents.id}
173
- />
174
- </FieldContent>
175
- </Field>
176
-
177
- {/* ───────────────────────────────────────────────── */}
178
- {/* ImageField — <ImageUpload> for edit mode */}
179
- {/* Single image, nullable */}
180
- {/* ───────────────────────────────────────────────── */}
181
- <Field>
182
- <FieldLabel>{fieldTest.Thumbnail.label}</FieldLabel>
183
- <FieldContent>
184
- <ImageUpload
185
- field={fieldTest.Thumbnail}
186
- value={watch(fieldTest.Thumbnail.id)}
187
- boId={fieldTest.meta._id}
188
- instanceId={item._id}
189
- fieldId={fieldTest.Thumbnail.id}
190
- />
191
- </FieldContent>
192
- </Field>
193
-
194
- <button type="submit" disabled={isSubmitting}>
195
- {isSubmitting ? "Saving..." : "Save"}
196
- </button>
197
- </form>
198
- );
199
- }
200
- ```
201
-
202
- ## Read-Only Display
203
-
204
- For tables and detail pages, use `<FilePreview>` and `<ImageThumbnail>`:
205
-
206
- ```tsx
207
- import { FilePreview } from "@/components/ui/file-preview";
208
- import { ImageThumbnail } from "@/components/ui/image-thumbnail";
209
-
210
- // In a table row or detail page
211
- function RecordRow({ record }: { record: ItemType<...> }) {
212
- return (
213
- <div>
214
- {/* File preview — clickable thumbnails for images, file icons for others */}
215
- <FilePreview
216
- boId={fieldTest.meta._id}
217
- instanceId={record._id}
218
- fieldId="Documents"
219
- value={record.Documents.get()}
220
- />
221
-
222
- {/* Image thumbnail — proxy URL with view_type=thumbnail */}
223
- <ImageThumbnail
224
- boId={fieldTest.meta._id}
225
- instanceId={record._id}
226
- fieldId="Thumbnail"
227
- value={record.Thumbnail.get()}
228
- />
229
- </div>
230
- );
231
- }
232
- ```
233
-
234
- ## Key Patterns
235
-
236
- - **All complex fields use `watch()` + `setValue()`** — never `register()`. Select, Reference, User, File, and Image fields use custom components that don't fire native change events.
237
-
238
- - **`fetchOptions()` lazy-loaded with `useQuery`** — Gate with `enabled: dropdownOpen && !!item._id` to avoid fetching until the dropdown opens. Use `staleTime: Infinity` to cache options.
239
-
240
- - **ReferenceField stores the full object** — Not just an ID. The value is `{ _id, SupplierName, Email, ... }`. The `setValue()` call must pass the complete object.
241
-
242
- - **File/Image runtime methods are on `item.Field`** — The `upload()`, `getDownloadUrl()`, `deleteAttachment()` methods live on the `item` accessor (created by the `Item` proxy), not on the BDO field class. The `<FileUpload>` and `<ImageUpload>` components handle this internally.
243
-
244
- - **`<ReferenceSelect>` abstracts the full pattern** — Handles `fetchOptions()`, dropdown UI, search, and option display. Pass `bdoField`, `instanceId`, `value`, and `onChange`.
245
-
246
- - **`<FileUpload>` / `<ImageUpload>` handle the upload lifecycle** — File selection, upload to storage, progress indication, preview, and delete. They need `boId`, `instanceId`, and `fieldId` for API calls.
247
-
248
- - **`<FilePreview>` / `<ImageThumbnail>` for read-only display** — Use in tables, detail pages, or any context where files should be viewable but not editable. They construct proxy URLs for same-origin access.