@ram_28/kf-ai-sdk 2.0.2 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BaseField-B6da88U7.js +40 -0
- package/dist/BaseField-Drp0-OxL.cjs +1 -0
- package/dist/api/client.d.ts +7 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api.cjs +1 -1
- package/dist/api.mjs +3 -3
- package/dist/auth.cjs +1 -1
- package/dist/auth.mjs +2 -2
- package/dist/bdo/core/BaseBdo.d.ts +4 -0
- package/dist/bdo/core/BaseBdo.d.ts.map +1 -1
- package/dist/bdo.cjs +1 -1
- package/dist/bdo.mjs +91 -118
- package/dist/client-BULEEaCP.js +222 -0
- package/dist/client-DtPpfJc1.cjs +1 -0
- package/dist/components/hooks/useForm/index.d.ts +1 -1
- package/dist/components/hooks/useForm/index.d.ts.map +1 -1
- package/dist/components/hooks/useForm/types.d.ts +15 -2
- package/dist/components/hooks/useForm/types.d.ts.map +1 -1
- package/dist/components/hooks/useForm/useDraftInteraction.d.ts +26 -0
- package/dist/components/hooks/useForm/useDraftInteraction.d.ts.map +1 -0
- package/dist/components/hooks/useForm/useForm.d.ts +1 -0
- package/dist/components/hooks/useForm/useForm.d.ts.map +1 -1
- package/dist/{constants-CM9xOACN.js → constants-BQrBcCON.js} +5 -5
- package/dist/error-handling-CAoD0Kwb.cjs +1 -0
- package/dist/error-handling-CrhTtD88.js +14 -0
- package/dist/filter.mjs +1 -1
- package/dist/form.cjs +1 -1
- package/dist/form.mjs +308 -1187
- package/dist/form.types.d.ts +1 -1
- package/dist/form.types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm-Cj63v5ny.js +1014 -0
- package/dist/index.esm-DuwT11sx.cjs +1 -0
- package/dist/{metadata-BN57S6W9.cjs → metadata-BJWukIqS.cjs} +1 -1
- package/dist/{metadata-P7DGCgIG.js → metadata-CJuFxytC.js} +1 -1
- package/dist/table.cjs +1 -1
- package/dist/table.mjs +83 -93
- package/dist/types/constants.d.ts +2 -2
- package/dist/types/constants.d.ts.map +1 -1
- package/dist/workflow/Activity.d.ts +85 -0
- package/dist/workflow/Activity.d.ts.map +1 -0
- package/dist/workflow/ActivityInstance.d.ts +96 -0
- package/dist/workflow/ActivityInstance.d.ts.map +1 -0
- package/dist/workflow/client.d.ts +39 -0
- package/dist/workflow/client.d.ts.map +1 -0
- package/dist/workflow/components/useActivityForm/createActivityItemProxy.d.ts +16 -0
- package/dist/workflow/components/useActivityForm/createActivityItemProxy.d.ts.map +1 -0
- package/dist/workflow/components/useActivityForm/createActivityResolver.d.ts +22 -0
- package/dist/workflow/components/useActivityForm/createActivityResolver.d.ts.map +1 -0
- package/dist/workflow/components/useActivityForm/index.d.ts +3 -0
- package/dist/workflow/components/useActivityForm/index.d.ts.map +1 -0
- package/dist/workflow/components/useActivityForm/types.d.ts +80 -0
- package/dist/workflow/components/useActivityForm/types.d.ts.map +1 -0
- package/dist/workflow/components/useActivityForm/useActivityForm.d.ts +4 -0
- package/dist/workflow/components/useActivityForm/useActivityForm.d.ts.map +1 -0
- package/dist/workflow/index.d.ts +8 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/types.d.ts +53 -0
- package/dist/workflow/types.d.ts.map +1 -0
- package/dist/workflow.cjs +1 -0
- package/dist/workflow.d.ts +8 -0
- package/dist/workflow.d.ts.map +1 -0
- package/dist/workflow.mjs +565 -0
- package/dist/workflow.types.cjs +1 -0
- package/dist/workflow.types.d.ts +2 -0
- package/dist/workflow.types.d.ts.map +1 -0
- package/dist/workflow.types.mjs +1 -0
- package/docs/workflow.md +703 -0
- package/package.json +21 -1
- package/sdk/api/client.ts +85 -52
- package/sdk/api/index.ts +1 -0
- package/sdk/bdo/core/BaseBdo.ts +10 -0
- package/sdk/components/hooks/useForm/index.ts +1 -0
- package/sdk/components/hooks/useForm/types.ts +17 -3
- package/sdk/components/hooks/useForm/useDraftInteraction.ts +251 -0
- package/sdk/components/hooks/useForm/useForm.ts +106 -19
- package/sdk/form.types.ts +1 -0
- package/sdk/index.ts +6 -0
- package/sdk/types/constants.ts +2 -2
- package/sdk/workflow/Activity.ts +181 -0
- package/sdk/workflow/ActivityInstance.ts +339 -0
- package/sdk/workflow/client.ts +208 -0
- package/sdk/workflow/components/useActivityForm/createActivityItemProxy.ts +126 -0
- package/sdk/workflow/components/useActivityForm/createActivityResolver.ts +61 -0
- package/sdk/workflow/components/useActivityForm/index.ts +5 -0
- package/sdk/workflow/components/useActivityForm/types.ts +166 -0
- package/sdk/workflow/components/useActivityForm/useActivityForm.ts +386 -0
- package/sdk/workflow/index.ts +20 -0
- package/sdk/workflow/types.ts +84 -0
- package/sdk/workflow.ts +25 -0
- package/sdk/workflow.types.ts +11 -0
- package/dist/client-Bo-RLKJi.cjs +0 -1
- package/dist/client-eA4VvNTo.js +0 -178
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
+
import type { UseFormReturn, FieldValues } from "react-hook-form";
|
|
3
|
+
import type { BaseField } from "../../../bdo/fields/BaseField";
|
|
4
|
+
import type { InteractiveCreatableBdo } from "./types";
|
|
5
|
+
|
|
6
|
+
// ============================================================
|
|
7
|
+
// TYPES
|
|
8
|
+
// ============================================================
|
|
9
|
+
|
|
10
|
+
interface UseDraftInteractionOptions {
|
|
11
|
+
/** The BDO instance — must expose interactive draft methods */
|
|
12
|
+
bdo: InteractiveCreatableBdo;
|
|
13
|
+
/** RHF form instance */
|
|
14
|
+
form: UseFormReturn<FieldValues>;
|
|
15
|
+
/** RHF validation mode */
|
|
16
|
+
mode: "onBlur" | "onChange" | "onSubmit" | "onTouched" | "all";
|
|
17
|
+
/** BDO field definitions (for determining dirty fields) */
|
|
18
|
+
fields: Record<string, BaseField<unknown>>;
|
|
19
|
+
/** Whether interactive mode is enabled */
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface UseDraftInteractionReturn {
|
|
24
|
+
draftId: string | undefined;
|
|
25
|
+
isInitializingDraft: boolean;
|
|
26
|
+
isInteracting: boolean;
|
|
27
|
+
interactionError: Error | null;
|
|
28
|
+
triggerInteraction: () => void;
|
|
29
|
+
commitDraft: (dirtyData: Record<string, unknown>) => Promise<unknown>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ============================================================
|
|
33
|
+
// DEBOUNCE DELAY
|
|
34
|
+
// ============================================================
|
|
35
|
+
|
|
36
|
+
const DEBOUNCE_MS = 300;
|
|
37
|
+
|
|
38
|
+
// ============================================================
|
|
39
|
+
// HOOK
|
|
40
|
+
// ============================================================
|
|
41
|
+
|
|
42
|
+
export function useDraftInteraction(
|
|
43
|
+
options: UseDraftInteractionOptions,
|
|
44
|
+
): UseDraftInteractionReturn {
|
|
45
|
+
const { bdo, form, mode, fields, enabled } = options;
|
|
46
|
+
|
|
47
|
+
// ============================================================
|
|
48
|
+
// STATE
|
|
49
|
+
// ============================================================
|
|
50
|
+
|
|
51
|
+
const [draftId, setDraftId] = useState<string | undefined>(undefined);
|
|
52
|
+
const [isInitializingDraft, setIsInitializingDraft] = useState(false);
|
|
53
|
+
const [isInteracting, setIsInteracting] = useState(false);
|
|
54
|
+
const [interactionError, setInteractionError] = useState<Error | null>(null);
|
|
55
|
+
|
|
56
|
+
// ============================================================
|
|
57
|
+
// REFS (for concurrency control & loop prevention)
|
|
58
|
+
// ============================================================
|
|
59
|
+
|
|
60
|
+
/** Tracks the latest interaction call — stale responses are discarded */
|
|
61
|
+
const interactionCounterRef = useRef(0);
|
|
62
|
+
|
|
63
|
+
/** Prevents re-trigger when applying computed values via setValue */
|
|
64
|
+
const isApplyingComputedRef = useRef(false);
|
|
65
|
+
|
|
66
|
+
/** Debounce timer for onChange mode */
|
|
67
|
+
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
68
|
+
|
|
69
|
+
/** AbortController for cleanup on unmount */
|
|
70
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
71
|
+
|
|
72
|
+
// ============================================================
|
|
73
|
+
// DRAFT INITIALIZATION (Create mode only)
|
|
74
|
+
// ============================================================
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!enabled) return;
|
|
78
|
+
|
|
79
|
+
let cancelled = false;
|
|
80
|
+
setIsInitializingDraft(true);
|
|
81
|
+
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
abortControllerRef.current = controller;
|
|
84
|
+
|
|
85
|
+
bdo
|
|
86
|
+
.draftInteraction({})
|
|
87
|
+
.then((response) => {
|
|
88
|
+
if (cancelled || controller.signal.aborted) return;
|
|
89
|
+
const id = response._id;
|
|
90
|
+
setDraftId(id);
|
|
91
|
+
// Set _id into form without marking dirty
|
|
92
|
+
form.setValue("_id" as any, id, { shouldDirty: false });
|
|
93
|
+
setInteractionError(null);
|
|
94
|
+
})
|
|
95
|
+
.catch((error) => {
|
|
96
|
+
if (cancelled || controller.signal.aborted) return;
|
|
97
|
+
setInteractionError(error instanceof Error ? error : new Error(String(error)));
|
|
98
|
+
})
|
|
99
|
+
.finally(() => {
|
|
100
|
+
if (!cancelled) setIsInitializingDraft(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return () => {
|
|
104
|
+
cancelled = true;
|
|
105
|
+
controller.abort();
|
|
106
|
+
};
|
|
107
|
+
}, [enabled, bdo, form]);
|
|
108
|
+
|
|
109
|
+
// ============================================================
|
|
110
|
+
// CORE INTERACTION LOGIC
|
|
111
|
+
// ============================================================
|
|
112
|
+
|
|
113
|
+
const executeInteraction = useCallback(async () => {
|
|
114
|
+
if (!enabled || !draftId) return;
|
|
115
|
+
if (isApplyingComputedRef.current) return;
|
|
116
|
+
|
|
117
|
+
// Build payload from dirty fields
|
|
118
|
+
const dirtyFields = form.formState.dirtyFields;
|
|
119
|
+
const allValues = form.getValues();
|
|
120
|
+
const payload: Record<string, unknown> = {};
|
|
121
|
+
|
|
122
|
+
for (const [key, isDirty] of Object.entries(dirtyFields)) {
|
|
123
|
+
if (isDirty && fields[key] && !fields[key].readOnly) {
|
|
124
|
+
payload[key] = allValues[key];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Skip if nothing changed
|
|
129
|
+
if (Object.keys(payload).length === 0) return;
|
|
130
|
+
|
|
131
|
+
// Increment counter for concurrency control
|
|
132
|
+
const currentCounter = ++interactionCounterRef.current;
|
|
133
|
+
|
|
134
|
+
setIsInteracting(true);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const response = await bdo.draftInteraction({
|
|
138
|
+
_id: draftId,
|
|
139
|
+
...payload,
|
|
140
|
+
} as any);
|
|
141
|
+
|
|
142
|
+
// Only apply if this is still the latest interaction
|
|
143
|
+
if (currentCounter !== interactionCounterRef.current) return;
|
|
144
|
+
|
|
145
|
+
// Apply computed values back to form
|
|
146
|
+
isApplyingComputedRef.current = true;
|
|
147
|
+
try {
|
|
148
|
+
for (const [key, value] of Object.entries(response)) {
|
|
149
|
+
// Skip _id and fields the user has dirty (don't overwrite user input)
|
|
150
|
+
if (key === "_id") continue;
|
|
151
|
+
if (dirtyFields[key]) continue;
|
|
152
|
+
|
|
153
|
+
form.setValue(key as any, value, {
|
|
154
|
+
shouldDirty: false,
|
|
155
|
+
shouldValidate: false,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
} finally {
|
|
159
|
+
// Reset flag after React settles
|
|
160
|
+
// Using setTimeout(0) ensures setValue batch is complete
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
isApplyingComputedRef.current = false;
|
|
163
|
+
}, 0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
setInteractionError(null);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
// Only set error if this is still the latest interaction
|
|
169
|
+
if (currentCounter !== interactionCounterRef.current) return;
|
|
170
|
+
setInteractionError(error instanceof Error ? error : new Error(String(error)));
|
|
171
|
+
} finally {
|
|
172
|
+
if (currentCounter === interactionCounterRef.current) {
|
|
173
|
+
setIsInteracting(false);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}, [enabled, draftId, form, fields, bdo]);
|
|
177
|
+
|
|
178
|
+
// ============================================================
|
|
179
|
+
// TRIGGER INTERACTION (with optional debounce)
|
|
180
|
+
// ============================================================
|
|
181
|
+
|
|
182
|
+
const triggerInteraction = useCallback(() => {
|
|
183
|
+
if (!enabled) return;
|
|
184
|
+
|
|
185
|
+
// For onChange/all modes, debounce the interaction
|
|
186
|
+
if (mode === "onChange" || mode === "all") {
|
|
187
|
+
if (debounceTimerRef.current) {
|
|
188
|
+
clearTimeout(debounceTimerRef.current);
|
|
189
|
+
}
|
|
190
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
191
|
+
executeInteraction();
|
|
192
|
+
}, DEBOUNCE_MS);
|
|
193
|
+
} else {
|
|
194
|
+
// For onBlur/onTouched/onSubmit, trigger immediately
|
|
195
|
+
executeInteraction();
|
|
196
|
+
}
|
|
197
|
+
}, [enabled, mode, executeInteraction]);
|
|
198
|
+
|
|
199
|
+
// ============================================================
|
|
200
|
+
// COMMIT DRAFT (for handleSubmit)
|
|
201
|
+
// ============================================================
|
|
202
|
+
|
|
203
|
+
const commitDraft = useCallback(
|
|
204
|
+
async (dirtyData: Record<string, unknown>): Promise<unknown> => {
|
|
205
|
+
return bdo.draft({
|
|
206
|
+
_id: draftId,
|
|
207
|
+
...dirtyData,
|
|
208
|
+
} as any);
|
|
209
|
+
},
|
|
210
|
+
[bdo, draftId],
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// ============================================================
|
|
214
|
+
// CLEANUP
|
|
215
|
+
// ============================================================
|
|
216
|
+
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
return () => {
|
|
219
|
+
if (debounceTimerRef.current) {
|
|
220
|
+
clearTimeout(debounceTimerRef.current);
|
|
221
|
+
}
|
|
222
|
+
abortControllerRef.current?.abort();
|
|
223
|
+
};
|
|
224
|
+
}, []);
|
|
225
|
+
|
|
226
|
+
// ============================================================
|
|
227
|
+
// DISABLED MODE (return no-ops)
|
|
228
|
+
// ============================================================
|
|
229
|
+
|
|
230
|
+
if (!enabled) {
|
|
231
|
+
return {
|
|
232
|
+
draftId: undefined,
|
|
233
|
+
isInitializingDraft: false,
|
|
234
|
+
isInteracting: false,
|
|
235
|
+
interactionError: null,
|
|
236
|
+
triggerInteraction: () => {},
|
|
237
|
+
commitDraft: async () => {
|
|
238
|
+
throw new Error("Draft interaction is not enabled");
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
draftId,
|
|
245
|
+
isInitializingDraft,
|
|
246
|
+
isInteracting,
|
|
247
|
+
interactionError,
|
|
248
|
+
triggerInteraction,
|
|
249
|
+
commitDraft,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { useQuery } from "@tanstack/react-query";
|
|
11
11
|
import { createResolver } from "./createResolver";
|
|
12
12
|
import { createItemProxy } from "./createItemProxy";
|
|
13
|
+
import { useDraftInteraction } from "./useDraftInteraction";
|
|
13
14
|
import { getBdoSchema } from "../../../api/metadata";
|
|
14
15
|
import type { BaseBdo } from "../../../bdo";
|
|
15
16
|
import type {
|
|
@@ -19,6 +20,7 @@ import type {
|
|
|
19
20
|
AllFieldsType,
|
|
20
21
|
CreatableBdo,
|
|
21
22
|
UpdatableBdo,
|
|
23
|
+
InteractiveCreatableBdo,
|
|
22
24
|
} from "./types";
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -33,6 +35,7 @@ import type {
|
|
|
33
35
|
* - Smart register: auto-disables readonly fields
|
|
34
36
|
* - Payload filtering: handleSubmit auto-filters to editable fields only
|
|
35
37
|
* - Constraint validation: auto-validates required, length, etc. from field meta
|
|
38
|
+
* - Interactive draft mode: real-time server-side computation on field blur/change
|
|
36
39
|
*/
|
|
37
40
|
export function useForm<B extends BaseBdo<any, any, any>>(
|
|
38
41
|
options: UseFormOptionsType<B>,
|
|
@@ -43,11 +46,30 @@ export function useForm<B extends BaseBdo<any, any, any>>(
|
|
|
43
46
|
operation: explicitOperation,
|
|
44
47
|
defaultValues,
|
|
45
48
|
mode = "onBlur",
|
|
46
|
-
enableDraft
|
|
49
|
+
enableDraft,
|
|
47
50
|
enableConstraintValidation,
|
|
48
51
|
enableExpressionValidation,
|
|
49
52
|
} = options;
|
|
50
53
|
|
|
54
|
+
// ============================================================
|
|
55
|
+
// INTERACTION MODE RESOLUTION
|
|
56
|
+
// ============================================================
|
|
57
|
+
|
|
58
|
+
const explicitInteractionMode = (options as any).interactionMode as
|
|
59
|
+
| "interactive"
|
|
60
|
+
| "non-interactive"
|
|
61
|
+
| undefined;
|
|
62
|
+
|
|
63
|
+
const interactionMode =
|
|
64
|
+
explicitInteractionMode ??
|
|
65
|
+
(enableDraft === true
|
|
66
|
+
? "interactive"
|
|
67
|
+
: enableDraft === false
|
|
68
|
+
? "non-interactive"
|
|
69
|
+
: "interactive");
|
|
70
|
+
|
|
71
|
+
const isInteractive = interactionMode === "interactive";
|
|
72
|
+
|
|
51
73
|
// Infer operation from recordId if not explicitly provided
|
|
52
74
|
const operation = explicitOperation ?? (recordId ? "update" : "create");
|
|
53
75
|
|
|
@@ -114,6 +136,31 @@ export function useForm<B extends BaseBdo<any, any, any>>(
|
|
|
114
136
|
operation === "update" && record ? (record as FieldValues) : undefined,
|
|
115
137
|
});
|
|
116
138
|
|
|
139
|
+
// ============================================================
|
|
140
|
+
// FIELD DEFINITIONS
|
|
141
|
+
// ============================================================
|
|
142
|
+
|
|
143
|
+
const fields = bdo.getFields();
|
|
144
|
+
|
|
145
|
+
// ============================================================
|
|
146
|
+
// DRAFT INTERACTION
|
|
147
|
+
// ============================================================
|
|
148
|
+
|
|
149
|
+
const {
|
|
150
|
+
draftId,
|
|
151
|
+
isInitializingDraft,
|
|
152
|
+
isInteracting,
|
|
153
|
+
interactionError,
|
|
154
|
+
triggerInteraction,
|
|
155
|
+
commitDraft,
|
|
156
|
+
} = useDraftInteraction({
|
|
157
|
+
bdo: bdo as unknown as InteractiveCreatableBdo,
|
|
158
|
+
form,
|
|
159
|
+
mode,
|
|
160
|
+
fields,
|
|
161
|
+
enabled: isInteractive && operation === "create",
|
|
162
|
+
});
|
|
163
|
+
|
|
117
164
|
// ============================================================
|
|
118
165
|
// ITEM PROXY
|
|
119
166
|
// ============================================================
|
|
@@ -125,11 +172,9 @@ export function useForm<B extends BaseBdo<any, any, any>>(
|
|
|
125
172
|
);
|
|
126
173
|
|
|
127
174
|
// ============================================================
|
|
128
|
-
// SMART REGISTER (auto-disables readonly fields)
|
|
175
|
+
// SMART REGISTER (auto-disables readonly fields + interaction trigger)
|
|
129
176
|
// ============================================================
|
|
130
177
|
|
|
131
|
-
const fields = bdo.getFields();
|
|
132
|
-
|
|
133
178
|
const smartRegister = useCallback(
|
|
134
179
|
(name: string, registerOptions?: RegisterOptions) => {
|
|
135
180
|
const rhfResult = form.register(name as any, registerOptions);
|
|
@@ -139,11 +184,44 @@ export function useForm<B extends BaseBdo<any, any, any>>(
|
|
|
139
184
|
return { ...rhfResult, disabled: true };
|
|
140
185
|
}
|
|
141
186
|
|
|
187
|
+
// In interactive mode, wrap onBlur for blur-triggered interaction
|
|
188
|
+
if (
|
|
189
|
+
isInteractive &&
|
|
190
|
+
(mode === "onBlur" || mode === "onTouched" || mode === "all")
|
|
191
|
+
) {
|
|
192
|
+
const originalOnBlur = rhfResult.onBlur;
|
|
193
|
+
return {
|
|
194
|
+
...rhfResult,
|
|
195
|
+
onBlur: async (e: React.FocusEvent) => {
|
|
196
|
+
await originalOnBlur(e); // RHF validation first
|
|
197
|
+
triggerInteraction(); // then server interaction
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
142
202
|
return rhfResult;
|
|
143
203
|
},
|
|
144
|
-
[form, fields],
|
|
204
|
+
[form, fields, isInteractive, mode, triggerInteraction],
|
|
145
205
|
);
|
|
146
206
|
|
|
207
|
+
// ============================================================
|
|
208
|
+
// WATCH SUBSCRIPTION (for onChange/all mode interaction)
|
|
209
|
+
// ============================================================
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (!isInteractive || (mode !== "onChange" && mode !== "all")) return;
|
|
213
|
+
|
|
214
|
+
const subscription = form.watch((_value, { type }) => {
|
|
215
|
+
// RHF fires with type: "change" for user input,
|
|
216
|
+
// type: undefined for programmatic setValue — prevents re-trigger loops
|
|
217
|
+
if (type === "change") {
|
|
218
|
+
triggerInteraction(); // debounced inside useDraftInteraction
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return () => subscription.unsubscribe();
|
|
223
|
+
}, [isInteractive, mode, form, triggerInteraction]);
|
|
224
|
+
|
|
147
225
|
// ============================================================
|
|
148
226
|
// CUSTOM HANDLE SUBMIT (with API call + payload filtering)
|
|
149
227
|
// ============================================================
|
|
@@ -155,31 +233,34 @@ export function useForm<B extends BaseBdo<any, any, any>>(
|
|
|
155
233
|
async (data, e) => {
|
|
156
234
|
try {
|
|
157
235
|
const filteredData: Record<string, unknown> = {};
|
|
236
|
+
let result: unknown;
|
|
158
237
|
|
|
159
|
-
if (operation === "create") {
|
|
160
|
-
//
|
|
238
|
+
if (isInteractive && operation === "create") {
|
|
239
|
+
// Interactive create - send only dirty, non-readonly fields
|
|
240
|
+
// (record already exists from init, so only send changes)
|
|
241
|
+
const dirtyFields = form.formState.dirtyFields;
|
|
242
|
+
for (const [key, value] of Object.entries(data)) {
|
|
243
|
+
if (fields[key] && !fields[key].readOnly && dirtyFields[key]) {
|
|
244
|
+
filteredData[key] = value;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
result = await commitDraft(filteredData);
|
|
248
|
+
} else if (operation === "create") {
|
|
249
|
+
// Non-interactive create - send all known, non-readonly fields
|
|
161
250
|
for (const [key, value] of Object.entries(data)) {
|
|
162
251
|
if (fields[key] && !fields[key].readOnly) {
|
|
163
252
|
filteredData[key] = value;
|
|
164
253
|
}
|
|
165
254
|
}
|
|
255
|
+
result = await (bdo as unknown as CreatableBdo).create(filteredData);
|
|
166
256
|
} else {
|
|
167
|
-
// Update
|
|
257
|
+
// Update (always non-interactive) - send only dirty, non-readonly fields
|
|
168
258
|
const dirtyFields = form.formState.dirtyFields;
|
|
169
259
|
for (const [key, value] of Object.entries(data)) {
|
|
170
260
|
if (fields[key] && !fields[key].readOnly && dirtyFields[key]) {
|
|
171
261
|
filteredData[key] = value;
|
|
172
262
|
}
|
|
173
263
|
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
let result: unknown;
|
|
177
|
-
|
|
178
|
-
if (operation === "create") {
|
|
179
|
-
// Safe: create operation requires CreatableBdo (enforced by UseFormOptionsType)
|
|
180
|
-
result = await (bdo as unknown as CreatableBdo).create(filteredData);
|
|
181
|
-
} else {
|
|
182
|
-
// Safe: update operation requires UpdatableBdo (enforced by UseFormOptionsType)
|
|
183
264
|
result = await (bdo as unknown as UpdatableBdo).update(recordId!, filteredData);
|
|
184
265
|
}
|
|
185
266
|
|
|
@@ -196,7 +277,7 @@ export function useForm<B extends BaseBdo<any, any, any>>(
|
|
|
196
277
|
},
|
|
197
278
|
);
|
|
198
279
|
},
|
|
199
|
-
[form, bdo, operation, recordId, fields],
|
|
280
|
+
[form, bdo, operation, recordId, fields, isInteractive, commitDraft],
|
|
200
281
|
);
|
|
201
282
|
|
|
202
283
|
// ============================================================
|
|
@@ -236,10 +317,16 @@ export function useForm<B extends BaseBdo<any, any, any>>(
|
|
|
236
317
|
dirtyFields: form.formState.dirtyFields as any,
|
|
237
318
|
|
|
238
319
|
// Loading states
|
|
239
|
-
isLoading: isLoadingRecord,
|
|
320
|
+
isLoading: isLoadingRecord || isInitializingDraft,
|
|
240
321
|
isFetching: isFetchingRecord,
|
|
241
322
|
|
|
242
323
|
// Error
|
|
243
324
|
loadError: recordError as Error | null,
|
|
325
|
+
|
|
326
|
+
// Draft / Interactive mode
|
|
327
|
+
draftId,
|
|
328
|
+
isInitializingDraft,
|
|
329
|
+
isInteracting,
|
|
330
|
+
interactionError,
|
|
244
331
|
};
|
|
245
332
|
}
|
package/sdk/form.types.ts
CHANGED
package/sdk/index.ts
CHANGED
|
@@ -40,6 +40,12 @@ export type {
|
|
|
40
40
|
|
|
41
41
|
export { api, setApiBaseUrl, getApiBaseUrl, getBdoSchema } from './api';
|
|
42
42
|
|
|
43
|
+
// ============================================================
|
|
44
|
+
// WORKFLOW CLIENT - Business Process / Activity operations
|
|
45
|
+
// ============================================================
|
|
46
|
+
|
|
47
|
+
export { Workflow, useActivityForm } from './workflow';
|
|
48
|
+
|
|
43
49
|
// ============================================================
|
|
44
50
|
// TYPES - Core type definitions
|
|
45
51
|
// ============================================================
|
package/sdk/types/constants.ts
CHANGED
|
@@ -211,9 +211,9 @@ export const FormOperation = {
|
|
|
211
211
|
* });
|
|
212
212
|
*/
|
|
213
213
|
export const InteractionMode = {
|
|
214
|
-
/** Real-time server-side validation and computation on
|
|
214
|
+
/** Real-time server-side validation and computation on field blur/change */
|
|
215
215
|
Interactive: "interactive",
|
|
216
|
-
/**
|
|
216
|
+
/** No server interaction during editing — submit only */
|
|
217
217
|
NonInteractive: "non-interactive",
|
|
218
218
|
} as const;
|
|
219
219
|
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// ACTIVITY BASE CLASS
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Lean abstract base class for typed workflow activities.
|
|
5
|
+
// Replaces ActivityForm — no public getFields(), no phantom types,
|
|
6
|
+
// no exported meta type.
|
|
7
|
+
//
|
|
8
|
+
// Each activity subclass defines:
|
|
9
|
+
// - meta: { businessProcessId, activityId }
|
|
10
|
+
// - Typed field instances as class properties
|
|
11
|
+
//
|
|
12
|
+
// Methods:
|
|
13
|
+
// activity.getInstanceList(options) // list activity instances
|
|
14
|
+
// activity.instanceMetrics(options) // get aggregated metrics
|
|
15
|
+
// activity.getInstance(instanceId) // get typed ActivityInstance
|
|
16
|
+
|
|
17
|
+
import { Workflow } from "./client";
|
|
18
|
+
import { createActivityInstance } from "./ActivityInstance";
|
|
19
|
+
import type { ActivityInstanceType } from "./ActivityInstance";
|
|
20
|
+
import type { ActivityInstanceFieldsType, ActivityOperations } from "./types";
|
|
21
|
+
import type {
|
|
22
|
+
ListOptionsType,
|
|
23
|
+
ListResponseType,
|
|
24
|
+
MetricOptionsType,
|
|
25
|
+
MetricResponseType,
|
|
26
|
+
} from "../types/common";
|
|
27
|
+
import { BaseField } from "../bdo/fields/BaseField";
|
|
28
|
+
|
|
29
|
+
// ============================================================
|
|
30
|
+
// ABSTRACT BASE CLASS
|
|
31
|
+
// ============================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Abstract base class for workflow activities.
|
|
35
|
+
*
|
|
36
|
+
* Extend this class to define typed activity input fields.
|
|
37
|
+
* Each activity is a typed class with field definitions and data operations.
|
|
38
|
+
*
|
|
39
|
+
* @template TEntity - The full entity type with all fields
|
|
40
|
+
* @template TEditable - Fields the user can edit
|
|
41
|
+
* @template TReadonly - Fields that are read-only (computed / system)
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* class EmployeeInputActivity extends Activity<
|
|
46
|
+
* EmployeeInputEntityType,
|
|
47
|
+
* EmployeeInputEditable,
|
|
48
|
+
* EmployeeInputReadonly
|
|
49
|
+
* > {
|
|
50
|
+
* readonly meta = {
|
|
51
|
+
* businessProcessId: "SimpleLeaveProcess",
|
|
52
|
+
* activityId: "EMPLOYEE_INPUT",
|
|
53
|
+
* };
|
|
54
|
+
*
|
|
55
|
+
* readonly StartDate = new DateTimeField({ id: "StartDate", label: "Start Date" });
|
|
56
|
+
* readonly EndDate = new DateTimeField({ id: "EndDate", label: "End Date" });
|
|
57
|
+
* readonly LeaveDays = new NumberField({ id: "LeaveDays", label: "Leave Days", editable: false });
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
* const activity = new EmployeeInputActivity();
|
|
61
|
+
* const items = await activity.getInstanceList({ Page: 1, PageSize: 10 });
|
|
62
|
+
* const instance = await activity.getInstance("inst_123");
|
|
63
|
+
* instance.StartDate.get(); // typed value
|
|
64
|
+
* instance.StartDate.set("2026-03-01");
|
|
65
|
+
* await instance.update({ StartDate: "2026-03-01" });
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export abstract class Activity<
|
|
69
|
+
TEntity extends Record<string, unknown>,
|
|
70
|
+
TEditable extends Record<string, unknown> = TEntity,
|
|
71
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
72
|
+
TReadonly extends Record<string, unknown> = {},
|
|
73
|
+
> {
|
|
74
|
+
/**
|
|
75
|
+
* Activity metadata — identifies the business process and activity
|
|
76
|
+
*/
|
|
77
|
+
abstract readonly meta: {
|
|
78
|
+
readonly businessProcessId: string;
|
|
79
|
+
readonly activityId: string;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ============================================================
|
|
83
|
+
// ACTIVITY OPERATIONS (internal)
|
|
84
|
+
// ============================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get ActivityOperations for this activity.
|
|
88
|
+
* @internal
|
|
89
|
+
*/
|
|
90
|
+
private _ops(): ActivityOperations<TEntity> {
|
|
91
|
+
return new Workflow<TEntity>(this.meta.businessProcessId)
|
|
92
|
+
.activity(this.meta.activityId);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================================
|
|
96
|
+
// FIELD DISCOVERY (internal)
|
|
97
|
+
// ============================================================
|
|
98
|
+
|
|
99
|
+
private _fieldsCache: Record<string, BaseField<unknown>> | null = null;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Discover BaseField instances from subclass properties.
|
|
103
|
+
* @internal
|
|
104
|
+
*/
|
|
105
|
+
private _discoverFields(): Record<string, BaseField<unknown>> {
|
|
106
|
+
if (this._fieldsCache) return this._fieldsCache;
|
|
107
|
+
|
|
108
|
+
const fields: Record<string, BaseField<unknown>> = {};
|
|
109
|
+
for (const key of Object.keys(this)) {
|
|
110
|
+
const value = (this as Record<string, unknown>)[key];
|
|
111
|
+
if (value instanceof BaseField) {
|
|
112
|
+
fields[key] = value;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this._fieldsCache = fields;
|
|
117
|
+
return fields;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============================================================
|
|
121
|
+
// PUBLIC METHODS
|
|
122
|
+
// ============================================================
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* List activity instances with optional filtering/pagination.
|
|
126
|
+
*/
|
|
127
|
+
async getInstanceList(
|
|
128
|
+
options?: ListOptionsType,
|
|
129
|
+
): Promise<ListResponseType<ActivityInstanceFieldsType & TEntity>> {
|
|
130
|
+
return this._ops().list(options);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get aggregated metrics for activity instances.
|
|
135
|
+
*/
|
|
136
|
+
async instanceMetrics(
|
|
137
|
+
options: Omit<MetricOptionsType, "Type">,
|
|
138
|
+
): Promise<MetricResponseType> {
|
|
139
|
+
return this._ops().metric(options);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get a typed ActivityInstance with field accessors and persistence methods.
|
|
144
|
+
*
|
|
145
|
+
* @param instanceId - The activity instance identifier
|
|
146
|
+
* @returns ActivityInstance with typed field accessors
|
|
147
|
+
*/
|
|
148
|
+
async getInstance(
|
|
149
|
+
instanceId: string,
|
|
150
|
+
): Promise<ActivityInstanceType<TEntity, TEditable, TReadonly>> {
|
|
151
|
+
const ops = this._ops();
|
|
152
|
+
const data = await ops.read(instanceId);
|
|
153
|
+
const fields = this._discoverFields();
|
|
154
|
+
return createActivityInstance<TEntity, TEditable, TReadonly>(
|
|
155
|
+
ops,
|
|
156
|
+
instanceId,
|
|
157
|
+
data as TEntity,
|
|
158
|
+
fields,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================================
|
|
163
|
+
// INTERNAL ACCESSORS (used by useActivityForm hook)
|
|
164
|
+
// ============================================================
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get discovered fields — used internally by useActivityForm hook.
|
|
168
|
+
* @internal
|
|
169
|
+
*/
|
|
170
|
+
_getFields(): Record<string, BaseField<unknown>> {
|
|
171
|
+
return this._discoverFields();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get ActivityOperations — used internally by useActivityForm hook.
|
|
176
|
+
* @internal
|
|
177
|
+
*/
|
|
178
|
+
_getOps(): ActivityOperations<TEntity> {
|
|
179
|
+
return this._ops();
|
|
180
|
+
}
|
|
181
|
+
}
|