@ram_28/kf-ai-sdk 1.0.18 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -12
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api.cjs +1 -1
- package/dist/api.mjs +2 -2
- package/dist/auth.cjs +1 -1
- package/dist/auth.mjs +1 -1
- package/dist/{client-C15j4O5B.cjs → client-DgtkT50N.cjs} +1 -1
- package/dist/{client-CfvLiGfP.js → client-V-WzUb8H.js} +9 -5
- package/dist/components/hooks/useFilter/types.d.ts +14 -11
- package/dist/components/hooks/useFilter/types.d.ts.map +1 -1
- package/dist/components/hooks/useFilter/useFilter.d.ts +1 -1
- package/dist/components/hooks/useFilter/useFilter.d.ts.map +1 -1
- package/dist/components/hooks/useForm/apiClient.d.ts.map +1 -1
- package/dist/components/hooks/useForm/useForm.d.ts.map +1 -1
- package/dist/components/hooks/useKanban/context.d.ts +1 -1
- package/dist/components/hooks/useKanban/context.d.ts.map +1 -1
- package/dist/components/hooks/useKanban/types.d.ts +5 -22
- package/dist/components/hooks/useKanban/types.d.ts.map +1 -1
- package/dist/components/hooks/useKanban/useKanban.d.ts.map +1 -1
- package/dist/components/hooks/useTable/types.d.ts +19 -31
- package/dist/components/hooks/useTable/types.d.ts.map +1 -1
- package/dist/components/hooks/useTable/useTable.d.ts.map +1 -1
- package/dist/error-handling-CAoD0Kwb.cjs +1 -0
- package/dist/error-handling-CrhTtD88.js +14 -0
- package/dist/filter.cjs +1 -1
- package/dist/filter.mjs +1 -1
- package/dist/form.cjs +1 -1
- package/dist/form.mjs +825 -814
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/kanban.cjs +2 -2
- package/dist/kanban.mjs +335 -323
- package/dist/{metadata-2FLBsFcf.cjs → metadata-0lZAfuTP.cjs} +1 -1
- package/dist/{metadata-DBcoDth-.js → metadata-B88D_pVS.js} +1 -1
- package/dist/table.cjs +1 -1
- package/dist/table.mjs +113 -96
- package/dist/table.types.d.ts +1 -1
- package/dist/table.types.d.ts.map +1 -1
- package/dist/types/common.d.ts +26 -6
- package/dist/types/common.d.ts.map +1 -1
- package/dist/useFilter-DzpP_ag0.cjs +1 -0
- package/dist/useFilter-H5bgAZQF.js +120 -0
- package/dist/utils/api/buildListOptions.d.ts +43 -0
- package/dist/utils/api/buildListOptions.d.ts.map +1 -0
- package/dist/utils/api/index.d.ts +2 -0
- package/dist/utils/api/index.d.ts.map +1 -0
- package/dist/utils/error-handling.d.ts +41 -0
- package/dist/utils/error-handling.d.ts.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/docs/QUICK_REFERENCE.md +142 -420
- package/docs/useAuth.md +52 -340
- package/docs/useFilter.md +858 -162
- package/docs/useForm.md +712 -501
- package/docs/useKanban.md +534 -279
- package/docs/useTable.md +725 -214
- package/package.json +1 -1
- package/sdk/api/client.ts +7 -1
- package/sdk/components/hooks/useFilter/types.ts +14 -11
- package/sdk/components/hooks/useFilter/useFilter.ts +20 -18
- package/sdk/components/hooks/useForm/apiClient.ts +2 -1
- package/sdk/components/hooks/useForm/useForm.ts +47 -13
- package/sdk/components/hooks/useKanban/context.ts +5 -3
- package/sdk/components/hooks/useKanban/types.ts +7 -23
- package/sdk/components/hooks/useKanban/useKanban.ts +54 -18
- package/sdk/components/hooks/useTable/types.ts +26 -32
- package/sdk/components/hooks/useTable/useTable.llm.txt +8 -22
- package/sdk/components/hooks/useTable/useTable.ts +70 -25
- package/sdk/index.ts +154 -10
- package/sdk/table.types.ts +3 -0
- package/sdk/types/common.ts +31 -6
- package/sdk/utils/api/buildListOptions.ts +120 -0
- package/sdk/utils/api/index.ts +2 -0
- package/sdk/utils/error-handling.ts +150 -0
- package/sdk/utils/index.ts +6 -0
- package/dist/useFilter-Dofowpr_.cjs +0 -1
- package/dist/useFilter-Dv-mr9QW.js +0 -117
package/docs/useForm.md
CHANGED
|
@@ -1,207 +1,48 @@
|
|
|
1
1
|
# useForm
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Form state management with automatic schema validation, computed fields, and react-hook-form integration.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- Fetches schema metadata from the API and generates validation rules, default values, and field permissions automatically
|
|
7
|
-
- Supports both create and update operations with automatic draft API calls for server-side computed fields
|
|
8
|
-
- Provides flattened state accessors (`errors`, `isValid`, `isDirty`) alongside react-hook-form's standard methods
|
|
9
|
-
|
|
10
|
-
## Type Reference
|
|
5
|
+
## Imports
|
|
11
6
|
|
|
12
7
|
```typescript
|
|
13
|
-
import { useForm } from "@ram_28/kf-ai-sdk/form";
|
|
14
|
-
import type {
|
|
15
|
-
// Core types
|
|
16
|
-
UseFormOptionsType,
|
|
17
|
-
UseFormReturnType,
|
|
18
|
-
FormOperationType,
|
|
19
|
-
FormModeType,
|
|
20
|
-
|
|
21
|
-
// Form field configuration
|
|
22
|
-
FormFieldConfigType,
|
|
23
|
-
FormSchemaConfigType,
|
|
24
|
-
FormFieldTypeType,
|
|
25
|
-
SelectOptionType,
|
|
26
|
-
FieldPermissionType,
|
|
27
|
-
|
|
28
|
-
// Result types
|
|
29
|
-
FieldValidationResultType,
|
|
30
|
-
SubmissionResultType,
|
|
31
|
-
|
|
32
|
-
// BDO Schema types (advanced)
|
|
33
|
-
BDOSchemaType,
|
|
34
|
-
BDOFieldDefinitionType,
|
|
35
|
-
SchemaValidationRuleType,
|
|
36
|
-
} from "@ram_28/kf-ai-sdk/form/types";
|
|
37
|
-
|
|
38
|
-
// Error utilities
|
|
39
8
|
import {
|
|
9
|
+
useForm,
|
|
40
10
|
parseApiError,
|
|
41
11
|
isNetworkError,
|
|
42
12
|
isValidationError,
|
|
43
13
|
clearFormCache,
|
|
44
14
|
} from "@ram_28/kf-ai-sdk/form";
|
|
45
|
-
|
|
46
|
-
// Re-exported from react-hook-form (available from SDK)
|
|
47
15
|
import type {
|
|
48
|
-
UseFormRegister,
|
|
49
16
|
FieldErrors,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// FieldErrors type from react-hook-form
|
|
56
|
-
// Maps field names to their error objects (message, type, etc.)
|
|
57
|
-
// Used in handleSubmit's onError callback for validation errors
|
|
58
|
-
type FieldErrors<T> = {
|
|
59
|
-
[K in keyof T]?: {
|
|
60
|
-
type: string;
|
|
61
|
-
message?: string;
|
|
62
|
-
ref?: React.RefObject<any>;
|
|
63
|
-
};
|
|
64
|
-
} & {
|
|
65
|
-
root?: Record<string, { type: string; message?: string }>; // Cross-field errors
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
// FormState type from react-hook-form
|
|
69
|
-
// Contains all form state information
|
|
70
|
-
interface FormState<T> {
|
|
71
|
-
isDirty: boolean; // Form has been modified
|
|
72
|
-
isValid: boolean; // All validations pass
|
|
73
|
-
isSubmitting: boolean; // Form is being submitted
|
|
74
|
-
isSubmitted: boolean; // Form has been submitted at least once
|
|
75
|
-
isSubmitSuccessful: boolean; // Last submission was successful
|
|
76
|
-
isLoading: boolean; // Form is loading async default values
|
|
77
|
-
isValidating: boolean; // Form is validating
|
|
78
|
-
submitCount: number; // Number of times form was submitted
|
|
79
|
-
errors: FieldErrors<T>; // Current validation errors
|
|
80
|
-
touchedFields: Partial<Record<keyof T, boolean>>; // Fields that have been touched
|
|
81
|
-
dirtyFields: Partial<Record<keyof T, boolean>>; // Fields that have been modified
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Form operation type
|
|
85
|
-
type FormOperationType = "create" | "update";
|
|
86
|
-
|
|
87
|
-
// Form field input types
|
|
88
|
-
type FormFieldTypeType =
|
|
89
|
-
| "text"
|
|
90
|
-
| "number"
|
|
91
|
-
| "email"
|
|
92
|
-
| "password"
|
|
93
|
-
| "date"
|
|
94
|
-
| "datetime-local"
|
|
95
|
-
| "checkbox"
|
|
96
|
-
| "select"
|
|
97
|
-
| "textarea"
|
|
98
|
-
| "reference";
|
|
99
|
-
|
|
100
|
-
// Select option for dropdown fields
|
|
101
|
-
interface SelectOptionType {
|
|
102
|
-
value: any;
|
|
103
|
-
label: string;
|
|
104
|
-
}
|
|
17
|
+
UseFormOptionsType,
|
|
18
|
+
UseFormReturnType,
|
|
19
|
+
} from "@ram_28/kf-ai-sdk/form/types";
|
|
20
|
+
```
|
|
105
21
|
|
|
106
|
-
|
|
107
|
-
interface FieldPermissionType {
|
|
108
|
-
editable: boolean;
|
|
109
|
-
readable: boolean;
|
|
110
|
-
hidden: boolean;
|
|
111
|
-
}
|
|
22
|
+
## Product Class Pattern
|
|
112
23
|
|
|
113
|
-
|
|
114
|
-
interface FieldRuleIdsType {
|
|
115
|
-
validation: string[];
|
|
116
|
-
computation: string[];
|
|
117
|
-
businessLogic: string[];
|
|
118
|
-
}
|
|
24
|
+
The recommended pattern uses a role-based BDO wrapper class (like `Product`) instead of hardcoded source strings:
|
|
119
25
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
type: FormFieldTypeType;
|
|
124
|
-
label: string;
|
|
125
|
-
required: boolean;
|
|
126
|
-
computed: boolean;
|
|
127
|
-
defaultValue?: any;
|
|
128
|
-
options?: SelectOptionType[];
|
|
129
|
-
validation: any;
|
|
130
|
-
description?: string;
|
|
131
|
-
_bdoField: BDOFieldDefinitionType;
|
|
132
|
-
permission: FieldPermissionType;
|
|
133
|
-
rules: FieldRuleIdsType;
|
|
134
|
-
}
|
|
26
|
+
```tsx
|
|
27
|
+
// sources/Product.ts - Role-based BDO wrapper
|
|
28
|
+
import { Role, Roles } from "./roles";
|
|
135
29
|
|
|
136
|
-
|
|
137
|
-
interface FormSchemaConfigType {
|
|
138
|
-
fields: Record<string, FormFieldConfigType>;
|
|
139
|
-
fieldOrder: string[];
|
|
140
|
-
computedFields: string[];
|
|
141
|
-
requiredFields: string[];
|
|
142
|
-
crossFieldValidation: SchemaValidationRuleType[];
|
|
143
|
-
rules: {
|
|
144
|
-
validation: Record<string, SchemaValidationRuleType>;
|
|
145
|
-
computation: Record<string, SchemaValidationRuleType>;
|
|
146
|
-
businessLogic: Record<string, SchemaValidationRuleType>;
|
|
147
|
-
};
|
|
148
|
-
fieldRules: Record<string, FieldRuleIdsType>;
|
|
149
|
-
rolePermissions?: Record<string, RolePermissionType>;
|
|
150
|
-
}
|
|
30
|
+
export type ProductType<TRole extends Role> = /* role-mapped types */;
|
|
151
31
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
Type:
|
|
157
|
-
| "String"
|
|
158
|
-
| "Number"
|
|
159
|
-
| "Boolean"
|
|
160
|
-
| "Date"
|
|
161
|
-
| "DateTime"
|
|
162
|
-
| "Reference"
|
|
163
|
-
| "Array"
|
|
164
|
-
| "Object";
|
|
165
|
-
Required?: boolean;
|
|
166
|
-
Unique?: boolean;
|
|
167
|
-
DefaultValue?: DefaultValueExpressionType;
|
|
168
|
-
Formula?: ComputedFieldFormulaType;
|
|
169
|
-
Computed?: boolean;
|
|
170
|
-
Validation?: string[] | SchemaValidationRuleType[];
|
|
171
|
-
Values?: FieldOptionsConfigType;
|
|
172
|
-
Description?: string;
|
|
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.
|
|
173
36
|
}
|
|
37
|
+
```
|
|
174
38
|
|
|
175
|
-
|
|
176
|
-
interface BDOSchemaType {
|
|
177
|
-
Id: string;
|
|
178
|
-
Name: string;
|
|
179
|
-
Kind: "BusinessObject";
|
|
180
|
-
Description: string;
|
|
181
|
-
Rules: {
|
|
182
|
-
Computation?: Record<string, SchemaValidationRuleType>;
|
|
183
|
-
Validation?: Record<string, SchemaValidationRuleType>;
|
|
184
|
-
BusinessLogic?: Record<string, SchemaValidationRuleType>;
|
|
185
|
-
};
|
|
186
|
-
Fields: Record<string, BDOFieldDefinitionType>;
|
|
187
|
-
RolePermission: Record<string, RolePermissionType>;
|
|
188
|
-
Roles: Record<string, { Name: string; Description: string }>;
|
|
189
|
-
}
|
|
39
|
+
This pattern provides type-safe role-based field access, consistent with `useTable` and other hooks.
|
|
190
40
|
|
|
191
|
-
|
|
192
|
-
interface FieldValidationResultType<T = Record<string, any>> {
|
|
193
|
-
isValid: boolean;
|
|
194
|
-
message?: string;
|
|
195
|
-
fieldName?: keyof T;
|
|
196
|
-
}
|
|
41
|
+
## Type Definitions
|
|
197
42
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
data?: any;
|
|
202
|
-
error?: Error;
|
|
203
|
-
recordId?: string;
|
|
204
|
-
}
|
|
43
|
+
```typescript
|
|
44
|
+
// Form operation type
|
|
45
|
+
type FormOperationType = "create" | "update";
|
|
205
46
|
|
|
206
47
|
// Hook options
|
|
207
48
|
interface UseFormOptionsType<T> {
|
|
@@ -212,61 +53,30 @@ interface UseFormOptionsType<T> {
|
|
|
212
53
|
mode?: "onBlur" | "onChange" | "onSubmit" | "onTouched" | "all";
|
|
213
54
|
enabled?: boolean;
|
|
214
55
|
userRole?: string;
|
|
215
|
-
|
|
216
|
-
onSchemaError?: (error: Error) => void; // Schema loading errors only
|
|
56
|
+
onSchemaError?: (error: Error) => void;
|
|
217
57
|
}
|
|
218
58
|
|
|
219
59
|
// Hook return type
|
|
220
60
|
interface UseFormReturnType<T> {
|
|
221
61
|
// React Hook Form methods
|
|
222
62
|
register: UseFormRegister<T>;
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
* form.handleSubmit() // No callbacks
|
|
230
|
-
* form.handleSubmit(onSuccess) // Success only
|
|
231
|
-
* form.handleSubmit(onSuccess, onError) // Both callbacks
|
|
232
|
-
* form.handleSubmit(undefined, onError) // Error only
|
|
233
|
-
*/
|
|
234
|
-
handleSubmit: (
|
|
235
|
-
onSuccess?: (data: T, e?: React.FormEvent) => void | Promise<void>,
|
|
236
|
-
onError?: (
|
|
237
|
-
error: FieldErrors<T> | Error,
|
|
238
|
-
e?: React.FormEvent,
|
|
239
|
-
) => void | Promise<void>,
|
|
240
|
-
) => (e?: React.FormEvent) => Promise<void>;
|
|
241
|
-
watch: <K extends Path<T>>(
|
|
242
|
-
name?: K,
|
|
243
|
-
) => K extends Path<T> ? PathValue<T, K> : T;
|
|
244
|
-
setValue: <K extends Path<T>>(name: K, value: PathValue<T, K>) => void;
|
|
245
|
-
reset: (values?: T) => void;
|
|
246
|
-
|
|
247
|
-
// Flattened state (recommended)
|
|
63
|
+
handleSubmit: (onSuccess?, onError?) => (e?) => Promise<void>;
|
|
64
|
+
watch: (name?) => any;
|
|
65
|
+
setValue: (name, value) => void;
|
|
66
|
+
reset: (values?) => void;
|
|
67
|
+
|
|
68
|
+
// Form state
|
|
248
69
|
errors: FieldErrors<T>;
|
|
249
70
|
isValid: boolean;
|
|
250
71
|
isDirty: boolean;
|
|
251
72
|
isSubmitting: boolean;
|
|
252
73
|
isSubmitSuccessful: boolean;
|
|
253
74
|
|
|
254
|
-
// Legacy (backward compatible)
|
|
255
|
-
formState: FormState<T>;
|
|
256
|
-
|
|
257
75
|
// Loading states
|
|
258
76
|
isLoadingInitialData: boolean;
|
|
259
77
|
isLoadingRecord: boolean;
|
|
260
78
|
isLoading: boolean;
|
|
261
79
|
|
|
262
|
-
// Draft state
|
|
263
|
-
draftId: string | null;
|
|
264
|
-
isCreatingDraft: boolean;
|
|
265
|
-
|
|
266
|
-
// Error handling
|
|
267
|
-
loadError: Error | null;
|
|
268
|
-
hasError: boolean;
|
|
269
|
-
|
|
270
80
|
// Schema info
|
|
271
81
|
schema: BDOSchemaType | null;
|
|
272
82
|
schemaConfig: FormSchemaConfigType | null;
|
|
@@ -274,338 +84,605 @@ interface UseFormReturnType<T> {
|
|
|
274
84
|
requiredFields: Array<keyof T>;
|
|
275
85
|
|
|
276
86
|
// Field helpers
|
|
277
|
-
getField:
|
|
87
|
+
getField: (fieldName) => FormFieldConfigType | null;
|
|
278
88
|
getFields: () => Record<keyof T, FormFieldConfigType>;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
89
|
+
isFieldRequired: (fieldName) => boolean;
|
|
90
|
+
isFieldComputed: (fieldName) => boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Field configuration
|
|
94
|
+
interface FormFieldConfigType {
|
|
95
|
+
name: string;
|
|
96
|
+
type:
|
|
97
|
+
| "text"
|
|
98
|
+
| "number"
|
|
99
|
+
| "email"
|
|
100
|
+
| "date"
|
|
101
|
+
| "checkbox"
|
|
102
|
+
| "select"
|
|
103
|
+
| "textarea";
|
|
104
|
+
label: string;
|
|
105
|
+
required: boolean;
|
|
106
|
+
computed: boolean;
|
|
107
|
+
defaultValue?: any;
|
|
108
|
+
options?: { value: any; label: string }[];
|
|
109
|
+
permission: { editable: boolean; readable: boolean; hidden: boolean };
|
|
287
110
|
}
|
|
288
111
|
```
|
|
289
112
|
|
|
290
|
-
##
|
|
113
|
+
## Basic Example
|
|
114
|
+
|
|
115
|
+
A minimal create form with field registration and submission.
|
|
291
116
|
|
|
292
117
|
```tsx
|
|
293
|
-
import { useState } from "react";
|
|
294
|
-
import { useTable } from "@ram_28/kf-ai-sdk/table";
|
|
295
118
|
import { useForm } from "@ram_28/kf-ai-sdk/form";
|
|
296
119
|
import type {
|
|
297
120
|
UseFormOptionsType,
|
|
298
121
|
UseFormReturnType,
|
|
299
|
-
FormOperationType,
|
|
300
|
-
FormFieldConfigType,
|
|
301
|
-
FormSchemaConfigType,
|
|
302
|
-
BDOFieldDefinitionType,
|
|
303
|
-
BDOSchemaType,
|
|
304
122
|
} from "@ram_28/kf-ai-sdk/form/types";
|
|
305
|
-
import type {
|
|
306
|
-
UseTableOptionsType,
|
|
307
|
-
UseTableReturnType,
|
|
308
|
-
ColumnDefinitionType,
|
|
309
|
-
} from "@ram_28/kf-ai-sdk/table/types";
|
|
310
|
-
import type { FieldErrors } from "@ram_28/kf-ai-sdk/form.types";
|
|
311
123
|
import { Product, ProductType } from "../sources";
|
|
312
124
|
import { Roles } from "../sources/roles";
|
|
313
125
|
|
|
314
|
-
// Get the typed product for the Seller role (who can create/update products)
|
|
315
126
|
type SellerProduct = ProductType<typeof Roles.Seller>;
|
|
316
127
|
|
|
317
|
-
function
|
|
318
|
-
// Instantiate the Product source with role
|
|
128
|
+
function CreateProductForm() {
|
|
319
129
|
const product = new Product(Roles.Seller);
|
|
320
130
|
|
|
321
|
-
const
|
|
322
|
-
const [selectedProduct, setSelectedProduct] = useState<SellerProduct | null>(
|
|
323
|
-
null,
|
|
324
|
-
);
|
|
325
|
-
const [formMode, setFormMode] = useState<FormOperationType>("create");
|
|
326
|
-
|
|
327
|
-
// Table for listing products
|
|
328
|
-
const table = useTable<SellerProduct>({
|
|
329
|
-
source: product._id,
|
|
330
|
-
columns: [
|
|
331
|
-
{ fieldId: "Title", label: "Name" },
|
|
332
|
-
{ fieldId: "Price", label: "Price" },
|
|
333
|
-
{ fieldId: "Discount", label: "Discount %" },
|
|
334
|
-
],
|
|
335
|
-
enablePagination: true,
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
// Form configuration with all options
|
|
339
|
-
const formOptions: UseFormOptionsType<SellerProduct> = {
|
|
131
|
+
const options: UseFormOptionsType<SellerProduct> = {
|
|
340
132
|
source: product._id,
|
|
341
|
-
operation:
|
|
342
|
-
recordId: selectedProduct?._id,
|
|
343
|
-
enabled: showForm,
|
|
344
|
-
mode: "onBlur",
|
|
133
|
+
operation: "create",
|
|
345
134
|
defaultValues: {
|
|
346
135
|
Title: "",
|
|
347
|
-
Description: "",
|
|
348
136
|
Price: 0,
|
|
349
|
-
|
|
350
|
-
Category: "Electronics",
|
|
351
|
-
Stock: 0,
|
|
137
|
+
Category: "",
|
|
352
138
|
},
|
|
353
|
-
onSchemaError: (error: Error) =>
|
|
354
|
-
console.error("Schema error:", error.message),
|
|
355
139
|
};
|
|
356
140
|
|
|
357
|
-
const form: UseFormReturnType<SellerProduct> =
|
|
358
|
-
useForm<SellerProduct>(formOptions);
|
|
141
|
+
const form: UseFormReturnType<SellerProduct> = useForm<SellerProduct>(options);
|
|
359
142
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
console.log("Product saved:", data);
|
|
363
|
-
setShowForm(false);
|
|
364
|
-
table.refetch();
|
|
143
|
+
const onSuccess = (data: SellerProduct) => {
|
|
144
|
+
console.log("Product created:", data);
|
|
365
145
|
};
|
|
366
146
|
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
// API error
|
|
370
|
-
console.error("Submit error:", error.message);
|
|
371
|
-
} else {
|
|
372
|
-
// Validation errors (FieldErrors)
|
|
373
|
-
console.error("Validation errors:", error);
|
|
374
|
-
}
|
|
147
|
+
const onError = (error: Error) => {
|
|
148
|
+
console.error("Failed to create:", error.message);
|
|
375
149
|
};
|
|
376
150
|
|
|
377
|
-
|
|
378
|
-
const displaySchemaInfo = () => {
|
|
379
|
-
const schema: FormSchemaConfigType | null = form.schemaConfig;
|
|
380
|
-
if (!schema) return null;
|
|
151
|
+
if (form.isLoading) return <div>Loading form...</div>;
|
|
381
152
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
<
|
|
386
|
-
<
|
|
153
|
+
return (
|
|
154
|
+
<form onSubmit={form.handleSubmit(onSuccess, onError)}>
|
|
155
|
+
<div>
|
|
156
|
+
<label>Title</label>
|
|
157
|
+
<input {...form.register("Title")} />
|
|
158
|
+
{form.errors.Title && <span>{form.errors.Title.message}</span>}
|
|
387
159
|
</div>
|
|
388
|
-
);
|
|
389
|
-
};
|
|
390
160
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
161
|
+
<div>
|
|
162
|
+
<label>Price</label>
|
|
163
|
+
<input type="number" {...form.register("Price")} />
|
|
164
|
+
{form.errors.Price && <span>{form.errors.Price.message}</span>}
|
|
165
|
+
</div>
|
|
395
166
|
|
|
396
|
-
|
|
397
|
-
|
|
167
|
+
<div>
|
|
168
|
+
<label>Category</label>
|
|
169
|
+
<input {...form.register("Category")} />
|
|
170
|
+
</div>
|
|
398
171
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
172
|
+
<button type="submit" disabled={form.isSubmitting}>
|
|
173
|
+
{form.isSubmitting ? "Creating..." : "Create Product"}
|
|
174
|
+
</button>
|
|
175
|
+
</form>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Create vs Update
|
|
183
|
+
|
|
184
|
+
### Create Form
|
|
185
|
+
|
|
186
|
+
Initialize a form for creating new records.
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
import { Product, ProductType } from "../sources";
|
|
190
|
+
import { Roles } from "../sources/roles";
|
|
191
|
+
|
|
192
|
+
type SellerProduct = ProductType<typeof Roles.Seller>;
|
|
193
|
+
|
|
194
|
+
function CreateForm() {
|
|
195
|
+
const product = new Product(Roles.Seller);
|
|
196
|
+
|
|
197
|
+
const form = useForm<SellerProduct>({
|
|
198
|
+
source: product._id,
|
|
199
|
+
operation: "create",
|
|
200
|
+
defaultValues: {
|
|
201
|
+
Title: "",
|
|
202
|
+
Price: 0,
|
|
203
|
+
Stock: 0,
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<form onSubmit={form.handleSubmit()}>
|
|
209
|
+
<input {...form.register("Title")} placeholder="Product title" />
|
|
210
|
+
<input type="number" {...form.register("Price")} placeholder="Price" />
|
|
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);
|
|
289
|
+
|
|
290
|
+
if (!field || field.permission.hidden) return null;
|
|
291
|
+
|
|
292
|
+
const isDisabled = !field.permission.editable || field.computed;
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<div className="field">
|
|
296
|
+
<label>
|
|
297
|
+
{field.label}
|
|
298
|
+
{field.required && <span className="required">*</span>}
|
|
299
|
+
{field.computed && <span className="computed">(auto)</span>}
|
|
300
|
+
</label>
|
|
301
|
+
|
|
302
|
+
{field.type === "select" && field.options ? (
|
|
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
|
+
});
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<form onSubmit={form.handleSubmit()}>
|
|
342
|
+
<div>
|
|
343
|
+
<label>MRP</label>
|
|
344
|
+
<input type="number" {...form.register("MRP")} />
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<div>
|
|
348
|
+
<label>Price</label>
|
|
349
|
+
<input type="number" {...form.register("Price")} />
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
{/* Discount is computed from MRP and Price */}
|
|
353
|
+
{form.isFieldComputed("Discount") && (
|
|
354
|
+
<div>
|
|
355
|
+
<label>Discount % (auto-calculated)</label>
|
|
421
356
|
<input
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
value={form.watch(fieldName as any) ?? field.defaultValue ?? ""}
|
|
357
|
+
type="number"
|
|
358
|
+
value={form.watch("Discount") ?? 0}
|
|
425
359
|
readOnly
|
|
426
360
|
disabled
|
|
427
361
|
/>
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
362
|
+
</div>
|
|
363
|
+
)}
|
|
364
|
+
|
|
365
|
+
<button type="submit">Save</button>
|
|
366
|
+
</form>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Render All Fields Dynamically
|
|
372
|
+
|
|
373
|
+
Generate form fields from schema configuration.
|
|
374
|
+
|
|
375
|
+
```tsx
|
|
376
|
+
function DynamicForm() {
|
|
377
|
+
const product = new Product(Roles.Seller);
|
|
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
|
|
416
|
+
|
|
417
|
+
Show validation errors inline with fields.
|
|
418
|
+
|
|
419
|
+
```tsx
|
|
420
|
+
function FormWithValidation() {
|
|
421
|
+
const product = new Product(Roles.Seller);
|
|
422
|
+
|
|
423
|
+
const form = useForm<SellerProduct>({
|
|
424
|
+
source: product._id,
|
|
425
|
+
operation: "create",
|
|
426
|
+
mode: "onBlur", // Validate on blur
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
return (
|
|
430
|
+
<form onSubmit={form.handleSubmit()}>
|
|
431
|
+
<div className={form.errors.Title ? "field-error" : ""}>
|
|
432
|
+
<label>Title *</label>
|
|
433
|
+
<input {...form.register("Title")} />
|
|
434
|
+
{form.errors.Title && (
|
|
435
|
+
<span className="error-message">{form.errors.Title.message}</span>
|
|
435
436
|
)}
|
|
437
|
+
</div>
|
|
436
438
|
|
|
437
|
-
|
|
439
|
+
<div className={form.errors.Price ? "field-error" : ""}>
|
|
440
|
+
<label>Price *</label>
|
|
441
|
+
<input type="number" {...form.register("Price")} />
|
|
442
|
+
{form.errors.Price && (
|
|
443
|
+
<span className="error-message">{form.errors.Price.message}</span>
|
|
444
|
+
)}
|
|
438
445
|
</div>
|
|
439
|
-
);
|
|
440
|
-
};
|
|
441
446
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
447
|
+
<button type="submit" disabled={!form.isValid}>
|
|
448
|
+
Submit
|
|
449
|
+
</button>
|
|
450
|
+
</form>
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
```
|
|
446
454
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
)
|
|
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>}
|
|
481
|
+
</div>
|
|
482
|
+
|
|
483
|
+
<button type="submit" disabled={form.isSubmitting || !form.isDirty}>
|
|
484
|
+
Save Changes
|
|
485
|
+
</button>
|
|
486
|
+
|
|
487
|
+
<button
|
|
488
|
+
type="button"
|
|
489
|
+
onClick={() => form.reset()}
|
|
490
|
+
disabled={!form.isDirty}
|
|
491
|
+
>
|
|
492
|
+
Discard Changes
|
|
493
|
+
</button>
|
|
494
|
+
</form>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## Submission
|
|
502
|
+
|
|
503
|
+
### Handle Submit with Callbacks
|
|
504
|
+
|
|
505
|
+
Process successful submissions and errors.
|
|
506
|
+
|
|
507
|
+
```tsx
|
|
508
|
+
function FormWithCallbacks() {
|
|
509
|
+
const product = new Product(Roles.Seller);
|
|
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();
|
|
463
519
|
};
|
|
464
520
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
+
}
|
|
472
530
|
};
|
|
473
531
|
|
|
474
|
-
|
|
475
|
-
|
|
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
|
|
476
543
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
544
|
+
Submit form without a submit button.
|
|
545
|
+
|
|
546
|
+
```tsx
|
|
547
|
+
function FormWithProgrammaticSubmit() {
|
|
548
|
+
const product = new Product(Roles.Seller);
|
|
549
|
+
|
|
550
|
+
const form = useForm<SellerProduct>({
|
|
551
|
+
source: product._id,
|
|
552
|
+
operation: "create",
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
const handleSaveAndClose = async () => {
|
|
556
|
+
const submitHandler = form.handleSubmit(
|
|
557
|
+
(data) => {
|
|
558
|
+
console.log("Saved:", data);
|
|
559
|
+
// Close modal or navigate away
|
|
560
|
+
},
|
|
561
|
+
(error) => {
|
|
562
|
+
console.error("Validation failed:", error);
|
|
563
|
+
},
|
|
564
|
+
);
|
|
483
565
|
|
|
484
|
-
|
|
485
|
-
const handleEdit = (productItem: SellerProduct) => {
|
|
486
|
-
setFormMode("update");
|
|
487
|
-
setSelectedProduct(productItem);
|
|
488
|
-
setShowForm(true);
|
|
566
|
+
await submitHandler();
|
|
489
567
|
};
|
|
490
568
|
|
|
491
569
|
return (
|
|
492
|
-
<div
|
|
493
|
-
<
|
|
494
|
-
<
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
</tr>
|
|
505
|
-
</thead>
|
|
506
|
-
<tbody>
|
|
507
|
-
{table.rows.map((row: SellerProduct) => (
|
|
508
|
-
<tr key={row._id}>
|
|
509
|
-
<td>{row.Title}</td>
|
|
510
|
-
<td>${row.Price.toFixed(2)}</td>
|
|
511
|
-
<td>{row.Discount}%</td>
|
|
512
|
-
<td>
|
|
513
|
-
<button onClick={() => handleEdit(row)}>Edit</button>
|
|
514
|
-
</td>
|
|
515
|
-
</tr>
|
|
516
|
-
))}
|
|
517
|
-
</tbody>
|
|
518
|
-
</table>
|
|
519
|
-
|
|
520
|
-
{/* Form Dialog */}
|
|
521
|
-
{showForm && (
|
|
522
|
-
<dialog open>
|
|
523
|
-
{form.isLoadingInitialData ? (
|
|
524
|
-
<div>Loading form...</div>
|
|
525
|
-
) : form.loadError ? (
|
|
526
|
-
<div>
|
|
527
|
-
<p>Error: {form.loadError.message}</p>
|
|
528
|
-
<button onClick={() => form.refreshSchema()}>Retry</button>
|
|
529
|
-
</div>
|
|
530
|
-
) : (
|
|
531
|
-
<form onSubmit={form.handleSubmit(onFormSuccess, onFormError)}>
|
|
532
|
-
<h2>
|
|
533
|
-
{formMode === "create" ? "Create Product" : "Edit Product"}
|
|
534
|
-
</h2>
|
|
535
|
-
|
|
536
|
-
{/* Schema Info */}
|
|
537
|
-
{displaySchemaInfo()}
|
|
538
|
-
|
|
539
|
-
{/* Form Fields */}
|
|
540
|
-
{renderField("Title")}
|
|
541
|
-
{renderField("Description")}
|
|
542
|
-
{renderField("MRP")}
|
|
543
|
-
{renderField("Price")}
|
|
544
|
-
{renderField("Discount")}
|
|
545
|
-
{renderField("Category")}
|
|
546
|
-
{renderField("Stock")}
|
|
547
|
-
|
|
548
|
-
{/* Form Status */}
|
|
549
|
-
<div className="form-status">
|
|
550
|
-
{form.isDirty && <span>Unsaved changes</span>}
|
|
551
|
-
{!form.isValid && <span>Please fix validation errors</span>}
|
|
552
|
-
</div>
|
|
553
|
-
|
|
554
|
-
{/* Cross-field validation errors */}
|
|
555
|
-
{form.errors.root && (
|
|
556
|
-
<div className="cross-field-errors">
|
|
557
|
-
{Object.values(form.errors.root).map((err, i) => (
|
|
558
|
-
<p key={i} className="error">
|
|
559
|
-
{(err as any).message}
|
|
560
|
-
</p>
|
|
561
|
-
))}
|
|
562
|
-
</div>
|
|
563
|
-
)}
|
|
564
|
-
|
|
565
|
-
{/* Note: Submit errors are now handled via onError callback */}
|
|
566
|
-
|
|
567
|
-
{/* Raw Schema Debug */}
|
|
568
|
-
{displayRawSchema()}
|
|
569
|
-
|
|
570
|
-
{/* Form Actions */}
|
|
571
|
-
<div className="form-actions">
|
|
572
|
-
<button type="button" onClick={() => setShowForm(false)}>
|
|
573
|
-
Cancel
|
|
574
|
-
</button>
|
|
575
|
-
<button type="button" onClick={() => form.reset()}>
|
|
576
|
-
Reset
|
|
577
|
-
</button>
|
|
578
|
-
<button type="button" onClick={() => form.validateForm()}>
|
|
579
|
-
Validate
|
|
580
|
-
</button>
|
|
581
|
-
<button
|
|
582
|
-
type="submit"
|
|
583
|
-
disabled={form.isSubmitting || form.isLoading}
|
|
584
|
-
>
|
|
585
|
-
{form.isSubmitting
|
|
586
|
-
? "Saving..."
|
|
587
|
-
: formMode === "create"
|
|
588
|
-
? "Create"
|
|
589
|
-
: "Update"}
|
|
590
|
-
</button>
|
|
591
|
-
</div>
|
|
592
|
-
|
|
593
|
-
{/* Watch specific fields */}
|
|
594
|
-
<div className="field-preview">
|
|
595
|
-
<p>Current Price: ${form.watch("Price")}</p>
|
|
596
|
-
<p>Current Discount: {form.watch("Discount")}%</p>
|
|
597
|
-
</div>
|
|
598
|
-
</form>
|
|
599
|
-
)}
|
|
600
|
-
</dialog>
|
|
601
|
-
)}
|
|
570
|
+
<div>
|
|
571
|
+
<input {...form.register("Title")} />
|
|
572
|
+
<input type="number" {...form.register("Price")} />
|
|
573
|
+
|
|
574
|
+
<div className="actions">
|
|
575
|
+
<button type="button" onClick={() => form.reset()}>
|
|
576
|
+
Cancel
|
|
577
|
+
</button>
|
|
578
|
+
<button type="button" onClick={handleSaveAndClose}>
|
|
579
|
+
Save & Close
|
|
580
|
+
</button>
|
|
581
|
+
</div>
|
|
602
582
|
</div>
|
|
603
583
|
);
|
|
604
584
|
}
|
|
605
585
|
```
|
|
606
586
|
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
## Watch and setValue
|
|
590
|
+
|
|
591
|
+
### Watch Field Values
|
|
592
|
+
|
|
593
|
+
React to field value changes in real-time.
|
|
594
|
+
|
|
595
|
+
```tsx
|
|
596
|
+
function FormWithWatcher() {
|
|
597
|
+
const product = new Product(Roles.Seller);
|
|
598
|
+
|
|
599
|
+
const form = useForm<SellerProduct>({
|
|
600
|
+
source: product._id,
|
|
601
|
+
operation: "create",
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const mrp = form.watch("MRP");
|
|
605
|
+
const price = form.watch("Price");
|
|
606
|
+
const discount = mrp > 0 ? (((mrp - price) / mrp) * 100).toFixed(1) : 0;
|
|
607
|
+
|
|
608
|
+
return (
|
|
609
|
+
<form onSubmit={form.handleSubmit()}>
|
|
610
|
+
<div>
|
|
611
|
+
<label>MRP</label>
|
|
612
|
+
<input type="number" {...form.register("MRP")} />
|
|
613
|
+
</div>
|
|
614
|
+
|
|
615
|
+
<div>
|
|
616
|
+
<label>Price</label>
|
|
617
|
+
<input type="number" {...form.register("Price")} />
|
|
618
|
+
</div>
|
|
619
|
+
|
|
620
|
+
<div className="preview">
|
|
621
|
+
<p>Discount: {discount}%</p>
|
|
622
|
+
<p>You save: ${(mrp - price).toFixed(2)}</p>
|
|
623
|
+
</div>
|
|
624
|
+
|
|
625
|
+
<button type="submit">Save</button>
|
|
626
|
+
</form>
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### Set Field Values Programmatically
|
|
632
|
+
|
|
633
|
+
Update field values from external actions.
|
|
634
|
+
|
|
635
|
+
```tsx
|
|
636
|
+
function FormWithSetValue() {
|
|
637
|
+
const product = new Product(Roles.Seller);
|
|
638
|
+
|
|
639
|
+
const form = useForm<SellerProduct>({
|
|
640
|
+
source: product._id,
|
|
641
|
+
operation: "create",
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const applyTemplate = (template: string) => {
|
|
645
|
+
switch (template) {
|
|
646
|
+
case "electronics":
|
|
647
|
+
form.setValue("Category", "Electronics");
|
|
648
|
+
form.setValue("Price", 99.99);
|
|
649
|
+
break;
|
|
650
|
+
case "books":
|
|
651
|
+
form.setValue("Category", "Books");
|
|
652
|
+
form.setValue("Price", 19.99);
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
return (
|
|
658
|
+
<form onSubmit={form.handleSubmit()}>
|
|
659
|
+
<div className="templates">
|
|
660
|
+
<button type="button" onClick={() => applyTemplate("electronics")}>
|
|
661
|
+
Electronics Template
|
|
662
|
+
</button>
|
|
663
|
+
<button type="button" onClick={() => applyTemplate("books")}>
|
|
664
|
+
Books Template
|
|
665
|
+
</button>
|
|
666
|
+
</div>
|
|
667
|
+
|
|
668
|
+
<input {...form.register("Title")} placeholder="Title" />
|
|
669
|
+
<input {...form.register("Category")} placeholder="Category" />
|
|
670
|
+
<input type="number" {...form.register("Price")} placeholder="Price" />
|
|
671
|
+
|
|
672
|
+
<button type="submit">Create</button>
|
|
673
|
+
</form>
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
607
680
|
## Error Utilities
|
|
608
681
|
|
|
682
|
+
The SDK provides utilities to help handle and categorize errors from form operations.
|
|
683
|
+
|
|
684
|
+
### Available Utilities
|
|
685
|
+
|
|
609
686
|
```typescript
|
|
610
687
|
import {
|
|
611
688
|
parseApiError,
|
|
@@ -613,17 +690,151 @@ import {
|
|
|
613
690
|
isValidationError,
|
|
614
691
|
clearFormCache,
|
|
615
692
|
} from "@ram_28/kf-ai-sdk/form";
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
| Utility | Description |
|
|
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
|
|
703
|
+
|
|
704
|
+
Use built-in utilities for comprehensive error handling.
|
|
705
|
+
|
|
706
|
+
```tsx
|
|
707
|
+
import {
|
|
708
|
+
parseApiError,
|
|
709
|
+
isNetworkError,
|
|
710
|
+
isValidationError,
|
|
711
|
+
clearFormCache,
|
|
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);
|
|
616
724
|
|
|
617
|
-
|
|
618
|
-
|
|
725
|
+
if (isNetworkError(error)) {
|
|
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
|
+
};
|
|
733
|
+
|
|
734
|
+
const handleClearCache = () => {
|
|
735
|
+
clearFormCache();
|
|
736
|
+
window.location.reload();
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
return (
|
|
740
|
+
<form onSubmit={form.handleSubmit(undefined, onError)}>
|
|
741
|
+
{/* form fields */}
|
|
742
|
+
<button type="submit">Save</button>
|
|
743
|
+
<button type="button" onClick={handleClearCache}>
|
|
744
|
+
Clear Cache & Reload
|
|
745
|
+
</button>
|
|
746
|
+
</form>
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
### Handling Different Error Types
|
|
752
|
+
|
|
753
|
+
The `handleSubmit` callback receives different error types depending on the failure stage:
|
|
754
|
+
|
|
755
|
+
```tsx
|
|
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
|
+
}
|
|
619
784
|
|
|
620
|
-
//
|
|
621
|
-
if (isNetworkError(error)) {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
+
};
|
|
797
|
+
|
|
798
|
+
return (
|
|
799
|
+
<form onSubmit={form.handleSubmit(onSubmit, onError)}>
|
|
800
|
+
{/* form fields */}
|
|
801
|
+
</form>
|
|
802
|
+
);
|
|
625
803
|
}
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
### Schema Loading Errors
|
|
807
|
+
|
|
808
|
+
Handle errors when the form schema fails to load.
|
|
809
|
+
|
|
810
|
+
```tsx
|
|
811
|
+
function FormWithSchemaErrorHandling() {
|
|
812
|
+
const product = new Product(Roles.Seller);
|
|
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
|
+
});
|
|
626
823
|
|
|
627
|
-
//
|
|
628
|
-
|
|
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
|
+
}
|
|
629
840
|
```
|