@ram_28/kf-ai-sdk 1.0.0

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.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +840 -0
  3. package/dist/api/client.d.ts +78 -0
  4. package/dist/api/client.d.ts.map +1 -0
  5. package/dist/api/datetime.d.ts +21 -0
  6. package/dist/api/datetime.d.ts.map +1 -0
  7. package/dist/api/index.d.ts +7 -0
  8. package/dist/api/index.d.ts.map +1 -0
  9. package/dist/api/metadata.d.ts +75 -0
  10. package/dist/api/metadata.d.ts.map +1 -0
  11. package/dist/components/hooks/index.d.ts +8 -0
  12. package/dist/components/hooks/index.d.ts.map +1 -0
  13. package/dist/components/hooks/useFilter/index.d.ts +5 -0
  14. package/dist/components/hooks/useFilter/index.d.ts.map +1 -0
  15. package/dist/components/hooks/useFilter/payloadBuilder.utils.d.ts +33 -0
  16. package/dist/components/hooks/useFilter/payloadBuilder.utils.d.ts.map +1 -0
  17. package/dist/components/hooks/useFilter/types.d.ts +137 -0
  18. package/dist/components/hooks/useFilter/types.d.ts.map +1 -0
  19. package/dist/components/hooks/useFilter/useFilter.d.ts +3 -0
  20. package/dist/components/hooks/useFilter/useFilter.d.ts.map +1 -0
  21. package/dist/components/hooks/useFilter/validation.utils.d.ts +38 -0
  22. package/dist/components/hooks/useFilter/validation.utils.d.ts.map +1 -0
  23. package/dist/components/hooks/useForm/apiClient.d.ts +71 -0
  24. package/dist/components/hooks/useForm/apiClient.d.ts.map +1 -0
  25. package/dist/components/hooks/useForm/expressionValidator.utils.d.ts +28 -0
  26. package/dist/components/hooks/useForm/expressionValidator.utils.d.ts.map +1 -0
  27. package/dist/components/hooks/useForm/index.d.ts +6 -0
  28. package/dist/components/hooks/useForm/index.d.ts.map +1 -0
  29. package/dist/components/hooks/useForm/optimizedExpressionValidator.utils.d.ts +88 -0
  30. package/dist/components/hooks/useForm/optimizedExpressionValidator.utils.d.ts.map +1 -0
  31. package/dist/components/hooks/useForm/ruleClassifier.utils.d.ts +28 -0
  32. package/dist/components/hooks/useForm/ruleClassifier.utils.d.ts.map +1 -0
  33. package/dist/components/hooks/useForm/schemaParser.utils.d.ts +29 -0
  34. package/dist/components/hooks/useForm/schemaParser.utils.d.ts.map +1 -0
  35. package/dist/components/hooks/useForm/types.d.ts +412 -0
  36. package/dist/components/hooks/useForm/types.d.ts.map +1 -0
  37. package/dist/components/hooks/useForm/useForm.d.ts +3 -0
  38. package/dist/components/hooks/useForm/useForm.d.ts.map +1 -0
  39. package/dist/components/hooks/useKanban/apiClient.d.ts +99 -0
  40. package/dist/components/hooks/useKanban/apiClient.d.ts.map +1 -0
  41. package/dist/components/hooks/useKanban/context.d.ts +4 -0
  42. package/dist/components/hooks/useKanban/context.d.ts.map +1 -0
  43. package/dist/components/hooks/useKanban/dragDropManager.d.ts +27 -0
  44. package/dist/components/hooks/useKanban/dragDropManager.d.ts.map +1 -0
  45. package/dist/components/hooks/useKanban/index.d.ts +6 -0
  46. package/dist/components/hooks/useKanban/index.d.ts.map +1 -0
  47. package/dist/components/hooks/useKanban/types.d.ts +438 -0
  48. package/dist/components/hooks/useKanban/types.d.ts.map +1 -0
  49. package/dist/components/hooks/useKanban/useKanban.d.ts +3 -0
  50. package/dist/components/hooks/useKanban/useKanban.d.ts.map +1 -0
  51. package/dist/components/hooks/useKanban/useKanbanSimple.d.ts +62 -0
  52. package/dist/components/hooks/useKanban/useKanbanSimple.d.ts.map +1 -0
  53. package/dist/components/hooks/useTable/index.d.ts +3 -0
  54. package/dist/components/hooks/useTable/index.d.ts.map +1 -0
  55. package/dist/components/hooks/useTable/types.d.ts +107 -0
  56. package/dist/components/hooks/useTable/types.d.ts.map +1 -0
  57. package/dist/components/hooks/useTable/useTable.d.ts +8 -0
  58. package/dist/components/hooks/useTable/useTable.d.ts.map +1 -0
  59. package/dist/components/index.d.ts +3 -0
  60. package/dist/components/index.d.ts.map +1 -0
  61. package/dist/components/ui/index.d.ts +2 -0
  62. package/dist/components/ui/index.d.ts.map +1 -0
  63. package/dist/components/ui/kanban/Kanban.d.ts +12 -0
  64. package/dist/components/ui/kanban/Kanban.d.ts.map +1 -0
  65. package/dist/components/ui/kanban/index.d.ts +2 -0
  66. package/dist/components/ui/kanban/index.d.ts.map +1 -0
  67. package/dist/index.cjs +45 -0
  68. package/dist/index.d.ts +5 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.mjs +6522 -0
  71. package/dist/types/base-fields.d.ts +182 -0
  72. package/dist/types/base-fields.d.ts.map +1 -0
  73. package/dist/types/common.d.ts +238 -0
  74. package/dist/types/common.d.ts.map +1 -0
  75. package/dist/types/index.d.ts +3 -0
  76. package/dist/types/index.d.ts.map +1 -0
  77. package/dist/utils/cn.d.ts +7 -0
  78. package/dist/utils/cn.d.ts.map +1 -0
  79. package/dist/utils/formatting.d.ts +52 -0
  80. package/dist/utils/formatting.d.ts.map +1 -0
  81. package/dist/utils/index.d.ts +3 -0
  82. package/dist/utils/index.d.ts.map +1 -0
  83. package/package.json +98 -0
  84. package/sdk/api/client.ts +447 -0
  85. package/sdk/api/datetime.ts +33 -0
  86. package/sdk/api/index.ts +61 -0
  87. package/sdk/api/metadata.ts +148 -0
  88. package/sdk/components/hooks/index.ts +34 -0
  89. package/sdk/components/hooks/useFilter/index.ts +37 -0
  90. package/sdk/components/hooks/useFilter/payloadBuilder.utils.ts +298 -0
  91. package/sdk/components/hooks/useFilter/types.ts +158 -0
  92. package/sdk/components/hooks/useFilter/useFilter.llm.txt +497 -0
  93. package/sdk/components/hooks/useFilter/useFilter.ts +494 -0
  94. package/sdk/components/hooks/useFilter/validation.utils.ts +401 -0
  95. package/sdk/components/hooks/useForm/apiClient.ts +441 -0
  96. package/sdk/components/hooks/useForm/expressionValidator.utils.ts +444 -0
  97. package/sdk/components/hooks/useForm/index.ts +64 -0
  98. package/sdk/components/hooks/useForm/optimizedExpressionValidator.utils.ts +482 -0
  99. package/sdk/components/hooks/useForm/ruleClassifier.utils.ts +424 -0
  100. package/sdk/components/hooks/useForm/schemaParser.utils.ts +519 -0
  101. package/sdk/components/hooks/useForm/types.ts +630 -0
  102. package/sdk/components/hooks/useForm/useForm.llm.txt +340 -0
  103. package/sdk/components/hooks/useForm/useForm.ts +821 -0
  104. package/sdk/components/hooks/useKanban/apiClient.ts +494 -0
  105. package/sdk/components/hooks/useKanban/context.ts +14 -0
  106. package/sdk/components/hooks/useKanban/dragDropManager.ts +529 -0
  107. package/sdk/components/hooks/useKanban/index.ts +63 -0
  108. package/sdk/components/hooks/useKanban/types.ts +606 -0
  109. package/sdk/components/hooks/useKanban/useKanban.llm.txt +482 -0
  110. package/sdk/components/hooks/useKanban/useKanban.ts +725 -0
  111. package/sdk/components/hooks/useKanban/useKanbanSimple.ts +389 -0
  112. package/sdk/components/hooks/useTable/index.ts +5 -0
  113. package/sdk/components/hooks/useTable/types.ts +154 -0
  114. package/sdk/components/hooks/useTable/useTable.llm.txt +344 -0
  115. package/sdk/components/hooks/useTable/useTable.ts +413 -0
  116. package/sdk/components/index.ts +15 -0
  117. package/sdk/components/ui/index.ts +2 -0
  118. package/sdk/components/ui/kanban/Kanban.tsx +134 -0
  119. package/sdk/components/ui/kanban/index.ts +11 -0
  120. package/sdk/index.ts +13 -0
  121. package/sdk/types/base-fields.ts +221 -0
  122. package/sdk/types/common.ts +306 -0
  123. package/sdk/types/index.ts +5 -0
  124. package/sdk/utils/cn.ts +10 -0
  125. package/sdk/utils/formatting.ts +212 -0
  126. package/sdk/utils/index.ts +5 -0
