@ram_28/kf-ai-sdk 2.0.12 → 2.0.14
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/dist/api/client.d.ts.map +1 -1
- package/dist/api.cjs +1 -1
- package/dist/api.mjs +2 -2
- package/dist/attachment-constants-B5jlqoKI.cjs +1 -0
- package/dist/attachment-constants-C2UHWxmp.js +63 -0
- package/dist/auth.cjs +1 -1
- package/dist/auth.mjs +1 -1
- package/dist/bdo/core/types.d.ts +4 -0
- package/dist/bdo/core/types.d.ts.map +1 -1
- package/dist/bdo/fields/NumberField.d.ts.map +1 -1
- package/dist/bdo/fields/ReferenceField.d.ts +3 -2
- 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 +5 -0
- package/dist/bdo/fields/UserField.d.ts.map +1 -1
- package/dist/bdo.cjs +1 -1
- package/dist/bdo.mjs +107 -153
- package/dist/client-DnO2KKrw.cjs +1 -0
- package/dist/{client-CMERmrC-.js → client-iQTqFDNI.js} +34 -30
- package/dist/components/hooks/useForm/createItemProxy.d.ts +4 -0
- package/dist/components/hooks/useForm/createItemProxy.d.ts.map +1 -1
- package/dist/components/hooks/useForm/createResolver.d.ts.map +1 -1
- package/dist/components/hooks/useForm/useForm.d.ts +1 -0
- package/dist/components/hooks/useForm/useForm.d.ts.map +1 -1
- package/dist/form.cjs +1 -1
- package/dist/form.mjs +368 -203
- package/dist/{metadata-BfJtHz84.cjs → metadata-DgLSJkF5.cjs} +1 -1
- package/dist/{metadata-CwAo6a8e.js → metadata-DpfI3zRN.js} +1 -1
- package/dist/table.cjs +1 -1
- package/dist/table.mjs +1 -1
- package/dist/workflow/types.d.ts +3 -2
- package/dist/workflow/types.d.ts.map +1 -1
- package/dist/workflow.cjs +1 -1
- package/dist/workflow.d.ts +0 -2
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.mjs +204 -274
- package/dist/workflow.types.d.ts +0 -1
- package/dist/workflow.types.d.ts.map +1 -1
- package/docs/api.md +45 -253
- package/docs/bdo.md +130 -711
- package/docs/useAuth.md +42 -104
- package/docs/useFilter.md +117 -1591
- package/docs/useForm.md +266 -861
- package/docs/useTable.md +255 -1096
- package/docs/workflow.md +10 -155
- package/package.json +1 -1
- package/sdk/api/client.ts +18 -4
- package/sdk/bdo/core/types.ts +1 -0
- package/sdk/bdo/fields/NumberField.ts +2 -1
- package/sdk/bdo/fields/ReferenceField.ts +4 -3
- package/sdk/bdo/fields/SelectField.ts +2 -2
- package/sdk/bdo/fields/UserField.ts +14 -0
- package/sdk/components/hooks/useForm/createItemProxy.ts +221 -4
- package/sdk/components/hooks/useForm/createResolver.ts +16 -1
- package/sdk/components/hooks/useForm/useForm.ts +151 -50
- package/sdk/workflow/types.ts +3 -2
- package/sdk/workflow.ts +0 -7
- package/sdk/workflow.types.ts +0 -7
- package/dist/client-BnVxSHAm.cjs +0 -1
- package/dist/workflow/components/useActivityTable/index.d.ts +0 -4
- package/dist/workflow/components/useActivityTable/index.d.ts.map +0 -1
- package/dist/workflow/components/useActivityTable/types.d.ts +0 -53
- package/dist/workflow/components/useActivityTable/types.d.ts.map +0 -1
- package/dist/workflow/components/useActivityTable/useActivityTable.d.ts +0 -4
- package/dist/workflow/components/useActivityTable/useActivityTable.d.ts.map +0 -1
- package/sdk/workflow/components/useActivityTable/index.ts +0 -8
- package/sdk/workflow/components/useActivityTable/types.ts +0 -67
- package/sdk/workflow/components/useActivityTable/useActivityTable.ts +0 -145
package/docs/useForm.md
CHANGED
|
@@ -1,971 +1,376 @@
|
|
|
1
1
|
# Form SDK API
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
API integration, and type-safe field handling.
|
|
3
|
+
React hook for forms with validation, API integration, and typed field handling.
|
|
5
4
|
|
|
6
5
|
## Imports
|
|
7
6
|
|
|
8
7
|
```typescript
|
|
9
8
|
import { useForm } from "@ram_28/kf-ai-sdk/form";
|
|
10
9
|
import { ValidationMode, FormOperation } from "@ram_28/kf-ai-sdk/form";
|
|
11
|
-
import type {
|
|
12
|
-
|
|
13
|
-
UseFormReturnType,
|
|
14
|
-
FormItemType,
|
|
15
|
-
FormRegisterType,
|
|
16
|
-
HandleSubmitType,
|
|
17
|
-
ValidationModeType,
|
|
18
|
-
EditableFormFieldAccessorType,
|
|
19
|
-
ReadonlyFormFieldAccessorType,
|
|
20
|
-
} from "@ram_28/kf-ai-sdk/form/types";
|
|
21
|
-
|
|
22
|
-
// For filter conditions in forms
|
|
23
|
-
import { ConditionOperator, GroupOperator, RHSType } from "@ram_28/kf-ai-sdk/filter";
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Type Definitions
|
|
27
|
-
|
|
28
|
-
### UseFormOptionsType
|
|
29
|
-
|
|
30
|
-
```typescript
|
|
31
|
-
// Create mode — no recordId
|
|
32
|
-
interface UseFormCreateOptionsType<B extends BaseBdo<any, any, any>> {
|
|
33
|
-
bdo: B;
|
|
34
|
-
defaultValues?: Partial<EditableFieldType>;
|
|
35
|
-
mode?: ValidationModeType;
|
|
36
|
-
enableDraft?: boolean;
|
|
37
|
-
enableConstraintValidation?: boolean; // default: true
|
|
38
|
-
enableExpressionValidation?: boolean; // default: true
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Edit mode — recordId required
|
|
42
|
-
interface UseFormEditOptionsType<B extends BaseBdo<any, any, any>> {
|
|
43
|
-
bdo: B;
|
|
44
|
-
recordId: string;
|
|
45
|
-
mode?: ValidationModeType;
|
|
46
|
-
enableDraft?: boolean;
|
|
47
|
-
enableConstraintValidation?: boolean; // default: true
|
|
48
|
-
enableExpressionValidation?: boolean; // default: true
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Explicit operation mode
|
|
52
|
-
interface UseFormExplicitOptionsType<B extends BaseBdo<any, any, any>> {
|
|
53
|
-
bdo: B;
|
|
54
|
-
operation: "create" | "update";
|
|
55
|
-
recordId?: string;
|
|
56
|
-
defaultValues?: Partial<EditableFieldType>;
|
|
57
|
-
mode?: ValidationModeType;
|
|
58
|
-
enableDraft?: boolean;
|
|
59
|
-
enableConstraintValidation?: boolean; // default: true
|
|
60
|
-
enableExpressionValidation?: boolean; // default: true
|
|
61
|
-
}
|
|
10
|
+
import type { UseFormOptionsType, UseFormReturnType, FormItemType, FormRegisterType, HandleSubmitType } from "@ram_28/kf-ai-sdk/form/types";
|
|
11
|
+
import type { CreateUpdateResponseType } from "@ram_28/kf-ai-sdk/api/types";
|
|
62
12
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
13
|
+
// Pre-built components for special field types
|
|
14
|
+
import { ReferenceSelect } from "@/components/ui/reference-select";
|
|
15
|
+
import { ImageUpload } from "@/components/ui/image-upload";
|
|
16
|
+
import { FileUpload } from "@/components/ui/file-upload";
|
|
67
17
|
```
|
|
68
18
|
|
|
69
|
-
|
|
19
|
+
---
|
|
70
20
|
|
|
71
|
-
|
|
72
|
-
interface UseFormReturnType<B extends BaseBdo<any, any, any>> {
|
|
73
|
-
// ============================================================
|
|
74
|
-
// CORE
|
|
75
|
-
// ============================================================
|
|
76
|
-
|
|
77
|
-
// Item proxy with typed field accessors
|
|
78
|
-
item: FormItemType<EditableFieldType, ReadonlyFieldType>;
|
|
79
|
-
|
|
80
|
-
// BDO reference and operation info
|
|
81
|
-
bdo: B;
|
|
82
|
-
operation: "create" | "update";
|
|
83
|
-
recordId?: string;
|
|
84
|
-
|
|
85
|
-
// Smart register (auto-disables readonly fields)
|
|
86
|
-
register: FormRegisterType<EditableFieldType, ReadonlyFieldType>;
|
|
87
|
-
|
|
88
|
-
// Custom handleSubmit (handles API call + payload filtering)
|
|
89
|
-
handleSubmit: HandleSubmitType;
|
|
90
|
-
|
|
91
|
-
// ============================================================
|
|
92
|
-
// REACT HOOK FORM METHODS
|
|
93
|
-
// ============================================================
|
|
94
|
-
|
|
95
|
-
watch: UseFormWatch<AllFieldsType>;
|
|
96
|
-
setValue: UseFormSetValue<EditableFieldType>;
|
|
97
|
-
getValues: UseFormGetValues<AllFieldsType>;
|
|
98
|
-
reset: UseFormReset<AllFieldsType>;
|
|
99
|
-
trigger: UseFormTrigger<AllFieldsType>;
|
|
100
|
-
control: Control<AllFieldsType>;
|
|
101
|
-
|
|
102
|
-
// ============================================================
|
|
103
|
-
// FORM STATE
|
|
104
|
-
// ============================================================
|
|
105
|
-
|
|
106
|
-
formState: FormState<AllFieldsType>;
|
|
107
|
-
errors: FieldErrors<AllFieldsType>;
|
|
108
|
-
isDirty: boolean;
|
|
109
|
-
isValid: boolean;
|
|
110
|
-
isSubmitting: boolean;
|
|
111
|
-
isSubmitSuccessful: boolean;
|
|
112
|
-
dirtyFields: Partial<Record<keyof AllFieldsType, boolean>>;
|
|
21
|
+
## Common Mistakes (READ FIRST)
|
|
113
22
|
|
|
114
|
-
|
|
115
|
-
// LOADING & ERROR
|
|
116
|
-
// ============================================================
|
|
23
|
+
### 1. Missing `operation` in useForm options (TS2345)
|
|
117
24
|
|
|
118
|
-
|
|
119
|
-
isFetching: boolean; // True during any background refetch
|
|
120
|
-
loadError: Error | null; // Schema/record fetch error
|
|
25
|
+
ALWAYS include `operation`. Without it, TypeScript cannot resolve the union type.
|
|
121
26
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
27
|
+
```typescript
|
|
28
|
+
// ❌ WRONG — missing operation (TS2345)
|
|
29
|
+
useForm({ bdo: product, mode: ValidationMode.OnBlur });
|
|
125
30
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
31
|
+
// ✅ CORRECT — always include operation
|
|
32
|
+
useForm({ bdo: product, operation: FormOperation.Create, mode: ValidationMode.OnBlur });
|
|
33
|
+
useForm({ bdo: product, operation: FormOperation.Update, recordId: id, mode: ValidationMode.OnBlur });
|
|
129
34
|
```
|
|
130
35
|
|
|
131
|
-
###
|
|
132
|
-
|
|
133
|
-
```typescript
|
|
134
|
-
type FormItemType<TEditable, TReadonly> = {
|
|
135
|
-
// Editable field accessors
|
|
136
|
-
[K in keyof TEditable]: EditableFormFieldAccessorType<TEditable[K]>;
|
|
137
|
-
} & {
|
|
138
|
-
// Readonly field accessors
|
|
139
|
-
[K in keyof TReadonly]: ReadonlyFormFieldAccessorType<TReadonly[K]>;
|
|
140
|
-
} & {
|
|
141
|
-
// Direct access
|
|
142
|
-
readonly _id: string | undefined;
|
|
143
|
-
|
|
144
|
-
// Methods
|
|
145
|
-
toJSON(): Partial<TEditable & TReadonly>;
|
|
146
|
-
validate(): Promise<boolean>;
|
|
147
|
-
};
|
|
148
|
-
```
|
|
36
|
+
### 2. Annotating ternary with UseFormOptionsType (TS2322)
|
|
149
37
|
|
|
150
|
-
|
|
38
|
+
`UseFormOptionsType` is a discriminated union. Type-annotating a variable prevents TS narrowing. NEVER annotate — call useForm inline in each branch.
|
|
151
39
|
|
|
152
40
|
```typescript
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// For readonly fields (no set method)
|
|
166
|
-
interface ReadonlyFormFieldAccessorType<T> {
|
|
167
|
-
readonly label: string;
|
|
168
|
-
readonly required: boolean;
|
|
169
|
-
readonly readOnly: true;
|
|
170
|
-
readonly defaultValue: unknown;
|
|
171
|
-
readonly meta: BaseFieldMetaType;
|
|
172
|
-
get(): T | undefined;
|
|
173
|
-
validate(): ValidationResultType;
|
|
174
|
-
}
|
|
41
|
+
// ❌ WRONG — type annotation prevents union narrowing (TS2322)
|
|
42
|
+
const options: UseFormOptionsType<typeof bdo> = id
|
|
43
|
+
? { bdo, operation: FormOperation.Update, recordId: id, mode: ValidationMode.OnBlur }
|
|
44
|
+
: { bdo, operation: FormOperation.Create, mode: ValidationMode.OnBlur };
|
|
45
|
+
const formResult = useForm(options);
|
|
46
|
+
|
|
47
|
+
// ✅ CORRECT — call useForm inline, no type annotation
|
|
48
|
+
const formResult = id
|
|
49
|
+
? useForm({ bdo, operation: FormOperation.Update, recordId: id, mode: ValidationMode.OnBlur })
|
|
50
|
+
: useForm({ bdo, operation: FormOperation.Create, mode: ValidationMode.OnBlur });
|
|
51
|
+
const { register, handleSubmit, watch, setValue, item, formState: { errors, isSubmitting }, isLoading } = formResult;
|
|
175
52
|
```
|
|
176
53
|
|
|
177
|
-
###
|
|
54
|
+
### 3. Using `.options` on StringField (TS2339)
|
|
178
55
|
|
|
179
|
-
|
|
180
|
-
// Metadata for an uploaded file or image
|
|
181
|
-
interface FileType {
|
|
182
|
-
_id: string;
|
|
183
|
-
_name: string;
|
|
184
|
-
FileName: string;
|
|
185
|
-
FileExtension: string;
|
|
186
|
-
Size: number;
|
|
187
|
-
ContentType: string;
|
|
188
|
-
}
|
|
56
|
+
ONLY `SelectField` has `.options` getter. `StringField` with `Constraint.Enum` does NOT — use hardcoded `<option>` values from the BDO file.
|
|
189
57
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
//
|
|
194
|
-
|
|
58
|
+
```tsx
|
|
59
|
+
// Check the BDO class to determine field type:
|
|
60
|
+
// new SelectField({...}) → has .options → use bdo.field.options.map()
|
|
61
|
+
// new StringField({..."Constraint": {"Enum": [...]}}) → NO .options → hardcode from Enum array
|
|
62
|
+
|
|
63
|
+
// ❌ WRONG — StringField has no .options (TS2339)
|
|
64
|
+
// Given: readonly status = new StringField({... "Constraint": { "Enum": ["Active", "Discontinued"] }})
|
|
65
|
+
<select {...register(bdo.status.id)}>
|
|
66
|
+
{bdo.status.options.map((opt) => ( // TS2339: Property 'options' does not exist on StringField
|
|
67
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
68
|
+
))}
|
|
69
|
+
</select>
|
|
70
|
+
|
|
71
|
+
// ✅ CORRECT for StringField with Enum — hardcode options from BDO Constraint.Enum array
|
|
72
|
+
<select {...register(bdo.status.id)}>
|
|
73
|
+
<option value="">Select {bdo.status.label}</option>
|
|
74
|
+
<option value="Active">Active</option>
|
|
75
|
+
<option value="Discontinued">Discontinued</option>
|
|
76
|
+
</select>
|
|
77
|
+
|
|
78
|
+
// ✅ CORRECT for SelectField — use .options getter
|
|
79
|
+
// Given: readonly status = new SelectField({...})
|
|
80
|
+
<select {...register(bdo.status.id)}>
|
|
81
|
+
<option value="">Select {bdo.status.label}</option>
|
|
82
|
+
{bdo.status.options.map((opt) => (
|
|
83
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
84
|
+
))}
|
|
85
|
+
</select>
|
|
195
86
|
```
|
|
196
87
|
|
|
197
|
-
###
|
|
88
|
+
### 4. Using register() for BooleanField
|
|
198
89
|
|
|
199
|
-
|
|
200
|
-
Editable accessors get `upload` and `deleteAttachment`; readonly accessors only get download.
|
|
201
|
-
|
|
202
|
-
```typescript
|
|
203
|
-
// Editable Image — single file
|
|
204
|
-
interface EditableImageFieldAccessorType
|
|
205
|
-
extends EditableFormFieldAccessorType<ImageFieldType> {
|
|
206
|
-
upload(file: File): Promise<FileType>;
|
|
207
|
-
getDownloadUrl(viewType?: AttachmentViewType): Promise<FileDownloadResponseType>;
|
|
208
|
-
deleteAttachment(): Promise<void>;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Readonly Image — download only
|
|
212
|
-
interface ReadonlyImageFieldAccessorType
|
|
213
|
-
extends ReadonlyFormFieldAccessorType<ImageFieldType> {
|
|
214
|
-
getDownloadUrl(viewType?: AttachmentViewType): Promise<FileDownloadResponseType>;
|
|
215
|
-
}
|
|
90
|
+
HTML checkboxes don't work with register(). Use watch/setValue.
|
|
216
91
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
upload(files: File[]): Promise<FileType[]>;
|
|
221
|
-
getDownloadUrl(attachmentId: string, viewType?: AttachmentViewType): Promise<FileDownloadResponseType>;
|
|
222
|
-
getDownloadUrls(viewType?: AttachmentViewType): Promise<FileDownloadResponseType[]>;
|
|
223
|
-
deleteAttachment(attachmentId: string): Promise<void>;
|
|
224
|
-
}
|
|
92
|
+
```tsx
|
|
93
|
+
// ❌ WRONG
|
|
94
|
+
<input type="checkbox" {...register(bdo.is_active.id)} />
|
|
225
95
|
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
96
|
+
// ✅ CORRECT
|
|
97
|
+
<Checkbox
|
|
98
|
+
checked={Boolean(watch(bdo.is_active.id))}
|
|
99
|
+
onCheckedChange={(v) => setValue(bdo.is_active.id, v as boolean, { shouldDirty: true })}
|
|
100
|
+
/>
|
|
232
101
|
```
|
|
233
102
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
## Basic Example
|
|
103
|
+
### 5. Using `<input>` for ReferenceField (should use `<ReferenceSelect>`)
|
|
237
104
|
|
|
238
|
-
|
|
105
|
+
ReferenceField stores an object `{ _id, _name, ... }`, not a string. Use the pre-built component.
|
|
239
106
|
|
|
240
107
|
```tsx
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
import type { FieldErrors } from "react-hook-form";
|
|
252
|
-
import type { CreateUpdateResponseType } from "@ram_28/kf-ai-sdk/api/types";
|
|
253
|
-
import { SellerProduct } from "../bdo/seller/Product";
|
|
254
|
-
import type {
|
|
255
|
-
SellerProductEditableFieldType,
|
|
256
|
-
SellerProductReadonlyFieldType,
|
|
257
|
-
} from "../bdo/seller/Product";
|
|
258
|
-
|
|
259
|
-
function CreateProductForm() {
|
|
260
|
-
// 1. Instantiate BDO (memoized)
|
|
261
|
-
const product: SellerProduct = useMemo(() => new SellerProduct(), []);
|
|
262
|
-
|
|
263
|
-
// 2. Form options with explicit type
|
|
264
|
-
const formOptions: UseFormOptionsType<SellerProduct> = {
|
|
265
|
-
bdo: product,
|
|
266
|
-
defaultValues: {
|
|
267
|
-
Title: "",
|
|
268
|
-
Price: 0,
|
|
269
|
-
Stock: 0,
|
|
270
|
-
} satisfies Partial<SellerProductEditableFieldType>,
|
|
271
|
-
mode: ValidationMode.OnBlur,
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
// 3. Initialize form with typed destructuring
|
|
275
|
-
const {
|
|
276
|
-
register,
|
|
277
|
-
handleSubmit,
|
|
278
|
-
item,
|
|
279
|
-
errors,
|
|
280
|
-
isSubmitting,
|
|
281
|
-
}: {
|
|
282
|
-
register: FormRegisterType<SellerProductEditableFieldType, SellerProductReadonlyFieldType>;
|
|
283
|
-
handleSubmit: HandleSubmitType<CreateUpdateResponseType>;
|
|
284
|
-
item: FormItemType<SellerProductEditableFieldType, SellerProductReadonlyFieldType>;
|
|
285
|
-
errors: FieldErrors<SellerProductEditableFieldType & SellerProductReadonlyFieldType>;
|
|
286
|
-
isSubmitting: boolean;
|
|
287
|
-
} = useForm(formOptions);
|
|
288
|
-
|
|
289
|
-
// 4. Typed handlers
|
|
290
|
-
const onSuccess = (data: CreateUpdateResponseType): void => {
|
|
291
|
-
console.log("Created with ID:", data._id);
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
const onError = (error: FieldErrors | Error): void => {
|
|
295
|
-
if (error instanceof Error) {
|
|
296
|
-
console.error("API Error:", error.message);
|
|
297
|
-
} else {
|
|
298
|
-
console.error("Validation errors:", error);
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
// Type for item proxy
|
|
303
|
-
type ProductItem = FormItemType<
|
|
304
|
-
SellerProductEditableFieldType,
|
|
305
|
-
SellerProductReadonlyFieldType
|
|
306
|
-
>;
|
|
307
|
-
|
|
308
|
-
return (
|
|
309
|
-
<form onSubmit={handleSubmit(onSuccess, onError)}>
|
|
310
|
-
{/* Use field.id for register */}
|
|
311
|
-
<div>
|
|
312
|
-
<label>{product.Title.label}</label>
|
|
313
|
-
<input {...register(product.Title.id)} />
|
|
314
|
-
{errors.Title && <span>{errors.Title.message}</span>}
|
|
315
|
-
</div>
|
|
316
|
-
|
|
317
|
-
<div>
|
|
318
|
-
<label>{product.Price.label}</label>
|
|
319
|
-
<input type="number" {...register(product.Price.id)} />
|
|
320
|
-
{errors.Price && <span>{errors.Price.message}</span>}
|
|
321
|
-
</div>
|
|
322
|
-
|
|
323
|
-
<button type="submit" disabled={isSubmitting}>
|
|
324
|
-
{isSubmitting ? "Creating..." : "Create Product"}
|
|
325
|
-
</button>
|
|
326
|
-
</form>
|
|
327
|
-
);
|
|
328
|
-
}
|
|
108
|
+
// ❌ WRONG — text input for reference field
|
|
109
|
+
<input {...register(bdo.category.id)} />
|
|
110
|
+
|
|
111
|
+
// ✅ CORRECT
|
|
112
|
+
<ReferenceSelect
|
|
113
|
+
bdoField={bdo.category}
|
|
114
|
+
instanceId={id || String(watch("_id") ?? "")}
|
|
115
|
+
value={watch(bdo.category.id)}
|
|
116
|
+
onChange={(val) => setValue(bdo.category.id, val, { shouldDirty: true })}
|
|
117
|
+
/>
|
|
329
118
|
```
|
|
330
119
|
|
|
331
|
-
###
|
|
120
|
+
### 6. Using custom Image/File upload (should use template components)
|
|
332
121
|
|
|
333
|
-
|
|
334
|
-
import { useMemo } from "react";
|
|
335
|
-
import { useForm } from "@ram_28/kf-ai-sdk/form";
|
|
336
|
-
import { ValidationMode } from "@ram_28/kf-ai-sdk/form";
|
|
337
|
-
import type {
|
|
338
|
-
UseFormOptionsType,
|
|
339
|
-
UseFormReturnType,
|
|
340
|
-
} from "@ram_28/kf-ai-sdk/form/types";
|
|
341
|
-
import type { FieldErrors } from "react-hook-form";
|
|
342
|
-
import type { CreateUpdateResponseType } from "@ram_28/kf-ai-sdk/api/types";
|
|
343
|
-
import { SellerProduct } from "../bdo/seller/Product";
|
|
344
|
-
import type {
|
|
345
|
-
SellerProductEditableFieldType,
|
|
346
|
-
SellerProductReadonlyFieldType,
|
|
347
|
-
} from "../bdo/seller/Product";
|
|
348
|
-
|
|
349
|
-
interface EditProductFormProps {
|
|
350
|
-
productId: string;
|
|
351
|
-
onClose: () => void;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function EditProductForm({ productId, onClose }: EditProductFormProps) {
|
|
355
|
-
const product: SellerProduct = useMemo(() => new SellerProduct(), []);
|
|
122
|
+
Use `<ImageUpload>` and `<FileUpload>`. CRITICAL: `instanceId` must work for BOTH edit and create mode.
|
|
356
123
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
errors,
|
|
370
|
-
isLoading,
|
|
371
|
-
isSubmitting,
|
|
372
|
-
loadError,
|
|
373
|
-
}: {
|
|
374
|
-
register: FormRegisterType<SellerProductEditableFieldType, SellerProductReadonlyFieldType>;
|
|
375
|
-
handleSubmit: HandleSubmitType<CreateUpdateResponseType>;
|
|
376
|
-
item: FormItemType<SellerProductEditableFieldType, SellerProductReadonlyFieldType>;
|
|
377
|
-
errors: FieldErrors<SellerProductEditableFieldType & SellerProductReadonlyFieldType>;
|
|
378
|
-
isLoading: boolean;
|
|
379
|
-
isSubmitting: boolean;
|
|
380
|
-
loadError: Error | null;
|
|
381
|
-
} = useForm(formOptions);
|
|
382
|
-
|
|
383
|
-
if (isLoading) return <div>Loading...</div>;
|
|
384
|
-
if (loadError) return <div>Error: {loadError.message}</div>;
|
|
385
|
-
|
|
386
|
-
const onSuccess = (data: CreateUpdateResponseType): void => {
|
|
387
|
-
console.log("Updated:", data._id);
|
|
388
|
-
onClose();
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
const onError = (error: FieldErrors | Error): void => {
|
|
392
|
-
console.error("Error:", error);
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
return (
|
|
396
|
-
<form onSubmit={handleSubmit(onSuccess, onError)}>
|
|
397
|
-
{/* Readonly fields are auto-disabled */}
|
|
398
|
-
<div>
|
|
399
|
-
<label>{product.ASIN.label}</label>
|
|
400
|
-
<input {...register(product.ASIN.id)} />
|
|
401
|
-
{/* This input is automatically disabled: true for readonly fields */}
|
|
402
|
-
</div>
|
|
403
|
-
|
|
404
|
-
{/* Editable fields */}
|
|
405
|
-
<div>
|
|
406
|
-
<label>{product.Title.label}</label>
|
|
407
|
-
<input {...register(product.Title.id)} />
|
|
408
|
-
{errors.Title && <span>{errors.Title.message}</span>}
|
|
409
|
-
</div>
|
|
124
|
+
```tsx
|
|
125
|
+
// ❌ WRONG — instanceId={id} is undefined in create mode
|
|
126
|
+
<ImageUpload field={item.icon} value={watch(bdo.icon.id)} boId={bdo.meta._id} instanceId={id} fieldId={bdo.icon.id} />
|
|
127
|
+
|
|
128
|
+
// ✅ CORRECT — ImageUpload (single image)
|
|
129
|
+
<ImageUpload
|
|
130
|
+
field={item.product_image}
|
|
131
|
+
value={watch(bdo.product_image.id)}
|
|
132
|
+
boId={bdo.meta._id}
|
|
133
|
+
instanceId={id || String(watch("_id") ?? "")}
|
|
134
|
+
fieldId={bdo.product_image.id}
|
|
135
|
+
/>
|
|
410
136
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
137
|
+
// ✅ CORRECT — FileUpload (multi-file)
|
|
138
|
+
<FileUpload
|
|
139
|
+
field={item.specification_document}
|
|
140
|
+
value={watch(bdo.specification_document.id)}
|
|
141
|
+
boId={bdo.meta._id}
|
|
142
|
+
instanceId={id || String(watch("_id") ?? "")}
|
|
143
|
+
fieldId={bdo.specification_document.id}
|
|
144
|
+
/>
|
|
417
145
|
```
|
|
418
146
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
## register() Function
|
|
422
|
-
|
|
423
|
-
The `register` function extends React Hook Form's standard register with automatic readonly field handling.
|
|
147
|
+
Props: `field` = item accessor (`item.fieldName`), `value` = `watch(bdo.field.id)`, `boId` = `bdo.meta._id`, `instanceId` = `id || String(watch("_id") ?? "")`, `fieldId` = `bdo.field.id`. Parent component MUST have `"use no memo"` directive.
|
|
424
148
|
|
|
425
|
-
###
|
|
149
|
+
### 7. Wrong handleSubmit onSuccess type
|
|
426
150
|
|
|
427
151
|
```typescript
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
// => UseFormRegisterReturn
|
|
152
|
+
// ❌ WRONG
|
|
153
|
+
const onSuccess = (data: unknown) => { ... };
|
|
431
154
|
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
155
|
+
// ✅ CORRECT — CreateUpdateResponseType = { _id: string }
|
|
156
|
+
import type { CreateUpdateResponseType } from "@ram_28/kf-ai-sdk/api/types";
|
|
157
|
+
const onSuccess = (data: CreateUpdateResponseType) => { toast.success("Saved"); navigate("/list"); };
|
|
435
158
|
```
|
|
436
159
|
|
|
437
|
-
###
|
|
160
|
+
### 8. Passing FieldType instead of BDO instance
|
|
438
161
|
|
|
439
|
-
```
|
|
440
|
-
//
|
|
441
|
-
<
|
|
442
|
-
<input {...register(product.Price.id)} />
|
|
443
|
-
|
|
444
|
-
// Readonly fields are auto-disabled
|
|
445
|
-
<input {...register(product.ASIN.id)} /> // disabled: true added automatically
|
|
162
|
+
```typescript
|
|
163
|
+
// ❌ WRONG
|
|
164
|
+
useForm<ProductFieldType>({ ... });
|
|
446
165
|
|
|
447
|
-
//
|
|
448
|
-
|
|
166
|
+
// ✅ CORRECT — pass BDO class instance
|
|
167
|
+
const product = useMemo(() => new SellerProduct(), []);
|
|
168
|
+
useForm({ bdo: product, operation: FormOperation.Create, mode: ValidationMode.OnBlur });
|
|
449
169
|
```
|
|
450
170
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
## handleSubmit() Function
|
|
454
|
-
|
|
455
|
-
Custom handleSubmit that handles API calls automatically and filters payload to editable fields only.
|
|
456
|
-
|
|
457
|
-
### Type
|
|
171
|
+
### 9. Wrong default values for date fields
|
|
458
172
|
|
|
459
173
|
```typescript
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
onError?: (error: FieldErrors | Error, e?: React.BaseSyntheticEvent) => void | Promise<void>,
|
|
463
|
-
) => (e?: React.BaseSyntheticEvent) => Promise<void>;
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
### Execution Flow
|
|
467
|
-
|
|
468
|
-
1. Validation runs (type + expression validation)
|
|
469
|
-
2. If **valid**: Filters payload to editable fields -> Calls `bdo.create()` or `bdo.update()` -> Calls `onSuccess(result)`
|
|
470
|
-
3. If **invalid**: Calls `onError(FieldErrors)`
|
|
471
|
-
4. API errors: Calls `onError(Error)`
|
|
472
|
-
|
|
473
|
-
### Usage
|
|
174
|
+
// ❌ WRONG — empty string causes type errors
|
|
175
|
+
defaultValues: { start_date: "" }
|
|
474
176
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
console.log("Saved with ID:", data._id);
|
|
478
|
-
router.push("/products");
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
const onError = (error: FieldErrors | Error) => {
|
|
482
|
-
if (error instanceof Error) {
|
|
483
|
-
// API error
|
|
484
|
-
toast.error(`Failed: ${error.message}`);
|
|
485
|
-
} else {
|
|
486
|
-
// Validation errors (already shown by field error messages)
|
|
487
|
-
toast.error("Please fix the errors above");
|
|
488
|
-
}
|
|
489
|
-
};
|
|
490
|
-
|
|
491
|
-
<form onSubmit={handleSubmit(onSuccess, onError)}>
|
|
492
|
-
{/* form fields */}
|
|
493
|
-
</form>
|
|
177
|
+
// ✅ CORRECT
|
|
178
|
+
defaultValues: { start_date: undefined }
|
|
494
179
|
```
|
|
495
180
|
|
|
496
181
|
---
|
|
497
182
|
|
|
498
|
-
##
|
|
499
|
-
|
|
500
|
-
The `item` object provides field-level access with typed accessors.
|
|
501
|
-
|
|
502
|
-
### Field Access
|
|
183
|
+
## Complete Form Example (Create + Edit)
|
|
503
184
|
|
|
504
185
|
```tsx
|
|
505
|
-
|
|
506
|
-
const title = item.Title.get();
|
|
507
|
-
|
|
508
|
-
// Set field value (editable fields only)
|
|
509
|
-
item.Title.set("New Title");
|
|
186
|
+
"use no memo";
|
|
510
187
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
item.Title.readOnly // false
|
|
515
|
-
|
|
516
|
-
// Validate single field
|
|
517
|
-
const result = item.Title.validate();
|
|
518
|
-
// => { valid: boolean, errors: string[] }
|
|
519
|
-
|
|
520
|
-
// Direct _id access
|
|
521
|
-
const recordId = item._id;
|
|
522
|
-
|
|
523
|
-
// Get all values as JSON
|
|
524
|
-
const data = item.toJSON();
|
|
525
|
-
|
|
526
|
-
// Validate all fields
|
|
527
|
-
const isValid = await item.validate();
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
### Readonly Field Behavior
|
|
531
|
-
|
|
532
|
-
```tsx
|
|
533
|
-
// Readonly fields don't have set() method
|
|
534
|
-
item.ASIN.get() // Works
|
|
535
|
-
item.ASIN.set("...") // TypeScript error - method doesn't exist
|
|
536
|
-
item.ASIN.readOnly // true
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
---
|
|
540
|
-
|
|
541
|
-
## Validation
|
|
542
|
-
|
|
543
|
-
useForm provides three-phase validation:
|
|
544
|
-
|
|
545
|
-
### Phase 1: Type Validation
|
|
546
|
-
From field classes (StringField, NumberField, etc.)
|
|
547
|
-
|
|
548
|
-
### Phase 2: Constraint Validation
|
|
549
|
-
From field meta constraints (required, string length, number integerPart/fractionPart).
|
|
550
|
-
Controlled by the `enableConstraintValidation` option (default: `true`).
|
|
551
|
-
|
|
552
|
-
### Phase 3: Expression Validation
|
|
553
|
-
From backend schema rules (automatically fetched)
|
|
554
|
-
|
|
555
|
-
### Validation Timing
|
|
556
|
-
|
|
557
|
-
Controlled by the `mode` option:
|
|
558
|
-
|
|
559
|
-
```typescript
|
|
560
|
-
import { ValidationMode } from "@ram_28/kf-ai-sdk/form";
|
|
561
|
-
|
|
562
|
-
useForm({
|
|
563
|
-
bdo: product,
|
|
564
|
-
mode: ValidationMode.OnBlur, // Validate on blur (default)
|
|
565
|
-
// mode: ValidationMode.OnChange, // Validate on every keystroke
|
|
566
|
-
// mode: ValidationMode.OnSubmit, // Validate only on submit
|
|
567
|
-
});
|
|
568
|
-
```
|
|
569
|
-
|
|
570
|
-
### Readonly Field Handling
|
|
571
|
-
|
|
572
|
-
- Readonly fields are **skipped** during validation
|
|
573
|
-
- They never generate validation errors
|
|
574
|
-
- `register()` auto-adds `disabled: true`
|
|
575
|
-
|
|
576
|
-
---
|
|
577
|
-
|
|
578
|
-
## Complete Example
|
|
579
|
-
|
|
580
|
-
```tsx
|
|
581
|
-
import { useMemo, useState } from "react";
|
|
582
|
-
import { useForm } from "@ram_28/kf-ai-sdk/form";
|
|
583
|
-
import { ValidationMode, FormOperation } from "@ram_28/kf-ai-sdk/form";
|
|
584
|
-
import type {
|
|
585
|
-
UseFormOptionsType,
|
|
586
|
-
UseFormReturnType,
|
|
587
|
-
FormItemType,
|
|
588
|
-
FormRegisterType,
|
|
589
|
-
HandleSubmitType,
|
|
590
|
-
} from "@ram_28/kf-ai-sdk/form/types";
|
|
591
|
-
import type { FieldErrors, UseFormWatch, UseFormSetValue } from "react-hook-form";
|
|
188
|
+
import { useMemo } from "react";
|
|
189
|
+
import { useNavigate, useParams } from "react-router-dom";
|
|
190
|
+
import { useForm, ValidationMode, FormOperation } from "@ram_28/kf-ai-sdk/form";
|
|
592
191
|
import type { CreateUpdateResponseType } from "@ram_28/kf-ai-sdk/api/types";
|
|
593
|
-
import {
|
|
594
|
-
import
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
} from "
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
Price: 0,
|
|
619
|
-
Stock: 0,
|
|
620
|
-
} satisfies Partial<SellerProductEditableFieldType>,
|
|
621
|
-
mode: ValidationMode.OnBlur,
|
|
192
|
+
import { AdminProduct } from "@/bdo/admin/Product";
|
|
193
|
+
import { ReferenceSelect } from "@/components/ui/reference-select";
|
|
194
|
+
import { ImageUpload } from "@/components/ui/image-upload";
|
|
195
|
+
import { FileUpload } from "@/components/ui/file-upload";
|
|
196
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
197
|
+
import { toast } from "sonner";
|
|
198
|
+
|
|
199
|
+
export default function ProductForm() {
|
|
200
|
+
const { id } = useParams<{ id: string }>();
|
|
201
|
+
const navigate = useNavigate();
|
|
202
|
+
const bdo = useMemo(() => new AdminProduct(), []);
|
|
203
|
+
|
|
204
|
+
// Create/Edit ternary — NO type annotation on result
|
|
205
|
+
const formResult = id
|
|
206
|
+
? useForm({ bdo, operation: FormOperation.Update, recordId: id, mode: ValidationMode.OnBlur })
|
|
207
|
+
: useForm({ bdo, operation: FormOperation.Create, mode: ValidationMode.OnBlur });
|
|
208
|
+
|
|
209
|
+
const { register, handleSubmit, watch, setValue, item, formState: { errors, isSubmitting }, isLoading } = formResult;
|
|
210
|
+
|
|
211
|
+
// Loading guard AFTER all hooks
|
|
212
|
+
if (isLoading) return <div className="flex items-center justify-center h-full"><div className="animate-spin w-8 h-8 border-4 border-primary border-t-transparent rounded-full" /></div>;
|
|
213
|
+
|
|
214
|
+
const onSuccess = (data: CreateUpdateResponseType) => {
|
|
215
|
+
toast.success(id ? "Updated" : "Created");
|
|
216
|
+
navigate("/products");
|
|
622
217
|
};
|
|
623
218
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
register,
|
|
627
|
-
handleSubmit,
|
|
628
|
-
item,
|
|
629
|
-
errors,
|
|
630
|
-
isLoading,
|
|
631
|
-
isSubmitting,
|
|
632
|
-
loadError,
|
|
633
|
-
watch,
|
|
634
|
-
setValue,
|
|
635
|
-
operation,
|
|
636
|
-
}: {
|
|
637
|
-
register: FormRegisterType<SellerProductEditableFieldType, SellerProductReadonlyFieldType>;
|
|
638
|
-
handleSubmit: HandleSubmitType<CreateUpdateResponseType>;
|
|
639
|
-
item: FormItemType<SellerProductEditableFieldType, SellerProductReadonlyFieldType>;
|
|
640
|
-
errors: FieldErrors<AllFieldsType>;
|
|
641
|
-
isLoading: boolean;
|
|
642
|
-
isSubmitting: boolean;
|
|
643
|
-
loadError: Error | null;
|
|
644
|
-
watch: UseFormWatch<AllFieldsType>;
|
|
645
|
-
setValue: UseFormSetValue<SellerProductEditableFieldType>;
|
|
646
|
-
operation: "create" | "update";
|
|
647
|
-
} = useForm(formOptions);
|
|
648
|
-
|
|
649
|
-
// Loading state
|
|
650
|
-
if (isLoading) {
|
|
651
|
-
return <div>Loading product...</div>;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
if (loadError) {
|
|
655
|
-
return <div>Error loading product: {loadError.message}</div>;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// Typed handlers
|
|
659
|
-
const onFormSuccess = (data: CreateUpdateResponseType): void => {
|
|
660
|
-
setGeneralError(null);
|
|
661
|
-
console.log("Saved with ID:", data._id);
|
|
662
|
-
onSuccess();
|
|
219
|
+
const onError = (error: any) => {
|
|
220
|
+
toast.error(error instanceof Error ? error.message : "Please fix errors above");
|
|
663
221
|
};
|
|
664
222
|
|
|
665
|
-
const onFormError = (error: FieldErrors | Error): void => {
|
|
666
|
-
if (error instanceof Error) {
|
|
667
|
-
setGeneralError(error.message);
|
|
668
|
-
} else {
|
|
669
|
-
setGeneralError("Please fix the validation errors");
|
|
670
|
-
}
|
|
671
|
-
};
|
|
672
|
-
|
|
673
|
-
// Watch for dynamic behavior
|
|
674
|
-
const currentPrice = watch(product.Price.id);
|
|
675
|
-
|
|
676
223
|
return (
|
|
677
|
-
<form onSubmit={handleSubmit(
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
<
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
{/* Readonly field (auto-disabled) */}
|
|
685
|
-
{operation === FormOperation.Update && (
|
|
686
|
-
<div className="field">
|
|
687
|
-
<label>{product.ASIN.label}</label>
|
|
688
|
-
<input {...register(product.ASIN.id)} />
|
|
689
|
-
</div>
|
|
690
|
-
)}
|
|
691
|
-
|
|
692
|
-
{/* Title */}
|
|
693
|
-
<div className="field">
|
|
694
|
-
<label>{product.Title.label}</label>
|
|
695
|
-
<input
|
|
696
|
-
{...register(product.Title.id)}
|
|
697
|
-
placeholder="Enter product title"
|
|
698
|
-
/>
|
|
699
|
-
{errors.Title && (
|
|
700
|
-
<span className="error">{errors.Title.message}</span>
|
|
701
|
-
)}
|
|
224
|
+
<form onSubmit={handleSubmit(onSuccess, onError)} className="space-y-6">
|
|
225
|
+
{/* StringField — register */}
|
|
226
|
+
<div>
|
|
227
|
+
<label>{bdo.product_name.label}{bdo.product_name.required && <span className="text-red-500"> *</span>}</label>
|
|
228
|
+
<input {...register(bdo.product_name.id)} className="w-full border rounded px-3 py-2" />
|
|
229
|
+
{errors.product_name && <p className="text-red-600 text-sm">{errors.product_name.message}</p>}
|
|
702
230
|
</div>
|
|
703
231
|
|
|
704
|
-
{/*
|
|
705
|
-
<div
|
|
706
|
-
<label>{
|
|
707
|
-
<
|
|
708
|
-
{errors.
|
|
709
|
-
<span className="error">{errors.Description.message}</span>
|
|
710
|
-
)}
|
|
232
|
+
{/* NumberField — register */}
|
|
233
|
+
<div>
|
|
234
|
+
<label>{bdo.unit_price.label}</label>
|
|
235
|
+
<input type="number" step="0.01" {...register(bdo.unit_price.id)} className="w-full border rounded px-3 py-2" />
|
|
236
|
+
{errors.unit_price && <p className="text-red-600 text-sm">{errors.unit_price.message}</p>}
|
|
711
237
|
</div>
|
|
712
238
|
|
|
713
|
-
{/*
|
|
714
|
-
<div
|
|
715
|
-
<label>{
|
|
716
|
-
<
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
{errors.
|
|
722
|
-
<span className="error">{errors.Price.message}</span>
|
|
723
|
-
)}
|
|
724
|
-
{currentPrice > 1000 && (
|
|
725
|
-
<span className="warning">High price - verify before saving</span>
|
|
726
|
-
)}
|
|
239
|
+
{/* StringField with Constraint.Enum — hardcoded options (NO .options getter) */}
|
|
240
|
+
<div>
|
|
241
|
+
<label>{bdo.status.label}</label>
|
|
242
|
+
<select {...register(bdo.status.id)} className="w-full border rounded px-3 py-2">
|
|
243
|
+
<option value="">Select {bdo.status.label}</option>
|
|
244
|
+
<option value="Active">Active</option>
|
|
245
|
+
<option value="Discontinued">Discontinued</option>
|
|
246
|
+
</select>
|
|
247
|
+
{errors.status && <p className="text-red-600 text-sm">{errors.status.message}</p>}
|
|
727
248
|
</div>
|
|
728
249
|
|
|
729
|
-
{/*
|
|
730
|
-
<div
|
|
731
|
-
<label>{
|
|
732
|
-
<
|
|
733
|
-
|
|
734
|
-
{
|
|
250
|
+
{/* ReferenceField — ReferenceSelect component */}
|
|
251
|
+
<div>
|
|
252
|
+
<label>{bdo.category.label}</label>
|
|
253
|
+
<ReferenceSelect
|
|
254
|
+
bdoField={bdo.category}
|
|
255
|
+
instanceId={id || String(watch("_id") ?? "")}
|
|
256
|
+
value={watch(bdo.category.id)}
|
|
257
|
+
onChange={(val) => setValue(bdo.category.id, val, { shouldDirty: true })}
|
|
735
258
|
/>
|
|
736
|
-
{errors.
|
|
737
|
-
<span className="error">{errors.Stock.message}</span>
|
|
738
|
-
)}
|
|
259
|
+
{errors.category && <p className="text-red-600 text-sm">{String(errors.category.message ?? "")}</p>}
|
|
739
260
|
</div>
|
|
740
261
|
|
|
741
|
-
{/*
|
|
742
|
-
<div className="
|
|
743
|
-
<
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
>
|
|
748
|
-
<option value="">Select category</option>
|
|
749
|
-
<option value="Electronics">Electronics</option>
|
|
750
|
-
<option value="Clothing">Clothing</option>
|
|
751
|
-
<option value="Books">Books</option>
|
|
752
|
-
</select>
|
|
753
|
-
{errors.Category && (
|
|
754
|
-
<span className="error">{errors.Category.message}</span>
|
|
755
|
-
)}
|
|
262
|
+
{/* BooleanField — watch + setValue */}
|
|
263
|
+
<div className="flex items-center gap-2">
|
|
264
|
+
<Checkbox
|
|
265
|
+
checked={Boolean(watch(bdo.is_active.id))}
|
|
266
|
+
onCheckedChange={(v) => setValue(bdo.is_active.id, v as boolean, { shouldDirty: true })}
|
|
267
|
+
/>
|
|
268
|
+
<label>{bdo.is_active.label}</label>
|
|
756
269
|
</div>
|
|
757
270
|
|
|
758
|
-
{/*
|
|
759
|
-
<div
|
|
760
|
-
<
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
}
|
|
765
|
-
|
|
271
|
+
{/* ImageField — ImageUpload component */}
|
|
272
|
+
<div>
|
|
273
|
+
<label>{bdo.product_image.label}</label>
|
|
274
|
+
<ImageUpload
|
|
275
|
+
field={item.product_image}
|
|
276
|
+
value={watch(bdo.product_image.id)}
|
|
277
|
+
boId={bdo.meta._id}
|
|
278
|
+
instanceId={id || String(watch("_id") ?? "")}
|
|
279
|
+
fieldId={bdo.product_image.id}
|
|
280
|
+
/>
|
|
766
281
|
</div>
|
|
767
282
|
|
|
768
|
-
{/*
|
|
769
|
-
<
|
|
770
|
-
<
|
|
771
|
-
<
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
---
|
|
779
|
-
|
|
780
|
-
## File & Image Attachments in Forms
|
|
781
|
-
|
|
782
|
-
Image and File fields get attachment methods on the `item` accessor automatically.
|
|
783
|
-
`upload()` updates local field state only — the form's `handleSubmit` persists everything to the backend.
|
|
784
|
-
`deleteAttachment()` is atomic — it removes the file from storage and the backend record immediately.
|
|
785
|
-
|
|
786
|
-
```tsx
|
|
787
|
-
// Assuming SellerProduct BDO has:
|
|
788
|
-
// readonly ProductImage = new ImageField({ _id: "ProductImage", Name: "Product Image", Type: "Image" });
|
|
789
|
-
// readonly Attachments = new FileField({ _id: "Attachments", Name: "Attachments", Type: "File" });
|
|
790
|
-
|
|
791
|
-
function ProductFormWithAttachments({ productId }: { productId?: string }) {
|
|
792
|
-
const product = useMemo(() => new SellerProduct(), []);
|
|
793
|
-
|
|
794
|
-
const { register, handleSubmit, item, errors, isSubmitting, watch } = useForm({
|
|
795
|
-
bdo: product,
|
|
796
|
-
recordId: productId,
|
|
797
|
-
mode: ValidationMode.OnBlur,
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
// --- Image field (single file) ---
|
|
801
|
-
|
|
802
|
-
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
803
|
-
const file = e.target.files?.[0];
|
|
804
|
-
if (!file) return;
|
|
805
|
-
|
|
806
|
-
// Uploads to storage, sets local value (FileType)
|
|
807
|
-
// Validates extension client-side before uploading
|
|
808
|
-
const metadata = await item.ProductImage.upload(file);
|
|
809
|
-
console.log("Uploaded image:", metadata.FileName);
|
|
810
|
-
};
|
|
811
|
-
|
|
812
|
-
const handleImageDelete = async () => {
|
|
813
|
-
// Atomic — deletes from storage + backend immediately
|
|
814
|
-
await item.ProductImage.deleteAttachment();
|
|
815
|
-
};
|
|
816
|
-
|
|
817
|
-
const handleImagePreview = async () => {
|
|
818
|
-
// Get a thumbnail URL (backend-generated variant)
|
|
819
|
-
const { DownloadUrl } = await item.ProductImage.getDownloadUrl("thumbnail");
|
|
820
|
-
window.open(DownloadUrl);
|
|
821
|
-
};
|
|
822
|
-
|
|
823
|
-
// --- File field (multi-file) ---
|
|
824
|
-
|
|
825
|
-
const handleFilesUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
826
|
-
const files = Array.from(e.target.files ?? []);
|
|
827
|
-
if (!files.length) return;
|
|
828
|
-
|
|
829
|
-
// Uploads all files in parallel, appends to existing array
|
|
830
|
-
const uploaded = await item.Attachments.upload(files);
|
|
831
|
-
console.log(`Uploaded ${uploaded.length} files`);
|
|
832
|
-
};
|
|
833
|
-
|
|
834
|
-
const handleFileDelete = async (attachmentId: string) => {
|
|
835
|
-
// Removes single file from storage + backend + local array
|
|
836
|
-
await item.Attachments.deleteAttachment(attachmentId);
|
|
837
|
-
};
|
|
838
|
-
|
|
839
|
-
const handleFileDownload = async (attachmentId: string) => {
|
|
840
|
-
const { DownloadUrl } = await item.Attachments.getDownloadUrl(attachmentId);
|
|
841
|
-
window.open(DownloadUrl);
|
|
842
|
-
};
|
|
843
|
-
|
|
844
|
-
// Current field values
|
|
845
|
-
const currentImage = item.ProductImage.get(); // FileType | null
|
|
846
|
-
const currentFiles = item.Attachments.get() ?? []; // FileType[]
|
|
847
|
-
|
|
848
|
-
return (
|
|
849
|
-
<form onSubmit={handleSubmit((data) => console.log("Saved:", data._id))}>
|
|
850
|
-
{/* ... other fields with register() ... */}
|
|
851
|
-
|
|
852
|
-
{/* Image field */}
|
|
853
|
-
<div className="field">
|
|
854
|
-
<label>{product.ProductImage.label}</label>
|
|
855
|
-
{currentImage ? (
|
|
856
|
-
<div>
|
|
857
|
-
<span>{currentImage.FileName}</span>
|
|
858
|
-
<button type="button" onClick={handleImagePreview}>Preview</button>
|
|
859
|
-
<button type="button" onClick={handleImageDelete}>Remove</button>
|
|
860
|
-
</div>
|
|
861
|
-
) : (
|
|
862
|
-
<input type="file" accept="image/*" onChange={handleImageUpload} />
|
|
863
|
-
)}
|
|
283
|
+
{/* FileField — FileUpload component */}
|
|
284
|
+
<div>
|
|
285
|
+
<label>{bdo.specification_document.label}</label>
|
|
286
|
+
<FileUpload
|
|
287
|
+
field={item.specification_document}
|
|
288
|
+
value={watch(bdo.specification_document.id)}
|
|
289
|
+
boId={bdo.meta._id}
|
|
290
|
+
instanceId={id || String(watch("_id") ?? "")}
|
|
291
|
+
fieldId={bdo.specification_document.id}
|
|
292
|
+
/>
|
|
864
293
|
</div>
|
|
865
294
|
|
|
866
|
-
{/*
|
|
867
|
-
<div
|
|
868
|
-
<label>{
|
|
869
|
-
<
|
|
870
|
-
<ul>
|
|
871
|
-
{currentFiles.map((file) => (
|
|
872
|
-
<li key={file._id}>
|
|
873
|
-
{file.FileName} ({file.Size} bytes)
|
|
874
|
-
<button type="button" onClick={() => handleFileDownload(file._id)}>Download</button>
|
|
875
|
-
<button type="button" onClick={() => handleFileDelete(file._id)}>Delete</button>
|
|
876
|
-
</li>
|
|
877
|
-
))}
|
|
878
|
-
</ul>
|
|
295
|
+
{/* TextField — textarea with register */}
|
|
296
|
+
<div>
|
|
297
|
+
<label>{bdo.description.label}</label>
|
|
298
|
+
<textarea {...register(bdo.description.id)} rows={4} className="w-full border rounded px-3 py-2" />
|
|
879
299
|
</div>
|
|
880
300
|
|
|
881
|
-
<button type="submit" disabled={isSubmitting}>
|
|
882
|
-
{isSubmitting ? "Saving..." : "
|
|
301
|
+
<button type="submit" disabled={isSubmitting} className="px-4 py-2 bg-primary text-white rounded">
|
|
302
|
+
{isSubmitting ? "Saving..." : id ? "Update" : "Create"}
|
|
883
303
|
</button>
|
|
884
304
|
</form>
|
|
885
305
|
);
|
|
886
306
|
}
|
|
887
307
|
```
|
|
888
308
|
|
|
889
|
-
**Key points:**
|
|
890
|
-
- `upload()` validates file extensions client-side (throws on unsupported types)
|
|
891
|
-
- Image upload replaces the current value; File upload appends to the array
|
|
892
|
-
- `getDownloadUrl("thumbnail")` / `getDownloadUrl("preview")` request backend-generated variants
|
|
893
|
-
- Readonly Image/File fields only have `getDownloadUrl` — no `upload` or `deleteAttachment`
|
|
894
|
-
|
|
895
309
|
---
|
|
896
310
|
|
|
897
|
-
##
|
|
898
|
-
|
|
899
|
-
```typescript
|
|
900
|
-
import { ValidationMode, FormOperation } from "@ram_28/kf-ai-sdk/form";
|
|
311
|
+
## Type Definitions
|
|
901
312
|
|
|
902
|
-
|
|
903
|
-
ValidationMode.OnBlur // "onBlur"
|
|
904
|
-
ValidationMode.OnChange // "onChange"
|
|
905
|
-
ValidationMode.OnSubmit // "onSubmit"
|
|
906
|
-
ValidationMode.OnTouched // "onTouched"
|
|
907
|
-
ValidationMode.All // "all"
|
|
313
|
+
### UseFormOptionsType (Discriminated Union)
|
|
908
314
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
315
|
+
```typescript
|
|
316
|
+
type UseFormOptionsType<B extends BaseBdo<any, any, any>> =
|
|
317
|
+
| { bdo: B; operation: "create"; defaultValues?: Partial<EditableFieldType>; mode?: ValidationModeType; }
|
|
318
|
+
| { bdo: B; operation: "update"; recordId: string; mode?: ValidationModeType; }
|
|
319
|
+
| { bdo: B; recordId?: string; defaultValues?: Partial<EditableFieldType>; mode?: ValidationModeType; };
|
|
912
320
|
```
|
|
913
321
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
## Common Mistakes
|
|
917
|
-
|
|
918
|
-
### 1. Passing FieldType instead of BDO instance
|
|
919
|
-
|
|
920
|
-
`useForm` takes a BDO class **instance**, NOT a FieldType. The hook infers all types from the BDO class.
|
|
322
|
+
### UseFormReturnType
|
|
921
323
|
|
|
922
324
|
```typescript
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
//
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
325
|
+
interface UseFormReturnType<B> {
|
|
326
|
+
item: FormItemType<EditableFieldType, ReadonlyFieldType>; // Field accessors (.get(), .set(), .upload())
|
|
327
|
+
register: FormRegisterType; // Auto-disables readonly fields
|
|
328
|
+
handleSubmit: HandleSubmitType; // Auto-calls bdo.create() or bdo.update()
|
|
329
|
+
watch: UseFormWatch; // Watch field values by bdo.field.id
|
|
330
|
+
setValue: UseFormSetValue; // Set field values by bdo.field.id
|
|
331
|
+
getValues: UseFormGetValues;
|
|
332
|
+
control: Control;
|
|
333
|
+
formState: FormState;
|
|
334
|
+
errors: FieldErrors;
|
|
335
|
+
isLoading: boolean; // Fetching record data (edit mode)
|
|
336
|
+
isSubmitting: boolean;
|
|
337
|
+
isDirty: boolean;
|
|
338
|
+
loadError: Error | null;
|
|
339
|
+
}
|
|
932
340
|
```
|
|
933
341
|
|
|
934
|
-
###
|
|
342
|
+
### File & Image Types
|
|
935
343
|
|
|
936
344
|
```typescript
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
// ✅ CORRECT — CreateUpdateResponseType = { _id: string }
|
|
942
|
-
import type { CreateUpdateResponseType } from "@ram_28/kf-ai-sdk/api/types";
|
|
943
|
-
const onSuccess = (data: CreateUpdateResponseType): void => {
|
|
944
|
-
console.log("Saved:", data._id);
|
|
945
|
-
};
|
|
345
|
+
interface FileType { _id: string; _name: string; FileName: string; FileExtension: string; Size: number; ContentType: string; }
|
|
346
|
+
type ImageFieldType = FileType | null; // Single image, nullable
|
|
347
|
+
type FileFieldType = FileType[]; // Array of files
|
|
946
348
|
```
|
|
947
349
|
|
|
948
|
-
|
|
350
|
+
Image accessor: `item.field.get()` returns `FileType | null`. Has `upload(file: File)`, `deleteAttachment()`, `getDownloadUrl()`.
|
|
351
|
+
File accessor: `item.field.get()` returns `FileType[]`. Has `upload(files: File[])`, `deleteAttachment(id)`, `getDownloadUrl(id)`.
|
|
949
352
|
|
|
950
|
-
|
|
353
|
+
### Constants
|
|
951
354
|
|
|
952
355
|
```typescript
|
|
953
|
-
//
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
// ✅ CORRECT — use watch + setValue
|
|
957
|
-
<Checkbox
|
|
958
|
-
checked={Boolean(watch(bdo.IsActive.id))}
|
|
959
|
-
onCheckedChange={(v) => setValue(bdo.IsActive.id, v as boolean)}
|
|
960
|
-
/>
|
|
356
|
+
FormOperation.Create // "create"
|
|
357
|
+
FormOperation.Update // "update"
|
|
358
|
+
ValidationMode.OnBlur / .OnChange / .OnSubmit / .OnTouched / .All
|
|
961
359
|
```
|
|
962
360
|
|
|
963
|
-
###
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
361
|
+
### Field-Type to UI Component Mapping
|
|
362
|
+
|
|
363
|
+
| BDO Field Class | UI Pattern |
|
|
364
|
+
|---|---|
|
|
365
|
+
| `StringField` | `<input {...register(bdo.field.id)} />` |
|
|
366
|
+
| `StringField` with `Constraint.Enum` | `<select {...register(bdo.field.id)}>` with hardcoded `<option>` from Enum array |
|
|
367
|
+
| `SelectField` | `<select {...register(bdo.field.id)}>` with `bdo.field.options.map()` |
|
|
368
|
+
| `TextField` | `<textarea {...register(bdo.field.id)} />` |
|
|
369
|
+
| `NumberField` | `<input type="number" {...register(bdo.field.id)} />` |
|
|
370
|
+
| `BooleanField` | `<Checkbox checked={watch()} onCheckedChange={v => setValue()} />` |
|
|
371
|
+
| `DateField` | `<input type="date" {...register(bdo.field.id)} />` |
|
|
372
|
+
| `DateTimeField` | `<input type="datetime-local" {...register(bdo.field.id)} />` |
|
|
373
|
+
| `ReferenceField` | `<ReferenceSelect bdoField={bdo.field} instanceId={id \|\| String(watch("_id") ?? "")} value={watch()} onChange={...} />` |
|
|
374
|
+
| `UserField` | `<ReferenceSelect bdoField={bdo.field} instanceId={id \|\| String(watch("_id") ?? "")} value={watch()} onChange={...} />` |
|
|
375
|
+
| `ImageField` | `<ImageUpload field={item.field} value={watch()} boId={} instanceId={} fieldId={} />` |
|
|
376
|
+
| `FileField` | `<FileUpload field={item.field} value={watch()} boId={} instanceId={} fieldId={} />` |
|