@ram_28/kf-ai-sdk 2.0.19 → 2.0.20-beta.1
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/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.map +1 -1
- package/dist/bdo.cjs +1 -1
- package/dist/bdo.mjs +32 -32
- 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 +2 -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 +1 -1
- 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 +1 -2
- 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 +5 -9
- 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,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.
|
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
# Primitive Fields
|
|
2
|
-
|
|
3
|
-
> Complete form demonstrating all primitive field types with the correct form binding pattern for each: String, Number, Boolean, Date, DateTime, and Text.
|
|
4
|
-
|
|
5
|
-
```tsx
|
|
6
|
-
import { useMemo } from "react";
|
|
7
|
-
import { useBDOForm } from "@ram_28/kf-ai-sdk/form";
|
|
8
|
-
import type { UseBDOFormReturnType } from "@ram_28/kf-ai-sdk/form/types";
|
|
9
|
-
import { AdminFieldTest } from "@/bdo/admin/FieldTest";
|
|
10
|
-
import {
|
|
11
|
-
Field,
|
|
12
|
-
FieldContent,
|
|
13
|
-
FieldLabel,
|
|
14
|
-
FieldError,
|
|
15
|
-
} from "@/components/ui/field";
|
|
16
|
-
import { Input } from "@/components/ui/input";
|
|
17
|
-
import { Checkbox } from "@/components/ui/checkbox";
|
|
18
|
-
|
|
19
|
-
export default function PrimitiveFieldsForm({ recordId }: { recordId?: string }) {
|
|
20
|
-
const fieldTest = useMemo(() => new AdminFieldTest(), []);
|
|
21
|
-
|
|
22
|
-
const {
|
|
23
|
-
register,
|
|
24
|
-
handleSubmit,
|
|
25
|
-
errors,
|
|
26
|
-
watch,
|
|
27
|
-
setValue,
|
|
28
|
-
isLoading,
|
|
29
|
-
isSubmitting,
|
|
30
|
-
}: UseBDOFormReturnType<AdminFieldTest> = useBDOForm({
|
|
31
|
-
bdo: fieldTest,
|
|
32
|
-
recordId,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
if (isLoading) return <p>Loading...</p>;
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<form onSubmit={handleSubmit((data) => console.log("Saved", data._id))}>
|
|
39
|
-
{/* ───────────────────────────────────────────────── */}
|
|
40
|
-
{/* StringField — register() for native text inputs */}
|
|
41
|
-
{/* ───────────────────────────────────────────────── */}
|
|
42
|
-
<Field>
|
|
43
|
-
<FieldLabel>
|
|
44
|
-
{fieldTest.Name.label} {fieldTest.Name.required && <span>*</span>}
|
|
45
|
-
</FieldLabel>
|
|
46
|
-
<FieldContent>
|
|
47
|
-
<Input
|
|
48
|
-
{...register(fieldTest.Name.id)}
|
|
49
|
-
maxLength={fieldTest.Name.length}
|
|
50
|
-
placeholder="Enter name"
|
|
51
|
-
/>
|
|
52
|
-
{fieldTest.Name.length && (
|
|
53
|
-
<span className="text-xs text-gray-500">
|
|
54
|
-
Max {fieldTest.Name.length} characters
|
|
55
|
-
</span>
|
|
56
|
-
)}
|
|
57
|
-
</FieldContent>
|
|
58
|
-
{errors.Name && <FieldError>{errors.Name.message}</FieldError>}
|
|
59
|
-
</Field>
|
|
60
|
-
|
|
61
|
-
{/* ───────────────────────────────────────────────── */}
|
|
62
|
-
{/* NumberField — register() with type="number" */}
|
|
63
|
-
{/* Derive step from fractionPart */}
|
|
64
|
-
{/* ───────────────────────────────────────────────── */}
|
|
65
|
-
<Field>
|
|
66
|
-
<FieldLabel>
|
|
67
|
-
{fieldTest.Amount.label} {fieldTest.Amount.required && <span>*</span>}
|
|
68
|
-
</FieldLabel>
|
|
69
|
-
<FieldContent>
|
|
70
|
-
<Input
|
|
71
|
-
type="number"
|
|
72
|
-
step={
|
|
73
|
-
fieldTest.Amount.fractionPart
|
|
74
|
-
? `0.${"0".repeat(fieldTest.Amount.fractionPart - 1)}1`
|
|
75
|
-
: "1"
|
|
76
|
-
}
|
|
77
|
-
{...register(fieldTest.Amount.id)}
|
|
78
|
-
placeholder="0.00"
|
|
79
|
-
/>
|
|
80
|
-
<span className="text-xs text-gray-500">
|
|
81
|
-
IntegerPart: {fieldTest.Amount.integerPart}, FractionPart: {fieldTest.Amount.fractionPart ?? "none"}
|
|
82
|
-
</span>
|
|
83
|
-
</FieldContent>
|
|
84
|
-
{errors.Amount && <FieldError>{errors.Amount.message}</FieldError>}
|
|
85
|
-
</Field>
|
|
86
|
-
|
|
87
|
-
{/* Integer-only number (no fractionPart → step="1") */}
|
|
88
|
-
<Field>
|
|
89
|
-
<FieldLabel>{fieldTest.Quantity.label}</FieldLabel>
|
|
90
|
-
<FieldContent>
|
|
91
|
-
<Input
|
|
92
|
-
type="number"
|
|
93
|
-
step="1"
|
|
94
|
-
{...register(fieldTest.Quantity.id)}
|
|
95
|
-
placeholder="0"
|
|
96
|
-
/>
|
|
97
|
-
</FieldContent>
|
|
98
|
-
{errors.Quantity && <FieldError>{errors.Quantity.message}</FieldError>}
|
|
99
|
-
</Field>
|
|
100
|
-
|
|
101
|
-
{/* ───────────────────────────────────────────────── */}
|
|
102
|
-
{/* BooleanField — watch() + setValue(), NOT register */}
|
|
103
|
-
{/* Checkbox doesn't fire native change events */}
|
|
104
|
-
{/* ───────────────────────────────────────────────── */}
|
|
105
|
-
<Field>
|
|
106
|
-
<div className="flex items-center gap-3">
|
|
107
|
-
<Checkbox
|
|
108
|
-
id={fieldTest.IsEnabled.id}
|
|
109
|
-
checked={watch(fieldTest.IsEnabled.id) ?? false}
|
|
110
|
-
onCheckedChange={(checked) =>
|
|
111
|
-
setValue(fieldTest.IsEnabled.id, checked === true)
|
|
112
|
-
}
|
|
113
|
-
/>
|
|
114
|
-
<FieldLabel htmlFor={fieldTest.IsEnabled.id}>
|
|
115
|
-
{fieldTest.IsEnabled.label}
|
|
116
|
-
</FieldLabel>
|
|
117
|
-
</div>
|
|
118
|
-
</Field>
|
|
119
|
-
|
|
120
|
-
{/* ───────────────────────────────────────────────── */}
|
|
121
|
-
{/* DateField — register() with type="date" */}
|
|
122
|
-
{/* Strict YYYY-MM-DD — never default to "" */}
|
|
123
|
-
{/* ───────────────────────────────────────────────── */}
|
|
124
|
-
<Field>
|
|
125
|
-
<FieldLabel>
|
|
126
|
-
{fieldTest.StartDate.label} {fieldTest.StartDate.required && <span>*</span>}
|
|
127
|
-
</FieldLabel>
|
|
128
|
-
<FieldContent>
|
|
129
|
-
<Input type="date" {...register(fieldTest.StartDate.id)} />
|
|
130
|
-
</FieldContent>
|
|
131
|
-
{errors.StartDate && <FieldError>{errors.StartDate.message}</FieldError>}
|
|
132
|
-
</Field>
|
|
133
|
-
|
|
134
|
-
{/* ───────────────────────────────────────────────── */}
|
|
135
|
-
{/* DateTimeField — register() with datetime-local */}
|
|
136
|
-
{/* Add step for Millisecond precision */}
|
|
137
|
-
{/* ───────────────────────────────────────────────── */}
|
|
138
|
-
<Field>
|
|
139
|
-
<FieldLabel>{fieldTest.CreatedOn.label}</FieldLabel>
|
|
140
|
-
<FieldContent>
|
|
141
|
-
<Input
|
|
142
|
-
type="datetime-local"
|
|
143
|
-
step={fieldTest.CreatedOn.precision === "Millisecond" ? "0.001" : undefined}
|
|
144
|
-
{...register(fieldTest.CreatedOn.id)}
|
|
145
|
-
/>
|
|
146
|
-
<span className="text-xs text-gray-500">
|
|
147
|
-
Precision: {fieldTest.CreatedOn.precision}
|
|
148
|
-
</span>
|
|
149
|
-
</FieldContent>
|
|
150
|
-
{errors.CreatedOn && <FieldError>{errors.CreatedOn.message}</FieldError>}
|
|
151
|
-
</Field>
|
|
152
|
-
|
|
153
|
-
{/* ───────────────────────────────────────────────── */}
|
|
154
|
-
{/* TextField — register() with <textarea> */}
|
|
155
|
-
{/* Conditional rendering based on format */}
|
|
156
|
-
{/* ───────────────────────────────────────────────── */}
|
|
157
|
-
<Field>
|
|
158
|
-
<FieldLabel>{fieldTest.Description.label}</FieldLabel>
|
|
159
|
-
<FieldContent>
|
|
160
|
-
<textarea
|
|
161
|
-
{...register(fieldTest.Description.id)}
|
|
162
|
-
rows={4}
|
|
163
|
-
placeholder="Enter plain text description"
|
|
164
|
-
className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm"
|
|
165
|
-
/>
|
|
166
|
-
<span className="text-xs text-gray-500">
|
|
167
|
-
Format: {fieldTest.Description.format}
|
|
168
|
-
</span>
|
|
169
|
-
</FieldContent>
|
|
170
|
-
{errors.Description && <FieldError>{errors.Description.message}</FieldError>}
|
|
171
|
-
</Field>
|
|
172
|
-
|
|
173
|
-
{/* Markdown text field — use a markdown editor */}
|
|
174
|
-
<Field>
|
|
175
|
-
<FieldLabel>{fieldTest.RichContent.label}</FieldLabel>
|
|
176
|
-
<FieldContent>
|
|
177
|
-
{fieldTest.RichContent.format === "Markdown" ? (
|
|
178
|
-
<textarea
|
|
179
|
-
{...register(fieldTest.RichContent.id)}
|
|
180
|
-
rows={6}
|
|
181
|
-
placeholder="Enter markdown content..."
|
|
182
|
-
className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-mono"
|
|
183
|
-
/>
|
|
184
|
-
) : (
|
|
185
|
-
<textarea
|
|
186
|
-
{...register(fieldTest.RichContent.id)}
|
|
187
|
-
rows={4}
|
|
188
|
-
className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm"
|
|
189
|
-
/>
|
|
190
|
-
)}
|
|
191
|
-
<span className="text-xs text-gray-500">
|
|
192
|
-
Format: {fieldTest.RichContent.format}
|
|
193
|
-
</span>
|
|
194
|
-
</FieldContent>
|
|
195
|
-
{errors.RichContent && <FieldError>{errors.RichContent.message}</FieldError>}
|
|
196
|
-
</Field>
|
|
197
|
-
|
|
198
|
-
<button type="submit" disabled={isSubmitting}>
|
|
199
|
-
{isSubmitting ? "Saving..." : "Save"}
|
|
200
|
-
</button>
|
|
201
|
-
</form>
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
## Key Patterns
|
|
207
|
-
|
|
208
|
-
- **`register()` works for** String, Number, Date, DateTime, Text — native HTML inputs that fire change events
|
|
209
|
-
- **`watch()` + `setValue()` needed for** Boolean — Checkbox components don't fire native change events, so `register()` won't pick up value changes
|
|
210
|
-
- **Constraint-derived getters**:
|
|
211
|
-
- `field.length` — max characters (StringField)
|
|
212
|
-
- `field.integerPart` / `field.fractionPart` — numeric precision (NumberField)
|
|
213
|
-
- `field.precision` — `"Second"` or `"Millisecond"` (DateTimeField)
|
|
214
|
-
- `field.format` — `"Plain"` or `"Markdown"` (TextField)
|
|
215
|
-
- **Never default Date/DateTime to empty string** — Use `undefined` for unset dates. Empty strings (`""`) fail DateField validation (`YYYY-MM-DD` regex) and produce confusing errors
|
|
216
|
-
- **Derive `step` from `fractionPart`** — `fractionPart: 2` → `step="0.01"`, no `fractionPart` → `step="1"` (integer only)
|
|
217
|
-
- **Conditional rendering for TextField** — Check `field.format` to switch between `<textarea>` and a markdown editor
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# Approve Leave Request
|
|
2
|
-
|
|
3
|
-
> Manager reviews readonly employee data and approves/rejects using `useActivityForm`.
|
|
4
|
-
|
|
5
|
-
```tsx
|
|
6
|
-
import { useMemo } from "react";
|
|
7
|
-
import { useActivityForm } from "@ram_28/kf-ai-sdk/workflow";
|
|
8
|
-
import type { UseActivityFormReturn } from "@ram_28/kf-ai-sdk/workflow";
|
|
9
|
-
import { ManagerApprovalActivity } from "@/workflow/leave";
|
|
10
|
-
|
|
11
|
-
export default function ApprovalForm({ instanceId, onClose }: { instanceId: string; onClose: () => void }) {
|
|
12
|
-
const activity = useMemo(() => new ManagerApprovalActivity(), []);
|
|
13
|
-
|
|
14
|
-
const { register, handleSubmit, errors, isLoading, isSubmitting, watch, setValue }: UseActivityFormReturn<ManagerApprovalActivity> =
|
|
15
|
-
useActivityForm(activity, { activity_instance_id: instanceId, mode: "onBlur" });
|
|
16
|
-
|
|
17
|
-
if (isLoading) return <p>Loading...</p>;
|
|
18
|
-
|
|
19
|
-
return (
|
|
20
|
-
<div>
|
|
21
|
-
{/* Readonly context fields — auto-disabled by register() */}
|
|
22
|
-
<div>
|
|
23
|
-
<label>{activity.EmployeeName.label}</label>
|
|
24
|
-
<input {...register(activity.EmployeeName.id)} className="bg-gray-100" />
|
|
25
|
-
</div>
|
|
26
|
-
<div>
|
|
27
|
-
<label>{activity.StartDate.label}</label>
|
|
28
|
-
<input type="date" {...register(activity.StartDate.id)} className="bg-gray-100" />
|
|
29
|
-
</div>
|
|
30
|
-
<div>
|
|
31
|
-
<label>{activity.EndDate.label}</label>
|
|
32
|
-
<input type="date" {...register(activity.EndDate.id)} className="bg-gray-100" />
|
|
33
|
-
</div>
|
|
34
|
-
<div>
|
|
35
|
-
<label>{activity.LeaveDays.label}</label>
|
|
36
|
-
<input type="number" {...register(activity.LeaveDays.id)} className="bg-gray-100" />
|
|
37
|
-
</div>
|
|
38
|
-
|
|
39
|
-
<hr />
|
|
40
|
-
|
|
41
|
-
{/* Editable manager decision fields */}
|
|
42
|
-
<div>
|
|
43
|
-
<input
|
|
44
|
-
type="checkbox"
|
|
45
|
-
checked={watch(activity.ManagerApproved.id) ?? false}
|
|
46
|
-
onChange={(e) => setValue(activity.ManagerApproved.id, e.target.checked)}
|
|
47
|
-
/>
|
|
48
|
-
<label>
|
|
49
|
-
{activity.ManagerApproved.label}
|
|
50
|
-
{activity.ManagerApproved.required && <span> *</span>}
|
|
51
|
-
</label>
|
|
52
|
-
{errors.ManagerApproved && <p>{errors.ManagerApproved.message}</p>}
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
<div>
|
|
56
|
-
<label>{activity.ManagerReason.label}</label>
|
|
57
|
-
<textarea {...register(activity.ManagerReason.id)} rows={3} placeholder="Reason for approval or rejection..." />
|
|
58
|
-
{errors.ManagerReason && <p>{errors.ManagerReason.message}</p>}
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
<button onClick={onClose}>Cancel</button>
|
|
62
|
-
<button disabled={isSubmitting} onClick={handleSubmit(() => onClose(), console.error)}>
|
|
63
|
-
{watch(activity.ManagerApproved.id) ? "Approve" : "Reject"} & Submit
|
|
64
|
-
</button>
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## Key Patterns
|
|
71
|
-
|
|
72
|
-
- **Readonly context fields** -- fields from the prior employee activity have `ReadOnly: true` and are auto-disabled by `register()`
|
|
73
|
-
- **Editable decision fields** -- `ManagerApproved` and `ManagerReason` are the only editable fields
|
|
74
|
-
- **Checkbox with `watch()` + `setValue()`** -- booleans use `e.target.checked`, not `e.target.value`
|
|
75
|
-
- **Dynamic submit label** -- `watch(activity.ManagerApproved.id)` drives the button text
|
|
76
|
-
- **`handleSubmit` completes the approval** -- validates, syncs remaining fields, then completes the activity
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# Filtered Activity Table
|
|
2
|
-
|
|
3
|
-
> Filter activity instances by date range using `useActivityTable` with the integrated `table.filter` API.
|
|
4
|
-
|
|
5
|
-
```tsx
|
|
6
|
-
import { useState, useMemo } from "react";
|
|
7
|
-
import { useActivityTable, ActivityTableStatus } from "@ram_28/kf-ai-sdk/workflow";
|
|
8
|
-
import type { UseActivityTableReturnType } from "@ram_28/kf-ai-sdk/workflow";
|
|
9
|
-
import { ConditionOperator, RHSType } from "@ram_28/kf-ai-sdk/filter";
|
|
10
|
-
import { EmployeeInputActivity } from "@/workflow/leave";
|
|
11
|
-
|
|
12
|
-
export default function FilteredActivityTable() {
|
|
13
|
-
const activity = useMemo(() => new EmployeeInputActivity(), []);
|
|
14
|
-
const table: UseActivityTableReturnType<EmployeeInputActivity> = useActivityTable({ activity, status: ActivityTableStatus.InProgress });
|
|
15
|
-
|
|
16
|
-
// ── Date range filter on StartDate ─────────────────────────────
|
|
17
|
-
const [startDateFrom, setStartDateFrom] = useState("");
|
|
18
|
-
const [startDateTo, setStartDateTo] = useState("");
|
|
19
|
-
const [fromConditionId, setFromConditionId] = useState<string | null>(null);
|
|
20
|
-
const [toConditionId, setToConditionId] = useState<string | null>(null);
|
|
21
|
-
|
|
22
|
-
const applyDateFilter = () => {
|
|
23
|
-
if (fromConditionId) { table.filter.removeCondition(fromConditionId); setFromConditionId(null); }
|
|
24
|
-
if (toConditionId) { table.filter.removeCondition(toConditionId); setToConditionId(null); }
|
|
25
|
-
|
|
26
|
-
if (startDateFrom) {
|
|
27
|
-
const id = table.filter.addCondition({
|
|
28
|
-
LHSField: activity.StartDate.id,
|
|
29
|
-
Operator: ConditionOperator.GTE,
|
|
30
|
-
RHSType: RHSType.Constant,
|
|
31
|
-
RHSValue: startDateFrom,
|
|
32
|
-
});
|
|
33
|
-
setFromConditionId(id);
|
|
34
|
-
}
|
|
35
|
-
if (startDateTo) {
|
|
36
|
-
const id = table.filter.addCondition({
|
|
37
|
-
LHSField: activity.StartDate.id,
|
|
38
|
-
Operator: ConditionOperator.LTE,
|
|
39
|
-
RHSType: RHSType.Constant,
|
|
40
|
-
RHSValue: startDateTo,
|
|
41
|
-
});
|
|
42
|
-
setToConditionId(id);
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const clearDateFilter = () => {
|
|
47
|
-
if (fromConditionId) { table.filter.removeCondition(fromConditionId); setFromConditionId(null); }
|
|
48
|
-
if (toConditionId) { table.filter.removeCondition(toConditionId); setToConditionId(null); }
|
|
49
|
-
setStartDateFrom("");
|
|
50
|
-
setStartDateTo("");
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
if (table.isLoading) return <p>Loading...</p>;
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<div>
|
|
57
|
-
{/* Date range filter */}
|
|
58
|
-
<div>
|
|
59
|
-
<label>From</label>
|
|
60
|
-
<input type="date" value={startDateFrom} onChange={(e) => setStartDateFrom(e.target.value)} />
|
|
61
|
-
<label>To</label>
|
|
62
|
-
<input type="date" value={startDateTo} onChange={(e) => setStartDateTo(e.target.value)} />
|
|
63
|
-
<button onClick={applyDateFilter}>Apply</button>
|
|
64
|
-
{table.filter.hasConditions && <button onClick={clearDateFilter}>Clear</button>}
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
{/* Table */}
|
|
68
|
-
<table>
|
|
69
|
-
<thead>
|
|
70
|
-
<tr>
|
|
71
|
-
<th>{activity.StartDate.label}</th>
|
|
72
|
-
<th>{activity.EndDate.label}</th>
|
|
73
|
-
<th>{activity.LeaveType.label}</th>
|
|
74
|
-
<th>{activity.LeaveDays.label}</th>
|
|
75
|
-
</tr>
|
|
76
|
-
</thead>
|
|
77
|
-
<tbody>
|
|
78
|
-
{table.rows.map((row) => (
|
|
79
|
-
<tr key={row._id}>
|
|
80
|
-
<td>{row.StartDate.get()}</td>
|
|
81
|
-
<td>{row.EndDate.get()}</td>
|
|
82
|
-
<td>{row.LeaveType.get()}</td>
|
|
83
|
-
<td>{row.LeaveDays.get()}</td>
|
|
84
|
-
</tr>
|
|
85
|
-
))}
|
|
86
|
-
</tbody>
|
|
87
|
-
</table>
|
|
88
|
-
|
|
89
|
-
<span>Page {table.pagination.pageNo} of {table.pagination.totalPages}</span>
|
|
90
|
-
</div>
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## Key Patterns
|
|
96
|
-
|
|
97
|
-
- **Date range with GTE + LTE** -- two separate conditions tracked by their own IDs
|
|
98
|
-
- **`addCondition()` returns an ID** -- store it to later remove the condition with `removeCondition(id)`
|
|
99
|
-
- **`table.filter.hasConditions`** -- show a "Clear" button only when filters are active
|
|
100
|
-
- **Filter changes auto-reset pagination** -- the table resets to page 1 when conditions change
|
|
101
|
-
- **Same filter API as `useBDOTable`** -- `table.filter` works identically in both BDO and Activity tables
|