@ram_28/kf-ai-sdk 2.0.14 → 2.0.15

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 (119) hide show
  1. package/README.md +2 -1
  2. package/dist/FileField-BWrSHNRq.js +296 -0
  3. package/dist/FileField-eDeuzln8.cjs +1 -0
  4. package/dist/api.cjs +1 -1
  5. package/dist/api.mjs +2 -2
  6. package/dist/auth.cjs +1 -1
  7. package/dist/auth.mjs +1 -1
  8. package/dist/bdo.cjs +1 -1
  9. package/dist/bdo.mjs +228 -472
  10. package/dist/{client-DnO2KKrw.cjs → client-D5k4SYuw.cjs} +1 -1
  11. package/dist/{client-iQTqFDNI.js → client-_ayziI1d.js} +33 -32
  12. package/dist/components/hooks/index.d.ts +9 -3
  13. package/dist/components/hooks/index.d.ts.map +1 -1
  14. package/dist/{workflow/components → components/hooks}/useActivityForm/createActivityItemProxy.d.ts +9 -5
  15. package/dist/components/hooks/useActivityForm/createActivityItemProxy.d.ts.map +1 -0
  16. package/dist/components/hooks/useActivityForm/createActivityResolver.d.ts +23 -0
  17. package/dist/components/hooks/useActivityForm/createActivityResolver.d.ts.map +1 -0
  18. package/dist/components/hooks/useActivityForm/index.d.ts.map +1 -0
  19. package/dist/{workflow/components → components/hooks}/useActivityForm/types.d.ts +11 -7
  20. package/dist/components/hooks/useActivityForm/types.d.ts.map +1 -0
  21. package/dist/{workflow/components → components/hooks}/useActivityForm/useActivityForm.d.ts +2 -2
  22. package/dist/components/hooks/useActivityForm/useActivityForm.d.ts.map +1 -0
  23. package/dist/components/hooks/useActivityTable/index.d.ts +4 -0
  24. package/dist/components/hooks/useActivityTable/index.d.ts.map +1 -0
  25. package/dist/components/hooks/useActivityTable/types.d.ts +36 -0
  26. package/dist/components/hooks/useActivityTable/types.d.ts.map +1 -0
  27. package/dist/components/hooks/useActivityTable/useActivityTable.d.ts +4 -0
  28. package/dist/components/hooks/useActivityTable/useActivityTable.d.ts.map +1 -0
  29. package/dist/components/hooks/useBDOTable/index.d.ts +3 -0
  30. package/dist/components/hooks/useBDOTable/index.d.ts.map +1 -0
  31. package/dist/components/hooks/useBDOTable/types.d.ts +26 -0
  32. package/dist/components/hooks/useBDOTable/types.d.ts.map +1 -0
  33. package/dist/components/hooks/useBDOTable/useBDOTable.d.ts +3 -0
  34. package/dist/components/hooks/useBDOTable/useBDOTable.d.ts.map +1 -0
  35. package/dist/components/hooks/useTable/index.d.ts +2 -2
  36. package/dist/components/hooks/useTable/index.d.ts.map +1 -1
  37. package/dist/components/hooks/useTable/types.d.ts +11 -10
  38. package/dist/components/hooks/useTable/types.d.ts.map +1 -1
  39. package/dist/components/hooks/useTable/useTable.d.ts +1 -1
  40. package/dist/components/hooks/useTable/useTable.d.ts.map +1 -1
  41. package/dist/createResolver-AIgUwoS6.cjs +1 -0
  42. package/dist/createResolver-ZHXQ7QMa.js +1078 -0
  43. package/dist/form.cjs +1 -1
  44. package/dist/form.mjs +252 -314
  45. package/dist/{metadata-DpfI3zRN.js → metadata-Cc1mBcLS.js} +1 -1
  46. package/dist/{metadata-DgLSJkF5.cjs → metadata-DWXQPDav.cjs} +1 -1
  47. package/dist/table.cjs +1 -1
  48. package/dist/table.d.ts +1 -0
  49. package/dist/table.d.ts.map +1 -1
  50. package/dist/table.mjs +16 -192
  51. package/dist/table.types.d.ts +2 -1
  52. package/dist/table.types.d.ts.map +1 -1
  53. package/dist/types/base-fields.d.ts +4 -4
  54. package/dist/types/base-fields.d.ts.map +1 -1
  55. package/dist/useTable-CeRklbdT.cjs +1 -0
  56. package/dist/useTable-DS0-WInw.js +203 -0
  57. package/dist/workflow/Activity.d.ts +9 -9
  58. package/dist/workflow/Activity.d.ts.map +1 -1
  59. package/dist/workflow/client.d.ts.map +1 -1
  60. package/dist/workflow/createFieldFromMeta.d.ts +29 -0
  61. package/dist/workflow/createFieldFromMeta.d.ts.map +1 -0
  62. package/dist/workflow/index.d.ts +1 -2
  63. package/dist/workflow/index.d.ts.map +1 -1
  64. package/dist/workflow/types.d.ts +12 -12
  65. package/dist/workflow/types.d.ts.map +1 -1
  66. package/dist/workflow.cjs +1 -1
  67. package/dist/workflow.d.ts +5 -2
  68. package/dist/workflow.d.ts.map +1 -1
  69. package/dist/workflow.mjs +716 -338
  70. package/dist/workflow.types.d.ts +1 -0
  71. package/dist/workflow.types.d.ts.map +1 -1
  72. package/docs/gaps.md +410 -0
  73. package/docs/useActivityTable.md +481 -0
  74. package/docs/useBDOTable.md +317 -0
  75. package/docs/workflow.md +143 -34
  76. package/package.json +1 -1
  77. package/sdk/bdo/fields/UserField.ts +1 -1
  78. package/sdk/components/hooks/index.ts +28 -5
  79. package/sdk/components/hooks/useActivityForm/createActivityItemProxy.ts +400 -0
  80. package/sdk/components/hooks/useActivityForm/createActivityResolver.ts +87 -0
  81. package/sdk/{workflow/components → components/hooks}/useActivityForm/types.ts +21 -8
  82. package/sdk/components/hooks/useActivityForm/useActivityForm.ts +628 -0
  83. package/sdk/components/hooks/useActivityTable/index.ts +8 -0
  84. package/sdk/components/hooks/useActivityTable/types.ts +45 -0
  85. package/sdk/components/hooks/useActivityTable/useActivityTable.ts +71 -0
  86. package/sdk/components/hooks/useBDOTable/index.ts +2 -0
  87. package/sdk/components/hooks/useBDOTable/types.ts +24 -0
  88. package/sdk/components/hooks/useBDOTable/useBDOTable.ts +15 -0
  89. package/sdk/components/hooks/useTable/index.ts +3 -3
  90. package/sdk/components/hooks/useTable/types.ts +16 -12
  91. package/sdk/components/hooks/useTable/useTable.ts +56 -49
  92. package/sdk/table.ts +4 -1
  93. package/sdk/table.types.ts +7 -4
  94. package/sdk/types/base-fields.ts +4 -4
  95. package/sdk/workflow/Activity.ts +14 -13
  96. package/sdk/workflow/client.ts +21 -8
  97. package/sdk/workflow/createFieldFromMeta.ts +110 -0
  98. package/sdk/workflow/index.ts +1 -6
  99. package/sdk/workflow/types.ts +13 -12
  100. package/sdk/workflow.ts +11 -2
  101. package/sdk/workflow.types.ts +7 -0
  102. package/dist/BaseField-B6da88U7.js +0 -40
  103. package/dist/BaseField-Drp0-OxL.cjs +0 -1
  104. package/dist/error-handling-CAoD0Kwb.cjs +0 -1
  105. package/dist/error-handling-CrhTtD88.js +0 -14
  106. package/dist/index.esm-Cj63v5ny.js +0 -1014
  107. package/dist/index.esm-DuwT11sx.cjs +0 -1
  108. package/dist/workflow/components/useActivityForm/createActivityItemProxy.d.ts.map +0 -1
  109. package/dist/workflow/components/useActivityForm/createActivityResolver.d.ts +0 -22
  110. package/dist/workflow/components/useActivityForm/createActivityResolver.d.ts.map +0 -1
  111. package/dist/workflow/components/useActivityForm/index.d.ts.map +0 -1
  112. package/dist/workflow/components/useActivityForm/types.d.ts.map +0 -1
  113. package/dist/workflow/components/useActivityForm/useActivityForm.d.ts.map +0 -1
  114. package/docs/useTable.md +0 -369
  115. package/sdk/workflow/components/useActivityForm/createActivityItemProxy.ts +0 -130
  116. package/sdk/workflow/components/useActivityForm/createActivityResolver.ts +0 -61
  117. package/sdk/workflow/components/useActivityForm/useActivityForm.ts +0 -386
  118. /package/dist/{workflow/components → components/hooks}/useActivityForm/index.d.ts +0 -0
  119. /package/sdk/{workflow/components → components/hooks}/useActivityForm/index.ts +0 -0
