@ram_28/kf-ai-sdk 2.0.14 → 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 +10 -9
- package/dist/FileField-BWrSHNRq.js +296 -0
- package/dist/FileField-eDeuzln8.cjs +1 -0
- package/dist/api.cjs +1 -1
- package/dist/api.mjs +2 -2
- package/dist/auth.cjs +1 -1
- package/dist/auth.mjs +1 -1
- package/dist/bdo/core/BaseBdo.d.ts +1 -1
- package/dist/bdo.cjs +1 -1
- package/dist/bdo.mjs +230 -474
- package/dist/{client-DnO2KKrw.cjs → client-D5k4SYuw.cjs} +1 -1
- package/dist/{client-iQTqFDNI.js → client-_ayziI1d.js} +33 -32
- package/dist/components/hooks/index.d.ts +9 -3
- package/dist/components/hooks/index.d.ts.map +1 -1
- package/dist/{workflow/components → components/hooks}/useActivityForm/createActivityItemProxy.d.ts +9 -5
- package/dist/components/hooks/useActivityForm/createActivityItemProxy.d.ts.map +1 -0
- package/dist/components/hooks/useActivityForm/createActivityResolver.d.ts +23 -0
- package/dist/components/hooks/useActivityForm/createActivityResolver.d.ts.map +1 -0
- package/dist/components/hooks/useActivityForm/index.d.ts.map +1 -0
- package/dist/{workflow/components → components/hooks}/useActivityForm/types.d.ts +12 -8
- package/dist/components/hooks/useActivityForm/types.d.ts.map +1 -0
- package/dist/{workflow/components → components/hooks}/useActivityForm/useActivityForm.d.ts +2 -2
- package/dist/components/hooks/useActivityForm/useActivityForm.d.ts.map +1 -0
- package/dist/components/hooks/useActivityTable/index.d.ts +4 -0
- package/dist/components/hooks/useActivityTable/index.d.ts.map +1 -0
- package/dist/components/hooks/useActivityTable/types.d.ts +36 -0
- package/dist/components/hooks/useActivityTable/types.d.ts.map +1 -0
- package/dist/components/hooks/useActivityTable/useActivityTable.d.ts +4 -0
- package/dist/components/hooks/useActivityTable/useActivityTable.d.ts.map +1 -0
- 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/index.d.ts +3 -0
- package/dist/components/hooks/useBDOTable/index.d.ts.map +1 -0
- package/dist/components/hooks/useBDOTable/types.d.ts +24 -0
- package/dist/components/hooks/useBDOTable/types.d.ts.map +1 -0
- package/dist/components/hooks/useBDOTable/useBDOTable.d.ts +3 -0
- package/dist/components/hooks/useBDOTable/useBDOTable.d.ts.map +1 -0
- package/dist/components/hooks/useTable/index.d.ts +2 -2
- package/dist/components/hooks/useTable/index.d.ts.map +1 -1
- package/dist/components/hooks/useTable/types.d.ts +11 -10
- package/dist/components/hooks/useTable/types.d.ts.map +1 -1
- package/dist/components/hooks/useTable/useTable.d.ts +1 -1
- package/dist/components/hooks/useTable/useTable.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 +279 -344
- package/dist/form.types.d.ts +1 -1
- package/dist/form.types.d.ts.map +1 -1
- package/dist/{metadata-DpfI3zRN.js → metadata-Cc1mBcLS.js} +1 -1
- package/dist/{metadata-DgLSJkF5.cjs → metadata-DWXQPDav.cjs} +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.d.ts +1 -0
- package/dist/table.d.ts.map +1 -1
- package/dist/table.mjs +17 -192
- package/dist/table.types.d.ts +2 -1
- package/dist/table.types.d.ts.map +1 -1
- package/dist/types/base-fields.d.ts +4 -4
- package/dist/types/base-fields.d.ts.map +1 -1
- package/dist/types/constants.d.ts +3 -3
- package/dist/useTable-CeRklbdT.cjs +1 -0
- package/dist/useTable-DS0-WInw.js +203 -0
- package/dist/workflow/Activity.d.ts +19 -7
- 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/createFieldFromMeta.d.ts +29 -0
- package/dist/workflow/createFieldFromMeta.d.ts.map +1 -0
- package/dist/workflow/index.d.ts +1 -2
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/types.d.ts +16 -12
- package/dist/workflow/types.d.ts.map +1 -1
- package/dist/workflow.cjs +1 -1
- package/dist/workflow.d.ts +5 -2
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.mjs +687 -352
- package/dist/workflow.types.d.ts +1 -0
- package/dist/workflow.types.d.ts.map +1 -1
- package/docs/bdo.md +1 -1
- package/docs/gaps.md +360 -0
- package/docs/useActivityForm.md +393 -0
- package/docs/useActivityTable.md +418 -0
- package/docs/{useForm.md → useBDOForm.md} +24 -24
- package/docs/useBDOTable.md +284 -0
- package/docs/workflow.md +148 -297
- package/package.json +2 -2
- package/sdk/bdo/core/BaseBdo.ts +2 -2
- package/sdk/bdo/fields/UserField.ts +1 -1
- package/sdk/components/hooks/index.ts +28 -5
- package/sdk/components/hooks/useActivityForm/createActivityItemProxy.ts +400 -0
- package/sdk/components/hooks/useActivityForm/createActivityResolver.ts +87 -0
- package/sdk/{workflow/components → components/hooks}/useActivityForm/types.ts +24 -11
- package/sdk/components/hooks/useActivityForm/useActivityForm.ts +478 -0
- package/sdk/components/hooks/useActivityTable/index.ts +8 -0
- package/sdk/components/hooks/useActivityTable/types.ts +47 -0
- package/sdk/components/hooks/useActivityTable/useActivityTable.ts +40 -0
- 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/index.ts +2 -0
- package/sdk/components/hooks/useBDOTable/types.ts +22 -0
- package/sdk/components/hooks/useBDOTable/useBDOTable.ts +16 -0
- package/sdk/components/hooks/useTable/index.ts +3 -3
- package/sdk/components/hooks/useTable/types.ts +16 -12
- package/sdk/components/hooks/useTable/useTable.ts +56 -49
- package/sdk/form.ts +2 -2
- package/sdk/form.types.ts +4 -4
- package/sdk/table.ts +4 -1
- package/sdk/table.types.ts +7 -4
- package/sdk/types/base-fields.ts +4 -4
- package/sdk/types/constants.ts +3 -3
- package/sdk/workflow/Activity.ts +36 -12
- package/sdk/workflow/client.ts +65 -12
- package/sdk/workflow/createFieldFromMeta.ts +110 -0
- package/sdk/workflow/index.ts +1 -6
- package/sdk/workflow/types.ts +20 -11
- package/sdk/workflow.ts +11 -2
- package/sdk/workflow.types.ts +7 -0
- package/dist/BaseField-B6da88U7.js +0 -40
- package/dist/BaseField-Drp0-OxL.cjs +0 -1
- 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/error-handling-CAoD0Kwb.cjs +0 -1
- package/dist/error-handling-CrhTtD88.js +0 -14
- package/dist/index.esm-Cj63v5ny.js +0 -1014
- package/dist/index.esm-DuwT11sx.cjs +0 -1
- package/dist/workflow/components/useActivityForm/createActivityItemProxy.d.ts.map +0 -1
- package/dist/workflow/components/useActivityForm/createActivityResolver.d.ts +0 -22
- package/dist/workflow/components/useActivityForm/createActivityResolver.d.ts.map +0 -1
- package/dist/workflow/components/useActivityForm/index.d.ts.map +0 -1
- package/dist/workflow/components/useActivityForm/types.d.ts.map +0 -1
- package/dist/workflow/components/useActivityForm/useActivityForm.d.ts.map +0 -1
- package/docs/useTable.md +0 -369
- package/sdk/workflow/components/useActivityForm/createActivityItemProxy.ts +0 -130
- package/sdk/workflow/components/useActivityForm/createActivityResolver.ts +0 -61
- package/sdk/workflow/components/useActivityForm/useActivityForm.ts +0 -386
- /package/dist/{workflow/components → components/hooks}/useActivityForm/index.d.ts +0 -0
- /package/dist/components/hooks/{useForm → useBDOForm}/createItemProxy.d.ts +0 -0
- /package/dist/components/hooks/{useForm → useBDOForm}/createResolver.d.ts +0 -0
- /package/sdk/{workflow/components → components/hooks}/useActivityForm/index.ts +0 -0
- /package/sdk/components/hooks/{useForm → useBDOForm}/createItemProxy.ts +0 -0
- /package/sdk/components/hooks/{useForm → useBDOForm}/createResolver.ts +0 -0
|
@@ -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
|
+
}
|
|
@@ -33,7 +33,7 @@ export type AllFieldsType<B> = ExtractEditableType<B> & ExtractReadonlyType<B> &
|
|
|
33
33
|
// BDO CAPABILITY CONSTRAINTS
|
|
34
34
|
// ============================================================
|
|
35
35
|
|
|
36
|
-
/** Minimum BDO shape required for any
|
|
36
|
+
/** Minimum BDO shape required for any useBDOForm usage */
|
|
37
37
|
interface BaseBdoShape {
|
|
38
38
|
readonly meta: BdoMetaType;
|
|
39
39
|
getFields(): Record<string, BaseField<unknown>>;
|
|
@@ -81,13 +81,13 @@ export type HandleSubmitType<TRead = unknown> = (
|
|
|
81
81
|
// OPTIONS TYPE
|
|
82
82
|
// ============================================================
|
|
83
83
|
|
|
84
|
-
export type
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
|
|
|
84
|
+
export type UseBDOFormOptionsType<B extends BaseBdo<any, any, any>> =
|
|
85
|
+
| UseBDOFormCreateOptionsType<B>
|
|
86
|
+
| UseBDOFormUpdateOptionsType<B>
|
|
87
|
+
| UseBDOFormAutoOptionsType<B>;
|
|
88
88
|
|
|
89
89
|
/** Options when operation is explicitly "create" — BDO must have create() */
|
|
90
|
-
interface
|
|
90
|
+
interface UseBDOFormCreateOptionsType<B extends BaseBdo<any, any, any>> {
|
|
91
91
|
bdo: B & CreatableBdo<ExtractEditableType<B>>;
|
|
92
92
|
operation: "create";
|
|
93
93
|
recordId?: undefined;
|
|
@@ -99,7 +99,7 @@ interface UseFormCreateOptionsType<B extends BaseBdo<any, any, any>> {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/** Options when operation is explicitly "update" — BDO must have get() + update() */
|
|
102
|
-
interface
|
|
102
|
+
interface UseBDOFormUpdateOptionsType<B extends BaseBdo<any, any, any>> {
|
|
103
103
|
bdo: B & UpdatableBdo<ExtractEditableType<B>>;
|
|
104
104
|
operation: "update";
|
|
105
105
|
recordId: string;
|
|
@@ -111,7 +111,7 @@ interface UseFormUpdateOptionsType<B extends BaseBdo<any, any, any>> {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/** Options when operation is auto-inferred — BDO must support both */
|
|
114
|
-
interface
|
|
114
|
+
interface UseBDOFormAutoOptionsType<B extends BaseBdo<any, any, any>> {
|
|
115
115
|
bdo: B & FormBdo<ExtractEditableType<B>>;
|
|
116
116
|
operation?: undefined;
|
|
117
117
|
recordId?: string;
|
|
@@ -187,7 +187,7 @@ export type FormItemType<
|
|
|
187
187
|
// RETURN TYPE
|
|
188
188
|
// ============================================================
|
|
189
189
|
|
|
190
|
-
export interface
|
|
190
|
+
export interface UseBDOFormReturnType<B extends BaseBdo<any, any, any>> {
|
|
191
191
|
// Item with typed accessors
|
|
192
192
|
item: FormItemType<ExtractEditableType<B>, ExtractReadonlyType<B>>;
|
|
193
193
|
|
|
@@ -4,61 +4,30 @@ import {
|
|
|
4
4
|
type FieldValues,
|
|
5
5
|
type FieldErrors,
|
|
6
6
|
type Control,
|
|
7
|
-
type RegisterOptions,
|
|
8
7
|
type UseFormReturn as RHFUseFormReturn,
|
|
9
8
|
} from "react-hook-form";
|
|
10
9
|
import { useQuery } from "@tanstack/react-query";
|
|
11
10
|
import { createResolver } from "./createResolver";
|
|
12
11
|
import { createItemProxy } from "./createItemProxy";
|
|
12
|
+
import {
|
|
13
|
+
coerceFieldValue,
|
|
14
|
+
coerceRecordForForm,
|
|
15
|
+
createSyncField,
|
|
16
|
+
createEnhancedRegister,
|
|
17
|
+
createEnhancedControl,
|
|
18
|
+
} from "./shared";
|
|
13
19
|
import { getBdoSchema } from "../../../api/metadata";
|
|
14
20
|
import { api } from "../../../api/client";
|
|
15
21
|
import type { BaseBdo } from "../../../bdo";
|
|
16
22
|
import type { CreateUpdateResponseType } from "../../../types/common";
|
|
17
|
-
import type { BaseField } from "../../../bdo/fields/BaseField";
|
|
18
23
|
import type {
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
UseBDOFormOptionsType,
|
|
25
|
+
UseBDOFormReturnType,
|
|
21
26
|
HandleSubmitType,
|
|
22
27
|
AllFieldsType,
|
|
23
28
|
UpdatableBdo,
|
|
24
29
|
} from "./types";
|
|
25
30
|
|
|
26
|
-
/** Coerce form value to match field's expected type (HTML inputs return strings) */
|
|
27
|
-
function coerceFieldValue(field: BaseField<unknown>, value: unknown): unknown {
|
|
28
|
-
const type = field.meta.Type;
|
|
29
|
-
if (typeof value === "string" && type === "Number") {
|
|
30
|
-
return value === "" ? undefined : Number(value);
|
|
31
|
-
}
|
|
32
|
-
// Date/DateTime: empty string → undefined (don't send to backend)
|
|
33
|
-
if (typeof value === "string" && value === "" && (type === "Date" || type === "DateTime")) {
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
// DateTime: normalize to HH:MM:SS and ensure Z suffix for API request format
|
|
37
|
-
if (typeof value === "string" && value !== "" && type === "DateTime") {
|
|
38
|
-
let normalized = value;
|
|
39
|
-
if (normalized.endsWith("Z")) normalized = normalized.slice(0, -1);
|
|
40
|
-
// HTML datetime-local may omit seconds (e.g. "2026-02-18T15:12")
|
|
41
|
-
const timePart = normalized.split("T")[1] || "";
|
|
42
|
-
if ((timePart.match(/:/g) || []).length === 1) {
|
|
43
|
-
normalized += ":00";
|
|
44
|
-
}
|
|
45
|
-
return normalized + "Z";
|
|
46
|
-
}
|
|
47
|
-
return value;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Strip trailing Z from DateTime response values for HTML datetime-local inputs */
|
|
51
|
-
function coerceRecordForForm(bdo: BaseBdo<any, any, any>, data: Record<string, unknown>): Record<string, unknown> {
|
|
52
|
-
const fields = bdo.getFields();
|
|
53
|
-
const result = { ...data };
|
|
54
|
-
for (const [key, value] of Object.entries(result)) {
|
|
55
|
-
if (typeof value === "string" && fields[key]?.meta.Type === "DateTime" && value.endsWith("Z")) {
|
|
56
|
-
result[key] = value.slice(0, -1);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return result;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
31
|
/**
|
|
63
32
|
* A form hook that integrates with React Hook Form.
|
|
64
33
|
*
|
|
@@ -71,11 +40,11 @@ function coerceRecordForForm(bdo: BaseBdo<any, any, any>, data: Record<string, u
|
|
|
71
40
|
* - Smart register: auto-disables readonly fields
|
|
72
41
|
* - Payload filtering: handleSubmit auto-filters to editable fields only
|
|
73
42
|
* - Constraint validation: auto-validates required, length, etc. from field meta
|
|
74
|
-
* -
|
|
43
|
+
* - Per-field sync: validates, sends to API, resets dirty, updates computed fields
|
|
75
44
|
*/
|
|
76
|
-
export function
|
|
77
|
-
options:
|
|
78
|
-
):
|
|
45
|
+
export function useBDOForm<B extends BaseBdo<any, any, any>>(
|
|
46
|
+
options: UseBDOFormOptionsType<B>,
|
|
47
|
+
): UseBDOFormReturnType<B> {
|
|
79
48
|
const {
|
|
80
49
|
bdo,
|
|
81
50
|
recordId,
|
|
@@ -112,7 +81,7 @@ export function useForm<B extends BaseBdo<any, any, any>>(
|
|
|
112
81
|
queryKey: ["form-record", bdo.meta._id, recordId],
|
|
113
82
|
queryFn: async () => {
|
|
114
83
|
const item = await (bdo as unknown as UpdatableBdo).get(recordId!);
|
|
115
|
-
return coerceRecordForForm(bdo, item.toJSON() as Record<string, unknown>);
|
|
84
|
+
return coerceRecordForForm(bdo.getFields(), item.toJSON() as Record<string, unknown>);
|
|
116
85
|
},
|
|
117
86
|
enabled: operation === "update" && !!recordId,
|
|
118
87
|
staleTime: 0,
|
|
@@ -199,66 +168,71 @@ export function useForm<B extends BaseBdo<any, any, any>>(
|
|
|
199
168
|
);
|
|
200
169
|
|
|
201
170
|
// ============================================================
|
|
202
|
-
//
|
|
171
|
+
// PER-FIELD SYNC (validate → API call → reset dirty → update computed)
|
|
203
172
|
// ============================================================
|
|
204
173
|
|
|
205
174
|
const fields = bdo.getFields();
|
|
175
|
+
const isComputingRef = useRef(false);
|
|
206
176
|
|
|
207
|
-
const
|
|
208
|
-
(
|
|
209
|
-
|
|
210
|
-
if (fields[name]?.readOnly) {
|
|
211
|
-
return { ...rhfResult, disabled: true };
|
|
212
|
-
}
|
|
213
|
-
return rhfResult;
|
|
214
|
-
},
|
|
215
|
-
[form, fields],
|
|
177
|
+
const readonlyFieldNames = useMemo<string[]>(
|
|
178
|
+
() => Object.keys(fields).filter((k) => fields[k].readOnly),
|
|
179
|
+
[fields],
|
|
216
180
|
);
|
|
217
181
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (draftPatchTimeoutRef.current) {
|
|
231
|
-
clearTimeout(draftPatchTimeoutRef.current);
|
|
182
|
+
const syncApiFn = useCallback(
|
|
183
|
+
async (fieldName: string, value: unknown) => {
|
|
184
|
+
if (operation === "create" && draftData?._id) {
|
|
185
|
+
return api(bdo.meta._id).draftInteraction({
|
|
186
|
+
_id: draftData._id,
|
|
187
|
+
[fieldName]: value,
|
|
188
|
+
});
|
|
189
|
+
} else if (operation === "update" && recordId) {
|
|
190
|
+
return api(bdo.meta._id).update(recordId, {
|
|
191
|
+
[fieldName]: value,
|
|
192
|
+
});
|
|
232
193
|
}
|
|
194
|
+
},
|
|
195
|
+
[operation, draftData, recordId, bdo],
|
|
196
|
+
);
|
|
233
197
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
198
|
+
const syncField = useMemo(
|
|
199
|
+
() =>
|
|
200
|
+
createSyncField({
|
|
201
|
+
apiFn: syncApiFn,
|
|
202
|
+
allFields: fields,
|
|
203
|
+
readonlyFieldNames,
|
|
204
|
+
rhf: form,
|
|
205
|
+
isComputingRef,
|
|
206
|
+
}),
|
|
207
|
+
[syncApiFn, fields, readonlyFieldNames, form],
|
|
208
|
+
);
|
|
238
209
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
210
|
+
const syncOnChange = mode === "onChange" || mode === "all";
|
|
211
|
+
const syncOnBlur =
|
|
212
|
+
mode === "onBlur" || mode === "onTouched" || mode === "all";
|
|
213
|
+
|
|
214
|
+
const smartRegister = useMemo(
|
|
215
|
+
() =>
|
|
216
|
+
createEnhancedRegister({
|
|
217
|
+
rhf: form,
|
|
218
|
+
allFields: fields,
|
|
219
|
+
syncField,
|
|
220
|
+
syncOnBlur,
|
|
221
|
+
syncOnChange,
|
|
222
|
+
}),
|
|
223
|
+
[form, fields, syncField, syncOnBlur, syncOnChange],
|
|
224
|
+
);
|
|
244
225
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
},
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
return () => {
|
|
256
|
-
subscription.unsubscribe();
|
|
257
|
-
if (draftPatchTimeoutRef.current) {
|
|
258
|
-
clearTimeout(draftPatchTimeoutRef.current);
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
}, [form, operation, draftData, fields, bdo]);
|
|
226
|
+
const enhancedControl = useMemo(
|
|
227
|
+
() =>
|
|
228
|
+
createEnhancedControl({
|
|
229
|
+
control: form.control,
|
|
230
|
+
syncField,
|
|
231
|
+
syncOnBlur,
|
|
232
|
+
syncOnChange,
|
|
233
|
+
}),
|
|
234
|
+
[form.control, syncField, syncOnBlur, syncOnChange],
|
|
235
|
+
);
|
|
262
236
|
|
|
263
237
|
// ============================================================
|
|
264
238
|
// CUSTOM HANDLE SUBMIT (with API call + payload filtering)
|
|
@@ -332,7 +306,7 @@ export function useForm<B extends BaseBdo<any, any, any>>(
|
|
|
332
306
|
getValues: form.getValues as any,
|
|
333
307
|
reset: form.reset as any,
|
|
334
308
|
trigger: form.trigger as any,
|
|
335
|
-
control:
|
|
309
|
+
control: enhancedControl as unknown as Control<AllFieldsType<B>>,
|
|
336
310
|
formState: form.formState as any,
|
|
337
311
|
|
|
338
312
|
errors: form.formState.errors as any,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { UseTableReturnType, PaginationStateType } from '../useTable/types';
|
|
2
|
+
import type { UseFilterOptionsType } from '../useFilter/types';
|
|
3
|
+
import type { SortType } from '../../../types/common';
|
|
4
|
+
|
|
5
|
+
export interface UseBDOTableOptionsType<T> {
|
|
6
|
+
/** BDO instance — only meta._id is used (for API routing) */
|
|
7
|
+
bdo: {
|
|
8
|
+
meta: { readonly _id: string; readonly name: string };
|
|
9
|
+
};
|
|
10
|
+
/** Initial state */
|
|
11
|
+
initialState?: {
|
|
12
|
+
sort?: SortType;
|
|
13
|
+
pagination?: PaginationStateType;
|
|
14
|
+
filter?: UseFilterOptionsType<T>;
|
|
15
|
+
};
|
|
16
|
+
/** Error callback */
|
|
17
|
+
onError?: (error: Error) => void;
|
|
18
|
+
/** Success callback — receives rows from current page */
|
|
19
|
+
onSuccess?: (data: T[]) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type UseBDOTableReturnType<T> = UseTableReturnType<T>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { api } from '../../../api/client';
|
|
2
|
+
import { useTable } from '../useTable';
|
|
3
|
+
import type { UseBDOTableOptionsType, UseBDOTableReturnType } from './types';
|
|
4
|
+
|
|
5
|
+
export function useBDOTable<T = any>(
|
|
6
|
+
options: UseBDOTableOptionsType<T>,
|
|
7
|
+
): UseBDOTableReturnType<T> {
|
|
8
|
+
const { bdo, ...rest } = options;
|
|
9
|
+
|
|
10
|
+
return useTable<T>({
|
|
11
|
+
queryKey: ['table', bdo.meta._id],
|
|
12
|
+
listFn: (opts) => api<T>(bdo.meta._id).list(opts),
|
|
13
|
+
countFn: (opts) => api<T>(bdo.meta._id).count(opts),
|
|
14
|
+
...rest,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import type {
|
|
2
|
+
ListResponseType,
|
|
3
|
+
ListOptionsType,
|
|
4
|
+
CountResponseType,
|
|
5
|
+
SortType,
|
|
6
|
+
} from '../../../types/common';
|
|
7
|
+
import type { UseFilterReturnType, UseFilterOptionsType } from '../useFilter';
|
|
6
8
|
|
|
7
9
|
// ============================================================
|
|
8
10
|
// STATE TYPE DEFINITIONS
|
|
@@ -23,10 +25,12 @@ export interface PaginationStateType {
|
|
|
23
25
|
// ============================================================
|
|
24
26
|
|
|
25
27
|
export interface UseTableOptionsType<T> {
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
|
|
28
|
+
/** Unique query key for React Query caching */
|
|
29
|
+
queryKey: string[];
|
|
30
|
+
/** Fetch list data (POST with filter/sort/pagination) */
|
|
31
|
+
listFn: (options: ListOptionsType) => Promise<ListResponseType<T>>;
|
|
32
|
+
/** Fetch count (POST with filter only) */
|
|
33
|
+
countFn: (options: ListOptionsType) => Promise<CountResponseType>;
|
|
30
34
|
/** Initial state */
|
|
31
35
|
initialState?: {
|
|
32
36
|
/** Sort configuration: [{ "fieldName": "ASC" }] */
|
|
@@ -38,7 +42,7 @@ export interface UseTableOptionsType<T> {
|
|
|
38
42
|
};
|
|
39
43
|
/** Error callback */
|
|
40
44
|
onError?: (error: Error) => void;
|
|
41
|
-
/** Success callback */
|
|
45
|
+
/** Success callback — receives rows from current page */
|
|
42
46
|
onSuccess?: (data: T[]) => void;
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -65,10 +69,10 @@ export interface UseTableReturnType<T> {
|
|
|
65
69
|
// Sorting (Flat Access)
|
|
66
70
|
sort: {
|
|
67
71
|
field: keyof T | null;
|
|
68
|
-
direction:
|
|
72
|
+
direction: 'ASC' | 'DESC' | null;
|
|
69
73
|
toggle: (field: keyof T) => void;
|
|
70
74
|
clear: () => void;
|
|
71
|
-
set: (field: keyof T, direction:
|
|
75
|
+
set: (field: keyof T | null, direction: 'ASC' | 'DESC' | null) => void;
|
|
72
76
|
};
|
|
73
77
|
|
|
74
78
|
// Filter (Simplified chainable API)
|