form0-core 0.1.13 → 0.1.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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "form0-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org/"
|
|
6
6
|
},
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"main": "src/index.js",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "node tests/form0-core.test.js && node tests/dataset-descriptors.test.js && node tests/builtin-metadata.test.js && node tests/calculation-editor.test.js && node tests/form-event-editor.test.js && node tests/security-test.js",
|
|
11
|
+
"test": "node tests/form0-core.test.js && node tests/dataset-descriptors.test.js && node tests/structured-record.test.js && node tests/builtin-metadata.test.js && node tests/calculation-editor.test.js && node tests/form-event-editor.test.js && node tests/security-test.js",
|
|
12
12
|
"postinstall": "node scripts/postinstall.cjs",
|
|
13
13
|
"format": "prettier --write \"**/*.{js,jsx,json,ts,tsx,md}\"",
|
|
14
14
|
"format:check": "prettier --check \"**/*.{js,jsx,json,ts,tsx,md}\""
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export { createFormEngine } from './engine/form-engine.js';
|
|
2
2
|
export { WarningSystem } from './engine/warning-system.js';
|
|
3
3
|
export { createStructuredRecord } from './utilities/record-transformer.js';
|
|
4
|
+
export {
|
|
5
|
+
normalizeStructuredRecord,
|
|
6
|
+
buildFormRecordSnapshot,
|
|
7
|
+
} from './utilities/structured-record.js';
|
|
4
8
|
export { validateSchema } from './schema/schema-validator.js';
|
|
5
9
|
//export { validateSchema, resolveSupportingImagePath } from './schema/schema-validator.js';
|
|
6
10
|
export { generateKey } from './utilities/hash.js';
|
|
@@ -15,6 +15,20 @@ const toTrimmedString = (value) => {
|
|
|
15
15
|
return trimmed.length > 0 ? trimmed : null;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
const toFiniteNumberOrNull = (value) => {
|
|
19
|
+
if (typeof value === 'number') {
|
|
20
|
+
return Number.isFinite(value) ? value : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const trimmed = toTrimmedString(value);
|
|
24
|
+
if (!trimmed) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const parsed = Number(trimmed);
|
|
29
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
30
|
+
};
|
|
31
|
+
|
|
18
32
|
const toSchemaFormNode = (schema) => {
|
|
19
33
|
if (!isRecord(schema)) {
|
|
20
34
|
return null;
|
|
@@ -61,18 +75,44 @@ const toRepeatableLabel = (field) =>
|
|
|
61
75
|
toTrimmedString(field?.key) ??
|
|
62
76
|
'Repeatable section';
|
|
63
77
|
|
|
78
|
+
const normalizeCalculatedDisplayStyle = (value) => {
|
|
79
|
+
const normalized = toTrimmedString(value);
|
|
80
|
+
if (!normalized) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
switch (normalized.toLowerCase()) {
|
|
85
|
+
case 'number':
|
|
86
|
+
return 'numeric';
|
|
87
|
+
case 'datetime':
|
|
88
|
+
return 'date';
|
|
89
|
+
default:
|
|
90
|
+
return normalized.toLowerCase();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
64
94
|
const getCalculatedDisplayStyle = (field) => {
|
|
65
95
|
if (!isRecord(field)) {
|
|
66
96
|
return null;
|
|
67
97
|
}
|
|
68
98
|
|
|
99
|
+
const directStyle =
|
|
100
|
+
normalizeCalculatedDisplayStyle(field.display_mode) ??
|
|
101
|
+
normalizeCalculatedDisplayStyle(field.display_style);
|
|
102
|
+
if (directStyle) {
|
|
103
|
+
return directStyle;
|
|
104
|
+
}
|
|
105
|
+
|
|
69
106
|
const display = field.display;
|
|
70
107
|
if (typeof display === 'string') {
|
|
71
|
-
return
|
|
108
|
+
return normalizeCalculatedDisplayStyle(display);
|
|
72
109
|
}
|
|
73
110
|
|
|
74
111
|
if (isRecord(display)) {
|
|
75
|
-
return
|
|
112
|
+
return (
|
|
113
|
+
normalizeCalculatedDisplayStyle(display.style) ??
|
|
114
|
+
normalizeCalculatedDisplayStyle(display.type)
|
|
115
|
+
);
|
|
76
116
|
}
|
|
77
117
|
|
|
78
118
|
return null;
|
|
@@ -673,6 +713,13 @@ const normalizeScalarValue = (field, value) => {
|
|
|
673
713
|
return undefined;
|
|
674
714
|
}
|
|
675
715
|
|
|
716
|
+
if (field?.display_kind === 'number') {
|
|
717
|
+
const parsedNumber = toFiniteNumberOrNull(value);
|
|
718
|
+
if (parsedNumber !== null) {
|
|
719
|
+
return parsedNumber;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
676
723
|
return value;
|
|
677
724
|
};
|
|
678
725
|
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { createFormEngine } from '../engine/form-engine.js';
|
|
2
|
+
import {
|
|
3
|
+
buildDatasetDescriptors,
|
|
4
|
+
buildFieldIdentityMap,
|
|
5
|
+
projectDatasetRowValues,
|
|
6
|
+
} from './dataset-descriptors.js';
|
|
7
|
+
|
|
8
|
+
const CONTAINER_TYPES = new Set(['Section', 'BuildingPlanSection']);
|
|
9
|
+
const TIMESTAMP_KEYS = [
|
|
10
|
+
'created_at_client',
|
|
11
|
+
'updated_at_client',
|
|
12
|
+
'created_at_server',
|
|
13
|
+
'updated_at_server',
|
|
14
|
+
];
|
|
15
|
+
const ROOT_DATASET_ID = '__root__';
|
|
16
|
+
|
|
17
|
+
const isRecordObject = (value) =>
|
|
18
|
+
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
19
|
+
|
|
20
|
+
const cloneJson = (value) => {
|
|
21
|
+
if (typeof structuredClone === 'function') {
|
|
22
|
+
return structuredClone(value);
|
|
23
|
+
}
|
|
24
|
+
return JSON.parse(JSON.stringify(value));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const trimString = (value) => {
|
|
28
|
+
if (typeof value !== 'string') {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const trimmed = value.trim();
|
|
33
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const toNullableString = (value) =>
|
|
37
|
+
value === null || typeof value === 'string' ? value : undefined;
|
|
38
|
+
|
|
39
|
+
const toOutputKey = (field) => trimString(field?.data_name) ?? trimString(field?.key);
|
|
40
|
+
|
|
41
|
+
const toDataName = (field, fallback) => trimString(field?.data_name) ?? fallback;
|
|
42
|
+
|
|
43
|
+
const toAliasList = (field) => {
|
|
44
|
+
const aliases = [];
|
|
45
|
+
const outputKey = toOutputKey(field);
|
|
46
|
+
const dataName = trimString(field?.data_name);
|
|
47
|
+
const key = trimString(field?.key);
|
|
48
|
+
|
|
49
|
+
if (outputKey) {
|
|
50
|
+
aliases.push(outputKey);
|
|
51
|
+
}
|
|
52
|
+
if (dataName && !aliases.includes(dataName)) {
|
|
53
|
+
aliases.push(dataName);
|
|
54
|
+
}
|
|
55
|
+
if (key && !aliases.includes(key)) {
|
|
56
|
+
aliases.push(key);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return aliases;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const findFirstAliasEntry = (value, aliases) => {
|
|
63
|
+
if (!isRecordObject(value)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const alias of aliases) {
|
|
68
|
+
if (Object.prototype.hasOwnProperty.call(value, alias)) {
|
|
69
|
+
return {
|
|
70
|
+
key: alias,
|
|
71
|
+
value: value[alias],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return null;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const buildScopeFromElements = (elements) => {
|
|
80
|
+
const scope = {
|
|
81
|
+
valueFields: [],
|
|
82
|
+
repeatables: [],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (!Array.isArray(elements)) {
|
|
86
|
+
return scope;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const element of elements) {
|
|
90
|
+
if (!isRecordObject(element) || typeof element.type !== 'string') {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (CONTAINER_TYPES.has(element.type)) {
|
|
95
|
+
const childScope = buildScopeFromElements(
|
|
96
|
+
Array.isArray(element.elements) ? element.elements : []
|
|
97
|
+
);
|
|
98
|
+
scope.valueFields.push(...childScope.valueFields);
|
|
99
|
+
scope.repeatables.push(...childScope.repeatables);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (element.type === 'RepeatableSection') {
|
|
104
|
+
const outputKey = toOutputKey(element);
|
|
105
|
+
if (!outputKey) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
scope.repeatables.push({
|
|
110
|
+
aliases: toAliasList(element),
|
|
111
|
+
outputKey,
|
|
112
|
+
repeatableKey: trimString(element.key) ?? trimString(element.data_name) ?? outputKey,
|
|
113
|
+
childScope: buildScopeFromElements(Array.isArray(element.elements) ? element.elements : []),
|
|
114
|
+
});
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const outputKey = toOutputKey(element);
|
|
119
|
+
if (!outputKey) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
scope.valueFields.push({
|
|
124
|
+
aliases: toAliasList(element),
|
|
125
|
+
dataName: toDataName(element, outputKey),
|
|
126
|
+
outputKey,
|
|
127
|
+
field: element,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return scope;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const resolveChoiceLabel = (field, rawValue, fallbackLabel) => {
|
|
135
|
+
const normalizedValue = trimString(rawValue);
|
|
136
|
+
if (!normalizedValue) {
|
|
137
|
+
return fallbackLabel ?? null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (Array.isArray(field?.choices)) {
|
|
141
|
+
const matched = field.choices.find(
|
|
142
|
+
(choice) =>
|
|
143
|
+
isRecordObject(choice) &&
|
|
144
|
+
Object.prototype.hasOwnProperty.call(choice, 'value') &&
|
|
145
|
+
choice.value === normalizedValue
|
|
146
|
+
);
|
|
147
|
+
if (matched) {
|
|
148
|
+
return trimString(matched.label) ?? normalizedValue;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return fallbackLabel ?? normalizedValue;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const normalizeChoiceEntry = (field, entry) => {
|
|
156
|
+
if (!isRecordObject(entry)) {
|
|
157
|
+
return entry;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const rawValue = trimString(entry.value) ?? trimString(entry.label) ?? trimString(entry.id);
|
|
161
|
+
if (!rawValue) {
|
|
162
|
+
return { ...entry };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
...entry,
|
|
167
|
+
value: rawValue,
|
|
168
|
+
label: resolveChoiceLabel(field, rawValue, trimString(entry.label)),
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const normalizeChoiceList = (field, entries) => {
|
|
173
|
+
if (!Array.isArray(entries)) {
|
|
174
|
+
return entries;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return entries.map((entry) => normalizeChoiceEntry(field, entry));
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const normalizeChoiceStructuredValue = (field, rawValue) => {
|
|
181
|
+
if (!isRecordObject(rawValue)) {
|
|
182
|
+
return rawValue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const normalized = { ...rawValue };
|
|
186
|
+
|
|
187
|
+
if (field?.type === 'SingleChoiceField' || field?.type === 'BooleanField') {
|
|
188
|
+
if (Array.isArray(rawValue.choice_value)) {
|
|
189
|
+
normalized.choice_value = normalizeChoiceList(field, rawValue.choice_value);
|
|
190
|
+
}
|
|
191
|
+
if (Array.isArray(rawValue.choice)) {
|
|
192
|
+
normalized.choice = normalizeChoiceList(field, rawValue.choice);
|
|
193
|
+
}
|
|
194
|
+
if (Array.isArray(rawValue.other_value)) {
|
|
195
|
+
normalized.other_value = rawValue.other_value.map((entry) =>
|
|
196
|
+
isRecordObject(entry) ? { ...entry } : entry
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (Array.isArray(rawValue.other)) {
|
|
200
|
+
normalized.other = rawValue.other.map((entry) =>
|
|
201
|
+
isRecordObject(entry) ? { ...entry } : entry
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return normalized;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (field?.type === 'MultiChoiceField') {
|
|
208
|
+
if (Array.isArray(rawValue.choices_value)) {
|
|
209
|
+
normalized.choices_value = normalizeChoiceList(field, rawValue.choices_value);
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(rawValue.choices)) {
|
|
212
|
+
normalized.choices = normalizeChoiceList(field, rawValue.choices);
|
|
213
|
+
}
|
|
214
|
+
if (Array.isArray(rawValue.other_value)) {
|
|
215
|
+
normalized.other_value = rawValue.other_value.map((entry) =>
|
|
216
|
+
isRecordObject(entry) ? { ...entry } : entry
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
if (Array.isArray(rawValue.other)) {
|
|
220
|
+
normalized.other = rawValue.other.map((entry) =>
|
|
221
|
+
isRecordObject(entry) ? { ...entry } : entry
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return normalized;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return normalized;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const buildFieldReferenceMap = (schema) => {
|
|
231
|
+
const identityMap = buildFieldIdentityMap(schema);
|
|
232
|
+
const references = new Map();
|
|
233
|
+
|
|
234
|
+
Object.values(identityMap).forEach((field) => {
|
|
235
|
+
if (!isRecordObject(field)) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
[field.field_id, field.output_key, field.key, field.data_name].forEach((ref) => {
|
|
240
|
+
const normalizedRef = trimString(ref);
|
|
241
|
+
if (normalizedRef && !references.has(normalizedRef)) {
|
|
242
|
+
references.set(normalizedRef, field.field_id);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return references;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const toTitlePart = (value) => {
|
|
251
|
+
if (Array.isArray(value)) {
|
|
252
|
+
const parts = value
|
|
253
|
+
.map((entry) => toTitlePart(entry))
|
|
254
|
+
.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
255
|
+
return parts.length > 0 ? parts.join(', ') : null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
259
|
+
return String(value);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (typeof value === 'string') {
|
|
263
|
+
const normalized = value.trim();
|
|
264
|
+
return normalized.length > 0 ? normalized : null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return null;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const computeStructuredRecordTitle = (schema, record) => {
|
|
271
|
+
const formNode = isRecordObject(schema?.form) ? schema.form : null;
|
|
272
|
+
const titleField = isRecordObject(formNode?.title_field) ? formNode.title_field : null;
|
|
273
|
+
const titleElements = Array.isArray(titleField?.elements) ? titleField.elements : [];
|
|
274
|
+
|
|
275
|
+
if (!titleField || titleField.enabled === false || titleElements.length === 0) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const descriptors = buildDatasetDescriptors(schema);
|
|
280
|
+
const rootDescriptor =
|
|
281
|
+
descriptors.find((descriptor) => descriptor.id === ROOT_DATASET_ID) ?? descriptors[0] ?? null;
|
|
282
|
+
if (!rootDescriptor) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const projected = projectDatasetRowValues(rootDescriptor, record);
|
|
287
|
+
const references = buildFieldReferenceMap(schema);
|
|
288
|
+
const parts = [];
|
|
289
|
+
|
|
290
|
+
titleElements.forEach((entry) => {
|
|
291
|
+
const normalizedEntry = trimString(entry);
|
|
292
|
+
if (!normalizedEntry) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const fieldId = references.get(normalizedEntry);
|
|
297
|
+
if (!fieldId) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const part = toTitlePart(projected.displayValues[fieldId]);
|
|
302
|
+
if (part) {
|
|
303
|
+
parts.push(part);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return parts.length > 0 ? parts.join(', ') : null;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const normalizeRecordScope = (record, scope) => {
|
|
311
|
+
if (!isRecordObject(record)) {
|
|
312
|
+
return record;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const formValues = isRecordObject(record.form_values) ? record.form_values : null;
|
|
316
|
+
if (!formValues) {
|
|
317
|
+
return record;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
scope.valueFields.forEach((field) => {
|
|
321
|
+
const entry = findFirstAliasEntry(formValues, field.aliases);
|
|
322
|
+
if (!entry) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (
|
|
327
|
+
field.field?.type === 'SingleChoiceField' ||
|
|
328
|
+
field.field?.type === 'BooleanField' ||
|
|
329
|
+
field.field?.type === 'MultiChoiceField'
|
|
330
|
+
) {
|
|
331
|
+
formValues[entry.key] = normalizeChoiceStructuredValue(field.field, entry.value);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
scope.repeatables.forEach((repeatable) => {
|
|
336
|
+
const entry = findFirstAliasEntry(formValues, repeatable.aliases);
|
|
337
|
+
if (!entry || !Array.isArray(entry.value)) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
formValues[entry.key] = entry.value.map((childRecord) => {
|
|
342
|
+
if (!isRecordObject(childRecord)) {
|
|
343
|
+
return childRecord;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const normalizedChild = cloneJson(childRecord);
|
|
347
|
+
normalizeRecordScope(normalizedChild, repeatable.childScope);
|
|
348
|
+
return normalizedChild;
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return record;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const buildSnapshotState = (formValues, scope) => {
|
|
356
|
+
const rawValues = {};
|
|
357
|
+
const repeatable = {};
|
|
358
|
+
const source = isRecordObject(formValues) ? formValues : {};
|
|
359
|
+
|
|
360
|
+
scope.valueFields.forEach((field) => {
|
|
361
|
+
const entry = findFirstAliasEntry(source, field.aliases);
|
|
362
|
+
if (!entry) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
rawValues[field.dataName] = cloneJson(entry.value);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
scope.repeatables.forEach((repeatableField) => {
|
|
370
|
+
const entry = findFirstAliasEntry(source, repeatableField.aliases);
|
|
371
|
+
if (!entry || !Array.isArray(entry.value)) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const instances = entry.value
|
|
376
|
+
.map((instance) => buildRepeatableInstanceSnapshot(instance, repeatableField.childScope))
|
|
377
|
+
.filter(Boolean);
|
|
378
|
+
|
|
379
|
+
if (instances.length > 0) {
|
|
380
|
+
repeatable[repeatableField.repeatableKey] = instances;
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
rawValues,
|
|
386
|
+
repeatable,
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const buildRepeatableInstanceSnapshot = (record, scope) => {
|
|
391
|
+
if (!isRecordObject(record)) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const { rawValues, repeatable } = buildSnapshotState(record.form_values, scope);
|
|
396
|
+
const result = {
|
|
397
|
+
values: rawValues,
|
|
398
|
+
repeatable,
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const recordId = trimString(record.id) ?? trimString(record.record_id);
|
|
402
|
+
if (recordId) {
|
|
403
|
+
result.id = recordId;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const status = toNullableString(record['@status']);
|
|
407
|
+
if (typeof status !== 'undefined') {
|
|
408
|
+
result['@status'] = status;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const title = toNullableString(record['@title']);
|
|
412
|
+
if (typeof title !== 'undefined') {
|
|
413
|
+
result['@title'] = title;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (typeof record.draft === 'boolean') {
|
|
417
|
+
result.draft = record.draft;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
TIMESTAMP_KEYS.forEach((key) => {
|
|
421
|
+
const value = toNullableString(record[key]);
|
|
422
|
+
if (typeof value !== 'undefined') {
|
|
423
|
+
result[key] = value;
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
return result;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const extractSnapshotTimestamps = (record) => ({
|
|
431
|
+
created_at_client:
|
|
432
|
+
toNullableString(record?.created_at_client) ?? toNullableString(record?.created_at) ?? null,
|
|
433
|
+
updated_at_client:
|
|
434
|
+
toNullableString(record?.updated_at_client) ?? toNullableString(record?.updated_at) ?? null,
|
|
435
|
+
created_at_server: toNullableString(record?.created_at_server) ?? null,
|
|
436
|
+
updated_at_server: toNullableString(record?.updated_at_server) ?? null,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
export function normalizeStructuredRecord(schema, record, options = {}) {
|
|
440
|
+
const normalized = isRecordObject(record) ? cloneJson(record) : {};
|
|
441
|
+
const mode = trimString(options.mode) ?? 'derived';
|
|
442
|
+
|
|
443
|
+
if (mode !== 'derived') {
|
|
444
|
+
return normalized;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const formNode = isRecordObject(schema?.form) ? schema.form : null;
|
|
448
|
+
const scope = buildScopeFromElements(Array.isArray(formNode?.elements) ? formNode.elements : []);
|
|
449
|
+
|
|
450
|
+
normalizeRecordScope(normalized, scope);
|
|
451
|
+
|
|
452
|
+
if (formNode) {
|
|
453
|
+
normalized['@title'] = computeStructuredRecordTitle(schema, normalized);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return normalized;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function buildFormRecordSnapshot(schema, record, options = {}) {
|
|
460
|
+
const normalizedRecord = normalizeStructuredRecord(schema, record, {
|
|
461
|
+
mode: 'derived',
|
|
462
|
+
});
|
|
463
|
+
const formNode = isRecordObject(schema?.form) ? schema.form : null;
|
|
464
|
+
const scope = buildScopeFromElements(Array.isArray(formNode?.elements) ? formNode.elements : []);
|
|
465
|
+
const { rawValues: extractedRawValues, repeatable } = buildSnapshotState(
|
|
466
|
+
normalizedRecord.form_values,
|
|
467
|
+
scope
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const rawValues = cloneJson(extractedRawValues);
|
|
471
|
+
const statusField = isRecordObject(formNode?.status_field) ? formNode.status_field : null;
|
|
472
|
+
const statusFieldName = trimString(statusField?.data_name);
|
|
473
|
+
if (statusFieldName) {
|
|
474
|
+
const status = toNullableString(normalizedRecord['@status']);
|
|
475
|
+
if (typeof status !== 'undefined') {
|
|
476
|
+
rawValues[statusFieldName] = status;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
TIMESTAMP_KEYS.forEach((key) => {
|
|
481
|
+
const value = toNullableString(normalizedRecord[key]);
|
|
482
|
+
if (typeof value !== 'undefined') {
|
|
483
|
+
rawValues[key] = value;
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const mode = trimString(options.mode) ?? 'default';
|
|
488
|
+
const snapshotRawValues =
|
|
489
|
+
mode === 'editor' && formNode
|
|
490
|
+
? (() => {
|
|
491
|
+
const engine = createFormEngine({
|
|
492
|
+
schema,
|
|
493
|
+
initialValues: rawValues,
|
|
494
|
+
});
|
|
495
|
+
engine.eval();
|
|
496
|
+
return cloneJson(engine.getState().values);
|
|
497
|
+
})()
|
|
498
|
+
: rawValues;
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
raw_values: snapshotRawValues,
|
|
502
|
+
repeatable,
|
|
503
|
+
timestamps: extractSnapshotTimestamps(normalizedRecord),
|
|
504
|
+
};
|
|
505
|
+
}
|