@ram_28/kf-ai-sdk 2.0.15 → 2.0.16
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 +8 -8
- package/dist/bdo/core/BaseBdo.d.ts +1 -1
- package/dist/bdo.mjs +2 -2
- package/dist/components/hooks/useActivityForm/createActivityItemProxy.d.ts +1 -1
- package/dist/components/hooks/useActivityForm/createActivityItemProxy.d.ts.map +1 -1
- package/dist/components/hooks/useActivityForm/types.d.ts +2 -2
- package/dist/components/hooks/useActivityForm/types.d.ts.map +1 -1
- package/dist/components/hooks/useActivityForm/useActivityForm.d.ts.map +1 -1
- package/dist/components/hooks/useActivityTable/types.d.ts +4 -4
- package/dist/components/hooks/useActivityTable/types.d.ts.map +1 -1
- package/dist/components/hooks/useActivityTable/useActivityTable.d.ts +1 -1
- package/dist/components/hooks/useActivityTable/useActivityTable.d.ts.map +1 -1
- package/dist/components/hooks/useBDOForm/createItemProxy.d.ts.map +1 -0
- package/dist/components/hooks/useBDOForm/createResolver.d.ts.map +1 -0
- package/dist/components/hooks/useBDOForm/index.d.ts +6 -0
- package/dist/components/hooks/useBDOForm/index.d.ts.map +1 -0
- package/dist/components/hooks/useBDOForm/shared.d.ts +50 -0
- package/dist/components/hooks/useBDOForm/shared.d.ts.map +1 -0
- package/dist/components/hooks/{useForm → useBDOForm}/types.d.ts +6 -6
- package/dist/components/hooks/useBDOForm/types.d.ts.map +1 -0
- package/dist/components/hooks/{useForm/useForm.d.ts → useBDOForm/useBDOForm.d.ts} +4 -4
- package/dist/components/hooks/useBDOForm/useBDOForm.d.ts.map +1 -0
- package/dist/components/hooks/useBDOTable/types.d.ts +1 -3
- package/dist/components/hooks/useBDOTable/types.d.ts.map +1 -1
- package/dist/components/hooks/useBDOTable/useBDOTable.d.ts.map +1 -1
- package/dist/form.cjs +1 -1
- package/dist/form.d.ts +1 -1
- package/dist/form.d.ts.map +1 -1
- package/dist/form.mjs +250 -253
- package/dist/form.types.d.ts +1 -1
- package/dist/form.types.d.ts.map +1 -1
- package/dist/shared-5a7UkED1.js +1180 -0
- package/dist/shared-nnmlRVs7.cjs +1 -0
- package/dist/table.cjs +1 -1
- package/dist/table.mjs +12 -11
- package/dist/types/constants.d.ts +3 -3
- package/dist/workflow/Activity.d.ts +15 -3
- package/dist/workflow/Activity.d.ts.map +1 -1
- package/dist/workflow/client.d.ts +2 -2
- package/dist/workflow/client.d.ts.map +1 -1
- package/dist/workflow/types.d.ts +7 -3
- package/dist/workflow/types.d.ts.map +1 -1
- package/dist/workflow.cjs +1 -1
- package/dist/workflow.mjs +503 -546
- package/docs/bdo.md +1 -1
- package/docs/gaps.md +14 -64
- package/docs/useActivityForm.md +393 -0
- package/docs/useActivityTable.md +42 -105
- package/docs/{useForm.md → useBDOForm.md} +24 -24
- package/docs/useBDOTable.md +6 -39
- package/docs/workflow.md +43 -301
- package/package.json +2 -2
- package/sdk/bdo/core/BaseBdo.ts +2 -2
- package/sdk/components/hooks/useActivityForm/createActivityItemProxy.ts +1 -1
- package/sdk/components/hooks/useActivityForm/createActivityResolver.ts +1 -1
- package/sdk/components/hooks/useActivityForm/types.ts +4 -4
- package/sdk/components/hooks/useActivityForm/useActivityForm.ts +44 -194
- package/sdk/components/hooks/useActivityTable/types.ts +4 -2
- package/sdk/components/hooks/useActivityTable/useActivityTable.ts +8 -39
- package/sdk/components/hooks/{useForm → useBDOForm}/index.ts +4 -3
- package/sdk/components/hooks/useBDOForm/shared.ts +250 -0
- package/sdk/components/hooks/{useForm → useBDOForm}/types.ts +9 -9
- package/sdk/components/hooks/{useForm/useForm.ts → useBDOForm/useBDOForm.ts} +70 -96
- package/sdk/components/hooks/useBDOTable/types.ts +1 -3
- package/sdk/components/hooks/useBDOTable/useBDOTable.ts +3 -2
- package/sdk/form.ts +2 -2
- package/sdk/form.types.ts +4 -4
- package/sdk/types/constants.ts +3 -3
- package/sdk/workflow/Activity.ts +29 -6
- package/sdk/workflow/client.ts +65 -25
- package/sdk/workflow/types.ts +10 -2
- package/dist/components/hooks/useForm/createItemProxy.d.ts.map +0 -1
- package/dist/components/hooks/useForm/createResolver.d.ts.map +0 -1
- package/dist/components/hooks/useForm/index.d.ts +0 -5
- package/dist/components/hooks/useForm/index.d.ts.map +0 -1
- package/dist/components/hooks/useForm/types.d.ts.map +0 -1
- package/dist/components/hooks/useForm/useForm.d.ts.map +0 -1
- package/dist/createResolver-AIgUwoS6.cjs +0 -1
- package/dist/createResolver-ZHXQ7QMa.js +0 -1078
- /package/dist/components/hooks/{useForm → useBDOForm}/createItemProxy.d.ts +0 -0
- /package/dist/components/hooks/{useForm → useBDOForm}/createResolver.d.ts +0 -0
- /package/sdk/components/hooks/{useForm → useBDOForm}/createItemProxy.ts +0 -0
- /package/sdk/components/hooks/{useForm → useBDOForm}/createResolver.ts +0 -0
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
9
9
|
import { useForm as useReactHookForm } from 'react-hook-form';
|
|
10
10
|
import { useQuery } from '@tanstack/react-query';
|
|
11
|
-
import type { Path, FieldValues } from 'react-hook-form';
|
|
12
11
|
|
|
13
12
|
import type { Activity } from '../../../workflow/Activity';
|
|
14
13
|
import type {
|
|
@@ -19,6 +18,13 @@ import type {
|
|
|
19
18
|
|
|
20
19
|
import { createActivityResolver } from './createActivityResolver';
|
|
21
20
|
import { createActivityItemProxy } from './createActivityItemProxy';
|
|
21
|
+
import {
|
|
22
|
+
coerceFieldValue,
|
|
23
|
+
coerceRecordForForm,
|
|
24
|
+
createSyncField,
|
|
25
|
+
createEnhancedRegister,
|
|
26
|
+
createEnhancedControl,
|
|
27
|
+
} from '../useBDOForm/shared';
|
|
22
28
|
import { toError } from '../../../utils/error-handling';
|
|
23
29
|
import { getBdoSchema } from '../../../api/metadata';
|
|
24
30
|
import {
|
|
@@ -28,66 +34,6 @@ import {
|
|
|
28
34
|
} from '../../../workflow/createFieldFromMeta';
|
|
29
35
|
import type { BaseField } from '../../../bdo/fields/BaseField';
|
|
30
36
|
|
|
31
|
-
// ============================================================
|
|
32
|
-
// FIELD VALUE COERCION (HTML inputs return strings)
|
|
33
|
-
// ============================================================
|
|
34
|
-
|
|
35
|
-
/** Coerce form value to match field's expected type before sending to API */
|
|
36
|
-
function coerceFieldValue(
|
|
37
|
-
field: BaseField<unknown>,
|
|
38
|
-
value: unknown,
|
|
39
|
-
): unknown {
|
|
40
|
-
const type = field.meta.Type;
|
|
41
|
-
// Number: string → number
|
|
42
|
-
if (typeof value === 'string' && type === 'Number') {
|
|
43
|
-
return value === '' ? undefined : Number(value);
|
|
44
|
-
}
|
|
45
|
-
// Date/DateTime: empty string → undefined
|
|
46
|
-
if (
|
|
47
|
-
typeof value === 'string' &&
|
|
48
|
-
value === '' &&
|
|
49
|
-
(type === 'Date' || type === 'DateTime')
|
|
50
|
-
) {
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
// DateTime: normalize to HH:MM:SS and ensure Z suffix
|
|
54
|
-
if (typeof value === 'string' && value !== '' && type === 'DateTime') {
|
|
55
|
-
let normalized = value;
|
|
56
|
-
if (normalized.endsWith('Z')) normalized = normalized.slice(0, -1);
|
|
57
|
-
const timePart = normalized.split('T')[1] || '';
|
|
58
|
-
if ((timePart.match(/:/g) || []).length === 1) {
|
|
59
|
-
normalized += ':00';
|
|
60
|
-
}
|
|
61
|
-
return normalized + 'Z';
|
|
62
|
-
}
|
|
63
|
-
return value;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ============================================================
|
|
67
|
-
// RECORD COERCION (strip trailing Z from DateTime for HTML inputs)
|
|
68
|
-
// ============================================================
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Coerce record values for form display.
|
|
72
|
-
* Strips trailing 'Z' from DateTime values so `<input type="datetime-local">` works.
|
|
73
|
-
*/
|
|
74
|
-
function coerceRecordForForm(
|
|
75
|
-
fields: Record<string, BaseField<unknown>>,
|
|
76
|
-
data: Record<string, unknown>,
|
|
77
|
-
): Record<string, unknown> {
|
|
78
|
-
const result = { ...data };
|
|
79
|
-
for (const [key, value] of Object.entries(result)) {
|
|
80
|
-
if (
|
|
81
|
-
typeof value === 'string' &&
|
|
82
|
-
fields[key]?.meta.Type === 'DateTime' &&
|
|
83
|
-
value.endsWith('Z')
|
|
84
|
-
) {
|
|
85
|
-
result[key] = value.slice(0, -1);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return result;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
37
|
// ============================================================
|
|
92
38
|
// MAIN HOOK
|
|
93
39
|
// ============================================================
|
|
@@ -303,150 +249,54 @@ export function useActivityForm<A extends Activity<any, any, any>>(
|
|
|
303
249
|
}, [enabled, isMetadataLoading, activityRef, activity_instance_id]);
|
|
304
250
|
|
|
305
251
|
// ============================================================
|
|
306
|
-
//
|
|
252
|
+
// PER-FIELD SYNC (shared with useBDOForm)
|
|
307
253
|
// ============================================================
|
|
308
254
|
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const isValid = await rhf.trigger(fieldName as Path<FieldValues>);
|
|
316
|
-
if (!isValid) return;
|
|
317
|
-
|
|
318
|
-
const rawValue = rhf.getValues(fieldName as any);
|
|
319
|
-
const field = allFields[fieldName];
|
|
320
|
-
const value = field
|
|
321
|
-
? coerceFieldValue(field, rawValue)
|
|
322
|
-
: rawValue;
|
|
323
|
-
|
|
324
|
-
const response = await activityRef.update(activity_instance_id, {
|
|
325
|
-
[fieldName]: value,
|
|
326
|
-
} as any);
|
|
327
|
-
|
|
328
|
-
// Field saved — reset dirty state so it's not re-sent on submit
|
|
329
|
-
rhf.resetField(fieldName as Path<FieldValues>, {
|
|
330
|
-
defaultValue: rawValue,
|
|
331
|
-
keepTouched: true,
|
|
332
|
-
keepError: true,
|
|
333
|
-
} as any);
|
|
334
|
-
|
|
335
|
-
// Update computed/readonly fields from response
|
|
336
|
-
if (response && typeof response === 'object') {
|
|
337
|
-
const responseData =
|
|
338
|
-
(response as any).Data ?? (response as any);
|
|
339
|
-
if (responseData && typeof responseData === 'object') {
|
|
340
|
-
const readonlySet = new Set(readonlyFieldNames);
|
|
341
|
-
for (const key of Object.keys(responseData)) {
|
|
342
|
-
if (readonlySet.has(key) && responseData[key] !== undefined) {
|
|
343
|
-
const current = rhf.getValues(key as any);
|
|
344
|
-
if (current !== responseData[key]) {
|
|
345
|
-
rhf.setValue(
|
|
346
|
-
key as Path<FieldValues>,
|
|
347
|
-
responseData[key] as any,
|
|
348
|
-
{ shouldDirty: false, shouldValidate: false },
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
} catch (error) {
|
|
356
|
-
console.warn('syncField failed:', error);
|
|
357
|
-
} finally {
|
|
358
|
-
isComputingRef.current = false;
|
|
359
|
-
}
|
|
360
|
-
},
|
|
361
|
-
[activityRef, readonlyFieldNames, allFields, rhf, activity_instance_id],
|
|
255
|
+
const syncApiFn = useCallback(
|
|
256
|
+
(fieldName: string, value: unknown) =>
|
|
257
|
+
activityRef.update(activity_instance_id, {
|
|
258
|
+
[fieldName]: value,
|
|
259
|
+
} as any),
|
|
260
|
+
[activityRef, activity_instance_id],
|
|
362
261
|
);
|
|
363
262
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
263
|
+
const syncField = useMemo(
|
|
264
|
+
() =>
|
|
265
|
+
createSyncField({
|
|
266
|
+
apiFn: syncApiFn,
|
|
267
|
+
allFields,
|
|
268
|
+
readonlyFieldNames,
|
|
269
|
+
rhf,
|
|
270
|
+
isComputingRef,
|
|
271
|
+
}),
|
|
272
|
+
[syncApiFn, allFields, readonlyFieldNames, rhf],
|
|
273
|
+
);
|
|
368
274
|
|
|
369
275
|
const syncOnChange = mode === 'onChange' || mode === 'all';
|
|
370
|
-
const syncOnBlur =
|
|
371
|
-
|
|
372
|
-
// ============================================================
|
|
373
|
-
// REGISTER (enhanced with mode-aware sync + auto-disable)
|
|
374
|
-
// ============================================================
|
|
375
|
-
|
|
376
|
-
const register = useCallback(
|
|
377
|
-
(name: string, registerOptions?: any) => {
|
|
378
|
-
const field = allFields[name];
|
|
379
|
-
const isReadonly = field ? field.readOnly : false;
|
|
380
|
-
|
|
381
|
-
const result = rhf.register(name as Path<FieldValues>, {
|
|
382
|
-
...registerOptions,
|
|
383
|
-
...(syncOnBlur
|
|
384
|
-
? {
|
|
385
|
-
onBlur: async (e: any) => {
|
|
386
|
-
await registerOptions?.onBlur?.(e);
|
|
387
|
-
await syncField(name);
|
|
388
|
-
},
|
|
389
|
-
}
|
|
390
|
-
: {}),
|
|
391
|
-
...(syncOnChange
|
|
392
|
-
? {
|
|
393
|
-
onChange: async (e: any) => {
|
|
394
|
-
await registerOptions?.onChange?.(e);
|
|
395
|
-
await syncField(name);
|
|
396
|
-
},
|
|
397
|
-
}
|
|
398
|
-
: {}),
|
|
399
|
-
...(isReadonly ? { disabled: true } : {}),
|
|
400
|
-
});
|
|
276
|
+
const syncOnBlur =
|
|
277
|
+
mode === 'onBlur' || mode === 'onTouched' || mode === 'all';
|
|
401
278
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
279
|
+
const register = useMemo(
|
|
280
|
+
() =>
|
|
281
|
+
createEnhancedRegister({
|
|
282
|
+
rhf,
|
|
283
|
+
allFields,
|
|
284
|
+
syncField,
|
|
285
|
+
syncOnBlur,
|
|
286
|
+
syncOnChange,
|
|
287
|
+
}),
|
|
408
288
|
[rhf, allFields, syncField, syncOnBlur, syncOnChange],
|
|
409
289
|
) as UseActivityFormReturn<A>['register'];
|
|
410
290
|
|
|
411
|
-
// ============================================================
|
|
412
|
-
// ENHANCED CONTROL (for Controller — same sync behavior as register)
|
|
413
|
-
// ============================================================
|
|
414
|
-
|
|
415
291
|
const enhancedControl = useMemo(
|
|
416
292
|
() =>
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const originalOnChange = result.onChange;
|
|
423
|
-
const originalOnBlur = result.onBlur;
|
|
424
|
-
|
|
425
|
-
return {
|
|
426
|
-
...result,
|
|
427
|
-
...(syncOnChange
|
|
428
|
-
? {
|
|
429
|
-
onChange: async (event: any) => {
|
|
430
|
-
await originalOnChange(event);
|
|
431
|
-
await syncField(name);
|
|
432
|
-
},
|
|
433
|
-
}
|
|
434
|
-
: {}),
|
|
435
|
-
...(syncOnBlur
|
|
436
|
-
? {
|
|
437
|
-
onBlur: async (event: any) => {
|
|
438
|
-
await originalOnBlur(event);
|
|
439
|
-
await syncField(name);
|
|
440
|
-
},
|
|
441
|
-
}
|
|
442
|
-
: {}),
|
|
443
|
-
};
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
return Reflect.get(target, prop, receiver);
|
|
447
|
-
},
|
|
293
|
+
createEnhancedControl({
|
|
294
|
+
control: rhf.control,
|
|
295
|
+
syncField,
|
|
296
|
+
syncOnBlur,
|
|
297
|
+
syncOnChange,
|
|
448
298
|
}),
|
|
449
|
-
[rhf.control, syncField,
|
|
299
|
+
[rhf.control, syncField, syncOnBlur, syncOnChange],
|
|
450
300
|
);
|
|
451
301
|
|
|
452
302
|
// ============================================================
|
|
@@ -469,7 +319,7 @@ export function useActivityForm<A extends Activity<any, any, any>>(
|
|
|
469
319
|
setIsSubmitting(true);
|
|
470
320
|
|
|
471
321
|
try {
|
|
472
|
-
// Only send dirty (changed) fields — matches
|
|
322
|
+
// Only send dirty (changed) fields — matches useBDOForm update behavior
|
|
473
323
|
// Use getValues() to capture Image/File values set via setValue()
|
|
474
324
|
// that RHF resolver doesn't include in `data`
|
|
475
325
|
const cleanedData: Record<string, unknown> = {};
|
|
@@ -532,7 +382,7 @@ export function useActivityForm<A extends Activity<any, any, any>>(
|
|
|
532
382
|
setIsSubmitting(true);
|
|
533
383
|
|
|
534
384
|
try {
|
|
535
|
-
// Only send dirty (changed) fields — matches
|
|
385
|
+
// Only send dirty (changed) fields — matches useBDOForm update behavior
|
|
536
386
|
// Use getValues() to capture Image/File values set via setValue()
|
|
537
387
|
// that RHF resolver doesn't include in `data`
|
|
538
388
|
const cleanedData: Record<string, unknown> = {};
|
|
@@ -17,16 +17,18 @@ export type ActivityTableStatusType =
|
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Row type for activity table data.
|
|
20
|
-
* System fields
|
|
20
|
+
* System fields and entity fields are flat at the top level.
|
|
21
21
|
*/
|
|
22
22
|
export type ActivityRowType<A extends Activity<any, any, any>> =
|
|
23
23
|
A extends Activity<infer E, any, any>
|
|
24
|
-
? ActivityInstanceFieldsType &
|
|
24
|
+
? ActivityInstanceFieldsType & E
|
|
25
25
|
: never;
|
|
26
26
|
|
|
27
27
|
export interface UseActivityTableOptionsType<
|
|
28
28
|
A extends Activity<any, any, any>,
|
|
29
29
|
> {
|
|
30
|
+
/** The activity instance to fetch data for */
|
|
31
|
+
activity: A;
|
|
30
32
|
/** Which operation — determines endpoint (inprogress vs completed) */
|
|
31
33
|
status: ActivityTableStatusType;
|
|
32
34
|
/** Initial state */
|
|
@@ -7,58 +7,27 @@ import type {
|
|
|
7
7
|
ActivityRowType,
|
|
8
8
|
} from './types';
|
|
9
9
|
|
|
10
|
-
const ACTIVITY_SYSTEM_FIELDS = new Set([
|
|
11
|
-
'_id',
|
|
12
|
-
'BPInstanceId',
|
|
13
|
-
'Status',
|
|
14
|
-
'AssignedTo',
|
|
15
|
-
'CompletedAt',
|
|
16
|
-
]);
|
|
17
|
-
|
|
18
|
-
function nestEntityFields(
|
|
19
|
-
flatRow: Record<string, unknown>,
|
|
20
|
-
): Record<string, unknown> {
|
|
21
|
-
const result: Record<string, unknown> = {};
|
|
22
|
-
const ado: Record<string, unknown> = {};
|
|
23
|
-
for (const [key, value] of Object.entries(flatRow)) {
|
|
24
|
-
if (ACTIVITY_SYSTEM_FIELDS.has(key)) {
|
|
25
|
-
result[key] = value;
|
|
26
|
-
} else {
|
|
27
|
-
ado[key] = value;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
result.ADO = ado;
|
|
31
|
-
return result;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
10
|
export function useActivityTable<A extends Activity<any, any, any>>(
|
|
35
|
-
activity: A,
|
|
36
11
|
options: UseActivityTableOptionsType<A>,
|
|
37
12
|
): UseActivityTableReturnType<A> {
|
|
38
|
-
const { status, ...rest } = options;
|
|
13
|
+
const { activity, status, ...rest } = options;
|
|
39
14
|
const { businessProcessId, activityId } = activity.meta;
|
|
40
15
|
|
|
41
16
|
const ops = useMemo(() => activity._getOps(), [activity]);
|
|
42
17
|
|
|
43
|
-
const listFn = useMemo(
|
|
44
|
-
|
|
18
|
+
const listFn = useMemo(
|
|
19
|
+
() =>
|
|
45
20
|
status === 'inprogress'
|
|
46
21
|
? (opts: any) => ops.inProgressList(opts)
|
|
47
|
-
: (opts: any) => ops.completedList(opts)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
...result,
|
|
52
|
-
Data: result.Data.map(nestEntityFields),
|
|
53
|
-
} as typeof result;
|
|
54
|
-
};
|
|
55
|
-
}, [ops, status]);
|
|
22
|
+
: (opts: any) => ops.completedList(opts),
|
|
23
|
+
[ops, status],
|
|
24
|
+
);
|
|
56
25
|
|
|
57
26
|
const countFn = useMemo(
|
|
58
27
|
() =>
|
|
59
28
|
status === 'inprogress'
|
|
60
|
-
? (opts: any) => ops.
|
|
61
|
-
: (opts: any) => ops.
|
|
29
|
+
? (opts: any) => ops.inProgressCount(opts)
|
|
30
|
+
: (opts: any) => ops.completedCount(opts),
|
|
62
31
|
[ops, status],
|
|
63
32
|
);
|
|
64
33
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { useBDOForm } from "./useBDOForm";
|
|
2
2
|
export { createResolver } from "./createResolver";
|
|
3
3
|
export { createItemProxy } from "./createItemProxy";
|
|
4
|
+
export { coerceFieldValue, coerceRecordForForm } from "./shared";
|
|
4
5
|
export type {
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
UseBDOFormOptionsType,
|
|
7
|
+
UseBDOFormReturnType,
|
|
7
8
|
FormItemType,
|
|
8
9
|
EditableFormFieldAccessorType,
|
|
9
10
|
ReadonlyFormFieldAccessorType,
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// SHARED FORM UTILITIES
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Coercion functions shared between useBDOForm and useActivityForm.
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
UseFormReturn,
|
|
8
|
+
Control,
|
|
9
|
+
Path,
|
|
10
|
+
FieldValues,
|
|
11
|
+
} from 'react-hook-form';
|
|
12
|
+
import type { MutableRefObject } from 'react';
|
|
13
|
+
import type { BaseField } from '../../../bdo/fields/BaseField';
|
|
14
|
+
|
|
15
|
+
/** Coerce form value to match field's expected type (HTML inputs return strings) */
|
|
16
|
+
export function coerceFieldValue(
|
|
17
|
+
field: BaseField<unknown>,
|
|
18
|
+
value: unknown,
|
|
19
|
+
): unknown {
|
|
20
|
+
const type = field.meta.Type;
|
|
21
|
+
if (typeof value === 'string' && type === 'Number') {
|
|
22
|
+
return value === '' ? undefined : Number(value);
|
|
23
|
+
}
|
|
24
|
+
// Date/DateTime: empty string → undefined (don't send to backend)
|
|
25
|
+
if (
|
|
26
|
+
typeof value === 'string' &&
|
|
27
|
+
value === '' &&
|
|
28
|
+
(type === 'Date' || type === 'DateTime')
|
|
29
|
+
) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
// DateTime: normalize to HH:MM:SS and ensure Z suffix for API request format
|
|
33
|
+
if (typeof value === 'string' && value !== '' && type === 'DateTime') {
|
|
34
|
+
let normalized = value;
|
|
35
|
+
if (normalized.endsWith('Z')) normalized = normalized.slice(0, -1);
|
|
36
|
+
// HTML datetime-local may omit seconds (e.g. "2026-02-18T15:12")
|
|
37
|
+
const timePart = normalized.split('T')[1] || '';
|
|
38
|
+
if ((timePart.match(/:/g) || []).length === 1) {
|
|
39
|
+
normalized += ':00';
|
|
40
|
+
}
|
|
41
|
+
return normalized + 'Z';
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Strip trailing Z from DateTime response values for HTML datetime-local inputs.
|
|
48
|
+
* Takes a fields map (use bdo.getFields() for BDO forms).
|
|
49
|
+
*/
|
|
50
|
+
export function coerceRecordForForm(
|
|
51
|
+
fields: Record<string, BaseField<unknown>>,
|
|
52
|
+
data: Record<string, unknown>,
|
|
53
|
+
): Record<string, unknown> {
|
|
54
|
+
const result = { ...data };
|
|
55
|
+
for (const [key, value] of Object.entries(result)) {
|
|
56
|
+
if (
|
|
57
|
+
typeof value === 'string' &&
|
|
58
|
+
fields[key]?.meta.Type === 'DateTime' &&
|
|
59
|
+
value.endsWith('Z')
|
|
60
|
+
) {
|
|
61
|
+
result[key] = value.slice(0, -1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================
|
|
68
|
+
// SYNC UTILITIES
|
|
69
|
+
// ============================================================
|
|
70
|
+
// Shared per-field sync pattern used by useBDOForm and useActivityForm.
|
|
71
|
+
// createSyncField → validate → coerce → API call → reset dirty → update readonly
|
|
72
|
+
// createEnhancedRegister → inject syncField into register's onBlur/onChange
|
|
73
|
+
// createEnhancedControl → inject syncField into Controller's control.register
|
|
74
|
+
|
|
75
|
+
/** API function signature for per-field sync */
|
|
76
|
+
export type SyncApiFnType = (
|
|
77
|
+
fieldName: string,
|
|
78
|
+
value: unknown,
|
|
79
|
+
) => Promise<unknown>;
|
|
80
|
+
|
|
81
|
+
export interface CreateSyncFieldOptionsType {
|
|
82
|
+
apiFn: SyncApiFnType;
|
|
83
|
+
allFields: Record<string, BaseField<unknown>>;
|
|
84
|
+
readonlyFieldNames: string[];
|
|
85
|
+
rhf: UseFormReturn;
|
|
86
|
+
isComputingRef: MutableRefObject<boolean>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Factory that returns a `syncField(fieldName)` function.
|
|
91
|
+
* Validates the field, coerces its value, sends it to the API,
|
|
92
|
+
* resets dirty state, and updates computed/readonly fields from the response.
|
|
93
|
+
*/
|
|
94
|
+
export function createSyncField(
|
|
95
|
+
opts: CreateSyncFieldOptionsType,
|
|
96
|
+
): (fieldName: string) => Promise<void> {
|
|
97
|
+
const { apiFn, allFields, readonlyFieldNames, rhf, isComputingRef } = opts;
|
|
98
|
+
|
|
99
|
+
return async (fieldName: string) => {
|
|
100
|
+
if (isComputingRef.current) return;
|
|
101
|
+
isComputingRef.current = true;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const isValid = await rhf.trigger(fieldName as Path<FieldValues>);
|
|
105
|
+
if (!isValid) return;
|
|
106
|
+
|
|
107
|
+
const rawValue = rhf.getValues(fieldName as any);
|
|
108
|
+
const field = allFields[fieldName];
|
|
109
|
+
const value = field ? coerceFieldValue(field, rawValue) : rawValue;
|
|
110
|
+
|
|
111
|
+
const response = await apiFn(fieldName, value);
|
|
112
|
+
|
|
113
|
+
// If apiFn chose not to sync (e.g. draft not ready), skip cleanup
|
|
114
|
+
if (response === undefined) return;
|
|
115
|
+
|
|
116
|
+
// Field saved — reset dirty state so it's not re-sent on submit
|
|
117
|
+
rhf.resetField(fieldName as Path<FieldValues>, {
|
|
118
|
+
defaultValue: rawValue,
|
|
119
|
+
keepTouched: true,
|
|
120
|
+
keepError: true,
|
|
121
|
+
} as any);
|
|
122
|
+
|
|
123
|
+
// Update computed/readonly fields from response
|
|
124
|
+
if (response && typeof response === 'object') {
|
|
125
|
+
const responseData =
|
|
126
|
+
(response as any).Data ?? (response as any);
|
|
127
|
+
if (responseData && typeof responseData === 'object') {
|
|
128
|
+
const readonlySet = new Set(readonlyFieldNames);
|
|
129
|
+
for (const key of Object.keys(responseData)) {
|
|
130
|
+
if (readonlySet.has(key) && responseData[key] !== undefined) {
|
|
131
|
+
const current = rhf.getValues(key as any);
|
|
132
|
+
if (current !== responseData[key]) {
|
|
133
|
+
rhf.setValue(
|
|
134
|
+
key as Path<FieldValues>,
|
|
135
|
+
responseData[key] as any,
|
|
136
|
+
{ shouldDirty: false, shouldValidate: false },
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.warn('syncField failed:', error);
|
|
145
|
+
} finally {
|
|
146
|
+
isComputingRef.current = false;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface CreateEnhancedRegisterOptionsType {
|
|
152
|
+
rhf: UseFormReturn;
|
|
153
|
+
allFields: Record<string, BaseField<unknown>>;
|
|
154
|
+
syncField: (fieldName: string) => Promise<void>;
|
|
155
|
+
syncOnBlur: boolean;
|
|
156
|
+
syncOnChange: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Factory that returns an enhanced `register` function.
|
|
161
|
+
* Injects syncField into onBlur/onChange based on mode, and auto-disables readonly fields.
|
|
162
|
+
*/
|
|
163
|
+
export function createEnhancedRegister(
|
|
164
|
+
opts: CreateEnhancedRegisterOptionsType,
|
|
165
|
+
) {
|
|
166
|
+
const { rhf, allFields, syncField, syncOnBlur, syncOnChange } = opts;
|
|
167
|
+
|
|
168
|
+
return (name: string, registerOptions?: any) => {
|
|
169
|
+
const field = allFields[name];
|
|
170
|
+
const isReadonly = field ? field.readOnly : false;
|
|
171
|
+
|
|
172
|
+
const result = rhf.register(name as Path<FieldValues>, {
|
|
173
|
+
...registerOptions,
|
|
174
|
+
...(syncOnBlur
|
|
175
|
+
? {
|
|
176
|
+
onBlur: async (e: any) => {
|
|
177
|
+
await registerOptions?.onBlur?.(e);
|
|
178
|
+
await syncField(name);
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
: {}),
|
|
182
|
+
...(syncOnChange
|
|
183
|
+
? {
|
|
184
|
+
onChange: async (e: any) => {
|
|
185
|
+
await registerOptions?.onChange?.(e);
|
|
186
|
+
await syncField(name);
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
: {}),
|
|
190
|
+
...(isReadonly ? { disabled: true } : {}),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (isReadonly) {
|
|
194
|
+
return { ...result, disabled: true as const };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return result;
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface CreateEnhancedControlOptionsType {
|
|
202
|
+
control: Control;
|
|
203
|
+
syncField: (fieldName: string) => Promise<void>;
|
|
204
|
+
syncOnBlur: boolean;
|
|
205
|
+
syncOnChange: boolean;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Factory that returns a Proxy over RHF's `control` object.
|
|
210
|
+
* Intercepts `control.register` to inject syncField into onChange/onBlur
|
|
211
|
+
* for Controller components.
|
|
212
|
+
*/
|
|
213
|
+
export function createEnhancedControl(
|
|
214
|
+
opts: CreateEnhancedControlOptionsType,
|
|
215
|
+
): Control {
|
|
216
|
+
const { control, syncField, syncOnBlur, syncOnChange } = opts;
|
|
217
|
+
|
|
218
|
+
return new Proxy(control, {
|
|
219
|
+
get(target, prop, receiver) {
|
|
220
|
+
if (prop === 'register') {
|
|
221
|
+
return (name: string, options?: any) => {
|
|
222
|
+
const result = target.register(name as any, options);
|
|
223
|
+
const originalOnChange = result.onChange;
|
|
224
|
+
const originalOnBlur = result.onBlur;
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
...result,
|
|
228
|
+
...(syncOnChange
|
|
229
|
+
? {
|
|
230
|
+
onChange: async (event: any) => {
|
|
231
|
+
await originalOnChange(event);
|
|
232
|
+
await syncField(name);
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
: {}),
|
|
236
|
+
...(syncOnBlur
|
|
237
|
+
? {
|
|
238
|
+
onBlur: async (event: any) => {
|
|
239
|
+
await originalOnBlur(event);
|
|
240
|
+
await syncField(name);
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
: {}),
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return Reflect.get(target, prop, receiver);
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|