@ram_28/kf-ai-sdk 2.0.11 → 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.
- 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 +263 -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 +153 -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,373 @@
|
|
|
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
|
-
} from "@ram_28/kf-ai-sdk/form/types";
|
|
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
|
+
value={watch(bdo.category.id)}
|
|
115
|
+
onChange={(val) => setValue(bdo.category.id, val, { shouldDirty: true })}
|
|
116
|
+
/>
|
|
329
117
|
```
|
|
330
118
|
|
|
331
|
-
###
|
|
119
|
+
### 6. Using custom Image/File upload (should use template components)
|
|
332
120
|
|
|
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(), []);
|
|
121
|
+
Use `<ImageUpload>` and `<FileUpload>`. CRITICAL: `instanceId` must work for BOTH edit and create mode.
|
|
356
122
|
|
|
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>
|
|
123
|
+
```tsx
|
|
124
|
+
// ❌ WRONG — instanceId={id} is undefined in create mode
|
|
125
|
+
<ImageUpload field={item.icon} value={watch(bdo.icon.id)} boId={bdo.meta._id} instanceId={id} fieldId={bdo.icon.id} />
|
|
126
|
+
|
|
127
|
+
// ✅ CORRECT — ImageUpload (single image)
|
|
128
|
+
<ImageUpload
|
|
129
|
+
field={item.product_image}
|
|
130
|
+
value={watch(bdo.product_image.id)}
|
|
131
|
+
boId={bdo.meta._id}
|
|
132
|
+
instanceId={id || String(watch("_id") ?? "")}
|
|
133
|
+
fieldId={bdo.product_image.id}
|
|
134
|
+
/>
|
|
410
135
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
136
|
+
// ✅ CORRECT — FileUpload (multi-file)
|
|
137
|
+
<FileUpload
|
|
138
|
+
field={item.specification_document}
|
|
139
|
+
value={watch(bdo.specification_document.id)}
|
|
140
|
+
boId={bdo.meta._id}
|
|
141
|
+
instanceId={id || String(watch("_id") ?? "")}
|
|
142
|
+
fieldId={bdo.specification_document.id}
|
|
143
|
+
/>
|
|
417
144
|
```
|
|
418
145
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
## register() Function
|
|
422
|
-
|
|
423
|
-
The `register` function extends React Hook Form's standard register with automatic readonly field handling.
|
|
146
|
+
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
147
|
|
|
425
|
-
###
|
|
148
|
+
### 7. Wrong handleSubmit onSuccess type
|
|
426
149
|
|
|
427
150
|
```typescript
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
// => UseFormRegisterReturn
|
|
151
|
+
// ❌ WRONG
|
|
152
|
+
const onSuccess = (data: unknown) => { ... };
|
|
431
153
|
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
154
|
+
// ✅ CORRECT — CreateUpdateResponseType = { _id: string }
|
|
155
|
+
import type { CreateUpdateResponseType } from "@ram_28/kf-ai-sdk/api/types";
|
|
156
|
+
const onSuccess = (data: CreateUpdateResponseType) => { toast.success("Saved"); navigate("/list"); };
|
|
435
157
|
```
|
|
436
158
|
|
|
437
|
-
###
|
|
159
|
+
### 8. Passing FieldType instead of BDO instance
|
|
438
160
|
|
|
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
|
|
161
|
+
```typescript
|
|
162
|
+
// ❌ WRONG
|
|
163
|
+
useForm<ProductFieldType>({ ... });
|
|
446
164
|
|
|
447
|
-
//
|
|
448
|
-
|
|
165
|
+
// ✅ CORRECT — pass BDO class instance
|
|
166
|
+
const product = useMemo(() => new SellerProduct(), []);
|
|
167
|
+
useForm({ bdo: product, operation: FormOperation.Create, mode: ValidationMode.OnBlur });
|
|
449
168
|
```
|
|
450
169
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
## handleSubmit() Function
|
|
454
|
-
|
|
455
|
-
Custom handleSubmit that handles API calls automatically and filters payload to editable fields only.
|
|
456
|
-
|
|
457
|
-
### Type
|
|
170
|
+
### 9. Wrong default values for date fields
|
|
458
171
|
|
|
459
172
|
```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
|
|
173
|
+
// ❌ WRONG — empty string causes type errors
|
|
174
|
+
defaultValues: { start_date: "" }
|
|
474
175
|
|
|
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>
|
|
176
|
+
// ✅ CORRECT
|
|
177
|
+
defaultValues: { start_date: undefined }
|
|
494
178
|
```
|
|
495
179
|
|
|
496
180
|
---
|
|
497
181
|
|
|
498
|
-
##
|
|
499
|
-
|
|
500
|
-
The `item` object provides field-level access with typed accessors.
|
|
501
|
-
|
|
502
|
-
### Field Access
|
|
182
|
+
## Complete Form Example (Create + Edit)
|
|
503
183
|
|
|
504
184
|
```tsx
|
|
505
|
-
|
|
506
|
-
const title = item.Title.get();
|
|
507
|
-
|
|
508
|
-
// Set field value (editable fields only)
|
|
509
|
-
item.Title.set("New Title");
|
|
185
|
+
"use no memo";
|
|
510
186
|
|
|
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";
|
|
187
|
+
import { useMemo } from "react";
|
|
188
|
+
import { useNavigate, useParams } from "react-router-dom";
|
|
189
|
+
import { useForm, ValidationMode, FormOperation } from "@ram_28/kf-ai-sdk/form";
|
|
592
190
|
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,
|
|
191
|
+
import { AdminProduct } from "@/bdo/admin/Product";
|
|
192
|
+
import { ReferenceSelect } from "@/components/ui/reference-select";
|
|
193
|
+
import { ImageUpload } from "@/components/ui/image-upload";
|
|
194
|
+
import { FileUpload } from "@/components/ui/file-upload";
|
|
195
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
196
|
+
import { toast } from "sonner";
|
|
197
|
+
|
|
198
|
+
export default function ProductForm() {
|
|
199
|
+
const { id } = useParams<{ id: string }>();
|
|
200
|
+
const navigate = useNavigate();
|
|
201
|
+
const bdo = useMemo(() => new AdminProduct(), []);
|
|
202
|
+
|
|
203
|
+
// Create/Edit ternary — NO type annotation on result
|
|
204
|
+
const formResult = id
|
|
205
|
+
? useForm({ bdo, operation: FormOperation.Update, recordId: id, mode: ValidationMode.OnBlur })
|
|
206
|
+
: useForm({ bdo, operation: FormOperation.Create, mode: ValidationMode.OnBlur });
|
|
207
|
+
|
|
208
|
+
const { register, handleSubmit, watch, setValue, item, formState: { errors, isSubmitting }, isLoading } = formResult;
|
|
209
|
+
|
|
210
|
+
// Loading guard AFTER all hooks
|
|
211
|
+
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>;
|
|
212
|
+
|
|
213
|
+
const onSuccess = (data: CreateUpdateResponseType) => {
|
|
214
|
+
toast.success(id ? "Updated" : "Created");
|
|
215
|
+
navigate("/products");
|
|
622
216
|
};
|
|
623
217
|
|
|
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();
|
|
218
|
+
const onError = (error: any) => {
|
|
219
|
+
toast.error(error instanceof Error ? error.message : "Please fix errors above");
|
|
663
220
|
};
|
|
664
221
|
|
|
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
222
|
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
|
-
)}
|
|
223
|
+
<form onSubmit={handleSubmit(onSuccess, onError)} className="space-y-6">
|
|
224
|
+
{/* StringField — register */}
|
|
225
|
+
<div>
|
|
226
|
+
<label>{bdo.product_name.label}{bdo.product_name.required && <span className="text-red-500"> *</span>}</label>
|
|
227
|
+
<input {...register(bdo.product_name.id)} className="w-full border rounded px-3 py-2" />
|
|
228
|
+
{errors.product_name && <p className="text-red-600 text-sm">{errors.product_name.message}</p>}
|
|
702
229
|
</div>
|
|
703
230
|
|
|
704
|
-
{/*
|
|
705
|
-
<div
|
|
706
|
-
<label>{
|
|
707
|
-
<
|
|
708
|
-
{errors.
|
|
709
|
-
<span className="error">{errors.Description.message}</span>
|
|
710
|
-
)}
|
|
231
|
+
{/* NumberField — register */}
|
|
232
|
+
<div>
|
|
233
|
+
<label>{bdo.unit_price.label}</label>
|
|
234
|
+
<input type="number" step="0.01" {...register(bdo.unit_price.id)} className="w-full border rounded px-3 py-2" />
|
|
235
|
+
{errors.unit_price && <p className="text-red-600 text-sm">{errors.unit_price.message}</p>}
|
|
711
236
|
</div>
|
|
712
237
|
|
|
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
|
-
)}
|
|
238
|
+
{/* StringField with Constraint.Enum — hardcoded options (NO .options getter) */}
|
|
239
|
+
<div>
|
|
240
|
+
<label>{bdo.status.label}</label>
|
|
241
|
+
<select {...register(bdo.status.id)} className="w-full border rounded px-3 py-2">
|
|
242
|
+
<option value="">Select {bdo.status.label}</option>
|
|
243
|
+
<option value="Active">Active</option>
|
|
244
|
+
<option value="Discontinued">Discontinued</option>
|
|
245
|
+
</select>
|
|
246
|
+
{errors.status && <p className="text-red-600 text-sm">{errors.status.message}</p>}
|
|
727
247
|
</div>
|
|
728
248
|
|
|
729
|
-
{/*
|
|
730
|
-
<div
|
|
731
|
-
<label>{
|
|
732
|
-
<
|
|
733
|
-
|
|
734
|
-
{
|
|
249
|
+
{/* ReferenceField — ReferenceSelect component */}
|
|
250
|
+
<div>
|
|
251
|
+
<label>{bdo.category.label}</label>
|
|
252
|
+
<ReferenceSelect
|
|
253
|
+
bdoField={bdo.category}
|
|
254
|
+
value={watch(bdo.category.id)}
|
|
255
|
+
onChange={(val) => setValue(bdo.category.id, val, { shouldDirty: true })}
|
|
735
256
|
/>
|
|
736
|
-
{errors.
|
|
737
|
-
<span className="error">{errors.Stock.message}</span>
|
|
738
|
-
)}
|
|
257
|
+
{errors.category && <p className="text-red-600 text-sm">{String(errors.category.message ?? "")}</p>}
|
|
739
258
|
</div>
|
|
740
259
|
|
|
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
|
-
)}
|
|
260
|
+
{/* BooleanField — watch + setValue */}
|
|
261
|
+
<div className="flex items-center gap-2">
|
|
262
|
+
<Checkbox
|
|
263
|
+
checked={Boolean(watch(bdo.is_active.id))}
|
|
264
|
+
onCheckedChange={(v) => setValue(bdo.is_active.id, v as boolean, { shouldDirty: true })}
|
|
265
|
+
/>
|
|
266
|
+
<label>{bdo.is_active.label}</label>
|
|
756
267
|
</div>
|
|
757
268
|
|
|
758
|
-
{/*
|
|
759
|
-
<div
|
|
760
|
-
<
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
}
|
|
765
|
-
|
|
269
|
+
{/* ImageField — ImageUpload component */}
|
|
270
|
+
<div>
|
|
271
|
+
<label>{bdo.product_image.label}</label>
|
|
272
|
+
<ImageUpload
|
|
273
|
+
field={item.product_image}
|
|
274
|
+
value={watch(bdo.product_image.id)}
|
|
275
|
+
boId={bdo.meta._id}
|
|
276
|
+
instanceId={id || String(watch("_id") ?? "")}
|
|
277
|
+
fieldId={bdo.product_image.id}
|
|
278
|
+
/>
|
|
766
279
|
</div>
|
|
767
280
|
|
|
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
|
-
)}
|
|
281
|
+
{/* FileField — FileUpload component */}
|
|
282
|
+
<div>
|
|
283
|
+
<label>{bdo.specification_document.label}</label>
|
|
284
|
+
<FileUpload
|
|
285
|
+
field={item.specification_document}
|
|
286
|
+
value={watch(bdo.specification_document.id)}
|
|
287
|
+
boId={bdo.meta._id}
|
|
288
|
+
instanceId={id || String(watch("_id") ?? "")}
|
|
289
|
+
fieldId={bdo.specification_document.id}
|
|
290
|
+
/>
|
|
864
291
|
</div>
|
|
865
292
|
|
|
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>
|
|
293
|
+
{/* TextField — textarea with register */}
|
|
294
|
+
<div>
|
|
295
|
+
<label>{bdo.description.label}</label>
|
|
296
|
+
<textarea {...register(bdo.description.id)} rows={4} className="w-full border rounded px-3 py-2" />
|
|
879
297
|
</div>
|
|
880
298
|
|
|
881
|
-
<button type="submit" disabled={isSubmitting}>
|
|
882
|
-
{isSubmitting ? "Saving..." : "
|
|
299
|
+
<button type="submit" disabled={isSubmitting} className="px-4 py-2 bg-primary text-white rounded">
|
|
300
|
+
{isSubmitting ? "Saving..." : id ? "Update" : "Create"}
|
|
883
301
|
</button>
|
|
884
302
|
</form>
|
|
885
303
|
);
|
|
886
304
|
}
|
|
887
305
|
```
|
|
888
306
|
|
|
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
307
|
---
|
|
896
308
|
|
|
897
|
-
##
|
|
898
|
-
|
|
899
|
-
```typescript
|
|
900
|
-
import { ValidationMode, FormOperation } from "@ram_28/kf-ai-sdk/form";
|
|
309
|
+
## Type Definitions
|
|
901
310
|
|
|
902
|
-
|
|
903
|
-
ValidationMode.OnBlur // "onBlur"
|
|
904
|
-
ValidationMode.OnChange // "onChange"
|
|
905
|
-
ValidationMode.OnSubmit // "onSubmit"
|
|
906
|
-
ValidationMode.OnTouched // "onTouched"
|
|
907
|
-
ValidationMode.All // "all"
|
|
311
|
+
### UseFormOptionsType (Discriminated Union)
|
|
908
312
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
313
|
+
```typescript
|
|
314
|
+
type UseFormOptionsType<B extends BaseBdo<any, any, any>> =
|
|
315
|
+
| { bdo: B; operation: "create"; defaultValues?: Partial<EditableFieldType>; mode?: ValidationModeType; }
|
|
316
|
+
| { bdo: B; operation: "update"; recordId: string; mode?: ValidationModeType; }
|
|
317
|
+
| { bdo: B; recordId?: string; defaultValues?: Partial<EditableFieldType>; mode?: ValidationModeType; };
|
|
912
318
|
```
|
|
913
319
|
|
|
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.
|
|
320
|
+
### UseFormReturnType
|
|
921
321
|
|
|
922
322
|
```typescript
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
//
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
323
|
+
interface UseFormReturnType<B> {
|
|
324
|
+
item: FormItemType<EditableFieldType, ReadonlyFieldType>; // Field accessors (.get(), .set(), .upload())
|
|
325
|
+
register: FormRegisterType; // Auto-disables readonly fields
|
|
326
|
+
handleSubmit: HandleSubmitType; // Auto-calls bdo.create() or bdo.update()
|
|
327
|
+
watch: UseFormWatch; // Watch field values by bdo.field.id
|
|
328
|
+
setValue: UseFormSetValue; // Set field values by bdo.field.id
|
|
329
|
+
getValues: UseFormGetValues;
|
|
330
|
+
control: Control;
|
|
331
|
+
formState: FormState;
|
|
332
|
+
errors: FieldErrors;
|
|
333
|
+
isLoading: boolean; // Fetching record data (edit mode)
|
|
334
|
+
isSubmitting: boolean;
|
|
335
|
+
isDirty: boolean;
|
|
336
|
+
loadError: Error | null;
|
|
337
|
+
}
|
|
932
338
|
```
|
|
933
339
|
|
|
934
|
-
###
|
|
340
|
+
### File & Image Types
|
|
935
341
|
|
|
936
342
|
```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
|
-
};
|
|
343
|
+
interface FileType { _id: string; _name: string; FileName: string; FileExtension: string; Size: number; ContentType: string; }
|
|
344
|
+
type ImageFieldType = FileType | null; // Single image, nullable
|
|
345
|
+
type FileFieldType = FileType[]; // Array of files
|
|
946
346
|
```
|
|
947
347
|
|
|
948
|
-
|
|
348
|
+
Image accessor: `item.field.get()` returns `FileType | null`. Has `upload(file: File)`, `deleteAttachment()`, `getDownloadUrl()`.
|
|
349
|
+
File accessor: `item.field.get()` returns `FileType[]`. Has `upload(files: File[])`, `deleteAttachment(id)`, `getDownloadUrl(id)`.
|
|
949
350
|
|
|
950
|
-
|
|
351
|
+
### Constants
|
|
951
352
|
|
|
952
353
|
```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
|
-
/>
|
|
354
|
+
FormOperation.Create // "create"
|
|
355
|
+
FormOperation.Update // "update"
|
|
356
|
+
ValidationMode.OnBlur / .OnChange / .OnSubmit / .OnTouched / .All
|
|
961
357
|
```
|
|
962
358
|
|
|
963
|
-
###
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
359
|
+
### Field-Type to UI Component Mapping
|
|
360
|
+
|
|
361
|
+
| BDO Field Class | UI Pattern |
|
|
362
|
+
|---|---|
|
|
363
|
+
| `StringField` | `<input {...register(bdo.field.id)} />` |
|
|
364
|
+
| `StringField` with `Constraint.Enum` | `<select {...register(bdo.field.id)}>` with hardcoded `<option>` from Enum array |
|
|
365
|
+
| `SelectField` | `<select {...register(bdo.field.id)}>` with `bdo.field.options.map()` |
|
|
366
|
+
| `TextField` | `<textarea {...register(bdo.field.id)} />` |
|
|
367
|
+
| `NumberField` | `<input type="number" {...register(bdo.field.id)} />` |
|
|
368
|
+
| `BooleanField` | `<Checkbox checked={watch()} onCheckedChange={v => setValue()} />` |
|
|
369
|
+
| `DateField` | `<input type="date" {...register(bdo.field.id)} />` |
|
|
370
|
+
| `DateTimeField` | `<input type="datetime-local" {...register(bdo.field.id)} />` |
|
|
371
|
+
| `ReferenceField` | `<ReferenceSelect bdoField={bdo.field} value={watch()} onChange={...} />` |
|
|
372
|
+
| `ImageField` | `<ImageUpload field={item.field} value={watch()} boId={} instanceId={} fieldId={} />` |
|
|
373
|
+
| `FileField` | `<FileUpload field={item.field} value={watch()} boId={} instanceId={} fieldId={} />` |
|