@@ -0,0 +1,628 @@
1
+ // ============================================================
2
+ // USE ACTIVITY FORM HOOK (v2 — Metadata-driven)
3
+ // ============================================================
4
+ // React hook for building forms bound to workflow activity input fields.
5
+ // Fetches BP metadata at runtime, discovers Input fields dynamically,
6
+ // and uses update() on blur (not draftStart/draftEnd).
7
+
8
+ import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
9
+ import { useForm as useReactHookForm } from 'react-hook-form';
10
+ import { useQuery } from '@tanstack/react-query';
11
+ import type { Path, FieldValues } from 'react-hook-form';
12
+
13
+ import type { Activity } from '../../../workflow/Activity';
14
+ import type {
15
+ UseActivityFormOptions,
16
+ UseActivityFormReturn,
17
+ AllActivityFields,
18
+ } from './types';
19
+
20
+ import { createActivityResolver } from './createActivityResolver';
21
+ import { createActivityItemProxy } from './createActivityItemProxy';
22
+ import { toError } from '../../../utils/error-handling';
23
+ import { getBdoSchema } from '../../../api/metadata';
24
+ import {
25
+ buildFieldsFromInput,
26
+ createFieldFromMeta,
27
+ findFieldInBpActivities,
28
+ } from '../../../workflow/createFieldFromMeta';
29
+ import type { BaseField } from '../../../bdo/fields/BaseField';
30
+
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
+ // ============================================================
92
+ // MAIN HOOK
93
+ // ============================================================
94
+
95
+ export function useActivityForm<A extends Activity<any, any, any>>(
96
+ activity: A,
97
+ options: UseActivityFormOptions<A>,
98
+ ): UseActivityFormReturn<A> {
99
+ const {
100
+ activity_instance_id,
101
+ defaultValues = {},
102
+ mode = 'onBlur',
103
+ enabled = true,
104
+ } = options;
105
+
106
+ // ============================================================
107
+ // STATE
108
+ // ============================================================
109
+
110
+ const [isLoading, setIsLoading] = useState(false);
111
+ const [loadError, setLoadError] = useState<Error | null>(null);
112
+ const [isSubmitting, setIsSubmitting] = useState(false);
113
+
114
+ // Extra readonly fields discovered from read() response (Context-derived)
115
+ const [extraReadonlyFields, setExtraReadonlyFields] = useState<
116
+ Record<string, BaseField<unknown>>
117
+ >({});
118
+
119
+ // Prevent concurrent update calls
120
+ const isComputingRef = useRef(false);
121
+
122
+ // Track last data reference to avoid unnecessary resets
123
+ const lastResetDataRef = useRef<Record<string, unknown> | null>(null);
124
+
125
+ // ============================================================
126
+ // 1. FETCH BP METADATA
127
+ // ============================================================
128
+
129
+ const { data: bpSchema, isLoading: isBpLoading } = useQuery({
130
+ queryKey: ['bp-metadata', activity.meta.businessProcessId],
131
+ queryFn: () => getBdoSchema(activity.meta.businessProcessId),
132
+ staleTime: 30 * 60 * 1000,
133
+ });
134
+
135
+ // ============================================================
136
+ // 2. EXTRACT ACTIVITY DEFINITION FROM BP BLOB
137
+ // ============================================================
138
+
139
+ const activityDef = useMemo(() => {
140
+ const blob = bpSchema?.BDOBlob;
141
+ if (!blob?.Activity) return null;
142
+ return (
143
+ (blob.Activity as any[]).find(
144
+ (a: any) => a.Id === activity.meta.activityId,
145
+ ) ?? null
146
+ );
147
+ }, [bpSchema, activity.meta.activityId]);
148
+
149
+ const isMetadataLoading = isBpLoading;
150
+
151
+ // ============================================================
152
+ // 5. BUILD DYNAMIC FIELDS FROM INPUT METADATA
153
+ // ============================================================
154
+
155
+ const dynamicFields = useMemo(() => {
156
+ if (!activityDef?.Input) return activity._getFields(); // fallback to class fields
157
+ return buildFieldsFromInput(activityDef.Input as Record<string, Record<string, unknown>>);
158
+ }, [activityDef, activity]);
159
+
160
+ // ============================================================
161
+ // 5b. COMBINE ALL FIELDS (Input + extra readonly from Context)
162
+ // ============================================================
163
+
164
+ const allFields = useMemo(
165
+ () => ({
166
+ ...extraReadonlyFields, // readonly fields from other activities
167
+ ...dynamicFields, // Input fields (editable + readonly)
168
+ }),
169
+ [extraReadonlyFields, dynamicFields],
170
+ );
171
+
172
+ // Identify readonly fields (Formula fields, or fields with ReadOnly: true)
173
+ const readonlyFieldNames = useMemo<string[]>(
174
+ () => Object.keys(allFields).filter((k) => allFields[k].readOnly),
175
+ [allFields],
176
+ );
177
+
178
+ // ============================================================
179
+ // 6. RESOLVER (from dynamic fields — type + constraint validation)
180
+ // ============================================================
181
+
182
+ const resolver = useMemo(
183
+ () => createActivityResolver(dynamicFields),
184
+ [dynamicFields],
185
+ );
186
+
187
+ // ============================================================
188
+ // REACT HOOK FORM
189
+ // ============================================================
190
+
191
+ // NOTE: Don't use `values` prop — it continuously syncs and overrides
192
+ // setValue() calls for unregistered fields (Image/File attachments).
193
+ // Instead, we reset once when record arrives (see useEffect below).
194
+ const rhf = useReactHookForm({
195
+ mode,
196
+ defaultValues: defaultValues as any,
197
+ resolver,
198
+ });
199
+
200
+ // ============================================================
201
+ // ACTIVITY OPERATIONS
202
+ // ============================================================
203
+
204
+ const activityRef = useMemo(() => activity._getOps(), [activity]);
205
+
206
+ // ============================================================
207
+ // ITEM PROXY (always in sync with RHF state)
208
+ // ============================================================
209
+
210
+ const item = useMemo(
211
+ () =>
212
+ createActivityItemProxy(activity, rhf as any, activity_instance_id),
213
+ [activity, rhf, activity_instance_id],
214
+ );
215
+
216
+ // ============================================================
217
+ // LOAD EXISTING DATA — activity.read() on mount
218
+ // ============================================================
219
+
220
+ useEffect(() => {
221
+ if (!enabled || isMetadataLoading) return;
222
+
223
+ let active = true;
224
+
225
+ const loadData = async () => {
226
+ setIsLoading(true);
227
+ setLoadError(null);
228
+
229
+ try {
230
+ const data = await activityRef.read(activity_instance_id);
231
+
232
+ if (!active) return;
233
+
234
+ // Populate form with existing data
235
+ if (data && typeof data === 'object') {
236
+ const coerced = coerceRecordForForm(
237
+ dynamicFields,
238
+ data as Record<string, unknown>,
239
+ );
240
+ const merged = { ...defaultValues, ...coerced };
241
+
242
+ if (
243
+ lastResetDataRef.current === null ||
244
+ data !== lastResetDataRef.current
245
+ ) {
246
+ rhf.reset(merged as any);
247
+ lastResetDataRef.current = data as Record<string, unknown>;
248
+ }
249
+
250
+ // Detect extra fields from Context (not in Input metadata)
251
+ const activitySystemFields = new Set([
252
+ '_id',
253
+ 'BPInstanceId',
254
+ 'Status',
255
+ 'AssignedTo',
256
+ 'CompletedAt',
257
+ '_created_at',
258
+ '_modified_at',
259
+ '_created_by',
260
+ '_modified_by',
261
+ '_v',
262
+ '_m_v',
263
+ ]);
264
+
265
+ const extras: Record<string, BaseField<unknown>> = {};
266
+ for (const key of Object.keys(data)) {
267
+ if (!dynamicFields[key] && !activitySystemFields.has(key)) {
268
+ // Look up field definition from other activities in the BP
269
+ const fieldDef = findFieldInBpActivities(
270
+ activityDef,
271
+ bpSchema,
272
+ key,
273
+ );
274
+ if (fieldDef) {
275
+ extras[key] = createFieldFromMeta(key, {
276
+ ...fieldDef,
277
+ ReadOnly: true,
278
+ });
279
+ }
280
+ }
281
+ }
282
+ if (Object.keys(extras).length > 0) {
283
+ setExtraReadonlyFields(extras);
284
+ }
285
+ }
286
+ } catch (error) {
287
+ if (!active) return;
288
+ console.error('Failed to read activity data:', error);
289
+ setLoadError(toError(error));
290
+ } finally {
291
+ if (active) {
292
+ setIsLoading(false);
293
+ }
294
+ }
295
+ };
296
+
297
+ loadData();
298
+
299
+ return () => {
300
+ active = false;
301
+ };
302
+ // eslint-disable-next-line react-hooks/exhaustive-deps
303
+ }, [enabled, isMetadataLoading, activityRef, activity_instance_id]);
304
+
305
+ // ============================================================
306
+ // SYNC FIELD — validate + send single field value to API
307
+ // ============================================================
308
+
309
+ const syncField = useCallback(
310
+ async (fieldName: string) => {
311
+ if (isComputingRef.current) return;
312
+ isComputingRef.current = true;
313
+
314
+ try {
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],
362
+ );
363
+
364
+ // ============================================================
365
+ // MODE-AWARE HANDLER ENHANCEMENT
366
+ // Injects syncField into onChange/onBlur based on consumer's mode
367
+ // ============================================================
368
+
369
+ const syncOnChange = mode === 'onChange' || mode === 'all';
370
+ const syncOnBlur = mode === 'onBlur' || mode === 'onTouched' || mode === 'all';
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
+ });
401
+
402
+ if (isReadonly) {
403
+ return { ...result, disabled: true as const };
404
+ }
405
+
406
+ return result;
407
+ },
408
+ [rhf, allFields, syncField, syncOnBlur, syncOnChange],
409
+ ) as UseActivityFormReturn<A>['register'];
410
+
411
+ // ============================================================
412
+ // ENHANCED CONTROL (for Controller — same sync behavior as register)
413
+ // ============================================================
414
+
415
+ const enhancedControl = useMemo(
416
+ () =>
417
+ new Proxy(rhf.control, {
418
+ get(target, prop, receiver) {
419
+ if (prop === 'register') {
420
+ return (name: string, options?: any) => {
421
+ const result = target.register(name as any, options);
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
+ },
448
+ }),
449
+ [rhf.control, syncField, syncOnChange, syncOnBlur],
450
+ );
451
+
452
+ // ============================================================
453
+ // HANDLE SUBMIT — activity.update()
454
+ // ============================================================
455
+
456
+ const handleSubmit = useCallback(
457
+ (
458
+ onSuccess?: (
459
+ data: AllActivityFields<A>,
460
+ e?: React.BaseSyntheticEvent,
461
+ ) => void | Promise<void>,
462
+ onError?: (
463
+ error: any,
464
+ e?: React.BaseSyntheticEvent,
465
+ ) => void | Promise<void>,
466
+ ) => {
467
+ return rhf.handleSubmit(
468
+ async (data, event) => {
469
+ setIsSubmitting(true);
470
+
471
+ try {
472
+ // Only send dirty (changed) fields — matches useForm update behavior
473
+ // Use getValues() to capture Image/File values set via setValue()
474
+ // that RHF resolver doesn't include in `data`
475
+ const cleanedData: Record<string, unknown> = {};
476
+ const readonlySet = new Set(readonlyFieldNames);
477
+ const dirtyFields = rhf.formState.dirtyFields;
478
+ const allValues = rhf.getValues() as Record<string, unknown>;
479
+
480
+ for (const key of Object.keys(allValues)) {
481
+ if (readonlySet.has(key) || !dirtyFields[key]) continue;
482
+ const value =
483
+ allValues[key] !== undefined
484
+ ? allValues[key]
485
+ : (data as Record<string, unknown>)[key];
486
+ const field = allFields[key];
487
+ cleanedData[key] = field
488
+ ? coerceFieldValue(field, value)
489
+ : value;
490
+ }
491
+
492
+ // Save via activity.update()
493
+ if (Object.keys(cleanedData).length > 0) {
494
+ await activityRef.update(
495
+ activity_instance_id,
496
+ cleanedData as any,
497
+ );
498
+ }
499
+
500
+ await onSuccess?.(data as AllActivityFields<A>, event);
501
+ } catch (error) {
502
+ onError?.(toError(error), event);
503
+ } finally {
504
+ setIsSubmitting(false);
505
+ }
506
+ },
507
+ (errors, event) => {
508
+ onError?.(errors, event);
509
+ },
510
+ );
511
+ },
512
+ [rhf, activityRef, readonlyFieldNames, allFields, activity_instance_id],
513
+ ) as UseActivityFormReturn<A>['handleSubmit'];
514
+
515
+ // ============================================================
516
+ // HANDLE COMPLETE — activity.update() + activity.complete()
517
+ // ============================================================
518
+
519
+ const handleComplete = useCallback(
520
+ (
521
+ onSuccess?: (
522
+ data: AllActivityFields<A>,
523
+ e?: React.BaseSyntheticEvent,
524
+ ) => void | Promise<void>,
525
+ onError?: (
526
+ error: any,
527
+ e?: React.BaseSyntheticEvent,
528
+ ) => void | Promise<void>,
529
+ ) => {
530
+ return rhf.handleSubmit(
531
+ async (data, event) => {
532
+ setIsSubmitting(true);
533
+
534
+ try {
535
+ // Only send dirty (changed) fields — matches useForm update behavior
536
+ // Use getValues() to capture Image/File values set via setValue()
537
+ // that RHF resolver doesn't include in `data`
538
+ const cleanedData: Record<string, unknown> = {};
539
+ const readonlySet = new Set(readonlyFieldNames);
540
+ const dirtyFields = rhf.formState.dirtyFields;
541
+ const allValues = rhf.getValues() as Record<string, unknown>;
542
+
543
+ for (const key of Object.keys(allValues)) {
544
+ if (readonlySet.has(key) || !dirtyFields[key]) continue;
545
+ const value =
546
+ allValues[key] !== undefined
547
+ ? allValues[key]
548
+ : (data as Record<string, unknown>)[key];
549
+ const field = allFields[key];
550
+ cleanedData[key] = field
551
+ ? coerceFieldValue(field, value)
552
+ : value;
553
+ }
554
+
555
+ if (Object.keys(cleanedData).length > 0) {
556
+ await activityRef.update(
557
+ activity_instance_id,
558
+ cleanedData as any,
559
+ );
560
+ }
561
+ await activityRef.complete(activity_instance_id);
562
+ await onSuccess?.(data as AllActivityFields<A>, event);
563
+ } catch (error) {
564
+ onError?.(toError(error), event);
565
+ } finally {
566
+ setIsSubmitting(false);
567
+ }
568
+ },
569
+ (errors, event) => {
570
+ onError?.(errors, event);
571
+ },
572
+ );
573
+ },
574
+ [rhf, activityRef, readonlyFieldNames, allFields, activity_instance_id],
575
+ ) as UseActivityFormReturn<A>['handleComplete'];
576
+
577
+ // ============================================================
578
+ // CLEAR ERRORS
579
+ // ============================================================
580
+
581
+ const clearErrors = useCallback((): void => {
582
+ rhf.clearErrors();
583
+ }, [rhf]);
584
+
585
+ // ============================================================
586
+ // RETURN
587
+ // ============================================================
588
+
589
+ const hasError = !!loadError;
590
+
591
+ return {
592
+ // Item proxy
593
+ item,
594
+
595
+ // Activity reference
596
+ activity,
597
+
598
+ // Form methods
599
+ register,
600
+ handleSubmit,
601
+ handleComplete,
602
+ watch: rhf.watch as any,
603
+ setValue: rhf.setValue as any,
604
+ getValues: rhf.getValues as any,
605
+ reset: rhf.reset as any,
606
+ trigger: rhf.trigger as any,
607
+ control: enhancedControl as any,
608
+
609
+ // Flattened form state
610
+ errors: rhf.formState.errors as any,
611
+ isValid: rhf.formState.isValid,
612
+ isDirty: rhf.formState.isDirty,
613
+ isSubmitting: rhf.formState.isSubmitting || isSubmitting,
614
+ isSubmitSuccessful: rhf.formState.isSubmitSuccessful,
615
+
616
+ // Loading
617
+ isLoading: isLoading || isMetadataLoading,
618
+ isMetadataLoading,
619
+ loadError,
620
+ hasError,
621
+
622
+ // Metadata
623
+ bpMetadata: bpSchema ?? null,
624
+
625
+ // Operations
626
+ clearErrors,
627
+ };
628
+ }
@@ -0,0 +1,8 @@
1
+ export { useActivityTable } from './useActivityTable';
2
+ export { ActivityTableStatus } from './types';
3
+ export type {
4
+ UseActivityTableOptionsType,
5
+ UseActivityTableReturnType,
6
+ ActivityTableStatusType,
7
+ ActivityRowType,
8
+ } from './types';
@@ -0,0 +1,45 @@
1
+ import type { Activity } from '../../../workflow/Activity';
2
+ import type { ActivityInstanceFieldsType } from '../../../workflow/types';
3
+ import type { UseTableReturnType, PaginationStateType } from '../useTable/types';
4
+ import type { UseFilterOptionsType } from '../useFilter/types';
5
+ import type { SortType } from '../../../types/common';
6
+
7
+ /**
8
+ * Activity table status — determines which API to call
9
+ */
10
+ export const ActivityTableStatus = {
11
+ InProgress: 'inprogress',
12
+ Completed: 'completed',
13
+ } as const;
14
+
15
+ export type ActivityTableStatusType =
16
+ (typeof ActivityTableStatus)[keyof typeof ActivityTableStatus];
17
+
18
+ /**
19
+ * Row type for activity table data.
20
+ * System fields at top level, entity fields under ADO.
21
+ */
22
+ export type ActivityRowType<A extends Activity<any, any, any>> =
23
+ A extends Activity<infer E, any, any>
24
+ ? ActivityInstanceFieldsType & { ADO: E }
25
+ : never;
26
+
27
+ export interface UseActivityTableOptionsType<
28
+ A extends Activity<any, any, any>,
29
+ > {
30
+ /** Which operation — determines endpoint (inprogress vs completed) */
31
+ status: ActivityTableStatusType;
32
+ /** Initial state */
33
+ initialState?: {
34
+ sort?: SortType;
35
+ pagination?: PaginationStateType;
36
+ filter?: UseFilterOptionsType<ActivityRowType<A>>;
37
+ };
38
+ /** Error callback */
39
+ onError?: (error: Error) => void;
40
+ /** Success callback with row data */
41
+ onSuccess?: (data: ActivityRowType<A>[]) => void;
42
+ }
43
+
44
+ export type UseActivityTableReturnType<A extends Activity<any, any, any>> =
45
+ UseTableReturnType<ActivityRowType<A>>;