@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/workflow.md CHANGED
@@ -11,8 +11,6 @@ import {
11
11
  Activity,
12
12
  ActivityInstance,
13
13
  useActivityForm,
14
- useActivityTable,
15
- ActivityTableStatus,
16
14
  } from "@ram_28/kf-ai-sdk/workflow";
17
15
 
18
16
  // Type-only exports
@@ -22,9 +20,6 @@ import type {
22
20
  WorkflowStartResponseType,
23
21
  UseActivityFormOptions,
24
22
  UseActivityFormReturn,
25
- UseActivityTableOptionsType,
26
- UseActivityTableReturnType,
27
- ActivityRowType,
28
23
  } from "@ram_28/kf-ai-sdk/workflow";
29
24
 
30
25
  // Field classes (for defining Activity fields)
@@ -87,72 +82,11 @@ System fields present on every activity instance. Returned alongside activity-sp
87
82
  type ActivityInstanceFieldsType = {
88
83
  _id: StringFieldType;
89
84
  Status: SelectFieldType<"InProgress" | "Completed">;
90
- AssignedTo: UserFieldType;
85
+ AssignedTo: ReferenceFieldType<{ _id: StringFieldType; username: StringFieldType }>;
91
86
  CompletedAt: DateTimeFieldType;
92
87
  };
93
88
  ```
94
89
 
95
- ### ActivityTableStatus (constant)
96
-
97
- ```typescript
98
- const ActivityTableStatus = {
99
- InProgress: 'inprogress',
100
- Completed: 'completed',
101
- } as const;
102
-
103
- type ActivityTableStatusType =
104
- (typeof ActivityTableStatus)[keyof typeof ActivityTableStatus];
105
- ```
106
-
107
- ### ActivityRowType\<A\>
108
-
109
- Row type for activity table data. Combines activity instance system fields with entity-specific fields.
110
-
111
- ```typescript
112
- type ActivityRowType<A extends Activity<any, any, any>> =
113
- ActivityInstanceFieldsType & ExtractActivityEntity<A>;
114
- ```
115
-
116
- Concrete example — for `ManagerApprovalActivity` with `{ ManagerApproved: boolean, ManagerReason: string }`:
117
-
118
- ```typescript
119
- // ActivityRowType<ManagerApprovalActivity> resolves to:
120
- {
121
- // System fields (from ActivityInstanceFieldsType)
122
- _id: string;
123
- Status: "InProgress" | "Completed";
124
- AssignedTo: UserFieldType;
125
- CompletedAt: string;
126
-
127
- // Entity fields (from ManagerApprovalEntityType)
128
- ManagerApproved: boolean;
129
- ManagerReason: string;
130
- }
131
- ```
132
-
133
- ### UseActivityTableOptionsType\<A\>
134
-
135
- ```typescript
136
- interface UseActivityTableOptionsType<A extends Activity<any, any, any>> {
137
- status: ActivityTableStatusType;
138
- onError?: (error: Error) => void;
139
- onSuccess?: (data: ActivityRowType<A>[]) => void;
140
- }
141
- ```
142
-
143
- ### UseActivityTableReturnType\<A\>
144
-
145
- ```typescript
146
- interface UseActivityTableReturnType<A extends Activity<any, any, any>> {
147
- rows: ActivityRowType<A>[];
148
- totalItems: number;
149
- isLoading: boolean;
150
- isFetching: boolean;
151
- error: Error | null;
152
- refetch: () => Promise<ListResponseType<ActivityRowType<A>>>;
153
- }
154
- ```
155
-
156
90
  ### UseActivityFormOptions\<A\>
157
91
 
158
92
  ```typescript
@@ -448,40 +382,6 @@ User clicks Complete
448
382
 
449
383
  ---
450
384
 
