@ram_28/kf-ai-sdk 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/client.d.ts +10 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/components/hooks/useForm/types.d.ts +62 -19
- package/dist/components/hooks/useForm/types.d.ts.map +1 -1
- package/dist/components/hooks/useForm/useForm.d.ts.map +1 -1
- package/dist/index.cjs +12 -12
- package/dist/index.mjs +2252 -2171
- package/package.json +1 -1
- package/sdk/api/client.ts +27 -0
- package/sdk/components/hooks/useForm/types.ts +75 -28
- package/sdk/components/hooks/useForm/useForm.ts +239 -100
|
@@ -49,15 +49,16 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
49
49
|
mode = "onBlur", // Validation mode - controls when errors are shown (see types.ts for details)
|
|
50
50
|
enabled = true,
|
|
51
51
|
userRole,
|
|
52
|
-
onSuccess,
|
|
53
|
-
onError,
|
|
54
52
|
onSchemaError,
|
|
55
|
-
onSubmitError,
|
|
56
53
|
skipSchemaFetch = false,
|
|
57
54
|
schema: manualSchema,
|
|
58
55
|
draftOnEveryChange = false,
|
|
56
|
+
interactionMode = "interactive",
|
|
59
57
|
} = options;
|
|
60
58
|
|
|
59
|
+
// Derived interaction mode flags
|
|
60
|
+
const isInteractiveMode = interactionMode === "interactive";
|
|
61
|
+
|
|
61
62
|
// ============================================================
|
|
62
63
|
// STATE MANAGEMENT
|
|
63
64
|
// ============================================================
|
|
@@ -65,10 +66,14 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
65
66
|
const [schemaConfig, setSchemaConfig] =
|
|
66
67
|
useState<FormSchemaConfig | null>(null);
|
|
67
68
|
const [referenceData, setReferenceData] = useState<Record<string, any[]>>({});
|
|
68
|
-
const [submitError, setSubmitError] = useState<Error | null>(null);
|
|
69
69
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
70
70
|
const [lastFormValues] = useState<Partial<T>>({});
|
|
71
71
|
|
|
72
|
+
// Interactive mode state
|
|
73
|
+
const [draftId, setDraftId] = useState<string | null>(null);
|
|
74
|
+
const [isCreatingDraft, setIsCreatingDraft] = useState(false);
|
|
75
|
+
const [draftError, setDraftError] = useState<Error | null>(null);
|
|
76
|
+
|
|
72
77
|
// Prevent infinite loop in API calls
|
|
73
78
|
const isComputingRef = useRef(false);
|
|
74
79
|
const computeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
@@ -77,25 +82,10 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
77
82
|
// This allows us to detect changes since the last draft, not since form init
|
|
78
83
|
const lastSyncedValuesRef = useRef<Partial<T> | null>(null);
|
|
79
84
|
|
|
80
|
-
// Stable callback
|
|
81
|
-
const onSuccessRef = useRef(onSuccess);
|
|
82
|
-
const onErrorRef = useRef(onError);
|
|
83
|
-
const onSubmitErrorRef = useRef(onSubmitError);
|
|
85
|
+
// Stable callback ref to prevent dependency loops
|
|
84
86
|
const onSchemaErrorRef = useRef(onSchemaError);
|
|
85
87
|
|
|
86
|
-
// Update
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
onSuccessRef.current = onSuccess;
|
|
89
|
-
}, [onSuccess]);
|
|
90
|
-
|
|
91
|
-
useEffect(() => {
|
|
92
|
-
onErrorRef.current = onError;
|
|
93
|
-
}, [onError]);
|
|
94
|
-
|
|
95
|
-
useEffect(() => {
|
|
96
|
-
onSubmitErrorRef.current = onSubmitError;
|
|
97
|
-
}, [onSubmitError]);
|
|
98
|
-
|
|
88
|
+
// Update ref when callback changes
|
|
99
89
|
useEffect(() => {
|
|
100
90
|
onSchemaErrorRef.current = onSchemaError;
|
|
101
91
|
}, [onSchemaError]);
|
|
@@ -201,18 +191,67 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
201
191
|
}
|
|
202
192
|
}, [schema, userRole]); // Removed onSchemaError - using ref instead
|
|
203
193
|
|
|
204
|
-
// Handle schema
|
|
194
|
+
// Handle schema error
|
|
205
195
|
useEffect(() => {
|
|
206
196
|
if (schemaError) {
|
|
207
197
|
onSchemaErrorRef.current?.(schemaError);
|
|
208
198
|
}
|
|
209
199
|
}, [schemaError]);
|
|
210
200
|
|
|
201
|
+
// ============================================================
|
|
202
|
+
// INTERACTIVE MODE - INITIAL DRAFT CREATION
|
|
203
|
+
// ============================================================
|
|
204
|
+
|
|
205
|
+
// Create initial draft for interactive create mode
|
|
211
206
|
useEffect(() => {
|
|
212
|
-
|
|
213
|
-
|
|
207
|
+
// Only run for interactive mode + create operation + schema loaded + no draft yet
|
|
208
|
+
if (
|
|
209
|
+
!isInteractiveMode ||
|
|
210
|
+
operation !== "create" ||
|
|
211
|
+
!schemaConfig ||
|
|
212
|
+
!enabled ||
|
|
213
|
+
draftId
|
|
214
|
+
) {
|
|
215
|
+
return;
|
|
214
216
|
}
|
|
215
|
-
|
|
217
|
+
|
|
218
|
+
const createInitialDraft = async () => {
|
|
219
|
+
setIsCreatingDraft(true);
|
|
220
|
+
setDraftError(null);
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const client = api<T>(source);
|
|
224
|
+
// Call PATCH /{bdo_id}/draft with empty payload to get draft ID
|
|
225
|
+
const response = await client.draftInteraction({});
|
|
226
|
+
|
|
227
|
+
// Store the draft ID
|
|
228
|
+
setDraftId(response._id);
|
|
229
|
+
|
|
230
|
+
// Apply any computed fields returned from API
|
|
231
|
+
if (response && typeof response === "object") {
|
|
232
|
+
Object.entries(response).forEach(([fieldName, value]) => {
|
|
233
|
+
// Skip _id as it's the draft ID, not a form field
|
|
234
|
+
if (fieldName === "_id") return;
|
|
235
|
+
|
|
236
|
+
const currentValue = rhfForm.getValues(fieldName as any);
|
|
237
|
+
if (currentValue !== value) {
|
|
238
|
+
rhfForm.setValue(fieldName as any, value as any, {
|
|
239
|
+
shouldDirty: false,
|
|
240
|
+
shouldValidate: false,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error("Failed to create initial draft:", error);
|
|
247
|
+
setDraftError(error as Error);
|
|
248
|
+
} finally {
|
|
249
|
+
setIsCreatingDraft(false);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
createInitialDraft();
|
|
254
|
+
}, [isInteractiveMode, operation, schemaConfig, enabled, draftId, source, rhfForm]);
|
|
216
255
|
|
|
217
256
|
// ============================================================
|
|
218
257
|
// COMPUTED FIELD DEPENDENCY TRACKING AND OPTIMIZATION
|
|
@@ -278,21 +317,29 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
278
317
|
// Manual computation trigger - called on blur after validation passes
|
|
279
318
|
const triggerComputationAfterValidation = useCallback(
|
|
280
319
|
async (fieldName: string) => {
|
|
281
|
-
if (!schemaConfig
|
|
320
|
+
if (!schemaConfig) {
|
|
282
321
|
return;
|
|
283
322
|
}
|
|
284
323
|
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
const shouldTrigger =
|
|
289
|
-
|
|
290
|
-
computedFieldDependencies.
|
|
324
|
+
// Determine if draft should be triggered based on interaction mode
|
|
325
|
+
// Interactive mode: Always trigger draft API on blur
|
|
326
|
+
// Non-interactive mode: Only trigger for computed field dependencies (legacy behavior)
|
|
327
|
+
const shouldTrigger = isInteractiveMode
|
|
328
|
+
? true // Interactive mode: always trigger
|
|
329
|
+
: (computedFieldDependencies.length > 0 &&
|
|
330
|
+
(draftOnEveryChange ||
|
|
331
|
+
computedFieldDependencies.includes(fieldName as Path<T>)));
|
|
291
332
|
|
|
292
333
|
if (!shouldTrigger) {
|
|
293
334
|
return;
|
|
294
335
|
}
|
|
295
336
|
|
|
337
|
+
// For interactive create, check that we have a draftId (except for initial draft creation)
|
|
338
|
+
if (isInteractiveMode && operation === "create" && !draftId) {
|
|
339
|
+
console.warn("Interactive create mode: waiting for draft ID");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
296
343
|
// Prevent concurrent API calls
|
|
297
344
|
if (isComputingRef.current) {
|
|
298
345
|
return;
|
|
@@ -330,6 +377,11 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
330
377
|
(changedFields as any)._id = (currentValues as any)._id;
|
|
331
378
|
}
|
|
332
379
|
|
|
380
|
+
// For interactive create mode, include draft _id
|
|
381
|
+
if (isInteractiveMode && operation === "create" && draftId) {
|
|
382
|
+
(changedFields as any)._id = draftId;
|
|
383
|
+
}
|
|
384
|
+
|
|
333
385
|
// Use lastSyncedValues if available, otherwise use recordData (for update) or empty object (for create)
|
|
334
386
|
const baseline =
|
|
335
387
|
lastSyncedValuesRef.current ??
|
|
@@ -382,10 +434,18 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
382
434
|
|
|
383
435
|
lastSyncedValuesRef.current = baselineBeforeApiCall;
|
|
384
436
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
437
|
+
// Choose API method based on operation and interaction mode
|
|
438
|
+
let computedFieldsResponse;
|
|
439
|
+
if (operation === "update" && recordId) {
|
|
440
|
+
// Update mode: use draftPatch (both interactive and non-interactive)
|
|
441
|
+
computedFieldsResponse = await client.draftPatch(recordId, payload);
|
|
442
|
+
} else if (isInteractiveMode && draftId) {
|
|
443
|
+
// Interactive create: use draftInteraction with _id
|
|
444
|
+
computedFieldsResponse = await client.draftInteraction(payload);
|
|
445
|
+
} else {
|
|
446
|
+
// Non-interactive create: use draft (POST)
|
|
447
|
+
computedFieldsResponse = await client.draft(payload);
|
|
448
|
+
}
|
|
389
449
|
|
|
390
450
|
// Apply computed fields returned from API
|
|
391
451
|
if (
|
|
@@ -438,6 +498,8 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
438
498
|
rhfForm,
|
|
439
499
|
computedFieldDependencies,
|
|
440
500
|
draftOnEveryChange,
|
|
501
|
+
isInteractiveMode,
|
|
502
|
+
draftId,
|
|
441
503
|
]
|
|
442
504
|
);
|
|
443
505
|
|
|
@@ -495,76 +557,149 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
495
557
|
]);
|
|
496
558
|
|
|
497
559
|
// ============================================================
|
|
498
|
-
//
|
|
560
|
+
// HANDLE SUBMIT - RHF-style API with internal submission
|
|
499
561
|
// ============================================================
|
|
500
562
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
563
|
+
/**
|
|
564
|
+
* handleSubmit follows React Hook Form's signature pattern:
|
|
565
|
+
*
|
|
566
|
+
* handleSubmit(onSuccess?, onError?) => (e?) => Promise<void>
|
|
567
|
+
*
|
|
568
|
+
* Internal flow:
|
|
569
|
+
* 1. RHF validation + Cross-field validation → FAILS → onError(fieldErrors)
|
|
570
|
+
* 2. Clean data & call API → FAILS → onError(apiError)
|
|
571
|
+
* 3. SUCCESS → onSuccess(responseData)
|
|
572
|
+
*/
|
|
573
|
+
const handleSubmit = useCallback(
|
|
574
|
+
(
|
|
575
|
+
onSuccess?: (data: T, e?: React.BaseSyntheticEvent) => void | Promise<void>,
|
|
576
|
+
onError?: (
|
|
577
|
+
error: import("react-hook-form").FieldErrors<T> | Error,
|
|
578
|
+
e?: React.BaseSyntheticEvent
|
|
579
|
+
) => void | Promise<void>
|
|
580
|
+
) => {
|
|
581
|
+
return rhfForm.handleSubmit(
|
|
582
|
+
// RHF onValid handler - validation passed, now do cross-field + API
|
|
583
|
+
async (data, event) => {
|
|
584
|
+
if (!schemaConfig) {
|
|
585
|
+
const error = new Error("Schema not loaded");
|
|
586
|
+
onError?.(error, event);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
505
589
|
|
|
506
|
-
|
|
507
|
-
setSubmitError(null);
|
|
590
|
+
setIsSubmitting(true);
|
|
508
591
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
592
|
+
try {
|
|
593
|
+
// Cross-field validation
|
|
594
|
+
const transformedRules = schemaConfig.crossFieldValidation.map(
|
|
595
|
+
(rule) => ({
|
|
596
|
+
Id: rule.Id,
|
|
597
|
+
Condition: { ExpressionTree: rule.ExpressionTree },
|
|
598
|
+
Message: rule.Message || `Validation failed for ${rule.Name}`,
|
|
599
|
+
})
|
|
600
|
+
);
|
|
515
601
|
|
|
516
|
-
|
|
602
|
+
const crossFieldErrors = validateCrossField(
|
|
603
|
+
transformedRules,
|
|
604
|
+
data as any,
|
|
605
|
+
referenceData
|
|
606
|
+
);
|
|
517
607
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
608
|
+
if (crossFieldErrors.length > 0) {
|
|
609
|
+
// Set cross-field errors in form state
|
|
610
|
+
crossFieldErrors.forEach((error, index) => {
|
|
611
|
+
rhfForm.setError(`root.crossField${index}` as any, {
|
|
612
|
+
type: "validate",
|
|
613
|
+
message: error.message,
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
// Call onError with current form errors
|
|
617
|
+
onError?.(rhfForm.formState.errors, event);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
527
620
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
621
|
+
// Clean data for submission
|
|
622
|
+
const cleanedData = cleanFormData(
|
|
623
|
+
data as any,
|
|
624
|
+
schemaConfig.computedFields,
|
|
625
|
+
operation,
|
|
626
|
+
recordData as Partial<T> | undefined
|
|
627
|
+
);
|
|
535
628
|
|
|
536
|
-
|
|
537
|
-
throw result.error || new Error("Submission failed");
|
|
538
|
-
}
|
|
629
|
+
let result;
|
|
539
630
|
|
|
540
|
-
|
|
541
|
-
|
|
631
|
+
if (isInteractiveMode) {
|
|
632
|
+
// Interactive mode submission
|
|
633
|
+
const client = api<T>(source);
|
|
542
634
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
635
|
+
if (operation === "create") {
|
|
636
|
+
// Interactive create: must have draftId
|
|
637
|
+
if (!draftId) {
|
|
638
|
+
throw new Error(
|
|
639
|
+
"Interactive create mode requires a draft ID. Draft creation may have failed."
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
// POST /{bdo_id}/draft with _id in payload
|
|
643
|
+
const response = await client.draft({
|
|
644
|
+
...cleanedData,
|
|
645
|
+
_id: draftId,
|
|
646
|
+
} as any);
|
|
647
|
+
result = { success: true, data: response };
|
|
648
|
+
} else {
|
|
649
|
+
// Interactive update: POST /{bdo_id}/{id}/draft
|
|
650
|
+
const response = await client.draftUpdate(recordId!, cleanedData);
|
|
651
|
+
result = { success: true, data: response };
|
|
652
|
+
}
|
|
653
|
+
} else {
|
|
654
|
+
// Non-interactive mode: use existing submitFormData
|
|
655
|
+
result = await submitFormData<T>(
|
|
656
|
+
source,
|
|
657
|
+
operation,
|
|
658
|
+
cleanedData,
|
|
659
|
+
recordId
|
|
660
|
+
);
|
|
557
661
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
662
|
+
if (!result.success) {
|
|
663
|
+
throw result.error || new Error("Submission failed");
|
|
664
|
+
}
|
|
665
|
+
}
|
|
561
666
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
667
|
+
// Reset form for create operations
|
|
668
|
+
if (operation === "create") {
|
|
669
|
+
rhfForm.reset();
|
|
670
|
+
// Clear draft state for interactive mode
|
|
671
|
+
if (isInteractiveMode) {
|
|
672
|
+
setDraftId(null);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Success callback with response data
|
|
677
|
+
await onSuccess?.(result.data || data, event);
|
|
678
|
+
} catch (error) {
|
|
679
|
+
// API error - call onError with Error object
|
|
680
|
+
onError?.(error as Error, event);
|
|
681
|
+
} finally {
|
|
682
|
+
setIsSubmitting(false);
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
// RHF onInvalid handler - validation failed
|
|
686
|
+
(errors, event) => {
|
|
687
|
+
onError?.(errors, event);
|
|
688
|
+
}
|
|
689
|
+
);
|
|
690
|
+
},
|
|
691
|
+
[
|
|
692
|
+
rhfForm,
|
|
693
|
+
schemaConfig,
|
|
694
|
+
referenceData,
|
|
695
|
+
source,
|
|
696
|
+
operation,
|
|
697
|
+
recordId,
|
|
698
|
+
recordData,
|
|
699
|
+
isInteractiveMode,
|
|
700
|
+
draftId,
|
|
701
|
+
]
|
|
702
|
+
);
|
|
568
703
|
|
|
569
704
|
// ============================================================
|
|
570
705
|
// FIELD HELPERS
|
|
@@ -623,18 +758,20 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
623
758
|
|
|
624
759
|
const clearErrors = useCallback((): void => {
|
|
625
760
|
rhfForm.clearErrors();
|
|
626
|
-
setSubmitError(null);
|
|
627
761
|
}, [rhfForm]);
|
|
628
762
|
|
|
629
763
|
// ============================================================
|
|
630
764
|
// COMPUTED PROPERTIES
|
|
631
765
|
// ============================================================
|
|
632
766
|
|
|
767
|
+
// Loading state includes interactive mode draft creation
|
|
633
768
|
const isLoadingInitialData =
|
|
634
|
-
isLoadingSchema ||
|
|
769
|
+
isLoadingSchema ||
|
|
770
|
+
(operation === "update" && isLoadingRecord) ||
|
|
771
|
+
(isInteractiveMode && operation === "create" && isCreatingDraft);
|
|
635
772
|
const isLoading = isLoadingInitialData || isSubmitting;
|
|
636
|
-
const loadError = schemaError || recordError;
|
|
637
|
-
const hasError = !!
|
|
773
|
+
const loadError = schemaError || recordError || draftError;
|
|
774
|
+
const hasError = !!loadError;
|
|
638
775
|
|
|
639
776
|
const computedFields = useMemo<Array<keyof T>>(
|
|
640
777
|
() => (schemaConfig?.computedFields as Array<keyof T>) || [],
|
|
@@ -794,9 +931,12 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
794
931
|
isLoadingRecord,
|
|
795
932
|
isLoading,
|
|
796
933
|
|
|
934
|
+
// Interactive mode state
|
|
935
|
+
draftId,
|
|
936
|
+
isCreatingDraft,
|
|
937
|
+
|
|
797
938
|
// Error handling
|
|
798
939
|
loadError: loadError as Error | null,
|
|
799
|
-
submitError,
|
|
800
940
|
hasError,
|
|
801
941
|
|
|
802
942
|
// Schema information
|
|
@@ -813,7 +953,6 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
813
953
|
isFieldComputed,
|
|
814
954
|
|
|
815
955
|
// Operations
|
|
816
|
-
submit,
|
|
817
956
|
refreshSchema,
|
|
818
957
|
validateForm,
|
|
819
958
|
clearErrors,
|