@ram_28/kf-ai-sdk 1.0.16 → 1.0.19

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.
@@ -0,0 +1,629 @@
1
+ # useForm
2
+
3
+ ## Brief Description
4
+
5
+ - Integrates react-hook-form with backend Business Data Object (BDO) schemas for automatic validation and computed field handling
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
11
+
12
+ ```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
+ import {
40
+ parseApiError,
41
+ isNetworkError,
42
+ isValidationError,
43
+ clearFormCache,
44
+ } from "@ram_28/kf-ai-sdk/form";
45
+
46
+ // Re-exported from react-hook-form (available from SDK)
47
+ import type {
48
+ UseFormRegister,
49
+ FieldErrors,
50
+ FormState,
51
+ Path,
52
+ PathValue,
53
+ } from "@ram_28/kf-ai-sdk/form.types";
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
+ }
105
+
106
+ // Field permission for current user
107
+ interface FieldPermissionType {
108
+ editable: boolean;
109
+ readable: boolean;
110
+ hidden: boolean;
111
+ }
112
+
113
+ // Field rule IDs by category
114
+ interface FieldRuleIdsType {
115
+ validation: string[];
116
+ computation: string[];
117
+ businessLogic: string[];
118
+ }
119
+
120
+ // Form field configuration for rendering
121
+ interface FormFieldConfigType {
122
+ name: string;
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
+ }
135
+
136
+ // Form schema configuration after processing
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
+ }
151
+
152
+ // BDO field definition structure
153
+ interface BDOFieldDefinitionType {
154
+ Id: string;
155
+ Name: string;
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;
173
+ }
174
+
175
+ // BDO schema structure
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
+ }
190
+
191
+ // Field validation result
192
+ interface FieldValidationResultType<T = Record<string, any>> {
193
+ isValid: boolean;
194
+ message?: string;
195
+ fieldName?: keyof T;
196
+ }
197
+
198
+ // Form submission result
199
+ interface SubmissionResultType {
200
+ success: boolean;
201
+ data?: any;
202
+ error?: Error;
203
+ recordId?: string;
204
+ }
205
+
206
+ // Hook options
207
+ interface UseFormOptionsType<T> {
208
+ source: string;
209
+ operation: FormOperationType;
210
+ recordId?: string;
211
+ defaultValues?: Partial<T>;
212
+ mode?: "onBlur" | "onChange" | "onSubmit" | "onTouched" | "all";
213
+ enabled?: boolean;
214
+ userRole?: string;
215
+ schema?: BDOSchemaType;
216
+ onSchemaError?: (error: Error) => void; // Schema loading errors only
217
+ }
218
+
219
+ // Hook return type
220
+ interface UseFormReturnType<T> {
221
+ // React Hook Form methods
222
+ register: UseFormRegister<T>;
223
+ /**
224
+ * handleSubmit follows RHF pattern with optional callbacks:
225
+ * - onSuccess: Called with API response data on successful submission
226
+ * - onError: Called with FieldErrors (validation) or Error (API failure)
227
+ *
228
+ * Usage:
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)
248
+ errors: FieldErrors<T>;
249
+ isValid: boolean;
250
+ isDirty: boolean;
251
+ isSubmitting: boolean;
252
+ isSubmitSuccessful: boolean;
253
+
254
+ // Legacy (backward compatible)
255
+ formState: FormState<T>;
256
+
257
+ // Loading states
258
+ isLoadingInitialData: boolean;
259
+ isLoadingRecord: boolean;
260
+ isLoading: boolean;
261
+
262
+ // Draft state
263
+ draftId: string | null;
264
+ isCreatingDraft: boolean;
265
+
266
+ // Error handling
267
+ loadError: Error | null;
268
+ hasError: boolean;
269
+
270
+ // Schema info
271
+ schema: BDOSchemaType | null;
272
+ schemaConfig: FormSchemaConfigType | null;
273
+ computedFields: Array<keyof T>;
274
+ requiredFields: Array<keyof T>;
275
+
276
+ // Field helpers
277
+ getField: <K extends keyof T>(fieldName: K) => FormFieldConfigType | null;
278
+ getFields: () => Record<keyof T, FormFieldConfigType>;
279
+ hasField: <K extends keyof T>(fieldName: K) => boolean;
280
+ isFieldRequired: <K extends keyof T>(fieldName: K) => boolean;
281
+ isFieldComputed: <K extends keyof T>(fieldName: K) => boolean;
282
+
283
+ // Operations
284
+ refreshSchema: () => Promise<void>;
285
+ validateForm: () => Promise<boolean>;
286
+ clearErrors: () => void;
287
+ }
288
+ ```
289
+
290
+ ## Usage Example
291
+
292
+ ```tsx
293
+ import { useState } from "react";
294
+ import { useTable } from "@ram_28/kf-ai-sdk/table";
295
+ import { useForm } from "@ram_28/kf-ai-sdk/form";
296
+ import type {
297
+ UseFormOptionsType,
298
+ UseFormReturnType,
299
+ FormOperationType,
300
+ FormFieldConfigType,
301
+ FormSchemaConfigType,
302
+ BDOFieldDefinitionType,
303
+ BDOSchemaType,
304
+ } 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
+ import { Product, ProductType } from "../sources";
312
+ import { Roles } from "../sources/roles";
313
+
314
+ // Get the typed product for the Seller role (who can create/update products)
315
+ type SellerProduct = ProductType<typeof Roles.Seller>;
316
+
317
+ function ProductManagementPage() {
318
+ // Instantiate the Product source with role
319
+ const product = new Product(Roles.Seller);
320
+
321
+ const [showForm, setShowForm] = useState(false);
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> = {
340
+ source: product._id,
341
+ operation: formMode,
342
+ recordId: selectedProduct?._id,
343
+ enabled: showForm,
344
+ mode: "onBlur",
345
+ defaultValues: {
346
+ Title: "",
347
+ Description: "",
348
+ Price: 0,
349
+ MRP: 0,
350
+ Category: "Electronics",
351
+ Stock: 0,
352
+ },
353
+ onSchemaError: (error: Error) =>
354
+ console.error("Schema error:", error.message),
355
+ };
356
+
357
+ const form: UseFormReturnType<SellerProduct> =
358
+ useForm<SellerProduct>(formOptions);
359
+
360
+ // Handle form submission with new RHF-style callbacks
361
+ const onFormSuccess = (data: SellerProduct) => {
362
+ console.log("Product saved:", data);
363
+ setShowForm(false);
364
+ table.refetch();
365
+ };
366
+
367
+ const onFormError = (error: FieldErrors<SellerProduct> | Error) => {
368
+ if (error instanceof Error) {
369
+ // API error
370
+ console.error("Submit error:", error.message);
371
+ } else {
372
+ // Validation errors (FieldErrors)
373
+ console.error("Validation errors:", error);
374
+ }
375
+ };
376
+
377
+ // Access processed schema
378
+ const displaySchemaInfo = () => {
379
+ const schema: FormSchemaConfigType | null = form.schemaConfig;
380
+ if (!schema) return null;
381
+
382
+ return (
383
+ <div className="schema-info">
384
+ <p>Required: {schema.requiredFields.join(", ")}</p>
385
+ <p>Computed: {schema.computedFields.join(", ")}</p>
386
+ <p>Field Order: {schema.fieldOrder.join(", ")}</p>
387
+ </div>
388
+ );
389
+ };
390
+
391
+ // Render a field using FormFieldConfigType metadata
392
+ const renderField = (fieldName: keyof SellerProduct) => {
393
+ const field: FormFieldConfigType | null = form.getField(fieldName);
394
+ if (!field || field.permission.hidden) return null;
395
+
396
+ const isDisabled = !field.permission.editable || field.computed;
397
+ const error = form.errors[fieldName];
398
+
399
+ return (
400
+ <div key={field.name} className="form-field">
401
+ <label htmlFor={field.name}>
402
+ {field.label}
403
+ {field.required && <span className="required">*</span>}
404
+ {field.computed && <span className="computed">(auto)</span>}
405
+ </label>
406
+
407
+ {field.type === "select" && field.options ? (
408
+ <select
409
+ id={field.name}
410
+ disabled={isDisabled}
411
+ {...form.register(fieldName as any)}
412
+ >
413
+ <option value="">Select {field.label}</option>
414
+ {field.options.map((opt) => (
415
+ <option key={opt.value} value={opt.value}>
416
+ {opt.label}
417
+ </option>
418
+ ))}
419
+ </select>
420
+ ) : field.computed ? (
421
+ <input
422
+ id={field.name}
423
+ type={field.type === "number" ? "number" : "text"}
424
+ value={form.watch(fieldName as any) ?? field.defaultValue ?? ""}
425
+ readOnly
426
+ disabled
427
+ />
428
+ ) : (
429
+ <input
430
+ id={field.name}
431
+ type={field.type === "number" ? "number" : "text"}
432
+ disabled={isDisabled}
433
+ {...form.register(fieldName as any)}
434
+ />
435
+ )}
436
+
437
+ {error && <span className="error">{error.message as string}</span>}
438
+ </div>
439
+ );
440
+ };
441
+
442
+ // Access raw BDO schema
443
+ const displayRawSchema = () => {
444
+ const schema: BDOSchemaType | null = form.schema;
445
+ if (!schema) return null;
446
+
447
+ return (
448
+ <details>
449
+ <summary>Raw Schema: {schema.Name}</summary>
450
+ <ul>
451
+ {Object.entries(schema.Fields).map(
452
+ ([fieldName, field]: [string, BDOFieldDefinitionType]) => (
453
+ <li key={fieldName}>
454
+ {field.Name || fieldName} ({field.Type})
455
+ {field.Required && " - Required"}
456
+ {field.Computed && " - Computed"}
457
+ </li>
458
+ ),
459
+ )}
460
+ </ul>
461
+ </details>
462
+ );
463
+ };
464
+
465
+ // Check field properties using helpers
466
+ const checkFieldStatus = () => {
467
+ console.log("Is Title required?", form.isFieldRequired("Title"));
468
+ console.log("Is Discount computed?", form.isFieldComputed("Discount"));
469
+ console.log("Has Category field?", form.hasField("Category"));
470
+ console.log("All computed fields:", form.computedFields);
471
+ console.log("All required fields:", form.requiredFields);
472
+ };
473
+
474
+ // Handle form submission - use the new RHF-style API
475
+ // Note: form.handleSubmit returns a function that handles preventDefault internally
476
+
477
+ // Open create form
478
+ const handleCreate = () => {
479
+ setFormMode("create");
480
+ setSelectedProduct(null);
481
+ setShowForm(true);
482
+ };
483
+
484
+ // Open edit form
485
+ const handleEdit = (productItem: SellerProduct) => {
486
+ setFormMode("update");
487
+ setSelectedProduct(productItem);
488
+ setShowForm(true);
489
+ };
490
+
491
+ return (
492
+ <div className="product-management">
493
+ <h1>Products</h1>
494
+ <button onClick={handleCreate}>Add Product</button>
495
+
496
+ {/* Product Table */}
497
+ <table>
498
+ <thead>
499
+ <tr>
500
+ <th>Title</th>
501
+ <th>Price</th>
502
+ <th>Discount</th>
503
+ <th>Actions</th>
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
+ )}
602
+ </div>
603
+ );
604
+ }
605
+ ```
606
+
607
+ ## Error Utilities
608
+
609
+ ```typescript
610
+ import {
611
+ parseApiError,
612
+ isNetworkError,
613
+ isValidationError,
614
+ clearFormCache,
615
+ } from "@ram_28/kf-ai-sdk/form";
616
+
617
+ // Parse API error to user-friendly message
618
+ const message = parseApiError(error); // "Failed to save: Invalid data"
619
+
620
+ // Check error type for specific handling
621
+ if (isNetworkError(error)) {
622
+ showOfflineMessage();
623
+ } else if (isValidationError(error)) {
624
+ showValidationErrors();
625
+ }
626
+
627
+ // Clear form schema cache (useful after schema changes)
628
+ clearFormCache();
629
+ ```