form0-core 0.1.25 → 0.2.0
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 +111 -48
- package/SECURITY.md +38 -174
- package/package.json +27 -13
- package/src/builtins/registry.js +1 -4
- package/src/index.js +1 -0
- package/src/schema/ai-metadata-validator.js +1 -3
- package/src/schema/building-plan-expander.js +2 -1
- package/src/schema/field-specs.js +1 -0
- package/src/schema/schema-validator.js +78 -8
- package/src/utilities/ai-metadata.js +15 -3
- package/src/utilities/calculation-dependencies.js +1 -6
- package/src/utilities/calculation-expression-utils.js +11 -1
- package/src/utilities/dataset-descriptors.js +147 -0
- package/src/utilities/field-helpers.js +5 -1
- package/src/utilities/form-event-editor.js +2 -10
- package/src/utilities/form-link-helpers.js +16 -5
- package/src/utilities/record-transformer.js +49 -86
- package/src/utilities/repeatable-helpers.js +6 -5
- package/src/utilities/structured-record.js +80 -93
- package/src/utilities/uuid.js +2 -1
- package/scripts/legacy-postinstall-message.cjs +0 -42
|
@@ -38,7 +38,11 @@ export function getFormAITasks(form) {
|
|
|
38
38
|
|
|
39
39
|
export function getFormAINamingPolicy(form) {
|
|
40
40
|
const metadata = getFormAIMetadata(form);
|
|
41
|
-
if (
|
|
41
|
+
if (
|
|
42
|
+
!metadata ||
|
|
43
|
+
typeof metadata.namingPolicy !== 'object' ||
|
|
44
|
+
Array.isArray(metadata.namingPolicy)
|
|
45
|
+
) {
|
|
42
46
|
return null;
|
|
43
47
|
}
|
|
44
48
|
return metadata.namingPolicy;
|
|
@@ -78,7 +82,11 @@ export function isFieldAIInferrable(field) {
|
|
|
78
82
|
|
|
79
83
|
export function getFieldAIChoicePolicy(field) {
|
|
80
84
|
const metadata = getFieldAIMetadata(field);
|
|
81
|
-
if (
|
|
85
|
+
if (
|
|
86
|
+
!metadata ||
|
|
87
|
+
typeof metadata.choicePolicy !== 'object' ||
|
|
88
|
+
Array.isArray(metadata.choicePolicy)
|
|
89
|
+
) {
|
|
82
90
|
return null;
|
|
83
91
|
}
|
|
84
92
|
return metadata.choicePolicy;
|
|
@@ -86,7 +94,11 @@ export function getFieldAIChoicePolicy(field) {
|
|
|
86
94
|
|
|
87
95
|
export function getFieldAIProviderHints(field) {
|
|
88
96
|
const metadata = getFieldAIMetadata(field);
|
|
89
|
-
if (
|
|
97
|
+
if (
|
|
98
|
+
!metadata ||
|
|
99
|
+
typeof metadata.providerHints !== 'object' ||
|
|
100
|
+
Array.isArray(metadata.providerHints)
|
|
101
|
+
) {
|
|
90
102
|
return null;
|
|
91
103
|
}
|
|
92
104
|
return metadata.providerHints;
|
|
@@ -260,12 +260,7 @@ function extractEvalCallArguments(code) {
|
|
|
260
260
|
|
|
261
261
|
if (char === "'" && !inDoubleQuote && !inBacktick && !isEscapedCharacter(source, i)) {
|
|
262
262
|
inSingleQuote = !inSingleQuote;
|
|
263
|
-
} else if (
|
|
264
|
-
char === '"' &&
|
|
265
|
-
!inSingleQuote &&
|
|
266
|
-
!inBacktick &&
|
|
267
|
-
!isEscapedCharacter(source, i)
|
|
268
|
-
) {
|
|
263
|
+
} else if (char === '"' && !inSingleQuote && !inBacktick && !isEscapedCharacter(source, i)) {
|
|
269
264
|
inDoubleQuote = !inDoubleQuote;
|
|
270
265
|
} else if (
|
|
271
266
|
char === '`' &&
|
|
@@ -10,6 +10,16 @@ export function isMultilineCalculationExpression(expression) {
|
|
|
10
10
|
|
|
11
11
|
export function normalizeInlineCalculationExpression(expression) {
|
|
12
12
|
const normalizedExpression = typeof expression === 'string' ? expression : '';
|
|
13
|
+
const expressionWithoutTrailingWhitespace = normalizedExpression.trimEnd();
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
if (!expressionWithoutTrailingWhitespace.endsWith(';')) {
|
|
16
|
+
return normalizedExpression;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let expressionEnd = expressionWithoutTrailingWhitespace.length;
|
|
20
|
+
while (expressionEnd > 0 && expressionWithoutTrailingWhitespace[expressionEnd - 1] === ';') {
|
|
21
|
+
expressionEnd -= 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return expressionWithoutTrailingWhitespace.slice(0, expressionEnd);
|
|
15
25
|
}
|
|
@@ -6,6 +6,16 @@ import {
|
|
|
6
6
|
|
|
7
7
|
const ROOT_DATASET_ID = '__root__';
|
|
8
8
|
const CONTAINER_TYPES = new Set(['Section', 'BuildingPlanSection']);
|
|
9
|
+
const TITLE_ELIGIBLE_FIELD_TYPES = new Set([
|
|
10
|
+
'TextField',
|
|
11
|
+
'NumericField',
|
|
12
|
+
'DateField',
|
|
13
|
+
'TimeField',
|
|
14
|
+
'SingleChoiceField',
|
|
15
|
+
'MultiChoiceField',
|
|
16
|
+
'BooleanField',
|
|
17
|
+
'CalculatedField',
|
|
18
|
+
]);
|
|
9
19
|
|
|
10
20
|
const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
11
21
|
|
|
@@ -79,6 +89,8 @@ const toRepeatableLabel = (field) =>
|
|
|
79
89
|
toTrimmedString(field?.key) ??
|
|
80
90
|
'Repeatable section';
|
|
81
91
|
|
|
92
|
+
const cloneTitleField = (value) => (isRecord(value) ? { ...value } : null);
|
|
93
|
+
|
|
82
94
|
const normalizeCalculatedDisplayStyle = (value) => {
|
|
83
95
|
const normalized = toTrimmedString(value);
|
|
84
96
|
if (!normalized) {
|
|
@@ -435,6 +447,7 @@ const buildDatasetFieldDescriptor = (field, dataset) => {
|
|
|
435
447
|
data_name: toTrimmedString(field.data_name),
|
|
436
448
|
label: toFieldLabel(field),
|
|
437
449
|
field_type: toTrimmedString(field.type) ?? 'UnknownField',
|
|
450
|
+
title_eligible: TITLE_ELIGIBLE_FIELD_TYPES.has(field.type),
|
|
438
451
|
export_kind: getFieldExportKind(field),
|
|
439
452
|
choices: Array.isArray(field.choices)
|
|
440
453
|
? field.choices.filter((choice) => isRecord(choice)).map((choice) => ({ ...choice }))
|
|
@@ -510,6 +523,7 @@ const collectRepeatableDatasets = (elements, context, datasets) => {
|
|
|
510
523
|
root_dataset_id: ROOT_DATASET_ID,
|
|
511
524
|
location_enabled: toBooleanOrDefault(element.location_enabled),
|
|
512
525
|
location_required: toBooleanOrDefault(element.location_required),
|
|
526
|
+
title_field: cloneTitleField(element.title_field),
|
|
513
527
|
fields: [],
|
|
514
528
|
};
|
|
515
529
|
|
|
@@ -548,6 +562,7 @@ export function buildDatasetDescriptors(schema) {
|
|
|
548
562
|
root_dataset_id: ROOT_DATASET_ID,
|
|
549
563
|
location_enabled: toBooleanOrDefault(formNode.location_enabled),
|
|
550
564
|
location_required: toBooleanOrDefault(formNode.location_required),
|
|
565
|
+
title_field: cloneTitleField(formNode.title_field),
|
|
551
566
|
fields: [],
|
|
552
567
|
};
|
|
553
568
|
|
|
@@ -669,6 +684,138 @@ export function projectDatasetRowValues(descriptor, rowValues) {
|
|
|
669
684
|
};
|
|
670
685
|
}
|
|
671
686
|
|
|
687
|
+
const toLiveChoiceDisplayValues = (field, rawValue) => {
|
|
688
|
+
if (rawValue === null || typeof rawValue === 'undefined') {
|
|
689
|
+
return [];
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (typeof rawValue !== 'object' || Array.isArray(rawValue)) {
|
|
693
|
+
const matched = Array.isArray(field?.choices)
|
|
694
|
+
? field.choices.find((choice) => choice?.value === rawValue)
|
|
695
|
+
: null;
|
|
696
|
+
return [matched?.label ?? rawValue];
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const selectionKey = field?.field_type === 'MultiChoiceField' ? 'choices' : 'choice';
|
|
700
|
+
const selected = Array.isArray(rawValue[selectionKey]) ? rawValue[selectionKey] : [];
|
|
701
|
+
const other = Array.isArray(rawValue.other) ? rawValue.other : [];
|
|
702
|
+
const labels = selected.map((entry) => {
|
|
703
|
+
const value = isRecord(entry) ? entry.value : entry;
|
|
704
|
+
const matched = Array.isArray(field?.choices)
|
|
705
|
+
? field.choices.find((choice) => choice?.value === value)
|
|
706
|
+
: null;
|
|
707
|
+
return matched?.label ?? (isRecord(entry) ? (entry.label ?? entry.value) : entry);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
other.forEach((entry) => {
|
|
711
|
+
labels.push(isRecord(entry) ? (entry.label ?? entry.value) : entry);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
return labels.filter((entry) => entry !== null && typeof entry !== 'undefined');
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
const toTitleDisplayValue = (field, rawValue) => {
|
|
718
|
+
if (
|
|
719
|
+
isRecord(rawValue) &&
|
|
720
|
+
['choice', 'choices', 'other'].some((key) =>
|
|
721
|
+
Object.prototype.hasOwnProperty.call(rawValue, key)
|
|
722
|
+
)
|
|
723
|
+
) {
|
|
724
|
+
return toLiveChoiceDisplayValues(field, rawValue);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (
|
|
728
|
+
field?.field_type === 'SingleChoiceField' ||
|
|
729
|
+
field?.field_type === 'MultiChoiceField' ||
|
|
730
|
+
field?.field_type === 'BooleanField'
|
|
731
|
+
) {
|
|
732
|
+
if (!isRecord(rawValue)) {
|
|
733
|
+
return toLiveChoiceDisplayValues(field, rawValue);
|
|
734
|
+
}
|
|
735
|
+
return readStoredChoiceDisplayValues(field, rawValue, {
|
|
736
|
+
context: 'resolveDatasetRowTitle',
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return rawValue;
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
const toTitlePart = (value) => {
|
|
744
|
+
if (Array.isArray(value)) {
|
|
745
|
+
const parts = value.map(toTitlePart).filter(Boolean);
|
|
746
|
+
return parts.length > 0 ? parts.join(', ') : null;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (typeof value === 'string') {
|
|
750
|
+
return toTrimmedString(value);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
754
|
+
return String(value);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (value instanceof Date) {
|
|
758
|
+
return value.toISOString();
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (isRecord(value) && Object.prototype.hasOwnProperty.call(value, 'value')) {
|
|
762
|
+
return toTitlePart(value.value);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return null;
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Resolve a derived title for one root or repeatable dataset row.
|
|
770
|
+
* Supports renderer/live values and canonical stored record values.
|
|
771
|
+
*/
|
|
772
|
+
export function resolveDatasetRowTitle(descriptor, rowValues) {
|
|
773
|
+
const titleField = isRecord(descriptor?.title_field) ? descriptor.title_field : null;
|
|
774
|
+
const references = Array.isArray(titleField?.elements) ? titleField.elements : [];
|
|
775
|
+
if (!titleField || titleField.enabled === false || references.length === 0) {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const sourceRecord = isRecord(rowValues) ? rowValues : {};
|
|
780
|
+
const source = isRecord(sourceRecord.form_values) ? sourceRecord.form_values : sourceRecord;
|
|
781
|
+
const fields = Array.isArray(descriptor?.fields) ? descriptor.fields : [];
|
|
782
|
+
const fieldByReference = new Map();
|
|
783
|
+
|
|
784
|
+
fields.forEach((field) => {
|
|
785
|
+
if (field?.title_eligible !== true) {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
[
|
|
789
|
+
field.field_id,
|
|
790
|
+
field.output_key,
|
|
791
|
+
field.key,
|
|
792
|
+
field.data_name,
|
|
793
|
+
...(field.aliases ?? []),
|
|
794
|
+
].forEach((reference) => {
|
|
795
|
+
const normalized = toTrimmedString(reference);
|
|
796
|
+
if (normalized && !fieldByReference.has(normalized)) {
|
|
797
|
+
fieldByReference.set(normalized, field);
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
const parts = [];
|
|
803
|
+
references.forEach((reference) => {
|
|
804
|
+
const normalizedReference = toTrimmedString(reference);
|
|
805
|
+
const field = normalizedReference ? fieldByReference.get(normalizedReference) : null;
|
|
806
|
+
if (!field) {
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
const rawValue = readFirstAliasValue(source, field.aliases ?? []);
|
|
810
|
+
const part = toTitlePart(toTitleDisplayValue(field, rawValue));
|
|
811
|
+
if (part) {
|
|
812
|
+
parts.push(part);
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
return parts.length > 0 ? parts.join(', ') : null;
|
|
817
|
+
}
|
|
818
|
+
|
|
672
819
|
export function resolveDatasetDescriptorById(schema, datasetId) {
|
|
673
820
|
return buildDatasetDescriptors(schema).find((dataset) => dataset.id === datasetId) ?? null;
|
|
674
821
|
}
|
|
@@ -10,7 +10,11 @@ export function flattenFields(elements) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
return elements.flatMap((el) => {
|
|
13
|
-
if (
|
|
13
|
+
if (
|
|
14
|
+
el.type === 'Section' ||
|
|
15
|
+
el.type === 'RepeatableSection' ||
|
|
16
|
+
el.type === 'BuildingPlanSection'
|
|
17
|
+
) {
|
|
14
18
|
return [el, ...flattenFields(el.elements)];
|
|
15
19
|
}
|
|
16
20
|
|
|
@@ -17,11 +17,7 @@ const COMMON_GLOBAL_FUNCTIONS = new Set([
|
|
|
17
17
|
'Boolean',
|
|
18
18
|
]);
|
|
19
19
|
|
|
20
|
-
const STRUCTURAL_FIELD_TYPES = new Set([
|
|
21
|
-
'Section',
|
|
22
|
-
'RepeatableSection',
|
|
23
|
-
'BuildingPlanSection',
|
|
24
|
-
]);
|
|
20
|
+
const STRUCTURAL_FIELD_TYPES = new Set(['Section', 'RepeatableSection', 'BuildingPlanSection']);
|
|
25
21
|
|
|
26
22
|
const FORM_EVENT_BUILTIN_STATUS_BY_NAME = Object.freeze({
|
|
27
23
|
EVAL: 'advanced',
|
|
@@ -296,11 +292,7 @@ export function getFormEventReferenceCatalog({ schema }) {
|
|
|
296
292
|
* referencedFields: string[],
|
|
297
293
|
* }}
|
|
298
294
|
*/
|
|
299
|
-
export function analyzeFormEventCode({
|
|
300
|
-
code,
|
|
301
|
-
schema,
|
|
302
|
-
securityConfig = DEFAULT_SECURITY_CONFIG,
|
|
303
|
-
}) {
|
|
295
|
+
export function analyzeFormEventCode({ code, schema, securityConfig = DEFAULT_SECURITY_CONFIG }) {
|
|
304
296
|
const issues = [];
|
|
305
297
|
const issueKeys = new Set();
|
|
306
298
|
const usedBuiltins = [];
|
|
@@ -235,16 +235,22 @@ export function applyLinkedRecordSelection({
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
if (!values || typeof values !== 'object') {
|
|
238
|
-
throw new Error(
|
|
238
|
+
throw new Error(
|
|
239
|
+
'applyLinkedRecordSelection requires a values object from the form engine state'
|
|
240
|
+
);
|
|
239
241
|
}
|
|
240
242
|
|
|
241
243
|
const field = resolveFormField(form, fieldIdentifier);
|
|
242
244
|
if (!field) {
|
|
243
|
-
throw new Error(
|
|
245
|
+
throw new Error(
|
|
246
|
+
`applyLinkedRecordSelection could not find field "${fieldIdentifier}" in the provided form`
|
|
247
|
+
);
|
|
244
248
|
}
|
|
245
249
|
|
|
246
250
|
if (field.type !== 'FormLinkField') {
|
|
247
|
-
throw new Error(
|
|
251
|
+
throw new Error(
|
|
252
|
+
`applyLinkedRecordSelection can only target FormLinkField fields (received ${field.type})`
|
|
253
|
+
);
|
|
248
254
|
}
|
|
249
255
|
|
|
250
256
|
const normalizedRecords = normalizeLinkedRecords(records);
|
|
@@ -255,7 +261,8 @@ export function applyLinkedRecordSelection({
|
|
|
255
261
|
);
|
|
256
262
|
}
|
|
257
263
|
|
|
258
|
-
const selectedRecords =
|
|
264
|
+
const selectedRecords =
|
|
265
|
+
field.allow_multiple_records === true ? normalizedRecords : normalizedRecords.slice(0, 1);
|
|
259
266
|
|
|
260
267
|
values[field.data_name] = selectedRecords.map((record) => ({
|
|
261
268
|
record_id: record.record_id,
|
|
@@ -275,7 +282,11 @@ export function applyLinkedRecordSelection({
|
|
|
275
282
|
}
|
|
276
283
|
|
|
277
284
|
if (field.allow_multiple_records === true) {
|
|
278
|
-
const aggregated = aggregateDefaultValues(
|
|
285
|
+
const aggregated = aggregateDefaultValues(
|
|
286
|
+
selectedRecords,
|
|
287
|
+
mapping.source_field_id,
|
|
288
|
+
delimiter
|
|
289
|
+
);
|
|
279
290
|
values[destinationField.data_name] = aggregated;
|
|
280
291
|
return;
|
|
281
292
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { FIELD_SPECS } from '../schema/field-specs.js';
|
|
7
7
|
import { recordVersion } from './version-utils.js';
|
|
8
8
|
import { buildRepeatableMetadata } from './repeatable-helpers.js';
|
|
9
|
+
import { buildDatasetDescriptors, resolveDatasetRowTitle } from './dataset-descriptors.js';
|
|
9
10
|
|
|
10
11
|
const hasMeaningfulValue = (value) => {
|
|
11
12
|
if (value === null || value === undefined) {
|
|
@@ -178,26 +179,44 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
|
|
|
178
179
|
dataNameToFieldMap.set(field.data_name, field);
|
|
179
180
|
});
|
|
180
181
|
}
|
|
181
|
-
// Create mapping from key to field for title resolution
|
|
182
|
-
const keyToFieldMap = new Map();
|
|
183
|
-
if (Array.isArray(fields)) {
|
|
184
|
-
fields.forEach((field) => {
|
|
185
|
-
if (field.key) keyToFieldMap.set(field.key, field);
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
182
|
// Build a comprehensive tree structure for nested RepeatableSections
|
|
190
183
|
const elementsSource = Array.isArray(options.originalElements)
|
|
191
184
|
? options.originalElements
|
|
192
185
|
: Array.isArray(fields)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
186
|
+
? fields
|
|
187
|
+
: [];
|
|
188
|
+
|
|
189
|
+
const { repeatableSectionTree, fieldOwnership, sectionFields } =
|
|
190
|
+
buildRepeatableMetadata(elementsSource);
|
|
191
|
+
|
|
192
|
+
const titleDatasetDescriptors = buildDatasetDescriptors({
|
|
193
|
+
form: {
|
|
194
|
+
elements: elementsSource,
|
|
195
|
+
title_field: options.title_field || null,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const rootTitleDescriptor = titleDatasetDescriptors.find(
|
|
199
|
+
(descriptor) => descriptor.kind === 'root'
|
|
200
|
+
);
|
|
201
|
+
const repeatableTitleDescriptors = new Map();
|
|
202
|
+
titleDatasetDescriptors.forEach((descriptor) => {
|
|
203
|
+
if (descriptor.kind !== 'repeatable') {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
[descriptor.repeatable_field_id, descriptor.repeatable_output_key].forEach((reference) => {
|
|
207
|
+
if (reference) {
|
|
208
|
+
repeatableTitleDescriptors.set(reference, descriptor);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const resolveRepeatableTitle = (repInfo, rowValues) => {
|
|
214
|
+
const descriptor =
|
|
215
|
+
repeatableTitleDescriptors.get(repInfo?.preferredKey) ||
|
|
216
|
+
repeatableTitleDescriptors.get(repInfo?.field?.key) ||
|
|
217
|
+
repeatableTitleDescriptors.get(repInfo?.field?.data_name);
|
|
218
|
+
return descriptor ? resolveDatasetRowTitle(descriptor, rowValues) : null;
|
|
219
|
+
};
|
|
201
220
|
|
|
202
221
|
const hasStructuredRepeatableState =
|
|
203
222
|
state &&
|
|
@@ -237,12 +256,7 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
|
|
|
237
256
|
|
|
238
257
|
// Apply output formatting if field spec exists
|
|
239
258
|
let processedValue = value;
|
|
240
|
-
if (
|
|
241
|
-
field &&
|
|
242
|
-
field.type &&
|
|
243
|
-
FIELD_SPECS[field.type] &&
|
|
244
|
-
FIELD_SPECS[field.type].outputProducer
|
|
245
|
-
) {
|
|
259
|
+
if (field && field.type && FIELD_SPECS[field.type] && FIELD_SPECS[field.type].outputProducer) {
|
|
246
260
|
try {
|
|
247
261
|
processedValue = FIELD_SPECS[field.type].outputProducer(field, value);
|
|
248
262
|
} catch (err) {
|
|
@@ -384,6 +398,10 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
|
|
|
384
398
|
childRecord.form_values[childOutputKey] = childRepeatableArray;
|
|
385
399
|
}
|
|
386
400
|
|
|
401
|
+
if (repInfo.field?.title_field) {
|
|
402
|
+
childRecord['@title'] = resolveRepeatableTitle(repInfo, instanceValues);
|
|
403
|
+
}
|
|
404
|
+
|
|
387
405
|
if (hasMeaningfulRecord(childRecord)) {
|
|
388
406
|
childRecords.push(childRecord);
|
|
389
407
|
}
|
|
@@ -498,6 +516,13 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
|
|
|
498
516
|
}
|
|
499
517
|
}
|
|
500
518
|
|
|
519
|
+
if (repInfo.field?.title_field) {
|
|
520
|
+
childRecord['@title'] = resolveRepeatableTitle(
|
|
521
|
+
repInfo,
|
|
522
|
+
i === 0 ? state.values : childRecord.form_values
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
501
526
|
if (hasMeaningfulRecord(childRecord)) {
|
|
502
527
|
childRecords.push(childRecord);
|
|
503
528
|
}
|
|
@@ -600,70 +625,8 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
|
|
|
600
625
|
// Add top-level @title and @status
|
|
601
626
|
// ==============================
|
|
602
627
|
try {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
if (titleField && Array.isArray(titleField.elements)) {
|
|
606
|
-
const parts = [];
|
|
607
|
-
for (const ref of titleField.elements) {
|
|
608
|
-
let fieldDef = null;
|
|
609
|
-
// Prefer key lookup
|
|
610
|
-
if (keyToFieldMap.has(ref)) {
|
|
611
|
-
fieldDef = keyToFieldMap.get(ref);
|
|
612
|
-
} else if (dataNameToFieldMap.has(ref)) {
|
|
613
|
-
fieldDef = dataNameToFieldMap.get(ref);
|
|
614
|
-
}
|
|
615
|
-
if (!fieldDef) continue;
|
|
616
|
-
const value = state.values[fieldDef.data_name];
|
|
617
|
-
if (value == null) continue;
|
|
618
|
-
// Extract display text by field type
|
|
619
|
-
if (fieldDef.type === 'SingleChoiceField') {
|
|
620
|
-
let labels = [];
|
|
621
|
-
if (value.choice && Array.isArray(value.choice) && value.choice.length > 0) {
|
|
622
|
-
const v = value.choice[0].value;
|
|
623
|
-
const found = (fieldDef.choices || []).find((c) => c.value === v);
|
|
624
|
-
labels.push(found?.label || value.choice[0].label || v);
|
|
625
|
-
}
|
|
626
|
-
if (value.other && Array.isArray(value.other)) {
|
|
627
|
-
for (const o of value.other) {
|
|
628
|
-
if (o && (o.label || o.value)) labels.push(o.label || o.value);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
const text = labels.filter(Boolean).join(', ');
|
|
632
|
-
if (text) parts.push(text);
|
|
633
|
-
} else if (fieldDef.type === 'MultiChoiceField') {
|
|
634
|
-
let labels = [];
|
|
635
|
-
if (value.choices && Array.isArray(value.choices)) {
|
|
636
|
-
for (const c of value.choices) {
|
|
637
|
-
const found = (fieldDef.choices || []).find((cc) => cc.value === c.value);
|
|
638
|
-
labels.push(found?.label || c.label || c.value);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
if (value.other && Array.isArray(value.other)) {
|
|
642
|
-
for (const o of value.other) {
|
|
643
|
-
if (o && (o.label || o.value)) labels.push(o.label || o.value);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
const text = labels.filter(Boolean).join(', ');
|
|
647
|
-
if (text) parts.push(text);
|
|
648
|
-
} else if (fieldDef.type === 'BooleanField') {
|
|
649
|
-
let label = '';
|
|
650
|
-
if (value.choice && Array.isArray(value.choice) && value.choice.length > 0) {
|
|
651
|
-
const v = value.choice[0].value;
|
|
652
|
-
const found = (fieldDef.choices || []).find((c) => c.value === v);
|
|
653
|
-
label = found?.label || value.choice[0].label || v;
|
|
654
|
-
}
|
|
655
|
-
if (label) parts.push(label);
|
|
656
|
-
} else {
|
|
657
|
-
const text = typeof value === 'object' ? null : String(value);
|
|
658
|
-
if (text && text.trim() !== '') parts.push(text);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
const titleText = parts.join(', ');
|
|
662
|
-
if (titleText) {
|
|
663
|
-
record['@title'] = titleText;
|
|
664
|
-
} else {
|
|
665
|
-
record['@title'] = null;
|
|
666
|
-
}
|
|
628
|
+
if (options.title_field && rootTitleDescriptor) {
|
|
629
|
+
record['@title'] = resolveDatasetRowTitle(rootTitleDescriptor, state.values);
|
|
667
630
|
}
|
|
668
631
|
} catch (err) {
|
|
669
632
|
console.warn('[form0] createStructuredRecord: failed to compute @title:', err);
|
|
@@ -19,11 +19,12 @@ export function buildRepeatableSectionTree(
|
|
|
19
19
|
fieldOwnership = new Map(),
|
|
20
20
|
sectionFields = new Set()
|
|
21
21
|
) {
|
|
22
|
-
if (!Array.isArray(elements))
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
if (!Array.isArray(elements))
|
|
23
|
+
return {
|
|
24
|
+
repeatableSectionTree,
|
|
25
|
+
fieldOwnership,
|
|
26
|
+
sectionFields,
|
|
27
|
+
};
|
|
27
28
|
|
|
28
29
|
elements.forEach((element) => {
|
|
29
30
|
if (element.type === 'Section' || element.type === 'BuildingPlanSection') {
|