451
- ## useActivityTable Hook
452
-
453
- React hook for listing workflow activity instances. Fetches data from
454
- `getInProgressList()` or `getCompletedList()` and the corresponding
455
- metrics endpoint.
456
-
457
- ### Signature
458
-
459
- ```typescript
460
- useActivityTable(activity: A, options: UseActivityTableOptionsType<A>)
461
- : UseActivityTableReturnType<A>
462
- ```
463
-
464
- ### Options
465
-
466
- | Property | Type | Default | Description |
467
- |----------|------|---------|-------------|
468
- | `status` | `ActivityTableStatusType` | *required* | `ActivityTableStatus.InProgress` or `ActivityTableStatus.Completed` |
469
- | `onError` | `(error: Error) => void` | — | Error callback |
470
- | `onSuccess` | `(data: ActivityRowType<A>[]) => void` | — | Success callback |
471
-
472
- ### Return Value
473
-
474
- | Property | Type | Description |
475
- |----------|------|-------------|
476
- | `rows` | `ActivityRowType<A>[]` | Activity instance records (system + entity fields) |
477
- | `totalItems` | `number` | Total count (from metrics endpoint) |
478
- | `isLoading` | `boolean` | Initial load in progress |
479
- | `isFetching` | `boolean` | Any fetch in progress (including refetch) |
480
- | `error` | `Error \| null` | Fetch error |
481
- | `refetch` | `() => Promise<...>` | Refetch both list and metrics |
482
-
483
- ---
484
-
485
385
  ## Use Case: Employee Creating Leave
486
386
 
487
387
  ### Step 1 — Start the workflow