@@ -0,0 +1,821 @@
1
+ // ============================================================
2
+ // USE FORM HOOK
3
+ // ============================================================
4
+ // Main hook that integrates react-hook-form with backend schemas
5
+
6
+ import { useState, useEffect, useCallback, useMemo, useRef } from "react";
7
+ import { useForm as useReactHookForm } from "react-hook-form";
8
+ import { useQuery } from "@tanstack/react-query";
9
+ import type { Path } from "react-hook-form";
10
+
11
+ import type {
12
+ UseFormOptions,
13
+ UseFormReturn,
14
+ BackendSchema,
15
+ ProcessedSchema,
16
+ ProcessedField,
17
+ } from "./types";
18
+
19
+ import { processSchema, extractReferenceFields } from "./schemaParser.utils";
20
+
21
+ import {
22
+ fetchFormSchemaWithCache,
23
+ fetchRecord,
24
+ submitFormData,
25
+ fetchAllReferenceData,
26
+ cleanFormData,
27
+ } from "./apiClient";
28
+
29
+ import { api } from "../../../api";
30
+
31
+ import { validateCrossField } from "./expressionValidator.utils";
32
+ import {
33
+ validateFieldOptimized,
34
+ getFieldDependencies,
35
+ } from "./optimizedExpressionValidator.utils";
36
+
37
+ // ============================================================
38
+ // MAIN HOOK IMPLEMENTATION
39
+ // ============================================================
40
+
41
+ export function useForm<T extends Record<string, any> = Record<string, any>>(
42
+ options: UseFormOptions<T>
43
+ ): UseFormReturn<T> {
44
+ const {
45
+ source,
46
+ operation,
47
+ recordId,
48
+ defaultValues = {},
49
+ mode = "onBlur", // Validation mode - controls when errors are shown (see types.ts for details)
50
+ enabled = true,
51
+ userRole,
52
+ onSuccess,
53
+ onError,
54
+ onSchemaError,
55
+ onSubmitError,
56
+ skipSchemaFetch = false,
57
+ schema: manualSchema,
58
+ draftOnEveryChange = false,
59
+ } = options;
60
+
61
+ // ============================================================
62
+ // STATE MANAGEMENT
63
+ // ============================================================
64
+
65
+ const [processedSchema, setProcessedSchema] =
66
+ useState<ProcessedSchema | null>(null);
67
+ const [referenceData, setReferenceData] = useState<Record<string, any[]>>({});
68
+ const [submitError, setSubmitError] = useState<Error | null>(null);
69
+ const [isSubmitting, setIsSubmitting] = useState(false);
70
+ const [lastFormValues] = useState<Partial<T>>({});
71
+
72
+ // Prevent infinite loop in API calls
73
+ const isComputingRef = useRef(false);
74
+ const computeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
75
+
76
+ // Track values that have been synced with server (sent in draft calls)
77
+ // This allows us to detect changes since the last draft, not since form init
78
+ const lastSyncedValuesRef = useRef<Partial<T> | null>(null);
79
+
80
+ // Stable callback refs to prevent dependency loops
81
+ const onSuccessRef = useRef(onSuccess);
82
+ const onErrorRef = useRef(onError);
83
+ const onSubmitErrorRef = useRef(onSubmitError);
84
+ const onSchemaErrorRef = useRef(onSchemaError);
85
+
86
+ // Update refs when callbacks change
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
+
99
+ useEffect(() => {
100
+ onSchemaErrorRef.current = onSchemaError;
101
+ }, [onSchemaError]);
102
+
103
+ // ============================================================
104
+ // SCHEMA FETCHING
105
+ // ============================================================
106
+
107
+ const {
108
+ data: schema,
109
+ isLoading: isLoadingSchema,
110
+ error: schemaError,
111
+ refetch: refetchSchema,
112
+ } = useQuery({
113
+ queryKey: ["form-schema", source],
114
+ queryFn: () =>
115
+ skipSchemaFetch
116
+ ? Promise.resolve(manualSchema || {})
117
+ : fetchFormSchemaWithCache(source),
118
+ enabled: enabled && (!skipSchemaFetch || !!manualSchema),
119
+ retry: 3,
120
+ staleTime: 30 * 60 * 1000, // 30 minutes - schemas don't change frequently
121
+ gcTime: 60 * 60 * 1000, // 1 hour - keep schemas in cache longer
122
+ throwOnError: false,
123
+ });
124
+
125
+ // ============================================================
126
+ // RECORD FETCHING (for update operations)
127
+ // ============================================================
128
+
129
+ const {
130
+ data: recordData,
131
+ isLoading: isLoadingRecord,
132
+ error: recordError,
133
+ } = useQuery({
134
+ queryKey: ["form-record", source, recordId],
135
+ queryFn: () => fetchRecord<T>(source, recordId!),
136
+ enabled: enabled && operation === "update" && !!recordId,
137
+ retry: 3,
138
+ staleTime: 5 * 60 * 1000, // 5 minutes - records can change more frequently
139
+ gcTime: 15 * 60 * 1000, // 15 minutes - keep records for a reasonable time
140
+ throwOnError: false,
141
+ });
142
+
143
+ // ============================================================
144
+ // REACT HOOK FORM SETUP
145
+ // ============================================================
146
+
147
+ const defaultFormValues = useMemo(() => {
148
+ const values = { ...defaultValues };
149
+
150
+ // Merge record data for update operations
151
+ if (operation === "update" && recordData) {
152
+ Object.assign(values, recordData);
153
+ }
154
+
155
+ // Apply default values from schema
156
+ if (processedSchema) {
157
+ for (const [fieldName, field] of Object.entries(processedSchema.fields)) {
158
+ if (field.defaultValue !== undefined && !(fieldName in values)) {
159
+ (values as any)[fieldName] = field.defaultValue;
160
+ }
161
+ }
162
+ }
163
+
164
+ return values;
165
+ }, [defaultValues, recordData, operation, processedSchema]);
166
+
167
+ const rhfForm = useReactHookForm<T>({
168
+ mode,
169
+ defaultValues: defaultValues as any,
170
+ values:
171
+ operation === "update" && recordData
172
+ ? (defaultFormValues as any)
173
+ : undefined,
174
+ });
175
+
176
+ // ============================================================
177
+ // SCHEMA PROCESSING
178
+ // ============================================================
179
+
180
+ useEffect(() => {
181
+ if (schema) {
182
+ try {
183
+ const processed = processSchema(
184
+ schema as any,
185
+ {}, // Pass empty object - validation functions get live values from react-hook-form
186
+ userRole
187
+ );
188
+ setProcessedSchema(processed);
189
+
190
+ // Fetch reference data for reference fields
191
+ const refFields = extractReferenceFields(processed);
192
+ if (Object.keys(refFields).length > 0) {
193
+ fetchAllReferenceData(refFields)
194
+ .then(setReferenceData)
195
+ .catch(console.warn);
196
+ }
197
+ } catch (error) {
198
+ console.error("Schema processing failed:", error);
199
+ onSchemaErrorRef.current?.(error as Error);
200
+ }
201
+ }
202
+ }, [schema, userRole]); // Removed onSchemaError - using ref instead
203
+
204
+ // Handle schema and record errors
205
+ useEffect(() => {
206
+ if (schemaError) {
207
+ onSchemaErrorRef.current?.(schemaError);
208
+ }
209
+ }, [schemaError]);
210
+
211
+ useEffect(() => {
212
+ if (recordError) {
213
+ onErrorRef.current?.(recordError);
214
+ }
215
+ }, [recordError]);
216
+
217
+ // ============================================================
218
+ // COMPUTED FIELD DEPENDENCY TRACKING AND OPTIMIZATION
219
+ // ============================================================
220
+
221
+ // Extract computed field dependencies using optimized analyzer
222
+ const computedFieldDependencies = useMemo(() => {
223
+ if (!processedSchema) return [];
224
+
225
+ const dependencies = new Set<string>();
226
+ const computedFieldNames = new Set(processedSchema.computedFields);
227
+
228
+ // Analyze dependencies from computation rules
229
+ Object.entries(processedSchema.fieldRules).forEach(([fieldName, rules]) => {
230
+ rules.computation.forEach((ruleId) => {
231
+ const rule = processedSchema.rules.computation[ruleId];
232
+ if (rule?.ExpressionTree) {
233
+ const ruleDeps = getFieldDependencies(rule.ExpressionTree);
234
+ ruleDeps.forEach((dep) => {
235
+ // Only add non-computed fields as dependencies
236
+ if (
237
+ processedSchema.fields[dep] &&
238
+ dep !== fieldName &&
239
+ !computedFieldNames.has(dep)
240
+ ) {
241
+ dependencies.add(dep);
242
+ }
243
+ });
244
+ }
245
+ });
246
+ });
247
+
248
+ // Also check formulas (legacy support)
249
+ processedSchema.computedFields.forEach((fieldName: string) => {
250
+ const field = processedSchema.fields[fieldName];
251
+ if (field.backendField.Formula) {
252
+ const fieldDeps = getFieldDependencies(
253
+ field.backendField.Formula.ExpressionTree
254
+ );
255
+ fieldDeps.forEach((dep) => {
256
+ // Only add non-computed fields as dependencies
257
+ if (
258
+ processedSchema.fields[dep] &&
259
+ dep !== fieldName &&
260
+ !computedFieldNames.has(dep)
261
+ ) {
262
+ dependencies.add(dep);
263
+ }
264
+ });
265
+ }
266
+ });
267
+
268
+ return Array.from(dependencies) as Array<Path<T>>;
269
+ }, [processedSchema]);
270
+
271
+ // Watch dependencies are tracked but not used for automatic computation
272
+ // Computation is triggered manually on blur after validation passes
273
+
274
+ // ============================================================
275
+ // COMPUTATION RULE HANDLING
276
+ // ============================================================
277
+
278
+ // Manual computation trigger - called on blur after validation passes
279
+ const triggerComputationAfterValidation = useCallback(
280
+ async (fieldName: string) => {
281
+ if (!processedSchema || computedFieldDependencies.length === 0) {
282
+ return;
283
+ }
284
+
285
+ // Check if this field is a dependency for any computed fields
286
+ // If draftOnEveryChange is true, trigger for all fields
287
+ // If false (default), only trigger for computed field dependencies
288
+ const shouldTrigger =
289
+ draftOnEveryChange ||
290
+ computedFieldDependencies.includes(fieldName as Path<T>);
291
+
292
+ if (!shouldTrigger) {
293
+ return;
294
+ }
295
+
296
+ // Prevent concurrent API calls
297
+ if (isComputingRef.current) {
298
+ return;
299
+ }
300
+
301
+ // Debounce API calls
302
+ if (computeTimeoutRef.current) {
303
+ clearTimeout(computeTimeoutRef.current);
304
+ }
305
+
306
+ computeTimeoutRef.current = setTimeout(() => {
307
+ // Additional safety check
308
+ if (!processedSchema) return;
309
+
310
+ // Prevent concurrent API calls
311
+ if (isComputingRef.current) {
312
+ return;
313
+ }
314
+
315
+ const currentValues = rhfForm.getValues();
316
+
317
+ // Call draft API to compute fields on server
318
+ const computeFieldsViaAPI = async () => {
319
+ isComputingRef.current = true;
320
+
321
+ try {
322
+ // Use API client draft methods
323
+ const client = api<T>(source);
324
+
325
+ // Build payload with only fields that changed since last sync
326
+ const changedFields: Partial<T> = {};
327
+
328
+ // For update mode, always include _id
329
+ if (operation === "update" && recordId && "_id" in currentValues) {
330
+ (changedFields as any)._id = (currentValues as any)._id;
331
+ }
332
+
333
+ // Use lastSyncedValues if available, otherwise use recordData (for update) or empty object (for create)
334
+ const baseline =
335
+ lastSyncedValuesRef.current ??
336
+ (operation === "update" ? recordData : null) ??
337
+ {};
338
+
339
+ // Get computed field names to exclude from payload
340
+ const computedFieldNames = new Set(
341
+ processedSchema.computedFields || []
342
+ );
343
+
344
+ // Find fields that changed from baseline (excluding computed fields)
345
+ Object.keys(currentValues).forEach((key) => {
346
+ // Skip _id and computed fields
347
+ if (key === "_id" || computedFieldNames.has(key)) return;
348
+
349
+ const currentValue = (currentValues as any)[key];
350
+ const baselineValue = (baseline as any)[key];
351
+
352
+ // Include if value has changed (using JSON.stringify for deep comparison)
353
+ // For create mode with no baseline, only include non-empty values
354
+ const hasChanged =
355
+ JSON.stringify(currentValue) !== JSON.stringify(baselineValue);
356
+ const isNonEmpty =
357
+ currentValue !== "" &&
358
+ currentValue !== null &&
359
+ currentValue !== undefined;
360
+
361
+ if (hasChanged && isNonEmpty) {
362
+ (changedFields as any)[key] = currentValue;
363
+ }
364
+ });
365
+
366
+ const payload = changedFields;
367
+
368
+ // Update lastSyncedValuesRef BEFORE API call with what we're about to send
369
+ // This ensures that even if the API fails, the next draft only sends NEW changes
370
+ const baselineBeforeApiCall = {
371
+ ...lastSyncedValuesRef.current,
372
+ } as Partial<T>;
373
+
374
+ // Update baseline with non-computed fields from current form state
375
+ Object.keys(currentValues).forEach((key) => {
376
+ if (!computedFieldNames.has(key)) {
377
+ (baselineBeforeApiCall as any)[key] = (currentValues as any)[
378
+ key
379
+ ];
380
+ }
381
+ });
382
+
383
+ lastSyncedValuesRef.current = baselineBeforeApiCall;
384
+
385
+ const computedFieldsResponse =
386
+ operation === "update" && recordId
387
+ ? await client.draftPatch(recordId, payload)
388
+ : await client.draft(payload);
389
+
390
+ // Apply computed fields returned from API
391
+ if (
392
+ computedFieldsResponse &&
393
+ typeof computedFieldsResponse === "object"
394
+ ) {
395
+ Object.entries(computedFieldsResponse).forEach(
396
+ ([fieldName, value]) => {
397
+ const currentValue = currentValues[fieldName as keyof T];
398
+ if (currentValue !== value) {
399
+ rhfForm.setValue(fieldName as Path<T>, value as any, {
400
+ shouldDirty: false,
401
+ shouldValidate: false,
402
+ });
403
+ }
404
+ }
405
+ );
406
+
407
+ // Update baseline with computed fields from successful API response
408
+ Object.entries(computedFieldsResponse).forEach(
409
+ ([fieldName, value]) => {
410
+ if (computedFieldNames.has(fieldName)) {
411
+ (lastSyncedValuesRef.current as any)[fieldName] = value;
412
+ }
413
+ }
414
+ );
415
+ }
416
+ } catch (error) {
417
+ console.warn("Failed to compute fields via API:", error);
418
+ // Note: lastSyncedValuesRef was already updated before the API call
419
+ // This is correct - we want to track what we ATTEMPTED to send,
420
+ // so the next draft only includes NEW changes, not failed changes again
421
+ // Client-side formula computation fallback has been removed
422
+ // Formula fields remain as-is when API fails
423
+ } finally {
424
+ isComputingRef.current = false;
425
+ }
426
+ };
427
+
428
+ // Call API for computation (no client-side fallback)
429
+ computeFieldsViaAPI();
430
+ }, 300); // 300ms debounce
431
+ },
432
+ [
433
+ processedSchema,
434
+ operation,
435
+ recordId,
436
+ recordData,
437
+ source,
438
+ rhfForm,
439
+ computedFieldDependencies,
440
+ draftOnEveryChange,
441
+ ]
442
+ );
443
+
444
+ // ============================================================
445
+ // VALIDATION
446
+ // ============================================================
447
+
448
+ const validateForm = useCallback(async (): Promise<boolean> => {
449
+ if (!processedSchema) {
450
+ return false;
451
+ }
452
+
453
+ const values = rhfForm.getValues();
454
+
455
+ // Basic form validation
456
+ const isValid = await rhfForm.trigger();
457
+ if (!isValid) {
458
+ return false;
459
+ }
460
+
461
+ // Cross-field validation
462
+ // Transform ValidationRule[] to the format expected by validateCrossField
463
+ const transformedRules = processedSchema.crossFieldValidation.map(
464
+ (rule) => ({
465
+ Id: rule.Id,
466
+ Condition: { ExpressionTree: rule.ExpressionTree },
467
+ Message: rule.Message || `Validation failed for ${rule.Name}`,
468
+ })
469
+ );
470
+
471
+ const crossFieldErrors = validateCrossField(
472
+ transformedRules,
473
+ values as any,
474
+ referenceData
475
+ );
476
+
477
+ if (crossFieldErrors.length > 0) {
478
+ // Set cross-field errors
479
+ crossFieldErrors.forEach((error, index) => {
480
+ rhfForm.setError(`root.crossField${index}` as any, {
481
+ type: "validate",
482
+ message: error.message,
483
+ });
484
+ });
485
+ return false;
486
+ }
487
+
488
+ return true;
489
+ }, [
490
+ processedSchema,
491
+ rhfForm.getValues,
492
+ rhfForm.trigger,
493
+ rhfForm.setError,
494
+ referenceData,
495
+ ]);
496
+
497
+ // ============================================================
498
+ // FORM SUBMISSION
499
+ // ============================================================
500
+
501
+ const submit = useCallback(async (): Promise<void> => {
502
+ if (!processedSchema) {
503
+ throw new Error("Schema not loaded");
504
+ }
505
+
506
+ setIsSubmitting(true);
507
+ setSubmitError(null);
508
+
509
+ try {
510
+ // Validate form including cross-field validation
511
+ const isValid = await validateForm();
512
+ if (!isValid) {
513
+ throw new Error("Form validation failed");
514
+ }
515
+
516
+ const formValues = rhfForm.getValues();
517
+
518
+ // Clean data for submission
519
+ // - For create: includes all non-computed fields
520
+ // - For update: includes only fields that changed from recordData
521
+ const cleanedData = cleanFormData(
522
+ formValues as any,
523
+ processedSchema.computedFields,
524
+ operation,
525
+ recordData as Partial<T> | undefined
526
+ );
527
+
528
+ // Submit data
529
+ const result = await submitFormData<T>(
530
+ source,
531
+ operation,
532
+ cleanedData,
533
+ recordId
534
+ );
535
+
536
+ if (!result.success) {
537
+ throw result.error || new Error("Submission failed");
538
+ }
539
+
540
+ // Success callback
541
+ onSuccessRef.current?.(result.data || formValues);
542
+
543
+ // Reset form for create operations
544
+ if (operation === "create") {
545
+ rhfForm.reset();
546
+ }
547
+ } catch (error) {
548
+ const submitError = error as Error;
549
+ setSubmitError(submitError);
550
+ onSubmitErrorRef.current?.(submitError);
551
+ onErrorRef.current?.(submitError);
552
+ throw error;
553
+ } finally {
554
+ setIsSubmitting(false);
555
+ }
556
+ }, [processedSchema, validateForm, rhfForm, source, operation, recordId, recordData]);
557
+
558
+ // ============================================================
559
+ // HANDLE SUBMIT - Simplified API
560
+ // ============================================================
561
+
562
+ // Simplified handleSubmit that always uses SDK's submit function
563
+ const handleSubmit = useCallback(() => {
564
+ return rhfForm.handleSubmit(async () => {
565
+ await submit();
566
+ });
567
+ }, [rhfForm, submit]);
568
+
569
+ // ============================================================
570
+ // FIELD HELPERS
571
+ // ============================================================
572
+
573
+ const getField = useCallback(
574
+ <K extends keyof T>(fieldName: K): ProcessedField | null => {
575
+ return processedSchema?.fields[fieldName as string] || null;
576
+ },
577
+ [processedSchema]
578
+ );
579
+
580
+ const getFields = useCallback((): Record<keyof T, ProcessedField> => {
581
+ if (!processedSchema) return {} as Record<keyof T, ProcessedField>;
582
+
583
+ const typedFields: Record<keyof T, ProcessedField> = {} as any;
584
+ Object.entries(processedSchema.fields).forEach(([key, field]) => {
585
+ (typedFields as any)[key] = field;
586
+ });
587
+
588
+ return typedFields;
589
+ }, [processedSchema]);
590
+
591
+ const hasField = useCallback(
592
+ <K extends keyof T>(fieldName: K): boolean => {
593
+ return !!processedSchema?.fields[fieldName as string];
594
+ },
595
+ [processedSchema]
596
+ );
597
+
598
+ const isFieldRequired = useCallback(
599
+ <K extends keyof T>(fieldName: K): boolean => {
600
+ return (
601
+ processedSchema?.requiredFields.includes(fieldName as string) || false
602
+ );
603
+ },
604
+ [processedSchema]
605
+ );
606
+
607
+ const isFieldComputed = useCallback(
608
+ <K extends keyof T>(fieldName: K): boolean => {
609
+ return (
610
+ processedSchema?.computedFields.includes(fieldName as string) || false
611
+ );
612
+ },
613
+ [processedSchema]
614
+ );
615
+
616
+ // ============================================================
617
+ // OTHER OPERATIONS
618
+ // ============================================================
619
+
620
+ const refreshSchema = useCallback(async (): Promise<void> => {
621
+ await refetchSchema();
622
+ }, [refetchSchema]);
623
+
624
+ const clearErrors = useCallback((): void => {
625
+ rhfForm.clearErrors();
626
+ setSubmitError(null);
627
+ }, [rhfForm]);
628
+
629
+ // ============================================================
630
+ // COMPUTED PROPERTIES
631
+ // ============================================================
632
+
633
+ const isLoadingInitialData =
634
+ isLoadingSchema || (operation === "update" && isLoadingRecord);
635
+ const isLoading = isLoadingInitialData || isSubmitting;
636
+ const loadError = schemaError || recordError;
637
+ const hasError = !!(loadError || submitError);
638
+
639
+ const computedFields = useMemo<Array<keyof T>>(
640
+ () => (processedSchema?.computedFields as Array<keyof T>) || [],
641
+ [processedSchema]
642
+ );
643
+
644
+ const requiredFields = useMemo<Array<keyof T>>(
645
+ () => (processedSchema?.requiredFields as Array<keyof T>) || [],
646
+ [processedSchema]
647
+ );
648
+
649
+ // ============================================================
650
+ // RETURN OBJECT
651
+ // ============================================================
652
+
653
+ // Create validation rules from processed schema (client-side only)
654
+ const validationRules = useMemo(() => {
655
+ if (!processedSchema) return {};
656
+
657
+ const rules: Record<string, any> = {};
658
+
659
+ Object.entries(processedSchema.fields).forEach(([fieldName, field]) => {
660
+ const fieldRules: any = {};
661
+
662
+ // Required validation
663
+ if (field.required) {
664
+ fieldRules.required = `${field.label} is required`;
665
+ }
666
+
667
+ // Permission-based validation (read-only fields)
668
+ if (!field.permission.editable) {
669
+ fieldRules.disabled = true;
670
+ }
671
+
672
+ // Type-specific validation
673
+ switch (field.type) {
674
+ case "number":
675
+ fieldRules.valueAsNumber = true;
676
+ break;
677
+ case "date":
678
+ case "datetime-local":
679
+ fieldRules.valueAsDate = true;
680
+ break;
681
+ }
682
+
683
+ // Client-side validation rules only
684
+ const validationRuleIds = field.rules.validation;
685
+ if (validationRuleIds.length > 0) {
686
+ fieldRules.validate = {
687
+ expressionValidation: (value: any) => {
688
+ const currentValues = rhfForm.getValues();
689
+
690
+ // Execute client-side validation rules with optimization
691
+ for (const ruleId of validationRuleIds) {
692
+ const rule = processedSchema.rules.validation[ruleId];
693
+ if (rule) {
694
+ const result = validateFieldOptimized<T>(
695
+ fieldName,
696
+ value,
697
+ [rule],
698
+ currentValues,
699
+ lastFormValues as T | undefined
700
+ );
701
+ if (!result.isValid) {
702
+ return result.message || rule.Message || "Invalid value";
703
+ }
704
+ }
705
+ }
706
+ return true;
707
+ },
708
+ };
709
+ }
710
+
711
+ rules[fieldName] = fieldRules;
712
+ });
713
+
714
+ return rules;
715
+ }, [processedSchema, rhfForm, referenceData]);
716
+
717
+ /**
718
+ * Enhanced register function that wraps react-hook-form's register
719
+ *
720
+ * Custom onBlur behavior:
721
+ * 1. Respects validation mode - only triggers validation on blur when mode allows
722
+ * 2. Always fires computation (draft API calls) on blur for fields affecting computed fields
723
+ * 3. Ensures computation only happens when field is valid
724
+ *
725
+ * Mode-specific behavior:
726
+ * - "onBlur", "onTouched", "all": Validates on blur (shows errors)
727
+ * - "onSubmit": Doesn't validate on blur (checks existing errors only)
728
+ * - "onChange": Doesn't validate on blur (validation already happened on change)
729
+ */
730
+ const register = useCallback(
731
+ <K extends Path<T>>(name: K, options?: any) => {
732
+ const fieldValidation = validationRules[name as string];
733
+
734
+ // Create custom onBlur handler
735
+ const originalOnBlur = options?.onBlur;
736
+ const enhancedOnBlur = async (e: any) => {
737
+ // 1. Call original onBlur if provided
738
+ if (originalOnBlur) {
739
+ await originalOnBlur(e);
740
+ }
741
+
742
+ // 2. Mode-aware validation check
743
+ let isValid = true;
744
+
745
+ // Modes that should trigger validation on blur
746
+ const shouldTriggerOnBlur =
747
+ mode === "onBlur" || mode === "onTouched" || mode === "all";
748
+
749
+ if (shouldTriggerOnBlur) {
750
+ // Trigger validation (shows errors in UI)
751
+ isValid = await rhfForm.trigger(name);
752
+ } else {
753
+ // For "onSubmit" and "onChange" modes, check existing errors without triggering new validation
754
+ const fieldState = rhfForm.getFieldState(name, rhfForm.formState);
755
+ isValid = !fieldState.error;
756
+ }
757
+
758
+ // 3. Always fire computation on blur if valid
759
+ // This ensures computed fields update even in "onSubmit" mode
760
+ if (isValid) {
761
+ await triggerComputationAfterValidation(name as string);
762
+ }
763
+ };
764
+
765
+ return rhfForm.register(name, {
766
+ ...fieldValidation,
767
+ ...options,
768
+ onBlur: enhancedOnBlur,
769
+ });
770
+ },
771
+ [rhfForm, validationRules, triggerComputationAfterValidation, mode]
772
+ );
773
+
774
+ return {
775
+ // Form methods with strict typing
776
+ register,
777
+ handleSubmit,
778
+ watch: rhfForm.watch as any, // Type assertion for complex generic constraints
779
+ setValue: rhfForm.setValue,
780
+ reset: rhfForm.reset,
781
+
782
+ // Flattened form state (NEW - direct access, no nested formState)
783
+ errors: rhfForm.formState.errors,
784
+ isValid: rhfForm.formState.isValid,
785
+ isDirty: rhfForm.formState.isDirty,
786
+ isSubmitting: rhfForm.formState.isSubmitting || isSubmitting,
787
+ isSubmitSuccessful: rhfForm.formState.isSubmitSuccessful,
788
+
789
+ // BACKWARD COMPATIBILITY - Keep formState for existing components
790
+ formState: rhfForm.formState,
791
+
792
+ // Loading states
793
+ isLoadingInitialData,
794
+ isLoadingRecord,
795
+ isLoading,
796
+
797
+ // Error handling
798
+ loadError: loadError as Error | null,
799
+ submitError,
800
+ hasError,
801
+
802
+ // Schema information
803
+ schema: schema as BackendSchema | null,
804
+ processedSchema,
805
+ computedFields,
806
+ requiredFields,
807
+
808
+ // Field helpers
809
+ getField,
810
+ getFields,
811
+ hasField,
812
+ isFieldRequired,
813
+ isFieldComputed,
814
+
815
+ // Operations
816
+ submit,
817
+ refreshSchema,
818
+ validateForm,
819
+ clearErrors,
820
+ };
821
+ }