@ram_28/kf-ai-sdk 1.0.22 → 1.0.24
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/auth.cjs +1 -1
- package/dist/auth.mjs +15 -15
- package/dist/components/hooks/useForm/types.d.ts +3 -5
- package/dist/components/hooks/useForm/types.d.ts.map +1 -1
- package/dist/components/hooks/useForm/useForm.d.ts.map +1 -1
- package/dist/form.cjs +1 -1
- package/dist/form.mjs +193 -194
- package/docs/QUICK_REFERENCE.md +24 -0
- package/docs/api.md +648 -0
- package/docs/datetime.md +144 -0
- package/docs/useFilter.md +58 -18
- package/docs/useForm.md +447 -588
- package/docs/useTable.md +160 -26
- package/package.json +1 -1
- package/sdk/auth/authClient.ts +2 -2
- package/sdk/components/hooks/useForm/types.ts +4 -7
- package/sdk/components/hooks/useForm/useForm.ts +5 -4
package/docs/useForm.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# useForm
|
|
2
2
|
|
|
3
|
-
Form state management with
|
|
3
|
+
Form state management with schema validation, computed fields, and react-hook-form integration.
|
|
4
4
|
|
|
5
5
|
## Imports
|
|
6
6
|
|
|
@@ -13,122 +13,229 @@ import {
|
|
|
13
13
|
clearFormCache,
|
|
14
14
|
} from "@ram_28/kf-ai-sdk/form";
|
|
15
15
|
import type {
|
|
16
|
-
FieldErrors,
|
|
17
16
|
UseFormOptionsType,
|
|
18
17
|
UseFormReturnType,
|
|
18
|
+
FormFieldConfigType,
|
|
19
|
+
FieldErrors,
|
|
19
20
|
} from "@ram_28/kf-ai-sdk/form/types";
|
|
20
21
|
```
|
|
21
22
|
|
|
22
|
-
## Product Class Pattern
|
|
23
|
-
|
|
24
|
-
The recommended pattern uses a role-based BDO wrapper class (like `Product`) instead of hardcoded source strings:
|
|
25
|
-
|
|
26
|
-
```tsx
|
|
27
|
-
// sources/Product.ts - Role-based BDO wrapper
|
|
28
|
-
import { Role, Roles } from "./roles";
|
|
29
|
-
|
|
30
|
-
export type ProductType<TRole extends Role> = /* role-mapped types */;
|
|
31
|
-
|
|
32
|
-
export class Product<TRole extends Role = typeof Roles.Admin> {
|
|
33
|
-
constructor(_role: TRole) {}
|
|
34
|
-
get _id(): string { return "BDO_AmazonProductMaster"; }
|
|
35
|
-
// API methods: list, get, create, update, delete, draft, etc.
|
|
36
|
-
}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
This pattern provides type-safe role-based field access, consistent with `useTable` and other hooks.
|
|
40
|
-
|
|
41
23
|
## Type Definitions
|
|
42
24
|
|
|
43
|
-
|
|
44
|
-
// Form operation type
|
|
45
|
-
type FormOperationType = "create" | "update";
|
|
25
|
+
### UseFormOptionsType
|
|
46
26
|
|
|
47
|
-
|
|
27
|
+
```typescript
|
|
28
|
+
// Hook options for initializing the form
|
|
48
29
|
interface UseFormOptionsType<T> {
|
|
30
|
+
// Business Object ID (required)
|
|
31
|
+
// Example: product._id
|
|
49
32
|
source: string;
|
|
50
|
-
|
|
33
|
+
|
|
34
|
+
// Form operation mode (required)
|
|
35
|
+
// - "create": Creates new record
|
|
36
|
+
// - "update": Edit existing record, loads record data
|
|
37
|
+
operation: "create" | "update";
|
|
38
|
+
|
|
39
|
+
// Record ID - required for update operations (required for operation: update)
|
|
40
|
+
// The form will fetch this record's data on mount
|
|
51
41
|
recordId?: string;
|
|
42
|
+
|
|
43
|
+
// Initial form values
|
|
44
|
+
// Merged with schema defaults and (for update) fetched record data
|
|
52
45
|
defaultValues?: Partial<T>;
|
|
46
|
+
|
|
47
|
+
// Validation trigger mode (from react-hook-form)
|
|
48
|
+
// - "onBlur" (default): Validate when field loses focus
|
|
49
|
+
// - "onChange": Validate on every keystroke
|
|
50
|
+
// - "onSubmit": Validate only on form submission
|
|
51
|
+
// - "onTouched": Validate on first blur, then on every change
|
|
52
|
+
// - "all": Validate on both blur and change
|
|
53
|
+
// Note: Computation (draft API) always fires on blur regardless of this setting
|
|
53
54
|
mode?: "onBlur" | "onChange" | "onSubmit" | "onTouched" | "all";
|
|
55
|
+
|
|
56
|
+
// Enable form initialization (default: true)
|
|
57
|
+
// Set to false to defer schema fetching
|
|
54
58
|
enabled?: boolean;
|
|
55
|
-
|
|
59
|
+
|
|
60
|
+
// Callback when schema loading fails
|
|
61
|
+
// Use for error reporting or retry logic
|
|
56
62
|
onSchemaError?: (error: Error) => void;
|
|
57
63
|
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### UseFormReturnType
|
|
58
67
|
|
|
59
|
-
|
|
68
|
+
```typescript
|
|
69
|
+
// Hook return type with all form state and methods
|
|
60
70
|
interface UseFormReturnType<T> {
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
// ============================================================
|
|
72
|
+
// FORM METHODS (from react-hook-form)
|
|
73
|
+
// ============================================================
|
|
74
|
+
|
|
75
|
+
// Register a field for validation and form handling
|
|
76
|
+
// Returns props to spread on input elements
|
|
77
|
+
register: (name: keyof T, options?: RegisterOptions) => InputProps;
|
|
78
|
+
|
|
79
|
+
// Handle form submission
|
|
80
|
+
// - onSuccess: Called with response data on successful save
|
|
81
|
+
// - onError: Called with FieldErrors (validation) or Error (API)
|
|
82
|
+
handleSubmit: (
|
|
83
|
+
onSuccess?: (data: T) => void,
|
|
84
|
+
onError?: (error: FieldErrors<T> | Error) => void,
|
|
85
|
+
) => (e?: React.BaseSyntheticEvent) => Promise<void>;
|
|
86
|
+
|
|
87
|
+
// Watch field values reactively
|
|
88
|
+
// watch("Price") returns current value, re-renders on change
|
|
89
|
+
watch: (name?: keyof T) => any;
|
|
90
|
+
|
|
91
|
+
// Set field value programmatically
|
|
92
|
+
// Useful for computed previews, templates, external data
|
|
93
|
+
setValue: (name: keyof T, value: any, options?: SetValueConfig) => void;
|
|
94
|
+
|
|
95
|
+
// Reset form to initial/provided values
|
|
96
|
+
reset: (values?: T) => void;
|
|
97
|
+
|
|
98
|
+
// ============================================================
|
|
99
|
+
// FORM STATE (flattened - no nested formState object)
|
|
100
|
+
// ============================================================
|
|
101
|
+
|
|
102
|
+
// Current validation errors by field name
|
|
69
103
|
errors: FieldErrors<T>;
|
|
104
|
+
|
|
105
|
+
// True when all fields pass validation
|
|
70
106
|
isValid: boolean;
|
|
107
|
+
|
|
108
|
+
// True when any field has been modified
|
|
71
109
|
isDirty: boolean;
|
|
110
|
+
|
|
111
|
+
// True during form submission
|
|
72
112
|
isSubmitting: boolean;
|
|
113
|
+
|
|
114
|
+
// True after successful submission
|
|
73
115
|
isSubmitSuccessful: boolean;
|
|
74
116
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
117
|
+
// ============================================================
|
|
118
|
+
// LOADING STATES
|
|
119
|
+
// ============================================================
|
|
120
|
+
|
|
121
|
+
// True during initial load
|
|
78
122
|
isLoading: boolean;
|
|
79
123
|
|
|
80
|
-
//
|
|
124
|
+
// True during background operations
|
|
125
|
+
isFetching: boolean;
|
|
126
|
+
|
|
127
|
+
// ============================================================
|
|
128
|
+
// ERROR HANDLING
|
|
129
|
+
// ============================================================
|
|
130
|
+
|
|
131
|
+
// Error from schema/record loading (not submission errors)
|
|
132
|
+
loadError: Error | null;
|
|
133
|
+
|
|
134
|
+
// True when loadError is present
|
|
135
|
+
hasError: boolean;
|
|
136
|
+
|
|
137
|
+
// ============================================================
|
|
138
|
+
// SCHEMA INFORMATION
|
|
139
|
+
// ============================================================
|
|
140
|
+
|
|
141
|
+
// Raw BDO schema (for advanced use cases)
|
|
81
142
|
schema: BDOSchemaType | null;
|
|
143
|
+
|
|
144
|
+
// Processed schema with field configs ready for rendering
|
|
82
145
|
schemaConfig: FormSchemaConfigType | null;
|
|
146
|
+
|
|
147
|
+
// List of computed field names
|
|
83
148
|
computedFields: Array<keyof T>;
|
|
149
|
+
|
|
150
|
+
// List of required field names
|
|
84
151
|
requiredFields: Array<keyof T>;
|
|
85
152
|
|
|
86
|
-
//
|
|
87
|
-
|
|
153
|
+
// ============================================================
|
|
154
|
+
// FIELD HELPERS
|
|
155
|
+
// ============================================================
|
|
156
|
+
|
|
157
|
+
// Get configuration for a specific field
|
|
158
|
+
// Returns null if field doesn't exist
|
|
159
|
+
getField: (fieldName: keyof T) => FormFieldConfigType | null;
|
|
160
|
+
|
|
161
|
+
// Get all field configurations as a record
|
|
88
162
|
getFields: () => Record<keyof T, FormFieldConfigType>;
|
|
89
|
-
|
|
90
|
-
|
|
163
|
+
|
|
164
|
+
// Check if a field exists in the schema
|
|
165
|
+
hasField: (fieldName: keyof T) => boolean;
|
|
166
|
+
|
|
167
|
+
// Check if a field is required
|
|
168
|
+
isFieldRequired: (fieldName: keyof T) => boolean;
|
|
169
|
+
|
|
170
|
+
// Check if a field is computed (read-only, server-calculated)
|
|
171
|
+
isFieldComputed: (fieldName: keyof T) => boolean;
|
|
172
|
+
|
|
173
|
+
// ============================================================
|
|
174
|
+
// OPERATIONS
|
|
175
|
+
// ============================================================
|
|
176
|
+
|
|
177
|
+
// Refresh schema from server (clears cache)
|
|
178
|
+
refreshSchema: () => Promise<void>;
|
|
179
|
+
|
|
180
|
+
// Clear all validation errors
|
|
181
|
+
clearErrors: () => void;
|
|
91
182
|
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### FormFieldConfigType
|
|
92
186
|
|
|
93
|
-
|
|
187
|
+
```typescript
|
|
188
|
+
// Field configuration from processed schema
|
|
94
189
|
interface FormFieldConfigType {
|
|
190
|
+
// Field identifier (matches BDO field name)
|
|
95
191
|
name: string;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
| "textarea";
|
|
192
|
+
|
|
193
|
+
// Input type for rendering
|
|
194
|
+
// "text" | "number" | "email" | "date" | "datetime-local"
|
|
195
|
+
// "checkbox" | "select" | "textarea" | "reference"
|
|
196
|
+
type: FormFieldTypeType;
|
|
197
|
+
|
|
198
|
+
// Display label (from schema or derived from name)
|
|
104
199
|
label: string;
|
|
200
|
+
|
|
201
|
+
// Whether field is required for submission
|
|
105
202
|
required: boolean;
|
|
203
|
+
|
|
204
|
+
// Whether field is computed (read-only, server-calculated)
|
|
106
205
|
computed: boolean;
|
|
206
|
+
|
|
207
|
+
// Default value from schema
|
|
107
208
|
defaultValue?: any;
|
|
108
|
-
|
|
109
|
-
|
|
209
|
+
|
|
210
|
+
// Options for select/reference fields
|
|
211
|
+
// { value: any, label: string }[]
|
|
212
|
+
options?: SelectOptionType[];
|
|
213
|
+
|
|
214
|
+
// Field description for help text
|
|
215
|
+
description?: string;
|
|
216
|
+
|
|
217
|
+
// User permissions for this field
|
|
218
|
+
permission: {
|
|
219
|
+
editable: boolean; // Can user edit this field
|
|
220
|
+
readable: boolean; // Can user see this field
|
|
221
|
+
hidden: boolean; // Should field be completely hidden
|
|
222
|
+
};
|
|
110
223
|
}
|
|
111
224
|
```
|
|
112
225
|
|
|
113
226
|
## Basic Example
|
|
114
227
|
|
|
115
|
-
A minimal create form with field registration and submission.
|
|
116
|
-
|
|
117
228
|
```tsx
|
|
118
229
|
import { useForm } from "@ram_28/kf-ai-sdk/form";
|
|
119
|
-
import type
|
|
120
|
-
UseFormOptionsType,
|
|
121
|
-
UseFormReturnType,
|
|
122
|
-
} from "@ram_28/kf-ai-sdk/form/types";
|
|
123
|
-
import { Product, ProductType } from "../sources";
|
|
230
|
+
import { Product, type ProductForRole } from "../sources";
|
|
124
231
|
import { Roles } from "../sources/roles";
|
|
125
232
|
|
|
126
|
-
type
|
|
233
|
+
type BuyerProduct = ProductForRole<typeof Roles.Buyer>;
|
|
127
234
|
|
|
128
235
|
function CreateProductForm() {
|
|
129
|
-
const product = new Product(Roles.
|
|
236
|
+
const product = new Product(Roles.Buyer);
|
|
130
237
|
|
|
131
|
-
const
|
|
238
|
+
const form = useForm<BuyerProduct>({
|
|
132
239
|
source: product._id,
|
|
133
240
|
operation: "create",
|
|
134
241
|
defaultValues: {
|
|
@@ -136,22 +243,12 @@ function CreateProductForm() {
|
|
|
136
243
|
Price: 0,
|
|
137
244
|
Category: "",
|
|
138
245
|
},
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const form: UseFormReturnType<SellerProduct> = useForm<SellerProduct>(options);
|
|
142
|
-
|
|
143
|
-
const onSuccess = (data: SellerProduct) => {
|
|
144
|
-
console.log("Product created:", data);
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const onError = (error: Error) => {
|
|
148
|
-
console.error("Failed to create:", error.message);
|
|
149
|
-
};
|
|
246
|
+
});
|
|
150
247
|
|
|
151
|
-
if (form.isLoading) return <div>Loading
|
|
248
|
+
if (form.isLoading) return <div>Loading...</div>;
|
|
152
249
|
|
|
153
250
|
return (
|
|
154
|
-
<form onSubmit={form.handleSubmit(
|
|
251
|
+
<form onSubmit={form.handleSubmit((data) => console.log("Created:", data))}>
|
|
155
252
|
<div>
|
|
156
253
|
<label>Title</label>
|
|
157
254
|
<input {...form.register("Title")} />
|
|
@@ -161,7 +258,6 @@ function CreateProductForm() {
|
|
|
161
258
|
<div>
|
|
162
259
|
<label>Price</label>
|
|
163
260
|
<input type="number" {...form.register("Price")} />
|
|
164
|
-
{form.errors.Price && <span>{form.errors.Price.message}</span>}
|
|
165
261
|
</div>
|
|
166
262
|
|
|
167
263
|
<div>
|
|
@@ -177,311 +273,141 @@ function CreateProductForm() {
|
|
|
177
273
|
}
|
|
178
274
|
```
|
|
179
275
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
## Create vs Update
|
|
276
|
+
## Examples
|
|
183
277
|
|
|
184
|
-
###
|
|
278
|
+
### Product Listing Form
|
|
185
279
|
|
|
186
|
-
|
|
280
|
+
Create form with validation and success handling.
|
|
187
281
|
|
|
188
282
|
```tsx
|
|
189
|
-
import {
|
|
283
|
+
import { useForm } from "@ram_28/kf-ai-sdk/form";
|
|
284
|
+
import type { FieldErrors } from "@ram_28/kf-ai-sdk/form/types";
|
|
285
|
+
import { useNavigate } from "react-router-dom";
|
|
286
|
+
import { Product, type ProductForRole } from "../sources";
|
|
190
287
|
import { Roles } from "../sources/roles";
|
|
191
288
|
|
|
192
|
-
type
|
|
289
|
+
type BuyerProduct = ProductForRole<typeof Roles.Buyer>;
|
|
193
290
|
|
|
194
|
-
function
|
|
195
|
-
const product = new Product(Roles.
|
|
291
|
+
function ProductListingForm() {
|
|
292
|
+
const product = new Product(Roles.Buyer);
|
|
293
|
+
const navigate = useNavigate();
|
|
196
294
|
|
|
197
|
-
const form = useForm<
|
|
295
|
+
const form = useForm<BuyerProduct>({
|
|
198
296
|
source: product._id,
|
|
199
297
|
operation: "create",
|
|
200
298
|
defaultValues: {
|
|
201
299
|
Title: "",
|
|
300
|
+
Description: "",
|
|
202
301
|
Price: 0,
|
|
302
|
+
Category: "Electronics",
|
|
203
303
|
Stock: 0,
|
|
204
304
|
},
|
|
305
|
+
mode: "onBlur",
|
|
205
306
|
});
|
|
206
307
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
<button type="submit">Create</button>
|
|
212
|
-
</form>
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### Edit Form
|
|
218
|
-
|
|
219
|
-
Load existing record data for editing.
|
|
220
|
-
|
|
221
|
-
```tsx
|
|
222
|
-
function EditForm({ productId }: { productId: string }) {
|
|
223
|
-
const product = new Product(Roles.Seller);
|
|
224
|
-
|
|
225
|
-
const form = useForm<SellerProduct>({
|
|
226
|
-
source: product._id,
|
|
227
|
-
operation: "update",
|
|
228
|
-
recordId: productId,
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
if (form.isLoadingRecord) return <div>Loading product...</div>;
|
|
232
|
-
|
|
233
|
-
return (
|
|
234
|
-
<form onSubmit={form.handleSubmit()}>
|
|
235
|
-
<input {...form.register("Title")} />
|
|
236
|
-
<input type="number" {...form.register("Price")} />
|
|
237
|
-
<button type="submit" disabled={!form.isDirty}>
|
|
238
|
-
Save Changes
|
|
239
|
-
</button>
|
|
240
|
-
</form>
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
### Toggle Between Create and Edit
|
|
246
|
-
|
|
247
|
-
Switch form mode based on selection.
|
|
248
|
-
|
|
249
|
-
```tsx
|
|
250
|
-
function ProductForm({ existingProduct }: { existingProduct?: SellerProduct }) {
|
|
251
|
-
const product = new Product(Roles.Seller);
|
|
252
|
-
const isEditing = !!existingProduct;
|
|
253
|
-
|
|
254
|
-
const form = useForm<SellerProduct>({
|
|
255
|
-
source: product._id,
|
|
256
|
-
operation: isEditing ? "update" : "create",
|
|
257
|
-
recordId: existingProduct?._id,
|
|
258
|
-
defaultValues: existingProduct ?? { Title: "", Price: 0 },
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
return (
|
|
262
|
-
<form onSubmit={form.handleSubmit()}>
|
|
263
|
-
<h2>{isEditing ? "Edit Product" : "New Product"}</h2>
|
|
264
|
-
<input {...form.register("Title")} />
|
|
265
|
-
<input type="number" {...form.register("Price")} />
|
|
266
|
-
<button type="submit">{isEditing ? "Update" : "Create"}</button>
|
|
267
|
-
</form>
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
---
|
|
273
|
-
|
|
274
|
-
## Field Rendering
|
|
275
|
-
|
|
276
|
-
### Using getField for Metadata
|
|
277
|
-
|
|
278
|
-
Access field configuration from the schema.
|
|
279
|
-
|
|
280
|
-
```tsx
|
|
281
|
-
function DynamicField({
|
|
282
|
-
form,
|
|
283
|
-
fieldName,
|
|
284
|
-
}: {
|
|
285
|
-
form: UseFormReturnType<SellerProduct>;
|
|
286
|
-
fieldName: keyof SellerProduct;
|
|
287
|
-
}) {
|
|
288
|
-
const field = form.getField(fieldName);
|
|
308
|
+
const onSuccess = (data: BuyerProduct) => {
|
|
309
|
+
toast.success(`Product "${data.Title}" created!`);
|
|
310
|
+
navigate(`/products/${data._id}`);
|
|
311
|
+
};
|
|
289
312
|
|
|
290
|
-
|
|
313
|
+
const onError = (error: FieldErrors<BuyerProduct> | Error) => {
|
|
314
|
+
if (error instanceof Error) {
|
|
315
|
+
toast.error(`Failed: ${error.message}`);
|
|
316
|
+
} else {
|
|
317
|
+
toast.error("Please fix the validation errors");
|
|
318
|
+
}
|
|
319
|
+
};
|
|
291
320
|
|
|
292
|
-
|
|
321
|
+
if (form.isLoading) return <div>Loading form...</div>;
|
|
293
322
|
|
|
294
323
|
return (
|
|
295
|
-
<
|
|
296
|
-
<
|
|
297
|
-
{
|
|
298
|
-
{
|
|
299
|
-
{
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
<select {...form.register(fieldName)} disabled={isDisabled}>
|
|
304
|
-
<option value="">Select {field.label}</option>
|
|
305
|
-
{field.options.map((opt) => (
|
|
306
|
-
<option key={opt.value} value={opt.value}>
|
|
307
|
-
{opt.label}
|
|
308
|
-
</option>
|
|
309
|
-
))}
|
|
310
|
-
</select>
|
|
311
|
-
) : (
|
|
312
|
-
<input
|
|
313
|
-
type={field.type === "number" ? "number" : "text"}
|
|
314
|
-
{...form.register(fieldName)}
|
|
315
|
-
disabled={isDisabled}
|
|
316
|
-
/>
|
|
317
|
-
)}
|
|
318
|
-
|
|
319
|
-
{form.errors[fieldName] && (
|
|
320
|
-
<span className="error">{form.errors[fieldName]?.message}</span>
|
|
321
|
-
)}
|
|
322
|
-
</div>
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
### Handling Computed Fields
|
|
328
|
-
|
|
329
|
-
Display auto-calculated fields as read-only.
|
|
330
|
-
|
|
331
|
-
```tsx
|
|
332
|
-
function FormWithComputedFields() {
|
|
333
|
-
const product = new Product(Roles.Seller);
|
|
334
|
-
|
|
335
|
-
const form = useForm<SellerProduct>({
|
|
336
|
-
source: product._id,
|
|
337
|
-
operation: "create",
|
|
338
|
-
});
|
|
324
|
+
<form onSubmit={form.handleSubmit(onSuccess, onError)}>
|
|
325
|
+
<div>
|
|
326
|
+
<label>Title {form.isFieldRequired("Title") && "*"}</label>
|
|
327
|
+
<input {...form.register("Title")} />
|
|
328
|
+
{form.errors.Title && (
|
|
329
|
+
<span className="error">{form.errors.Title.message}</span>
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
339
332
|
|
|
340
|
-
return (
|
|
341
|
-
<form onSubmit={form.handleSubmit()}>
|
|
342
333
|
<div>
|
|
343
|
-
<label>
|
|
344
|
-
<
|
|
334
|
+
<label>Description</label>
|
|
335
|
+
<textarea {...form.register("Description")} rows={4} />
|
|
345
336
|
</div>
|
|
346
337
|
|
|
347
338
|
<div>
|
|
348
|
-
<label>Price
|
|
349
|
-
<input type="number" {...form.register("Price")} />
|
|
339
|
+
<label>Price *</label>
|
|
340
|
+
<input type="number" step="0.01" {...form.register("Price")} />
|
|
341
|
+
{form.errors.Price && (
|
|
342
|
+
<span className="error">{form.errors.Price.message}</span>
|
|
343
|
+
)}
|
|
350
344
|
</div>
|
|
351
345
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
<
|
|
355
|
-
|
|
356
|
-
<input
|
|
357
|
-
type="number"
|
|
358
|
-
value={form.watch("Discount") ?? 0}
|
|
359
|
-
readOnly
|
|
360
|
-
disabled
|
|
361
|
-
/>
|
|
362
|
-
</div>
|
|
363
|
-
)}
|
|
346
|
+
<div>
|
|
347
|
+
<label>Stock</label>
|
|
348
|
+
<input type="number" {...form.register("Stock")} />
|
|
349
|
+
</div>
|
|
364
350
|
|
|
365
|
-
<button type="submit">
|
|
351
|
+
<button type="submit" disabled={form.isSubmitting || !form.isValid}>
|
|
352
|
+
{form.isSubmitting ? "Creating..." : "Create Product"}
|
|
353
|
+
</button>
|
|
366
354
|
</form>
|
|
367
355
|
);
|
|
368
356
|
}
|
|
369
357
|
```
|
|
370
358
|
|
|
371
|
-
###
|
|
359
|
+
### Edit Product
|
|
372
360
|
|
|
373
|
-
|
|
361
|
+
Update mode with record loading state.
|
|
374
362
|
|
|
375
363
|
```tsx
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const form = useForm<SellerProduct>({
|
|
380
|
-
source: product._id,
|
|
381
|
-
operation: "create",
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
if (form.isLoading) return <div>Loading...</div>;
|
|
385
|
-
|
|
386
|
-
const fields = form.getFields();
|
|
387
|
-
|
|
388
|
-
return (
|
|
389
|
-
<form onSubmit={form.handleSubmit()}>
|
|
390
|
-
{Object.entries(fields).map(([fieldName, field]) => {
|
|
391
|
-
if (field.permission.hidden) return null;
|
|
392
|
-
|
|
393
|
-
return (
|
|
394
|
-
<div key={fieldName}>
|
|
395
|
-
<label>{field.label}</label>
|
|
396
|
-
<input
|
|
397
|
-
{...form.register(fieldName as keyof SellerProduct)}
|
|
398
|
-
type={field.type === "number" ? "number" : "text"}
|
|
399
|
-
disabled={!field.permission.editable || field.computed}
|
|
400
|
-
/>
|
|
401
|
-
</div>
|
|
402
|
-
);
|
|
403
|
-
})}
|
|
404
|
-
|
|
405
|
-
<button type="submit">Submit</button>
|
|
406
|
-
</form>
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
---
|
|
412
|
-
|
|
413
|
-
## Validation
|
|
414
|
-
|
|
415
|
-
### Displaying Field Errors
|
|
364
|
+
import { useForm } from "@ram_28/kf-ai-sdk/form";
|
|
365
|
+
import { Product, type ProductForRole } from "../sources";
|
|
366
|
+
import { Roles } from "../sources/roles";
|
|
416
367
|
|
|
417
|
-
|
|
368
|
+
type BuyerProduct = ProductForRole<typeof Roles.Buyer>;
|
|
418
369
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const product = new Product(Roles.Seller);
|
|
370
|
+
function EditProductForm({ productId }: { productId: string }) {
|
|
371
|
+
const product = new Product(Roles.Buyer);
|
|
422
372
|
|
|
423
|
-
const form = useForm<
|
|
373
|
+
const form = useForm<BuyerProduct>({
|
|
424
374
|
source: product._id,
|
|
425
|
-
operation: "
|
|
426
|
-
|
|
375
|
+
operation: "update",
|
|
376
|
+
recordId: productId,
|
|
427
377
|
});
|
|
428
378
|
|
|
379
|
+
if (form.isLoading) {
|
|
380
|
+
return <div>Loading product...</div>;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (form.hasError) {
|
|
384
|
+
return (
|
|
385
|
+
<div>
|
|
386
|
+
<p>Failed to load product: {form.loadError?.message}</p>
|
|
387
|
+
<button onClick={() => form.refreshSchema()}>Retry</button>
|
|
388
|
+
</div>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
429
392
|
return (
|
|
430
|
-
<form onSubmit={form.handleSubmit()}>
|
|
431
|
-
<div
|
|
432
|
-
<label>Title
|
|
393
|
+
<form onSubmit={form.handleSubmit(() => toast.success("Product updated!"))}>
|
|
394
|
+
<div>
|
|
395
|
+
<label>Title</label>
|
|
433
396
|
<input {...form.register("Title")} />
|
|
434
|
-
{form.errors.Title && (
|
|
435
|
-
<span className="error-message">{form.errors.Title.message}</span>
|
|
436
|
-
)}
|
|
437
397
|
</div>
|
|
438
398
|
|
|
439
|
-
<div
|
|
440
|
-
<label>Price
|
|
399
|
+
<div>
|
|
400
|
+
<label>Price</label>
|
|
441
401
|
<input type="number" {...form.register("Price")} />
|
|
442
|
-
{form.errors.Price && (
|
|
443
|
-
<span className="error-message">{form.errors.Price.message}</span>
|
|
444
|
-
)}
|
|
445
402
|
</div>
|
|
446
403
|
|
|
447
|
-
<
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
</form>
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
### Form State Indicators
|
|
456
|
-
|
|
457
|
-
Show form status to users.
|
|
458
|
-
|
|
459
|
-
```tsx
|
|
460
|
-
function FormWithStateIndicators() {
|
|
461
|
-
const product = new Product(Roles.Seller);
|
|
462
|
-
|
|
463
|
-
const form = useForm<SellerProduct>({
|
|
464
|
-
source: product._id,
|
|
465
|
-
operation: "update",
|
|
466
|
-
recordId: "PROD-001",
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
return (
|
|
470
|
-
<form onSubmit={form.handleSubmit()}>
|
|
471
|
-
{/* Form fields */}
|
|
472
|
-
<input {...form.register("Title")} />
|
|
473
|
-
<input type="number" {...form.register("Price")} />
|
|
474
|
-
|
|
475
|
-
{/* Status bar */}
|
|
476
|
-
<div className="form-status">
|
|
477
|
-
{form.isDirty && <span className="unsaved">Unsaved changes</span>}
|
|
478
|
-
{!form.isValid && <span className="invalid">Please fix errors</span>}
|
|
479
|
-
{form.isSubmitting && <span className="saving">Saving...</span>}
|
|
480
|
-
{form.isSubmitSuccessful && <span className="success">Saved!</span>}
|
|
404
|
+
<div>
|
|
405
|
+
<label>Stock</label>
|
|
406
|
+
<input type="number" {...form.register("Stock")} />
|
|
481
407
|
</div>
|
|
482
408
|
|
|
483
|
-
<button type="submit" disabled={form.
|
|
484
|
-
Save Changes
|
|
409
|
+
<button type="submit" disabled={!form.isDirty || form.isSubmitting}>
|
|
410
|
+
{form.isSubmitting ? "Saving..." : "Save Changes"}
|
|
485
411
|
</button>
|
|
486
412
|
|
|
487
413
|
<button
|
|
@@ -496,130 +422,127 @@ function FormWithStateIndicators() {
|
|
|
496
422
|
}
|
|
497
423
|
```
|
|
498
424
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
## Submission
|
|
502
|
-
|
|
503
|
-
### Handle Submit with Callbacks
|
|
425
|
+
### Auto-Calculate Discount
|
|
504
426
|
|
|
505
|
-
|
|
427
|
+
Working with computed fields and the watch function.
|
|
506
428
|
|
|
507
429
|
```tsx
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const form = useForm<SellerProduct>({
|
|
512
|
-
source: product._id,
|
|
513
|
-
operation: "create",
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
const onSuccess = (data: SellerProduct) => {
|
|
517
|
-
alert(`Product "${data.Title}" created successfully!`);
|
|
518
|
-
form.reset();
|
|
519
|
-
};
|
|
520
|
-
|
|
521
|
-
const onError = (error: FieldErrors<SellerProduct> | Error) => {
|
|
522
|
-
if (error instanceof Error) {
|
|
523
|
-
// API error
|
|
524
|
-
alert(`Server error: ${error.message}`);
|
|
525
|
-
} else {
|
|
526
|
-
// Validation errors
|
|
527
|
-
const fieldNames = Object.keys(error).join(", ");
|
|
528
|
-
alert(`Please fix errors in: ${fieldNames}`);
|
|
529
|
-
}
|
|
530
|
-
};
|
|
531
|
-
|
|
532
|
-
return (
|
|
533
|
-
<form onSubmit={form.handleSubmit(onSuccess, onError)}>
|
|
534
|
-
<input {...form.register("Title")} placeholder="Title" />
|
|
535
|
-
<input type="number" {...form.register("Price")} placeholder="Price" />
|
|
536
|
-
<button type="submit">Create</button>
|
|
537
|
-
</form>
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
### Programmatic Submission
|
|
430
|
+
import { useForm } from "@ram_28/kf-ai-sdk/form";
|
|
431
|
+
import { Product, type ProductForRole } from "../sources";
|
|
432
|
+
import { Roles } from "../sources/roles";
|
|
543
433
|
|
|
544
|
-
|
|
434
|
+
type BuyerProduct = ProductForRole<typeof Roles.Buyer>;
|
|
545
435
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const product = new Product(Roles.Seller);
|
|
436
|
+
function PricingForm() {
|
|
437
|
+
const product = new Product(Roles.Buyer);
|
|
549
438
|
|
|
550
|
-
const form = useForm<
|
|
439
|
+
const form = useForm<BuyerProduct>({
|
|
551
440
|
source: product._id,
|
|
552
441
|
operation: "create",
|
|
442
|
+
defaultValues: {
|
|
443
|
+
MRP: 0,
|
|
444
|
+
Price: 0,
|
|
445
|
+
},
|
|
553
446
|
});
|
|
554
447
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
(data) => {
|
|
558
|
-
console.log("Saved:", data);
|
|
559
|
-
// Close modal or navigate away
|
|
560
|
-
},
|
|
561
|
-
(error) => {
|
|
562
|
-
console.error("Validation failed:", error);
|
|
563
|
-
},
|
|
564
|
-
);
|
|
448
|
+
// Watch computed field value from server
|
|
449
|
+
const discount = form.watch("Discount");
|
|
565
450
|
|
|
566
|
-
|
|
567
|
-
};
|
|
451
|
+
if (form.isLoading) return <div>Loading...</div>;
|
|
568
452
|
|
|
569
453
|
return (
|
|
570
|
-
<
|
|
571
|
-
<
|
|
572
|
-
|
|
454
|
+
<form onSubmit={form.handleSubmit()}>
|
|
455
|
+
<div>
|
|
456
|
+
<label>MRP (Maximum Retail Price)</label>
|
|
457
|
+
<input type="number" {...form.register("MRP")} />
|
|
458
|
+
</div>
|
|
573
459
|
|
|
574
|
-
<div
|
|
575
|
-
<
|
|
576
|
-
|
|
577
|
-
</button>
|
|
578
|
-
<button type="button" onClick={handleSaveAndClose}>
|
|
579
|
-
Save & Close
|
|
580
|
-
</button>
|
|
460
|
+
<div>
|
|
461
|
+
<label>Selling Price</label>
|
|
462
|
+
<input type="number" {...form.register("Price")} />
|
|
581
463
|
</div>
|
|
582
|
-
|
|
464
|
+
|
|
465
|
+
{/* Computed field - read-only, server-calculated */}
|
|
466
|
+
{form.isFieldComputed("Discount") && (
|
|
467
|
+
<div>
|
|
468
|
+
<label>Discount % (auto-calculated)</label>
|
|
469
|
+
<input type="number" value={discount ?? 0} readOnly disabled />
|
|
470
|
+
</div>
|
|
471
|
+
)}
|
|
472
|
+
|
|
473
|
+
<button type="submit">Save Product</button>
|
|
474
|
+
</form>
|
|
583
475
|
);
|
|
584
476
|
}
|
|
585
477
|
```
|
|
586
478
|
|
|
587
|
-
|
|
479
|
+
### Static Dropdown
|
|
588
480
|
|
|
589
|
-
|
|
481
|
+
Static fields return options with `Value` and `Label`.
|
|
590
482
|
|
|
591
|
-
|
|
483
|
+
```tsx
|
|
484
|
+
import { useState } from "react";
|
|
485
|
+
import { useForm } from "@ram_28/kf-ai-sdk/form";
|
|
486
|
+
import { Product, type ProductForRole } from "../sources";
|
|
487
|
+
import { Roles } from "../sources/roles";
|
|
592
488
|
|
|
593
|
-
|
|
489
|
+
type BuyerProduct = ProductForRole<typeof Roles.Buyer>;
|
|
594
490
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
const
|
|
491
|
+
function ProductCategoryForm({ recordId }: { recordId: string }) {
|
|
492
|
+
const product = new Product(Roles.Buyer);
|
|
493
|
+
const [categoryOptions, setCategoryOptions] = useState<
|
|
494
|
+
{ Value: string; Label: string }[]
|
|
495
|
+
>([]);
|
|
496
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
497
|
+
const [loadingOptions, setLoadingOptions] = useState(false);
|
|
598
498
|
|
|
599
|
-
const form = useForm<
|
|
499
|
+
const form = useForm<BuyerProduct>({
|
|
600
500
|
source: product._id,
|
|
601
|
-
operation: "
|
|
501
|
+
operation: "update",
|
|
502
|
+
recordId,
|
|
602
503
|
});
|
|
603
504
|
|
|
604
|
-
|
|
605
|
-
const
|
|
606
|
-
|
|
505
|
+
// Fetch options only when dropdown is opened
|
|
506
|
+
const handleDropdownOpen = async () => {
|
|
507
|
+
if (categoryOptions.length > 0) {
|
|
508
|
+
setIsOpen(true);
|
|
509
|
+
return; // Already loaded
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
setLoadingOptions(true);
|
|
513
|
+
setIsOpen(true);
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
const options = await product.fetchField(recordId, "Category");
|
|
517
|
+
setCategoryOptions(options);
|
|
518
|
+
} catch (error) {
|
|
519
|
+
console.error(error);
|
|
520
|
+
} finally {
|
|
521
|
+
setLoadingOptions(false);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
if (form.isLoading) return <div>Loading...</div>;
|
|
607
526
|
|
|
608
527
|
return (
|
|
609
528
|
<form onSubmit={form.handleSubmit()}>
|
|
610
529
|
<div>
|
|
611
|
-
<label>
|
|
612
|
-
<
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
530
|
+
<label>Category</label>
|
|
531
|
+
<select
|
|
532
|
+
{...form.register("Category")}
|
|
533
|
+
onFocus={handleDropdownOpen}
|
|
534
|
+
>
|
|
535
|
+
<option value="">Select category</option>
|
|
536
|
+
{loadingOptions ? (
|
|
537
|
+
<option disabled>Loading...</option>
|
|
538
|
+
) : (
|
|
539
|
+
categoryOptions.map((opt) => (
|
|
540
|
+
<option key={opt.Value} value={opt.Value}>
|
|
541
|
+
{opt.Label}
|
|
542
|
+
</option>
|
|
543
|
+
))
|
|
544
|
+
)}
|
|
545
|
+
</select>
|
|
623
546
|
</div>
|
|
624
547
|
|
|
625
548
|
<button type="submit">Save</button>
|
|
@@ -628,61 +551,103 @@ function FormWithWatcher() {
|
|
|
628
551
|
}
|
|
629
552
|
```
|
|
630
553
|
|
|
631
|
-
###
|
|
554
|
+
### Dynamic Reference Dropdown
|
|
632
555
|
|
|
633
|
-
|
|
556
|
+
Reference fields return the full object structure. Use a custom dropdown to display rich cards.
|
|
634
557
|
|
|
635
558
|
```tsx
|
|
636
|
-
|
|
637
|
-
|
|
559
|
+
import { useState } from "react";
|
|
560
|
+
import { useForm } from "@ram_28/kf-ai-sdk/form";
|
|
561
|
+
import { Product, type ProductForRole } from "../sources";
|
|
562
|
+
import { Roles } from "../sources/roles";
|
|
563
|
+
|
|
564
|
+
type BuyerProduct = ProductForRole<typeof Roles.Buyer>;
|
|
638
565
|
|
|
639
|
-
|
|
566
|
+
type SupplierOption = {
|
|
567
|
+
_id: string;
|
|
568
|
+
SupplierName: string;
|
|
569
|
+
Rating: number;
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
function ProductSupplierForm({ recordId }: { recordId: string }) {
|
|
573
|
+
const product = new Product(Roles.Buyer);
|
|
574
|
+
const [supplierOptions, setSupplierOptions] = useState<SupplierOption[]>([]);
|
|
575
|
+
const [selectedSupplier, setSelectedSupplier] = useState<SupplierOption | null>(null);
|
|
576
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
577
|
+
const [loadingOptions, setLoadingOptions] = useState(false);
|
|
578
|
+
|
|
579
|
+
const form = useForm<BuyerProduct>({
|
|
640
580
|
source: product._id,
|
|
641
|
-
operation: "
|
|
581
|
+
operation: "update",
|
|
582
|
+
recordId,
|
|
642
583
|
});
|
|
643
584
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
585
|
+
// Fetch options only when dropdown is clicked
|
|
586
|
+
const handleDropdownClick = async () => {
|
|
587
|
+
setIsOpen(!isOpen);
|
|
588
|
+
|
|
589
|
+
if (supplierOptions.length > 0 || loadingOptions) return;
|
|
590
|
+
|
|
591
|
+
setLoadingOptions(true);
|
|
592
|
+
try {
|
|
593
|
+
const options = await product.fetchField(recordId, "SupplierInfo");
|
|
594
|
+
setSupplierOptions(options);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
console.error(error);
|
|
597
|
+
} finally {
|
|
598
|
+
setLoadingOptions(false);
|
|
654
599
|
}
|
|
655
600
|
};
|
|
656
601
|
|
|
602
|
+
const handleSelect = (supplier: SupplierOption) => {
|
|
603
|
+
setSelectedSupplier(supplier);
|
|
604
|
+
form.setValue("SupplierInfo", { _id: supplier._id, SupplierName: supplier.SupplierName });
|
|
605
|
+
setIsOpen(false);
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
if (form.isLoading) return <div>Loading...</div>;
|
|
609
|
+
|
|
657
610
|
return (
|
|
658
611
|
<form onSubmit={form.handleSubmit()}>
|
|
659
|
-
<div className="
|
|
660
|
-
<
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
<button type="button" onClick={
|
|
664
|
-
|
|
612
|
+
<div className="dropdown">
|
|
613
|
+
<label>Supplier</label>
|
|
614
|
+
|
|
615
|
+
{/* Selected value display */}
|
|
616
|
+
<button type="button" onClick={handleDropdownClick}>
|
|
617
|
+
{selectedSupplier ? selectedSupplier.SupplierName : "Select supplier"}
|
|
665
618
|
</button>
|
|
666
|
-
</div>
|
|
667
619
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
620
|
+
{/* Dropdown options with cards */}
|
|
621
|
+
{isOpen && (
|
|
622
|
+
<div className="dropdown-menu">
|
|
623
|
+
{loadingOptions ? (
|
|
624
|
+
<div className="dropdown-item">Loading...</div>
|
|
625
|
+
) : (
|
|
626
|
+
supplierOptions.map((supplier) => (
|
|
627
|
+
<div
|
|
628
|
+
key={supplier._id}
|
|
629
|
+
className="dropdown-card"
|
|
630
|
+
onClick={() => handleSelect(supplier)}
|
|
631
|
+
>
|
|
632
|
+
<span className="supplier-name">{supplier.SupplierName}</span>
|
|
633
|
+
<span className="supplier-rating">
|
|
634
|
+
{"★".repeat(supplier.Rating)}{"☆".repeat(5 - supplier.Rating)}
|
|
635
|
+
</span>
|
|
636
|
+
</div>
|
|
637
|
+
))
|
|
638
|
+
)}
|
|
639
|
+
</div>
|
|
640
|
+
)}
|
|
641
|
+
</div>
|
|
671
642
|
|
|
672
|
-
<button type="submit">
|
|
643
|
+
<button type="submit">Save</button>
|
|
673
644
|
</form>
|
|
674
645
|
);
|
|
675
646
|
}
|
|
676
647
|
```
|
|
677
648
|
|
|
678
|
-
---
|
|
679
|
-
|
|
680
649
|
## Error Utilities
|
|
681
650
|
|
|
682
|
-
The SDK provides utilities to help handle and categorize errors from form operations.
|
|
683
|
-
|
|
684
|
-
### Available Utilities
|
|
685
|
-
|
|
686
651
|
```typescript
|
|
687
652
|
import {
|
|
688
653
|
parseApiError,
|
|
@@ -692,149 +657,43 @@ import {
|
|
|
692
657
|
} from "@ram_28/kf-ai-sdk/form";
|
|
693
658
|
```
|
|
694
659
|
|
|
695
|
-
|
|
696
|
-
| -------------------------- | ---------------------------------------------------------------- |
|
|
697
|
-
| `parseApiError(error)` | Extracts a user-friendly message from any error type |
|
|
698
|
-
| `isNetworkError(error)` | Returns `true` if error is network-related (connection, timeout) |
|
|
699
|
-
| `isValidationError(error)` | Returns `true` if error is a validation/schema error |
|
|
700
|
-
| `clearFormCache()` | Clears the schema cache (30-minute TTL by default) |
|
|
701
|
-
|
|
702
|
-
### Parse and Handle API Errors
|
|
660
|
+
### parseApiError
|
|
703
661
|
|
|
704
|
-
|
|
662
|
+
Extract user-friendly message from any error type.
|
|
705
663
|
|
|
706
|
-
```
|
|
707
|
-
|
|
708
|
-
parseApiError
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
} from "@ram_28/kf-ai-sdk/form";
|
|
713
|
-
|
|
714
|
-
function FormWithErrorHandling() {
|
|
715
|
-
const product = new Product(Roles.Seller);
|
|
716
|
-
|
|
717
|
-
const form = useForm<SellerProduct>({
|
|
718
|
-
source: product._id,
|
|
719
|
-
operation: "create",
|
|
720
|
-
});
|
|
721
|
-
|
|
722
|
-
const onError = (error: Error) => {
|
|
723
|
-
const message = parseApiError(error);
|
|
664
|
+
```typescript
|
|
665
|
+
const onError = (error: Error) => {
|
|
666
|
+
const message = parseApiError(error);
|
|
667
|
+
toast.error(message);
|
|
668
|
+
};
|
|
669
|
+
```
|
|
724
670
|
|
|
725
|
-
|
|
726
|
-
alert("Network error. Please check your connection.");
|
|
727
|
-
} else if (isValidationError(error)) {
|
|
728
|
-
alert("Validation failed. Please check your input.");
|
|
729
|
-
} else {
|
|
730
|
-
alert(message);
|
|
731
|
-
}
|
|
732
|
-
};
|
|
671
|
+
### isNetworkError
|
|
733
672
|
|
|
734
|
-
|
|
735
|
-
clearFormCache();
|
|
736
|
-
window.location.reload();
|
|
737
|
-
};
|
|
673
|
+
Check if error is network-related (connection, timeout).
|
|
738
674
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
<button type="submit">Save</button>
|
|
743
|
-
<button type="button" onClick={handleClearCache}>
|
|
744
|
-
Clear Cache & Reload
|
|
745
|
-
</button>
|
|
746
|
-
</form>
|
|
747
|
-
);
|
|
675
|
+
```typescript
|
|
676
|
+
if (isNetworkError(error)) {
|
|
677
|
+
toast.error("Network error. Please check your connection.");
|
|
748
678
|
}
|
|
749
679
|
```
|
|
750
680
|
|
|
751
|
-
###
|
|
752
|
-
|
|
753
|
-
The `handleSubmit` callback receives different error types depending on the failure stage:
|
|
681
|
+
### isValidationError
|
|
754
682
|
|
|
755
|
-
|
|
756
|
-
function FormWithDetailedErrorHandling() {
|
|
757
|
-
const product = new Product(Roles.Seller);
|
|
758
|
-
|
|
759
|
-
const form = useForm<SellerProduct>({
|
|
760
|
-
source: product._id,
|
|
761
|
-
operation: "create",
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
const onSubmit = (data: SellerProduct) => {
|
|
765
|
-
console.log("Success:", data);
|
|
766
|
-
};
|
|
767
|
-
|
|
768
|
-
const onError = (
|
|
769
|
-
error: FieldErrors<SellerProduct> | Error,
|
|
770
|
-
event?: React.BaseSyntheticEvent,
|
|
771
|
-
) => {
|
|
772
|
-
// Check if it's a validation error (from react-hook-form)
|
|
773
|
-
if (!(error instanceof Error)) {
|
|
774
|
-
// Field validation errors - error is FieldErrors<SellerProduct>
|
|
775
|
-
const invalidFields = Object.keys(error);
|
|
776
|
-
console.log("Validation failed for fields:", invalidFields);
|
|
777
|
-
|
|
778
|
-
// Access specific field errors
|
|
779
|
-
if (error.Title) {
|
|
780
|
-
console.log("Title error:", error.Title.message);
|
|
781
|
-
}
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// API/Network error - error is Error
|
|
786
|
-
if (isNetworkError(error)) {
|
|
787
|
-
// Handle offline, timeout, connection refused
|
|
788
|
-
console.error("Network issue:", error.message);
|
|
789
|
-
} else if (isValidationError(error)) {
|
|
790
|
-
// Server-side validation error
|
|
791
|
-
console.error("Server validation:", parseApiError(error));
|
|
792
|
-
} else {
|
|
793
|
-
// Other server errors (500, etc.)
|
|
794
|
-
console.error("Server error:", parseApiError(error));
|
|
795
|
-
}
|
|
796
|
-
};
|
|
683
|
+
Check if error is a server-side validation error.
|
|
797
684
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
</form>
|
|
802
|
-
);
|
|
685
|
+
```typescript
|
|
686
|
+
if (isValidationError(error)) {
|
|
687
|
+
toast.error("Validation failed. Please check your input.");
|
|
803
688
|
}
|
|
804
689
|
```
|
|
805
690
|
|
|
806
|
-
###
|
|
691
|
+
### clearFormCache
|
|
807
692
|
|
|
808
|
-
|
|
693
|
+
Clear the schema cache (30-minute TTL by default).
|
|
809
694
|
|
|
810
|
-
```
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
const form = useForm<SellerProduct>({
|
|
815
|
-
source: product._id,
|
|
816
|
-
operation: "create",
|
|
817
|
-
onSchemaError: (error) => {
|
|
818
|
-
console.error("Failed to load form schema:", error.message);
|
|
819
|
-
// Optionally clear cache and retry
|
|
820
|
-
clearFormCache();
|
|
821
|
-
},
|
|
822
|
-
});
|
|
823
|
-
|
|
824
|
-
// Check for load errors
|
|
825
|
-
if (form.loadError) {
|
|
826
|
-
return (
|
|
827
|
-
<div className="error-state">
|
|
828
|
-
<p>Failed to load form: {form.loadError.message}</p>
|
|
829
|
-
<button onClick={() => window.location.reload()}>Retry</button>
|
|
830
|
-
</div>
|
|
831
|
-
);
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
if (form.isLoading) {
|
|
835
|
-
return <div>Loading form...</div>;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
return <form onSubmit={form.handleSubmit()}>{/* form fields */}</form>;
|
|
839
|
-
}
|
|
695
|
+
```typescript
|
|
696
|
+
// Clear cache and reload to get fresh schema
|
|
697
|
+
clearFormCache();
|
|
698
|
+
window.location.reload();
|
|
840
699
|
```
|