@@ -642,63 +542,18 @@ function LeaveRequestPage() {
642
542
 
643
543
  ## Use Case: Manager Approving Leave
644
544
 
645
- ### Step 1 — List in-progress items with useActivityTable
545
+ ### Step 1 — List in-progress items
646
546
 
647
- ```tsx
648
- import { useMemo, useState } from "react";
649
- import { useActivityTable, ActivityTableStatus } from "@ram_28/kf-ai-sdk/workflow";
650
- import { SimpleLeaveProcess, ManagerApprovalActivity } from "@/bdo/workflows/SimpleLeaveProcess";
651
-
652
- function ManagerApprovalPage() {
653
- const activity = useMemo(() => new SimpleLeaveProcess().managerApprovalActivity(), []);
654
- const [selectedId, setSelectedId] = useState<string | null>(null);
547
+ ```typescript
548
+ import { SimpleLeaveProcess } from "@/bdo/workflows/SimpleLeaveProcess";
655
549
 
656
- const { rows, totalItems, isLoading, error, refetch } = useActivityTable(activity, {
657
- status: ActivityTableStatus.InProgress,
658
- });
550
+ const wf = new SimpleLeaveProcess();
551
+ const activity = wf.managerApprovalActivity();
659
552
 
660
- if (isLoading) return <div>Loading...</div>;
661
- if (error) return <div>Error: {error.message}</div>;
662
-
663
- if (selectedId) {
664
- return (
665
- <ApprovalForm
666
- activityInstanceId={selectedId}
667
- onComplete={() => {
668
- setSelectedId(null);
669
- refetch();
670
- }}
671
- />
672
- );
673
- }
553
+ const result = await activity.getInProgressList();
674
554
 
675
- return (
676
- <div>
677
- <h2>Pending Approvals ({totalItems})</h2>
678
- <table>
679
- <thead>
680
- <tr>
681
- <th>ID</th>
682
- <th>Status</th>
683
- <th>Assigned To</th>
684
- <th>Action</th>
685
- </tr>
686
- </thead>
687
- <tbody>
688
- {rows.map((row) => (
689
- <tr key={row._id}>
690
- <td>{row._id}</td>
691
- <td>{row.Status}</td>
692
- <td>{row.AssignedTo._name}</td>
693
- <td>
694
- <button onClick={() => setSelectedId(row._id)}>Review</button>
695
- </td>
696
- </tr>
697
- ))}
698
- </tbody>
699
- </table>
700
- </div>
701
- );
555
+ for (const item of result.Data) {
556
+ console.log(item._id, item.Status, item.AssignedTo.username);
702
557
  }
703
558
  ```
704
559
 
@@ -848,7 +703,7 @@ const progressList = await wf.progress(BPInstanceId);
848
703
  |-------|------|-------------|
849
704
  | `_id` | `StringFieldType` | Unique activity instance identifier |
850
705
  | `Status` | `SelectFieldType<"InProgress" \| "Completed">` | Current status |
851
- | `AssignedTo` | `UserFieldType` | Assigned user (has `._id` and `._name`) |
706
+ | `AssignedTo` | `ReferenceFieldType<UserRefType>` | Assigned user (has `._id` and `.username`) |
852
707
  | `CompletedAt` | `DateTimeFieldType` | Completion timestamp (`"YYYY-MM-DDTHH:MM:SS"`) |
853
708
 
854
709
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ram_28/kf-ai-sdk",
3
- "version": "2.0.12",
3
+ "version": "2.0.13",
4
4
  "description": "Type-safe, AI-driven SDK for building modern web applications with role-based access control",
5
5
  "author": "Ramprasad",
6
6
  "license": "MIT",
package/sdk/api/client.ts CHANGED
@@ -253,7 +253,7 @@ export function createResourceClient<T = any>(
253
253
  throw new Error(`Failed to create ${basePath}: ${response.statusText}`);
254
254
  }
255
255
 
256
- return response.json();
256
+ return (await response.json()).Data;
257
257
  },
258
258
 
259
259
  async update(
@@ -272,7 +272,7 @@ export function createResourceClient<T = any>(
272
272
  );
273
273
  }
274
274
 
275
- return response.json();
275
+ return (await response.json()).Data;
276
276
  },
277
277
 
278
278
  async delete(id: string): Promise<DeleteResponseType> {
@@ -555,7 +555,12 @@ export function createResourceClient<T = any>(
555
555
 
556
556
  const responseData: { Data: FileDownloadResponseType } =
557
557
  await response.json();
558
- return responseData.Data;
558
+ const downloadData = responseData.Data;
559
+ // Normalize: runtime returns DownloadUrl, components expect URL
560
+ if (downloadData && (downloadData as any).DownloadUrl && !(downloadData as any).URL) {
561
+ (downloadData as any).URL = (downloadData as any).DownloadUrl;
562
+ }
563
+ return downloadData;
559
564
  },
560
565
 
561
566
  async getDownloadUrls(
@@ -578,7 +583,16 @@ export function createResourceClient<T = any>(
578
583
 
579
584
  const responseData: { Data: FileDownloadResponseType[] } =
580
585
  await response.json();
581
- return responseData.Data;
586
+ const downloadList = responseData.Data;
587
+ // Normalize: runtime returns DownloadUrl, components expect URL
588
+ if (Array.isArray(downloadList)) {
589
+ downloadList.forEach((item) => {
590
+ if (item && (item as any).DownloadUrl && !(item as any).URL) {
591
+ (item as any).URL = (item as any).DownloadUrl;
592
+ }
593
+ });
594
+ }
595
+ return downloadList;
582
596
  },
583
597
 
584
598
  async deleteAttachment(
@@ -119,6 +119,7 @@ export interface UserFieldMetaType extends BaseFieldMetaType {
119
119
  Type: "User";
120
120
  Constraint?: BaseConstraintType;
121
121
  View?: {
122
+ DataObject?: { Type: string; Id: string };
122
123
  Filter?: Record<string, unknown>;
123
124
  Sort?: unknown[];
124
125
  BusinessEntity?: string;
@@ -27,7 +27,8 @@ export class NumberField extends BaseField<NumberFieldType> {
27
27
  get fractionPart(): number | undefined { return (this._meta as NumberFieldMetaType).Constraint?.FractionPart; }
28
28
 
29
29
  validate(value: NumberFieldType | undefined): ValidationResultType {
30
- if (value !== undefined && value !== null && (typeof value !== "number" || isNaN(value))) {
30
+ if (value == null || value === ("" as any)) return { valid: true, errors: [] };
31
+ if (typeof value !== "number" || isNaN(value)) {
31
32
  return {
32
33
  valid: false,
33
34
  errors: [`${this.label} must be a valid number`],
@@ -68,14 +68,15 @@ export class ReferenceField<TRef = unknown> extends BaseField<
68
68
  }
69
69
 
70
70
  /**
71
- * Fetch referenced records from the backend, returned as typed TRef[]
71
+ * Fetch referenced records from the backend via the fetchField API.
72
+ * Requires the field to be bound to a parent BDO.
72
73
  */
73
- async fetchOptions(instanceId?: string): Promise<TRef[]> {
74
+ async fetchOptions(instanceId: string): Promise<TRef[]> {
74
75
  if (!this._parentBoId) {
75
76
  throw new Error(
76
77
  `Field ${this.id} not bound to a BDO. Cannot fetch options.`
77
78
  );
78
79
  }
79
- return api(this._parentBoId).fetchField<TRef>(instanceId ?? "new", this.id);
80
+ return api(this._parentBoId).fetchField<TRef>(instanceId, this.id);
80
81
  }
81
82
  }
@@ -57,14 +57,14 @@ export class SelectField<T extends string | number = string> extends BaseField<T
57
57
  /**
58
58
  * Fetch dynamic options from the backend, returned as typed SelectOption[]
59
59
  */
60
- async fetchOptions(instanceId?: string): Promise<SelectOptionType<T>[]> {
60
+ async fetchOptions(instanceId: string): Promise<SelectOptionType<T>[]> {
61
61
  if (!this._parentBoId) {
62
62
  throw new Error(
63
63
  `Field ${this.id} not bound to a BDO. Cannot fetch options.`
64
64
  );
65
65
  }
66
66
  const response = await api(this._parentBoId).fetchField<FetchFieldOptionType>(
67
- instanceId ?? "new",
67
+ instanceId,
68
68
  this.id
69
69
  );
70
70
  return response.map((item) => ({
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { UserFieldType } from "../../types/base-fields";
7
7
  import type { UserFieldMetaType, ValidationResultType } from "../core/types";
8
+ import { api } from "../../api/client";
8
9
  import { BaseField } from "./BaseField";
9
10
 
10
11
  /**
@@ -27,6 +28,19 @@ export class UserField extends BaseField<UserFieldType> {
27
28
  return (this._meta as UserFieldMetaType).View?.BusinessEntity;
28
29
  }
29
30
 
31
+ /**
32
+ * Fetch user records from the backend via the fetchField API.
33
+ * Requires the field to be bound to a parent BDO.
34
+ */
35
+ async fetchOptions(instanceId: string): Promise<UserFieldType[]> {
36
+ if (!this._parentBoId) {
37
+ throw new Error(
38
+ `Field ${this.id} not bound to a BDO. Cannot fetch options.`
39
+ );
40
+ }
41
+ return api(this._parentBoId).fetchField<UserFieldType>(instanceId, this.id);
42
+ }
43
+
30
44
  validate(value: UserFieldType | undefined): ValidationResultType {
31
45
  if (value === undefined || value === null) {
32
46
  return { valid: true, errors: [] };
@@ -2,6 +2,10 @@ import type { UseFormReturn, Path, FieldValues } from "react-hook-form";
2
2
  import type { BaseBdo } from "../../../bdo";
3
3
  import type { BaseFieldMetaType } from "../../../bdo/core/types";
4
4
  import type { BaseField } from "../../../bdo/fields/BaseField";
5
+ import type { FileType } from "../../../types/base-fields";
6
+ import type { FileDownloadResponseType, AttachmentViewType } from "../../../types/common";
7
+ import { api } from "../../../api/client";
8
+ import { validateFileExtension, extractFileExtension } from "../../../bdo/fields/attachment-constants";
5
9
  import { validateConstraints } from "./createResolver";
6
10
  import type {
7
11
  FormItemType,
@@ -17,6 +21,10 @@ import type {
17
21
  * Key principle: Item has NO state. It's a view over RHF's state.
18
22
  * Editable fields get set(), readonly fields do not.
19
23
  *
24
+ * Draft-based upload: In create mode (no _id), upload() automatically creates
25
+ * a draft record via draftInteraction() to get an _id, then uploads immediately.
26
+ * On form submit, if a draft _id exists, update() is used instead of create().
27
+ *
20
28
  * @param bdo - The BDO instance for field metadata
21
29
  * @param form - The RHF useForm return object
22
30
  * @returns SmartFormItem proxy
@@ -28,6 +36,35 @@ export function createItemProxy<B extends BaseBdo<any, any, any>>(
28
36
  const fields = bdo.getFields();
29
37
  const accessorCache = new Map<string, EditableFormFieldAccessorType<unknown> | ReadonlyFormFieldAccessorType<unknown>>();
30
38
 
39
+ // Draft tracking for create mode — shared across all attachment fields in this form
40
+ const boIdShared = bdo.getBoId();
41
+ let draftId: string | null = null;
42
+ let draftPromise: Promise<string> | null = null;
43
+
44
+ /**
45
+ * Ensures a record _id exists for attachment uploads.
46
+ * In edit mode, returns the existing _id.
47
+ * In create mode, creates a draft record via draftInteraction() to get an _id.
48
+ * The draft _id is shared across all attachment fields and only created once.
49
+ */
50
+ async function ensureDraft(): Promise<string> {
51
+ // If form already has an _id (edit mode or previous draft), use it
52
+ const existing = form.getValues("_id" as Path<FieldValues>) as string | undefined;
53
+ if (existing) return existing;
54
+ if (draftId) return draftId;
55
+ if (!draftPromise) {
56
+ draftPromise = api(boIdShared).draftInteraction({}).then((d: any) => {
57
+ draftId = d._id;
58
+ form.setValue("_id" as Path<FieldValues>, draftId as any, { shouldDirty: false });
59
+ return draftId!;
60
+ }).catch((err: Error) => {
61
+ draftPromise = null;
62
+ throw err;
63
+ });
64
+ }
65
+ return draftPromise;
66
+ }
67
+
31
68
  return new Proxy({} as FormItemType<ExtractEditableType<B>, ExtractReadonlyType<B>>, {
32
69
  get(_, prop: string | symbol) {
33
70
  // Handle symbol properties (e.g., Symbol.toStringTag)
@@ -50,6 +87,11 @@ export function createItemProxy<B extends BaseBdo<any, any, any>>(
50
87
  return () => form.trigger();
51
88
  }
52
89
 
90
+ // Internal: check if a draft was created (used by handleSubmit)
91
+ if (prop === "_hasDraft") {
92
+ return () => !!draftId;
93
+ }
94
+
53
95
  // Return cached accessor if available
54
96
  if (accessorCache.has(prop)) {
55
97
  return accessorCache.get(prop);
@@ -67,7 +109,21 @@ export function createItemProxy<B extends BaseBdo<any, any, any>>(
67
109
  // Full validation: type + constraint + expression (matches createResolver pipeline)
68
110
  const validate = () => {
69
111
  if (!bdoField) return { valid: true, errors: [] };
70
- const value = form.getValues(prop as Path<FieldValues>);
112
+ let value = form.getValues(prop as Path<FieldValues>);
113
+
114
+ // Coerce string → number for NumberField (HTML inputs always send strings)
115
+ if ("integerPart" in bdoField && typeof value === "string" && value !== "") {
116
+ const num = Number(value);
117
+ if (!isNaN(num)) {
118
+ value = num;
119
+ form.setValue(prop as Path<FieldValues>, num as any, { shouldDirty: false });
120
+ }
121
+ }
122
+
123
+ // Match backend BDO core: skip ALL validation for non-required empty fields
124
+ if (!bdoField.required && (value == null || value === "" || (Array.isArray(value) && value.length === 0))) {
125
+ return { valid: true, errors: [] };
126
+ }
71
127
 
72
128
  // 1. Type validation
73
129
  const typeResult = bdoField.validate(value);
@@ -98,13 +154,20 @@ export function createItemProxy<B extends BaseBdo<any, any, any>>(
98
154
  };
99
155
 
100
156
  if (!isReadOnly) {
157
+ // Defensive get(): File fields return [] instead of null/undefined, Image returns null
158
+ const fieldGet = () => {
159
+ const val = form.getValues(prop as Path<FieldValues>);
160
+ if (fieldMeta.Type === "File") return val ?? [];
161
+ return val;
162
+ };
163
+
101
164
  const accessor: EditableFormFieldAccessorType<unknown> = {
102
165
  label: bdoField?.label ?? prop,
103
166
  required: bdoField?.required ?? false,
104
167
  readOnly: false,
105
168
  defaultValue: bdoField?.defaultValue,
106
169
  meta: fieldMeta,
107
- get: () => form.getValues(prop as Path<FieldValues>),
170
+ get: fieldGet,
108
171
  getOrDefault,
109
172
  set: (value: unknown) => {
110
173
  form.setValue(prop as Path<FieldValues>, value as any, {
@@ -115,20 +178,172 @@ export function createItemProxy<B extends BaseBdo<any, any, any>>(
115
178
  },
116
179
  validate,
117
180
  };
181
+
182
+ // Enrich Image/File field accessors with attachment methods (draft-based upload)
183
+ if (fieldMeta.Type === "Image" || fieldMeta.Type === "File") {
184
+ const boId = boIdShared;
185
+ const requireInstanceId = (): string => {
186
+ const id = form.getValues("_id" as Path<FieldValues>) as string | undefined;
187
+ if (!id) throw new Error("Save the record before attachment operations");
188
+ return id;
189
+ };
190
+
191
+ if (fieldMeta.Type === "Image") {
192
+ // Image: single file upload — always uploads immediately (draft in create mode)
193
+ (accessor as any).upload = async (file: File): Promise<FileType> => {
194
+ validateFileExtension(file.name, "Image");
195
+ const id = await ensureDraft();
196
+
197
+ const [uploadInfo] = await api(boId).getUploadUrl(id, prop, [
198
+ { FileName: file.name, Size: file.size, FileExtension: extractFileExtension(file.name) },
199
+ ]);
200
+ await fetch(uploadInfo.UploadUrl.URL, {
201
+ method: "PUT",
202
+ headers: { "Content-Type": uploadInfo.ContentType },
203
+ body: file,
204
+ });
205
+ const metadata: FileType = {
206
+ _id: uploadInfo._id,
207
+ _name: uploadInfo._name,
208
+ FileName: uploadInfo.FileName,
209
+ FileExtension: uploadInfo.FileExtension,
210
+ Size: uploadInfo.Size,
211
+ ContentType: uploadInfo.ContentType,
212
+ };
213
+ form.setValue(prop as Path<FieldValues>, metadata as any, { shouldDirty: true });
214
+ return metadata;
215
+ };
216
+
217
+ (accessor as any).deleteAttachment = async (): Promise<void> => {
218
+ const val = form.getValues(prop as Path<FieldValues>) as any;
219
+ const instanceId = requireInstanceId();
220
+ if (!(val?._id)) throw new Error(`${prop} has no image to delete`);
221
+ await api(boId).deleteAttachment(instanceId, prop, val._id);
222
+ form.setValue(prop as Path<FieldValues>, null as any, { shouldDirty: true });
223
+ };
224
+
225
+ (accessor as any).getDownloadUrl = async (viewType?: AttachmentViewType): Promise<FileDownloadResponseType> => {
226
+ const val = form.getValues(prop as Path<FieldValues>) as any;
227
+ const instanceId = requireInstanceId();
228
+ if (!(val?._id)) throw new Error(`${prop} has no image`);
229
+ return api(boId).getDownloadUrl(instanceId, prop, val._id, viewType);
230
+ };
231
+ } else {
232
+ // File field — multi-file, always uploads immediately (draft in create mode)
233
+ (accessor as any).upload = async (files: File[]): Promise<FileType[]> => {
234
+ for (const file of files) validateFileExtension(file.name, "File");
235
+ const id = await ensureDraft();
236
+
237
+ const requests = files.map((file) => ({
238
+ FileName: file.name,
239
+ Size: file.size,
240
+ FileExtension: extractFileExtension(file.name),
241
+ }));
242
+ const uploadInfos = await api(boId).getUploadUrl(id, prop, requests);
243
+ const uploaded: FileType[] = await Promise.all(
244
+ files.map(async (file, i) => {
245
+ await fetch(uploadInfos[i].UploadUrl.URL, {
246
+ method: "PUT",
247
+ headers: { "Content-Type": uploadInfos[i].ContentType },
248
+ body: file,
249
+ });
250
+ return {
251
+ _id: uploadInfos[i]._id,
252
+ _name: uploadInfos[i]._name,
253
+ FileName: uploadInfos[i].FileName,
254
+ FileExtension: uploadInfos[i].FileExtension,
255
+ Size: uploadInfos[i].Size,
256
+ ContentType: uploadInfos[i].ContentType,
257
+ };
258
+ }),
259
+ );
260
+ const current = (form.getValues(prop as Path<FieldValues>) as FileType[] | undefined) ?? [];
261
+ form.setValue(prop as Path<FieldValues>, [...current, ...uploaded] as any, { shouldDirty: true });
262
+ return uploaded;
263
+ };
264
+
265
+ (accessor as any).deleteAttachment = async (attachmentId: string): Promise<void> => {
266
+ const current = (form.getValues(prop as Path<FieldValues>) as any[]) ?? [];
267
+ const instanceId = requireInstanceId();
268
+ await api(boId).deleteAttachment(instanceId, prop, attachmentId);
269
+ form.setValue(
270
+ prop as Path<FieldValues>,
271
+ current.filter((f) => f._id !== attachmentId) as any,
272
+ { shouldDirty: true },
273
+ );
274
+ };
275
+
276
+ (accessor as any).getDownloadUrl = async (
277
+ attachmentId: string,
278
+ viewType?: AttachmentViewType,
279
+ ): Promise<FileDownloadResponseType> => {
280
+ const instanceId = requireInstanceId();
281
+ return api(boId).getDownloadUrl(instanceId, prop, attachmentId, viewType);
282
+ };
283
+ (accessor as any).getDownloadUrls = async (
284
+ viewType?: AttachmentViewType,
285
+ ): Promise<FileDownloadResponseType[]> => {
286
+ const instanceId = requireInstanceId();
287
+ return api(boId).getDownloadUrls(instanceId, prop, viewType);
288
+ };
289
+ }
290
+ }
291
+
118
292
  accessorCache.set(prop, accessor);
119
293
  return accessor;
120
294
  }
121
295
 
296
+ // Defensive get() for readonly accessor too
297
+ const readonlyGet = () => {
298
+ const val = form.getValues(prop as Path<FieldValues>);
299
+ if (fieldMeta.Type === "File") return val ?? [];
300
+ return val;
301
+ };
302
+
122
303
  const accessor: ReadonlyFormFieldAccessorType<unknown> = {
123
304
  label: bdoField?.label ?? prop,
124
305
  required: bdoField?.required ?? false,
125
306
  readOnly: true,
126
307
  defaultValue: bdoField?.defaultValue,
127
308
  meta: fieldMeta,
128
- get: () => form.getValues(prop as Path<FieldValues>),
309
+ get: readonlyGet,
129
310
  getOrDefault,
130
311
  validate,
131
312
  };
313
+
314
+ // Enrich readonly Image/File field accessors with download methods
315
+ if (fieldMeta.Type === "Image" || fieldMeta.Type === "File") {
316
+ const boId = boIdShared;
317
+ const requireInstanceId = (): string => {
318
+ const id = form.getValues("_id" as Path<FieldValues>) as string | undefined;
319
+ if (!id) throw new Error("Cannot perform attachment operation: item has no _id. Save the item first.");
320
+ return id;
321
+ };
322
+
323
+ if (fieldMeta.Type === "Image") {
324
+ (accessor as any).getDownloadUrl = async (viewType?: AttachmentViewType): Promise<FileDownloadResponseType> => {
325
+ const val = form.getValues(prop as Path<FieldValues>) as any;
326
+ const instanceId = requireInstanceId();
327
+ if (!(val?._id)) throw new Error(`${prop} has no image to download`);
328
+ return api(boId).getDownloadUrl(instanceId, prop, val._id, viewType);
329
+ };
330
+ } else {
331
+ (accessor as any).getDownloadUrl = async (
332
+ attachmentId: string,
333
+ viewType?: AttachmentViewType,
334
+ ): Promise<FileDownloadResponseType> => {
335
+ const instanceId = requireInstanceId();
336
+ return api(boId).getDownloadUrl(instanceId, prop, attachmentId, viewType);
337
+ };
338
+ (accessor as any).getDownloadUrls = async (
339
+ viewType?: AttachmentViewType,
340
+ ): Promise<FileDownloadResponseType[]> => {
341
+ const instanceId = requireInstanceId();
342
+ return api(boId).getDownloadUrls(instanceId, prop, viewType);
343
+ };
344
+ }
345
+ }
346
+
132
347
  accessorCache.set(prop, accessor);
133
348
  return accessor;
134
349
  },
@@ -137,6 +352,8 @@ export function createItemProxy<B extends BaseBdo<any, any, any>>(
137
352
  if (typeof prop === "symbol") return false;
138
353
  if (prop === "_id" || prop === "toJSON" || prop === "validate")
139
354
  return true;
355
+ if (prop === "_hasDraft")
356
+ return true;
140
357
  return prop in fields;
141
358
  },
142
359
 
@@ -148,7 +365,7 @@ export function createItemProxy<B extends BaseBdo<any, any, any>>(
148
365
  if (typeof prop === "symbol") return undefined;
149
366
  return {
150
367
  configurable: true,
151
- enumerable: prop !== "toJSON" && prop !== "validate",
368
+ enumerable: prop !== "toJSON" && prop !== "validate" && prop !== "_hasDraft",
152
369
  };
153
370
  },
154
371
  });