@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.
- package/README.md +8 -16
- package/dist/{FileField-CZjS2uLh.js → FileField-BWrSHNRq.js} +3 -3
- package/dist/{FileField-DU4UWo_t.cjs → FileField-eDeuzln8.cjs} +1 -1
- package/dist/api.cjs +1 -1
- package/dist/api.mjs +1 -1
- package/dist/auth/authConfig.d.ts +1 -1
- package/dist/auth/types.d.ts +1 -1
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/auth.cjs +1 -1
- package/dist/auth.mjs +1 -1
- package/dist/bdo/core/Item.d.ts +4 -0
- package/dist/bdo/core/Item.d.ts.map +1 -1
- package/dist/bdo/fields/ReferenceField.d.ts +1 -1
- 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 +1 -1
- package/dist/bdo/fields/UserField.d.ts.map +1 -1
- package/dist/bdo.cjs +1 -1
- package/dist/bdo.mjs +62 -53
- package/dist/components/hooks/useActivityForm/types.d.ts +5 -4
- package/dist/components/hooks/useActivityForm/types.d.ts.map +1 -1
- package/dist/components/hooks/useActivityForm/useActivityForm.d.ts.map +1 -1
- package/dist/components/hooks/useActivityTable/types.d.ts +4 -5
- package/dist/components/hooks/useActivityTable/types.d.ts.map +1 -1
- package/dist/components/hooks/useActivityTable/useActivityTable.d.ts.map +1 -1
- package/dist/components/hooks/useBDOForm/createItemProxy.d.ts +3 -2
- package/dist/components/hooks/useBDOForm/createItemProxy.d.ts.map +1 -1
- package/dist/components/hooks/useBDOTable/types.d.ts +12 -20
- package/dist/components/hooks/useBDOTable/types.d.ts.map +1 -1
- package/dist/components/hooks/useBDOTable/useBDOTable.d.ts +2 -2
- package/dist/components/hooks/useBDOTable/useBDOTable.d.ts.map +1 -1
- package/dist/{constants-Cyi942Yr.js → constants-ConHc1oS.js} +5 -5
- package/dist/constants-QX2RX-wu.cjs +1 -0
- package/dist/filter.cjs +1 -1
- package/dist/filter.mjs +1 -1
- package/dist/form.cjs +1 -1
- package/dist/form.mjs +243 -226
- package/dist/table.cjs +1 -1
- package/dist/table.mjs +16 -15
- package/dist/table.types.d.ts +1 -1
- package/dist/table.types.d.ts.map +1 -1
- package/dist/types/constants.d.ts +1 -1
- package/dist/workflow/Activity.d.ts +5 -8
- package/dist/workflow/Activity.d.ts.map +1 -1
- package/dist/workflow.cjs +1 -1
- package/dist/workflow.mjs +476 -461
- package/docs/api.md +95 -0
- package/docs/bdo.md +224 -0
- package/docs/gaps.md +360 -0
- package/docs/useActivityForm.md +393 -0
- package/docs/useActivityTable.md +418 -0
- package/docs/useBDOForm.md +498 -0
- package/docs/useBDOTable.md +284 -0
- package/docs/useFilter.md +188 -0
- package/docs/workflow.md +560 -0
- package/package.json +14 -15
- package/sdk/auth/authConfig.ts +1 -1
- package/sdk/auth/types.ts +1 -1
- package/sdk/bdo/core/Item.ts +10 -1
- package/sdk/bdo/fields/ReferenceField.ts +1 -1
- package/sdk/bdo/fields/SelectField.ts +1 -1
- package/sdk/bdo/fields/UserField.ts +1 -1
- package/sdk/components/hooks/useActivityForm/types.ts +6 -4
- package/sdk/components/hooks/useActivityForm/useActivityForm.ts +73 -10
- package/sdk/components/hooks/useActivityTable/types.ts +5 -4
- package/sdk/components/hooks/useActivityTable/useActivityTable.ts +8 -10
- package/sdk/components/hooks/useBDOForm/createItemProxy.ts +58 -17
- package/sdk/components/hooks/useBDOTable/types.ts +10 -20
- package/sdk/components/hooks/useBDOTable/useBDOTable.ts +8 -12
- package/sdk/table.types.ts +0 -2
- package/sdk/types/constants.ts +1 -1
- package/sdk/workflow/Activity.ts +7 -39
- package/dist/constants-DEmYwKfC.cjs +0 -1
- package/docs/README.md +0 -57
- package/docs/bdo/README.md +0 -161
- package/docs/bdo/api_reference.md +0 -281
- package/docs/examples/bdo/create-product.md +0 -69
- package/docs/examples/bdo/edit-product-dialog.md +0 -95
- package/docs/examples/bdo/filtered-product-table.md +0 -100
- package/docs/examples/bdo/product-listing.md +0 -73
- package/docs/examples/bdo/supplier-dropdown.md +0 -60
- package/docs/examples/fields/complex-fields.md +0 -248
- package/docs/examples/fields/primitive-fields.md +0 -217
- package/docs/examples/workflow/approve-leave-request.md +0 -76
- package/docs/examples/workflow/filtered-activity-table.md +0 -101
- package/docs/examples/workflow/my-pending-requests.md +0 -90
- package/docs/examples/workflow/start-new-workflow.md +0 -47
- package/docs/examples/workflow/submit-leave-request.md +0 -72
- package/docs/examples/workflow/workflow-progress.md +0 -49
- package/docs/fields/README.md +0 -141
- package/docs/fields/api_reference.md +0 -134
- package/docs/useActivityForm/README.md +0 -244
- package/docs/useActivityForm/api_reference.md +0 -279
- package/docs/useActivityTable/README.md +0 -263
- package/docs/useActivityTable/api_reference.md +0 -294
- package/docs/useBDOForm/README.md +0 -175
- package/docs/useBDOForm/api_reference.md +0 -244
- package/docs/useBDOTable/README.md +0 -242
- package/docs/useBDOTable/api_reference.md +0 -253
- package/docs/useFilter/README.md +0 -323
- package/docs/useFilter/api_reference.md +0 -228
- package/docs/workflow/README.md +0 -158
- package/docs/workflow/api_reference.md +0 -161
- /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.
|