@servicenow/sdk-build-plugins 4.4.0 → 4.5.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/dist/acl-plugin.js +54 -4
- package/dist/acl-plugin.js.map +1 -1
- package/dist/applicability-plugin.js +2 -0
- package/dist/applicability-plugin.js.map +1 -1
- package/dist/application-menu-plugin.js +2 -0
- package/dist/application-menu-plugin.js.map +1 -1
- package/dist/arrow-function-plugin.d.ts +6 -1
- package/dist/arrow-function-plugin.js +105 -12
- package/dist/arrow-function-plugin.js.map +1 -1
- package/dist/atf/test-plugin.js +2 -0
- package/dist/atf/test-plugin.js.map +1 -1
- package/dist/basic-syntax-plugin.js +20 -0
- package/dist/basic-syntax-plugin.js.map +1 -1
- package/dist/call-expression-plugin.js +1 -0
- package/dist/call-expression-plugin.js.map +1 -1
- package/dist/claims-plugin.js +1 -0
- package/dist/claims-plugin.js.map +1 -1
- package/dist/client-script-plugin.js +1 -0
- package/dist/client-script-plugin.js.map +1 -1
- package/dist/column-plugin.js +1 -0
- package/dist/column-plugin.js.map +1 -1
- package/dist/cross-scope-privilege-plugin.js +1 -0
- package/dist/cross-scope-privilege-plugin.js.map +1 -1
- package/dist/dashboard/dashboard-plugin.js +2 -0
- package/dist/dashboard/dashboard-plugin.js.map +1 -1
- package/dist/data-plugin.js +1 -0
- package/dist/data-plugin.js.map +1 -1
- package/dist/email-notification-plugin.js +9 -13
- package/dist/email-notification-plugin.js.map +1 -1
- package/dist/flow/constants/flow-plugin-constants.d.ts +1 -1
- package/dist/flow/constants/flow-plugin-constants.js +1 -1
- package/dist/flow/constants/flow-plugin-constants.js.map +1 -1
- package/dist/flow/flow-logic/flow-logic-plugin-helpers.d.ts +82 -2
- package/dist/flow/flow-logic/flow-logic-plugin-helpers.js +48 -40
- package/dist/flow/flow-logic/flow-logic-plugin-helpers.js.map +1 -1
- package/dist/flow/flow-logic/flow-logic-plugin.js +1 -0
- package/dist/flow/flow-logic/flow-logic-plugin.js.map +1 -1
- package/dist/flow/plugins/approval-rules-plugin.js +1 -0
- package/dist/flow/plugins/approval-rules-plugin.js.map +1 -1
- package/dist/flow/plugins/flow-action-definition-plugin.js +4 -2
- package/dist/flow/plugins/flow-action-definition-plugin.js.map +1 -1
- package/dist/flow/plugins/flow-data-pill-plugin.js +1 -0
- package/dist/flow/plugins/flow-data-pill-plugin.js.map +1 -1
- package/dist/flow/plugins/flow-definition-plugin.js +8 -3
- package/dist/flow/plugins/flow-definition-plugin.js.map +1 -1
- package/dist/flow/plugins/flow-diagnostics-plugin.js +1 -0
- package/dist/flow/plugins/flow-diagnostics-plugin.js.map +1 -1
- package/dist/flow/plugins/flow-instance-plugin.js +68 -12
- package/dist/flow/plugins/flow-instance-plugin.js.map +1 -1
- package/dist/flow/plugins/flow-trigger-instance-plugin.js +1 -0
- package/dist/flow/plugins/flow-trigger-instance-plugin.js.map +1 -1
- package/dist/flow/plugins/inline-script-plugin.js +1 -0
- package/dist/flow/plugins/inline-script-plugin.js.map +1 -1
- package/dist/flow/plugins/step-definition-plugin.js +3 -2
- package/dist/flow/plugins/step-definition-plugin.js.map +1 -1
- package/dist/flow/plugins/step-instance-plugin.js +1 -0
- package/dist/flow/plugins/step-instance-plugin.js.map +1 -1
- package/dist/flow/plugins/trigger-plugin.js +2 -0
- package/dist/flow/plugins/trigger-plugin.js.map +1 -1
- package/dist/flow/plugins/wfa-datapill-plugin.js +1 -0
- package/dist/flow/plugins/wfa-datapill-plugin.js.map +1 -1
- package/dist/flow/post-install.d.ts +2 -0
- package/dist/flow/post-install.js +58 -0
- package/dist/flow/post-install.js.map +1 -0
- package/dist/flow/utils/complex-objects.js +4 -2
- package/dist/flow/utils/complex-objects.js.map +1 -1
- package/dist/flow/utils/flow-constants.d.ts +24 -0
- package/dist/flow/utils/flow-constants.js +29 -2
- package/dist/flow/utils/flow-constants.js.map +1 -1
- package/dist/flow/utils/flow-to-xml.d.ts +3 -2
- package/dist/flow/utils/flow-to-xml.js +3 -4
- package/dist/flow/utils/flow-to-xml.js.map +1 -1
- package/dist/flow/utils/label-cache-processor.d.ts +5 -0
- package/dist/flow/utils/label-cache-processor.js +14 -2
- package/dist/flow/utils/label-cache-processor.js.map +1 -1
- package/dist/flow/utils/service-catalog.js +5 -1
- package/dist/flow/utils/service-catalog.js.map +1 -1
- package/dist/form-plugin.d.ts +2 -0
- package/dist/form-plugin.js +1134 -0
- package/dist/form-plugin.js.map +1 -0
- package/dist/html-import-plugin.js +1 -0
- package/dist/html-import-plugin.js.map +1 -1
- package/dist/import-sets-plugin.js +2 -0
- package/dist/import-sets-plugin.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/instance-scan-plugin.d.ts +2 -0
- package/dist/instance-scan-plugin.js +298 -0
- package/dist/instance-scan-plugin.js.map +1 -0
- package/dist/json-plugin.js +1 -0
- package/dist/json-plugin.js.map +1 -1
- package/dist/list-plugin.js +1 -0
- package/dist/list-plugin.js.map +1 -1
- package/dist/now-attach-plugin.js +1 -0
- package/dist/now-attach-plugin.js.map +1 -1
- package/dist/now-config-plugin.js +659 -51
- package/dist/now-config-plugin.js.map +1 -1
- package/dist/now-id-plugin.js +1 -0
- package/dist/now-id-plugin.js.map +1 -1
- package/dist/now-include-plugin.js +1 -0
- package/dist/now-include-plugin.js.map +1 -1
- package/dist/now-ref-plugin.js +1 -0
- package/dist/now-ref-plugin.js.map +1 -1
- package/dist/now-unresolved-plugin.js +1 -0
- package/dist/now-unresolved-plugin.js.map +1 -1
- package/dist/package-json-plugin.js +1 -0
- package/dist/package-json-plugin.js.map +1 -1
- package/dist/property-plugin.js +3 -1
- package/dist/property-plugin.js.map +1 -1
- package/dist/record-plugin.d.ts +30 -0
- package/dist/record-plugin.js +37 -1
- package/dist/record-plugin.js.map +1 -1
- package/dist/repack/lint/Rules.d.ts +11 -2
- package/dist/repack/lint/Rules.js +160 -16
- package/dist/repack/lint/Rules.js.map +1 -1
- package/dist/repack/lint/index.d.ts +10 -5
- package/dist/repack/lint/index.js +76 -50
- package/dist/repack/lint/index.js.map +1 -1
- package/dist/rest-api-plugin.js +14 -0
- package/dist/rest-api-plugin.js.map +1 -1
- package/dist/role-plugin.js +1 -0
- package/dist/role-plugin.js.map +1 -1
- package/dist/schedule-script/index.d.ts +1 -0
- package/dist/schedule-script/index.js +18 -0
- package/dist/schedule-script/index.js.map +1 -0
- package/dist/schedule-script/scheduled-script-plugin.d.ts +2 -0
- package/dist/schedule-script/scheduled-script-plugin.js +551 -0
- package/dist/schedule-script/scheduled-script-plugin.js.map +1 -0
- package/dist/schedule-script/timeZoneConverter.d.ts +61 -0
- package/dist/schedule-script/timeZoneConverter.js +170 -0
- package/dist/schedule-script/timeZoneConverter.js.map +1 -0
- package/dist/script-action-plugin.js +2 -0
- package/dist/script-action-plugin.js.map +1 -1
- package/dist/script-include-plugin.js +2 -0
- package/dist/script-include-plugin.js.map +1 -1
- package/dist/server-module-plugin/index.js +13 -2
- package/dist/server-module-plugin/index.js.map +1 -1
- package/dist/service-catalog/catalog-clientscript-plugin.js +2 -0
- package/dist/service-catalog/catalog-clientscript-plugin.js.map +1 -1
- package/dist/service-catalog/catalog-item-plugin.js +2 -0
- package/dist/service-catalog/catalog-item-plugin.js.map +1 -1
- package/dist/service-catalog/catalog-ui-policy-plugin.js +2 -0
- package/dist/service-catalog/catalog-ui-policy-plugin.js.map +1 -1
- package/dist/service-catalog/sc-record-producer-plugin.js +2 -0
- package/dist/service-catalog/sc-record-producer-plugin.js.map +1 -1
- package/dist/service-catalog/service-catalog-diagnostics.d.ts +6 -0
- package/dist/service-catalog/service-catalog-diagnostics.js +20 -0
- package/dist/service-catalog/service-catalog-diagnostics.js.map +1 -1
- package/dist/service-catalog/shape-to-record.js +7 -2
- package/dist/service-catalog/shape-to-record.js.map +1 -1
- package/dist/service-catalog/variable-set-plugin.js +2 -0
- package/dist/service-catalog/variable-set-plugin.js.map +1 -1
- package/dist/service-portal/angular-provider-plugin.js +2 -0
- package/dist/service-portal/angular-provider-plugin.js.map +1 -1
- package/dist/service-portal/dependency-plugin.js +5 -31
- package/dist/service-portal/dependency-plugin.js.map +1 -1
- package/dist/service-portal/menu-plugin.d.ts +2 -0
- package/dist/service-portal/menu-plugin.js +353 -0
- package/dist/service-portal/menu-plugin.js.map +1 -0
- package/dist/service-portal/page-plugin.d.ts +2 -0
- package/dist/service-portal/page-plugin.js +702 -0
- package/dist/service-portal/page-plugin.js.map +1 -0
- package/dist/service-portal/portal-plugin.d.ts +2 -0
- package/dist/service-portal/portal-plugin.js +296 -0
- package/dist/service-portal/portal-plugin.js.map +1 -0
- package/dist/service-portal/theme-plugin.d.ts +2 -0
- package/dist/service-portal/theme-plugin.js +112 -0
- package/dist/service-portal/theme-plugin.js.map +1 -0
- package/dist/service-portal/utils.d.ts +8 -0
- package/dist/service-portal/utils.js +50 -0
- package/dist/service-portal/utils.js.map +1 -0
- package/dist/service-portal/widget-plugin.js +45 -8
- package/dist/service-portal/widget-plugin.js.map +1 -1
- package/dist/sla-plugin.js +2 -0
- package/dist/sla-plugin.js.map +1 -1
- package/dist/static-content-plugin.js +1 -0
- package/dist/static-content-plugin.js.map +1 -1
- package/dist/table-plugin.js +1 -0
- package/dist/table-plugin.js.map +1 -1
- package/dist/ui-action-plugin.js +2 -0
- package/dist/ui-action-plugin.js.map +1 -1
- package/dist/ui-page-plugin.js +33 -8
- package/dist/ui-page-plugin.js.map +1 -1
- package/dist/ui-policy-plugin.js +1 -0
- package/dist/ui-policy-plugin.js.map +1 -1
- package/dist/user-preference-plugin.js +2 -0
- package/dist/user-preference-plugin.js.map +1 -1
- package/dist/utils.d.ts +20 -2
- package/dist/utils.js +34 -3
- package/dist/utils.js.map +1 -1
- package/dist/ux-list-menu-config-plugin.js +2 -0
- package/dist/ux-list-menu-config-plugin.js.map +1 -1
- package/dist/view-plugin.js +1 -0
- package/dist/view-plugin.js.map +1 -1
- package/dist/workspace-plugin.js +2 -0
- package/dist/workspace-plugin.js.map +1 -1
- package/package.json +10 -11
- package/src/_types/eslint-community-eslint-utils.d.ts +15 -0
- package/src/acl-plugin.ts +97 -8
- package/src/applicability-plugin.ts +2 -0
- package/src/application-menu-plugin.ts +2 -0
- package/src/arrow-function-plugin.ts +128 -13
- package/src/atf/test-plugin.ts +2 -0
- package/src/basic-syntax-plugin.ts +21 -0
- package/src/call-expression-plugin.ts +1 -0
- package/src/claims-plugin.ts +1 -0
- package/src/client-script-plugin.ts +2 -1
- package/src/column-plugin.ts +1 -0
- package/src/cross-scope-privilege-plugin.ts +2 -1
- package/src/dashboard/dashboard-plugin.ts +2 -0
- package/src/data-plugin.ts +1 -0
- package/src/email-notification-plugin.ts +3 -23
- package/src/flow/constants/flow-plugin-constants.ts +1 -1
- package/src/flow/flow-logic/flow-logic-plugin-helpers.ts +47 -45
- package/src/flow/flow-logic/flow-logic-plugin.ts +1 -0
- package/src/flow/plugins/approval-rules-plugin.ts +1 -0
- package/src/flow/plugins/flow-action-definition-plugin.ts +4 -2
- package/src/flow/plugins/flow-data-pill-plugin.ts +1 -0
- package/src/flow/plugins/flow-definition-plugin.ts +10 -4
- package/src/flow/plugins/flow-diagnostics-plugin.ts +1 -0
- package/src/flow/plugins/flow-instance-plugin.ts +103 -14
- package/src/flow/plugins/flow-trigger-instance-plugin.ts +1 -0
- package/src/flow/plugins/inline-script-plugin.ts +1 -0
- package/src/flow/plugins/step-definition-plugin.ts +3 -2
- package/src/flow/plugins/step-instance-plugin.ts +1 -0
- package/src/flow/plugins/trigger-plugin.ts +2 -0
- package/src/flow/plugins/wfa-datapill-plugin.ts +1 -0
- package/src/flow/post-install.ts +92 -0
- package/src/flow/utils/complex-objects.ts +10 -2
- package/src/flow/utils/flow-constants.ts +30 -1
- package/src/flow/utils/flow-to-xml.ts +4 -4
- package/src/flow/utils/label-cache-processor.ts +14 -2
- package/src/flow/utils/service-catalog.ts +5 -2
- package/src/form-plugin.ts +1411 -0
- package/src/html-import-plugin.ts +1 -0
- package/src/import-sets-plugin.ts +2 -0
- package/src/index.ts +9 -0
- package/src/instance-scan-plugin.ts +318 -0
- package/src/json-plugin.ts +1 -0
- package/src/list-plugin.ts +2 -1
- package/src/now-attach-plugin.ts +1 -0
- package/src/now-config-plugin.ts +833 -53
- package/src/now-id-plugin.ts +1 -0
- package/src/now-include-plugin.ts +1 -0
- package/src/now-ref-plugin.ts +1 -0
- package/src/now-unresolved-plugin.ts +1 -0
- package/src/package-json-plugin.ts +1 -0
- package/src/property-plugin.ts +3 -1
- package/src/record-plugin.ts +42 -2
- package/src/repack/lint/Rules.ts +171 -22
- package/src/repack/lint/index.ts +80 -56
- package/src/rest-api-plugin.ts +21 -1
- package/src/role-plugin.ts +2 -1
- package/src/schedule-script/index.ts +1 -0
- package/src/schedule-script/scheduled-script-plugin.ts +679 -0
- package/src/schedule-script/timeZoneConverter.ts +188 -0
- package/src/script-action-plugin.ts +2 -0
- package/src/script-include-plugin.ts +2 -0
- package/src/server-module-plugin/index.ts +14 -2
- package/src/service-catalog/catalog-clientscript-plugin.ts +2 -0
- package/src/service-catalog/catalog-item-plugin.ts +2 -0
- package/src/service-catalog/catalog-ui-policy-plugin.ts +2 -0
- package/src/service-catalog/sc-record-producer-plugin.ts +2 -0
- package/src/service-catalog/service-catalog-diagnostics.ts +30 -0
- package/src/service-catalog/shape-to-record.ts +8 -2
- package/src/service-catalog/variable-set-plugin.ts +2 -0
- package/src/service-portal/angular-provider-plugin.ts +2 -0
- package/src/service-portal/dependency-plugin.ts +6 -53
- package/src/service-portal/menu-plugin.ts +435 -0
- package/src/service-portal/page-plugin.ts +830 -0
- package/src/service-portal/portal-plugin.ts +319 -0
- package/src/service-portal/theme-plugin.ts +135 -0
- package/src/service-portal/utils.ts +69 -0
- package/src/service-portal/widget-plugin.ts +79 -9
- package/src/sla-plugin.ts +2 -0
- package/src/static-content-plugin.ts +1 -0
- package/src/table-plugin.ts +2 -1
- package/src/ui-action-plugin.ts +2 -0
- package/src/ui-page-plugin.ts +34 -8
- package/src/ui-policy-plugin.ts +2 -1
- package/src/user-preference-plugin.ts +2 -0
- package/src/utils.ts +42 -2
- package/src/ux-list-menu-config-plugin.ts +2 -0
- package/src/view-plugin.ts +1 -0
- package/src/workspace-plugin.ts +2 -0
- package/src/_types/eslint-plugin-es-x.d.ts +0 -17
|
@@ -0,0 +1,1134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FormPlugin = void 0;
|
|
4
|
+
const sdk_build_core_1 = require("@servicenow/sdk-build-core");
|
|
5
|
+
const xmlbuilder2_1 = require("xmlbuilder2");
|
|
6
|
+
const utils_1 = require("./utils");
|
|
7
|
+
const now_id_plugin_1 = require("./now-id-plugin");
|
|
8
|
+
const DEFAULT_VIEW = 'Default view';
|
|
9
|
+
const DEFAULT_ANNOTATION_TYPE = '753f88a80f930000b12e6903cfe01206';
|
|
10
|
+
const FORM_XML_TABLES = ['sys_ui_form', 'sys_ui_form_section'];
|
|
11
|
+
// Derive sys_id → key name maps from the source-of-truth constants
|
|
12
|
+
function buildReverseMap(ns) {
|
|
13
|
+
const map = new Map();
|
|
14
|
+
for (const [key, value] of Object.entries(ns)) {
|
|
15
|
+
map.set(value, key);
|
|
16
|
+
}
|
|
17
|
+
return map;
|
|
18
|
+
}
|
|
19
|
+
//TODO:: Import AnnotationType and Formatter from sdk-api once Form plugin is fixed.
|
|
20
|
+
const AnnotationType = {};
|
|
21
|
+
const Formatter = {};
|
|
22
|
+
const ANNOTATION_TYPE_MAP = buildReverseMap(AnnotationType);
|
|
23
|
+
const FORMATTER_MAP = buildReverseMap(Formatter);
|
|
24
|
+
// Maps Formatter sys_id → element name (the `formatter` column on sys_ui_formatter)
|
|
25
|
+
const FORMATTER_ELEMENT_MAP = new Map([
|
|
26
|
+
['444ea5c6bf310100e628555b3f0739d6', 'activity.xml'],
|
|
27
|
+
['cfa76e850a0a0b1f01446f67c8538d00', 'attached_knowledge'],
|
|
28
|
+
]);
|
|
29
|
+
// Split type constants for form layout
|
|
30
|
+
const SPLIT_TYPE = {
|
|
31
|
+
BEGIN: '.begin_split',
|
|
32
|
+
MIDDLE: '.split',
|
|
33
|
+
END: '.end_split',
|
|
34
|
+
};
|
|
35
|
+
const SPLIT_TYPES = [SPLIT_TYPE.BEGIN, SPLIT_TYPE.MIDDLE, SPLIT_TYPE.END];
|
|
36
|
+
function sortByPosition(records) {
|
|
37
|
+
return records.sort((a, b) => (a.get('position')?.toNumber()?.getValue() ?? 0) - (b.get('position')?.toNumber()?.getValue() ?? 0));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Converts a UI element record to a FormElement for shape generation.
|
|
41
|
+
* Returns null for split marker elements (they are handled by groupElementsIntoLayoutBlocks).
|
|
42
|
+
*/
|
|
43
|
+
function convertElementToField(element, descendants) {
|
|
44
|
+
const elementValue = element.get('element').asString().getValue();
|
|
45
|
+
const type = getOptionalString(element, 'type');
|
|
46
|
+
const formatter = getOptionalString(element, 'sys_ui_formatter');
|
|
47
|
+
// Split markers are handled by groupElementsIntoLayoutBlocks, not emitted as elements
|
|
48
|
+
if (SPLIT_TYPES.includes(type)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (type === 'annotation' && descendants) {
|
|
52
|
+
// element field of sys_ui_element contains the sys_id of the sys_ui_annotation record
|
|
53
|
+
const annotation = descendants.query('sys_ui_annotation').find((a) => a.getId().getValue() === elementValue);
|
|
54
|
+
if (annotation) {
|
|
55
|
+
const isPlainText = annotation.get('is_plain_text').toBoolean()?.getValue() ?? true;
|
|
56
|
+
const annotationTypeSysId = getOptionalString(annotation, 'type');
|
|
57
|
+
const annotationTypeKey = ANNOTATION_TYPE_MAP.get(annotationTypeSysId);
|
|
58
|
+
return {
|
|
59
|
+
type: 'annotation',
|
|
60
|
+
annotationId: now_id_plugin_1.NowIdShape.from(annotation),
|
|
61
|
+
text: getOptionalString(annotation, 'text'),
|
|
62
|
+
isPlainText: isPlainText,
|
|
63
|
+
annotationType: annotationTypeKey ?? annotationTypeSysId,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
type: 'annotation',
|
|
68
|
+
annotationId: new now_id_plugin_1.NowIdShape({ source: element, id: elementValue }),
|
|
69
|
+
text: '',
|
|
70
|
+
annotationType: 'Info_Box_Blue',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (type === 'formatter') {
|
|
74
|
+
const formatterKey = FORMATTER_MAP.get(formatter);
|
|
75
|
+
const derivableName = FORMATTER_ELEMENT_MAP.get(formatter);
|
|
76
|
+
return {
|
|
77
|
+
type: 'formatter',
|
|
78
|
+
// Only emit formatterName when it differs from the derivable value
|
|
79
|
+
...(elementValue !== derivableName ? { formatterName: elementValue } : {}),
|
|
80
|
+
formatterRef: formatterKey ?? formatter,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (type === 'list') {
|
|
84
|
+
// Parse the encoded element string to extract listType and list components
|
|
85
|
+
const parsed = parseListElement(elementValue);
|
|
86
|
+
if (parsed.listType === '12M' || parsed.listType === 'M2M') {
|
|
87
|
+
return {
|
|
88
|
+
type: 'list',
|
|
89
|
+
listType: parsed.listType,
|
|
90
|
+
listRef: `${parsed.listTable}.${parsed.listColumn}`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (parsed.listType === 'custom') {
|
|
94
|
+
return {
|
|
95
|
+
type: 'list',
|
|
96
|
+
listType: 'custom',
|
|
97
|
+
listRef: parsed.relationship,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
// Default: table field element
|
|
103
|
+
return {
|
|
104
|
+
field: elementValue,
|
|
105
|
+
type: 'table_field',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Parses an encoded list element string into its components.
|
|
110
|
+
* Formats:
|
|
111
|
+
* - '12M.<parent>.<child>.<field>' → { listType: '12M', listTable: '<child>', listColumn: '<field>' }
|
|
112
|
+
* - 'M2M.<parent>.<join>.<field>' → { listType: 'M2M', listTable: '<join>', listColumn: '<field>' }
|
|
113
|
+
* - 'REL.<parent>.REL:<sys_id>' → { listType: 'custom', relationship: '<sys_id>' }
|
|
114
|
+
*/
|
|
115
|
+
function parseListElement(elementValue) {
|
|
116
|
+
if (elementValue.startsWith('12M.')) {
|
|
117
|
+
const parts = elementValue.substring(4).split('.');
|
|
118
|
+
// parts: [parent_table, child_table, ref_column]
|
|
119
|
+
return { listType: '12M', listTable: parts[1] ?? '', listColumn: parts[2] ?? '' };
|
|
120
|
+
}
|
|
121
|
+
if (elementValue.startsWith('M2M.')) {
|
|
122
|
+
const parts = elementValue.substring(4).split('.');
|
|
123
|
+
// parts: [parent_table, join_table, ref_column]
|
|
124
|
+
return { listType: 'M2M', listTable: parts[1] ?? '', listColumn: parts[2] ?? '' };
|
|
125
|
+
}
|
|
126
|
+
if (elementValue.startsWith('REL.')) {
|
|
127
|
+
// Format: REL.<table>.REL:<sys_id>
|
|
128
|
+
const relMatch = elementValue.match(/^REL\.[^.]+\.REL:(.+)$/);
|
|
129
|
+
return { listType: 'custom', relationship: relMatch?.[1] ?? elementValue };
|
|
130
|
+
}
|
|
131
|
+
// Fallback
|
|
132
|
+
return { listType: 'custom', relationship: elementValue };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Groups a flat array of sorted sys_ui_element records into LayoutBlock[] for the content array.
|
|
136
|
+
* Recognizes .begin_split / .split / .end_split sequences and wraps them into two-column blocks.
|
|
137
|
+
* Consecutive non-split elements are grouped into one-column blocks.
|
|
138
|
+
*/
|
|
139
|
+
function groupElementsIntoLayoutBlocks(elements, descendants) {
|
|
140
|
+
const blocks = [];
|
|
141
|
+
let currentOneColumnElements = [];
|
|
142
|
+
// Flush accumulated one-column elements into a block. The length check handles
|
|
143
|
+
// edge cases where flushOneColumn is called with no pending elements, e.g. when
|
|
144
|
+
// a .begin_split is the first element or two split blocks are adjacent.
|
|
145
|
+
const flushOneColumn = () => {
|
|
146
|
+
if (currentOneColumnElements.length > 0) {
|
|
147
|
+
blocks.push({ layout: 'one-column', elements: currentOneColumnElements });
|
|
148
|
+
currentOneColumnElements = [];
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
let i = 0;
|
|
152
|
+
while (i < elements.length) {
|
|
153
|
+
const elem = elements[i];
|
|
154
|
+
const type = getOptionalString(elem, 'type');
|
|
155
|
+
if (type === SPLIT_TYPE.BEGIN) {
|
|
156
|
+
// Flush any pending one-column elements
|
|
157
|
+
flushOneColumn();
|
|
158
|
+
// Collect left and right elements until .end_split
|
|
159
|
+
const leftElements = [];
|
|
160
|
+
const rightElements = [];
|
|
161
|
+
let side = 'left';
|
|
162
|
+
i++; // skip .begin_split
|
|
163
|
+
while (i < elements.length) {
|
|
164
|
+
const innerElem = elements[i];
|
|
165
|
+
const innerType = getOptionalString(innerElem, 'type');
|
|
166
|
+
if (innerType === SPLIT_TYPE.END) {
|
|
167
|
+
i++; // skip .end_split
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
if (innerType === SPLIT_TYPE.MIDDLE) {
|
|
171
|
+
side = 'right';
|
|
172
|
+
i++;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const converted = convertElementToField(innerElem, descendants);
|
|
176
|
+
if (converted) {
|
|
177
|
+
if (side === 'left') {
|
|
178
|
+
leftElements.push(converted);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
rightElements.push(converted);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
i++;
|
|
185
|
+
}
|
|
186
|
+
blocks.push({ layout: 'two-column', leftElements, rightElements });
|
|
187
|
+
}
|
|
188
|
+
else if (type === SPLIT_TYPE.MIDDLE) {
|
|
189
|
+
// Handle lone .split without .begin_split / .end_split.
|
|
190
|
+
// ServiceNow can produce this when fields are dragged across columns.
|
|
191
|
+
// Treat accumulated one-column elements as leftElements,
|
|
192
|
+
// and everything after .split until the next split marker or end as rightElements.
|
|
193
|
+
const leftElements = [...currentOneColumnElements];
|
|
194
|
+
currentOneColumnElements = [];
|
|
195
|
+
const rightElements = [];
|
|
196
|
+
i++; // skip .split
|
|
197
|
+
while (i < elements.length) {
|
|
198
|
+
const innerElem = elements[i];
|
|
199
|
+
const innerType = getOptionalString(innerElem, 'type');
|
|
200
|
+
if (innerType === SPLIT_TYPE.END || innerType === SPLIT_TYPE.MIDDLE || innerType === SPLIT_TYPE.BEGIN) {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
const converted = convertElementToField(innerElem, descendants);
|
|
204
|
+
if (converted) {
|
|
205
|
+
rightElements.push(converted);
|
|
206
|
+
}
|
|
207
|
+
i++;
|
|
208
|
+
}
|
|
209
|
+
blocks.push({ layout: 'two-column', leftElements, rightElements });
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
const converted = convertElementToField(elem, descendants);
|
|
213
|
+
if (converted) {
|
|
214
|
+
currentOneColumnElements.push(converted);
|
|
215
|
+
}
|
|
216
|
+
i++;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Flush remaining one-column elements
|
|
220
|
+
flushOneColumn();
|
|
221
|
+
return blocks;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Gets an optional string value from a record field with a default fallback
|
|
225
|
+
*/
|
|
226
|
+
function getOptionalString(record, field, defaultValue = '') {
|
|
227
|
+
return record.get(field).ifString()?.getValue() ?? defaultValue;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Resolves a view value to its sys_id and view name for XML output.
|
|
231
|
+
* The name is needed as an attribute on the <view> XML element so the parser
|
|
232
|
+
* can create a RecordId with proper coalesce keys when re-reading the XML.
|
|
233
|
+
*/
|
|
234
|
+
function resolveView(view, database) {
|
|
235
|
+
if (view.isRecordId() || view.isRecord()) {
|
|
236
|
+
const viewId = view.isRecordId() ? view : view.getId();
|
|
237
|
+
const sysId = viewId.getValue();
|
|
238
|
+
// Try to resolve the view record to get its name
|
|
239
|
+
const viewRecord = database.resolve(viewId);
|
|
240
|
+
if (viewRecord) {
|
|
241
|
+
const name = viewRecord.get('name').ifString()?.getValue() ?? '';
|
|
242
|
+
return { sysId, name };
|
|
243
|
+
}
|
|
244
|
+
// Fall back to primary key (view name) if available
|
|
245
|
+
if (view.isRecordId() && view.asRecordId().hasPrimaryKey()) {
|
|
246
|
+
const pk = view.asRecordId().getPrimaryKey();
|
|
247
|
+
if (pk && !(0, sdk_build_core_1.isGUID)(pk)) {
|
|
248
|
+
return { sysId, name: pk };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return { sysId, name: '' };
|
|
252
|
+
}
|
|
253
|
+
const viewValue = view.getValue();
|
|
254
|
+
if (viewValue === 'NULL' || viewValue === DEFAULT_VIEW) {
|
|
255
|
+
return { sysId: DEFAULT_VIEW, name: DEFAULT_VIEW };
|
|
256
|
+
}
|
|
257
|
+
// View is a string - try to resolve to record ID if it exists in database
|
|
258
|
+
const viewNameStr = view.ifString()?.getValue() ?? '';
|
|
259
|
+
if (!viewNameStr) {
|
|
260
|
+
return { sysId: '', name: '' };
|
|
261
|
+
}
|
|
262
|
+
const viewRecord = database.query('sys_ui_view').find((v) => v.get('name').ifString()?.getValue() === viewNameStr);
|
|
263
|
+
if (viewRecord) {
|
|
264
|
+
return { sysId: viewRecord.getId().getValue(), name: viewNameStr };
|
|
265
|
+
}
|
|
266
|
+
return { sysId: viewNameStr, name: (0, sdk_build_core_1.isGUID)(viewNameStr) ? '' : viewNameStr };
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Resolves a view value to its string representation (sys_id or name)
|
|
270
|
+
* Handles RecordId, Record, string values, and database lookups
|
|
271
|
+
*/
|
|
272
|
+
function resolveViewName(view, database) {
|
|
273
|
+
return resolveView(view, database).sysId;
|
|
274
|
+
}
|
|
275
|
+
exports.FormPlugin = sdk_build_core_1.Plugin.create({
|
|
276
|
+
name: 'FormPlugin',
|
|
277
|
+
docs: [
|
|
278
|
+
(0, utils_1.createSdkDocEntry)('Form', [
|
|
279
|
+
'sys_ui_form',
|
|
280
|
+
'sys_ui_form_section',
|
|
281
|
+
'sys_ui_section',
|
|
282
|
+
'sys_ui_element',
|
|
283
|
+
'sys_ui_annotation',
|
|
284
|
+
]),
|
|
285
|
+
],
|
|
286
|
+
records: {
|
|
287
|
+
sys_ui_section: {
|
|
288
|
+
composite: true,
|
|
289
|
+
coalesce: ['name', 'caption', 'view', 'sys_domain'],
|
|
290
|
+
relationships: {
|
|
291
|
+
sys_ui_element: {
|
|
292
|
+
via: 'sys_ui_section',
|
|
293
|
+
descendant: true,
|
|
294
|
+
relationships: {
|
|
295
|
+
sys_ui_annotation: {
|
|
296
|
+
// sys_ui_element.element field holds the sys_id of sys_ui_annotation
|
|
297
|
+
via: 'element',
|
|
298
|
+
inverse: true,
|
|
299
|
+
descendant: true,
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
sys_ui_view: {
|
|
304
|
+
via: 'view',
|
|
305
|
+
inverse: true,
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
toFile(section, { config, descendants, database }) {
|
|
309
|
+
// For DELETE records, output a simple delete XML
|
|
310
|
+
if (section.getAction() === 'DELETE') {
|
|
311
|
+
const xml = (0, xmlbuilder2_1.create)().ele('record_update', { table: 'sys_ui_section' });
|
|
312
|
+
const child = xml.ele('sys_ui_section', { action: 'DELETE' });
|
|
313
|
+
child.ele('sys_id').txt(section.getId().getValue());
|
|
314
|
+
child.ele('sys_scope', { display_value: config.scope }).txt(config.scopeId);
|
|
315
|
+
child.ele('sys_update_name').txt(`sys_ui_section_${section.getId().getValue()}`);
|
|
316
|
+
return {
|
|
317
|
+
success: true,
|
|
318
|
+
value: {
|
|
319
|
+
source: section,
|
|
320
|
+
name: `sys_ui_section_${section.getId().getValue()}.xml`,
|
|
321
|
+
category: section.getInstallCategory(),
|
|
322
|
+
content: xml.end({ prettyPrint: true }),
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const sectionName = section.get('name').asString().getValue();
|
|
327
|
+
const caption = getOptionalString(section, 'caption');
|
|
328
|
+
const sysDomain = getOptionalString(section, 'sys_domain', 'global');
|
|
329
|
+
const { sysId: viewName, name: viewDisplayName } = resolveView(section.get('view'), database);
|
|
330
|
+
const xml = (0, xmlbuilder2_1.create)().ele('record_update');
|
|
331
|
+
const root = xml.ele('sys_ui_section', {
|
|
332
|
+
caption,
|
|
333
|
+
section_id: section.getId().getValue(),
|
|
334
|
+
sys_domain: sysDomain,
|
|
335
|
+
table: sectionName,
|
|
336
|
+
view: viewName,
|
|
337
|
+
});
|
|
338
|
+
// Add all sys_ui_annotation records for this section (excluding deleted ones)
|
|
339
|
+
const annotations = descendants
|
|
340
|
+
.query('sys_ui_annotation')
|
|
341
|
+
.filter((annotation) => annotation.getAction() !== 'DELETE');
|
|
342
|
+
for (const annotation of annotations) {
|
|
343
|
+
const annotationChild = root.ele('sys_ui_annotation', { action: annotation.getAction() });
|
|
344
|
+
const isPlainTextVal = annotation.get('is_plain_text').ifBoolean()?.getValue() ?? true;
|
|
345
|
+
annotationChild.ele('is_plain_text').txt(String(isPlainTextVal));
|
|
346
|
+
annotationChild.ele('name').txt(getOptionalString(annotation, 'name'));
|
|
347
|
+
annotationChild.ele('sys_id').txt(annotation.getId().getValue());
|
|
348
|
+
annotationChild.ele('text').txt(getOptionalString(annotation, 'text'));
|
|
349
|
+
const annotationType = annotation.get('type');
|
|
350
|
+
if (annotationType?.isRecordId() || annotationType?.isRecord()) {
|
|
351
|
+
const typeId = annotationType.isRecordId() ? annotationType : annotationType.getId();
|
|
352
|
+
const typeDisplayValue = getOptionalString(annotation, 'type_display_value');
|
|
353
|
+
annotationChild.ele('type', { display_value: typeDisplayValue }).txt(typeId.getValue());
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
annotationChild.ele('type').txt(getOptionalString(annotation, 'type'));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Add all sys_ui_element records for this section (excluding deleted ones)
|
|
360
|
+
const elements = sortByPosition(descendants.query('sys_ui_element').filter((element) => element.getAction() !== 'DELETE'));
|
|
361
|
+
for (const element of elements) {
|
|
362
|
+
const child = root.ele('sys_ui_element', { action: element.getAction() });
|
|
363
|
+
child.ele('element').txt(element.get('element').asString().getValue());
|
|
364
|
+
child.ele('position').txt(element.get('position').toNumber().getValue().toString());
|
|
365
|
+
child.ele('sys_id').txt(element.getId().getValue());
|
|
366
|
+
child.ele('sys_ui_formatter').txt(getOptionalString(element, 'sys_ui_formatter'));
|
|
367
|
+
child
|
|
368
|
+
.ele('sys_ui_section', {
|
|
369
|
+
caption,
|
|
370
|
+
display_value: caption,
|
|
371
|
+
name: sectionName,
|
|
372
|
+
sys_domain: sysDomain,
|
|
373
|
+
view: viewName,
|
|
374
|
+
})
|
|
375
|
+
.txt(section.getId().getValue());
|
|
376
|
+
child.ele('sys_user');
|
|
377
|
+
child.ele('type').txt(getOptionalString(element, 'type'));
|
|
378
|
+
}
|
|
379
|
+
// Add the sys_ui_section record itself
|
|
380
|
+
const sectionChild = root.ele('sys_ui_section', { action: section.getAction() });
|
|
381
|
+
sectionChild.ele('caption').txt(caption);
|
|
382
|
+
sectionChild.ele('header').txt(String(section.get('header').ifBoolean()?.getValue() ?? false));
|
|
383
|
+
sectionChild.ele('name').txt(sectionName);
|
|
384
|
+
sectionChild.ele('roles').txt(getOptionalString(section, 'roles'));
|
|
385
|
+
sectionChild.ele('sys_domain').txt(sysDomain);
|
|
386
|
+
sectionChild.ele('sys_id').txt(section.getId().getValue());
|
|
387
|
+
sectionChild.ele('sys_scope', { display_value: config.scope }).txt(config.scopeId);
|
|
388
|
+
sectionChild.ele('sys_user');
|
|
389
|
+
sectionChild.ele('title').txt(String(section.get('title').ifBoolean()?.getValue() ?? false));
|
|
390
|
+
sectionChild.ele('view_name');
|
|
391
|
+
sectionChild.ele('view', viewDisplayName ? { name: viewDisplayName } : {}).txt(viewName);
|
|
392
|
+
return {
|
|
393
|
+
success: true,
|
|
394
|
+
value: {
|
|
395
|
+
source: section,
|
|
396
|
+
name: `sys_ui_section_${section.getId().getValue()}.xml`,
|
|
397
|
+
category: section.getInstallCategory(),
|
|
398
|
+
content: xml.end({ prettyPrint: true }),
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
},
|
|
402
|
+
async diff(existing, incoming, _, { factory }) {
|
|
403
|
+
// If either database is empty, return the incoming as-is or empty database
|
|
404
|
+
if (incoming.query().length === 0 || existing.query().length === 0) {
|
|
405
|
+
return {
|
|
406
|
+
success: true,
|
|
407
|
+
value: incoming.query().length === 0 ? new sdk_build_core_1.Database() : new sdk_build_core_1.Database(incoming.query()),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
const changeDatabase = new sdk_build_core_1.Database();
|
|
411
|
+
let hasChanges = false;
|
|
412
|
+
const existingSection = existing.query('sys_ui_section')[0];
|
|
413
|
+
const incomingSection = incoming.query('sys_ui_section')[0];
|
|
414
|
+
const existingElements = existing.query('sys_ui_element');
|
|
415
|
+
const incomingElements = incoming.query('sys_ui_element');
|
|
416
|
+
const existingAnnotations = existing.query('sys_ui_annotation');
|
|
417
|
+
const incomingAnnotations = incoming.query('sys_ui_annotation');
|
|
418
|
+
// 1. Compare and merge the main form record
|
|
419
|
+
if (incomingSection && existingSection) {
|
|
420
|
+
if (!existingSection.strictEquals(incomingSection)) {
|
|
421
|
+
changeDatabase.insert(existingSection.merge(incomingSection));
|
|
422
|
+
hasChanges = true;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else if (incomingSection) {
|
|
426
|
+
changeDatabase.insert(incomingSection);
|
|
427
|
+
hasChanges = true;
|
|
428
|
+
}
|
|
429
|
+
// 2. Compare and merge sys_ui_annotation records
|
|
430
|
+
const markAnnotationsForRemoval = [];
|
|
431
|
+
for (const annotation of existingAnnotations) {
|
|
432
|
+
const match = incoming.resolve(annotation.getId());
|
|
433
|
+
if (!match) {
|
|
434
|
+
hasChanges = true;
|
|
435
|
+
markAnnotationsForRemoval.push(annotation);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
hasChanges = hasChanges || !annotation.strictEquals(match);
|
|
439
|
+
changeDatabase.insert(annotation.merge(match));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Add new annotations
|
|
443
|
+
incomingAnnotations.forEach((annotation) => {
|
|
444
|
+
const match = changeDatabase.resolve(annotation.getId());
|
|
445
|
+
if (!match) {
|
|
446
|
+
changeDatabase.insert(annotation);
|
|
447
|
+
hasChanges = true;
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
// Delete removed annotations
|
|
451
|
+
for (const annotation of markAnnotationsForRemoval) {
|
|
452
|
+
const deleteRecord = await factory.createRecord({
|
|
453
|
+
source: annotation.getSource(),
|
|
454
|
+
table: 'sys_ui_annotation',
|
|
455
|
+
explicitId: annotation.getId(),
|
|
456
|
+
properties: annotation.properties(),
|
|
457
|
+
action: 'DELETE',
|
|
458
|
+
});
|
|
459
|
+
changeDatabase.insert(deleteRecord);
|
|
460
|
+
}
|
|
461
|
+
// 3. Compare and merge sys_ui_element records
|
|
462
|
+
const markElementsForRemoval = [];
|
|
463
|
+
for (const element of existingElements) {
|
|
464
|
+
const match = incoming.resolve(element.getId());
|
|
465
|
+
if (!match) {
|
|
466
|
+
hasChanges = true;
|
|
467
|
+
markElementsForRemoval.push(element);
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
hasChanges = hasChanges || !element.strictEquals(match);
|
|
471
|
+
changeDatabase.insert(element.merge(match));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// Add new elements
|
|
475
|
+
incomingElements.forEach((element) => {
|
|
476
|
+
const match = changeDatabase.resolve(element.getId());
|
|
477
|
+
if (!match) {
|
|
478
|
+
changeDatabase.insert(element);
|
|
479
|
+
hasChanges = true;
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
// Delete removed elements
|
|
483
|
+
for (const element of markElementsForRemoval) {
|
|
484
|
+
const deleteRecord = await factory.createRecord({
|
|
485
|
+
source: element.getSource(),
|
|
486
|
+
table: 'sys_ui_element',
|
|
487
|
+
explicitId: element.getId(),
|
|
488
|
+
properties: element.properties(),
|
|
489
|
+
action: 'DELETE',
|
|
490
|
+
});
|
|
491
|
+
changeDatabase.insert(deleteRecord);
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
success: true,
|
|
495
|
+
value: hasChanges ? changeDatabase : new sdk_build_core_1.Database(),
|
|
496
|
+
};
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
sys_ui_element: {
|
|
500
|
+
coalesce: ['sys_ui_section', 'element', 'position'],
|
|
501
|
+
},
|
|
502
|
+
sys_ui_form_section: {
|
|
503
|
+
coalesce: ['sys_ui_form', 'sys_ui_section'],
|
|
504
|
+
},
|
|
505
|
+
sys_ui_form: {
|
|
506
|
+
coalesce: ['name', 'view', 'sys_domain'],
|
|
507
|
+
relationships: {
|
|
508
|
+
sys_ui_form_section: {
|
|
509
|
+
via: 'sys_ui_form', // Reference column name on this table
|
|
510
|
+
descendant: true,
|
|
511
|
+
relationships: {
|
|
512
|
+
sys_ui_section: {
|
|
513
|
+
via: 'sys_ui_section',
|
|
514
|
+
inverse: true, // Indicates the parent refers to this table
|
|
515
|
+
descendant: true,
|
|
516
|
+
relationships: {
|
|
517
|
+
sys_ui_element: {
|
|
518
|
+
via: 'sys_ui_section',
|
|
519
|
+
descendant: true,
|
|
520
|
+
relationships: {
|
|
521
|
+
sys_ui_annotation: {
|
|
522
|
+
via: 'element',
|
|
523
|
+
inverse: true,
|
|
524
|
+
descendant: true,
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
sys_ui_view: {
|
|
529
|
+
via: 'view',
|
|
530
|
+
inverse: true,
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
sys_ui_view: {
|
|
537
|
+
via: 'view',
|
|
538
|
+
inverse: true,
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
toShape(record, { descendants, database }) {
|
|
542
|
+
const sections = sortByPosition(descendants.query('sys_ui_form_section'))
|
|
543
|
+
.map((formSection) => {
|
|
544
|
+
// Get section ID from form_section record
|
|
545
|
+
const sectionId = formSection.get('sys_ui_section');
|
|
546
|
+
// Find the section record
|
|
547
|
+
const section = descendants.query('sys_ui_section').find((s) => s.getId().equals(sectionId));
|
|
548
|
+
if (!section) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
// Get section caption
|
|
552
|
+
const caption = section.get('caption').ifString()?.getValue() ?? '';
|
|
553
|
+
const header = section.get('header')?.ifBoolean()?.getValue() ?? false;
|
|
554
|
+
const title = section.get('title')?.ifBoolean()?.getValue() ?? false;
|
|
555
|
+
// Get elements for this section, sorted by position
|
|
556
|
+
const sortedElements = sortByPosition(descendants
|
|
557
|
+
.query('sys_ui_element')
|
|
558
|
+
.filter((elem) => elem.get('sys_ui_section').equals(sectionId)));
|
|
559
|
+
// Group flat elements into layout blocks (one-column / two-column)
|
|
560
|
+
const content = groupElementsIntoLayoutBlocks(sortedElements, descendants);
|
|
561
|
+
return {
|
|
562
|
+
caption,
|
|
563
|
+
content,
|
|
564
|
+
...(header && { header }),
|
|
565
|
+
...(title && { title }),
|
|
566
|
+
};
|
|
567
|
+
})
|
|
568
|
+
.filter(Boolean); // Remove any nulls
|
|
569
|
+
return {
|
|
570
|
+
success: true,
|
|
571
|
+
value: new sdk_build_core_1.CallExpressionShape({
|
|
572
|
+
source: record,
|
|
573
|
+
callee: 'Form',
|
|
574
|
+
args: [
|
|
575
|
+
record.transform(({ $ }) => ({
|
|
576
|
+
table: $.from('name'),
|
|
577
|
+
view: $.map((v) => {
|
|
578
|
+
if (v.equals(DEFAULT_VIEW)) {
|
|
579
|
+
return new sdk_build_core_1.IdentifierShape({ source: record, name: 'default_view' });
|
|
580
|
+
}
|
|
581
|
+
// Reuse resolveView to resolve the view value to its name
|
|
582
|
+
const resolved = resolveView(v, database);
|
|
583
|
+
if (resolved.name && resolved.name !== DEFAULT_VIEW) {
|
|
584
|
+
return resolved.name;
|
|
585
|
+
}
|
|
586
|
+
return v.ifString() ?? v.toRecordId();
|
|
587
|
+
}),
|
|
588
|
+
user: $.from('sys_user').def(''),
|
|
589
|
+
roles: $.from('roles')
|
|
590
|
+
.map((v) => {
|
|
591
|
+
return v.isString() && !v.isEmpty()
|
|
592
|
+
? v
|
|
593
|
+
.asString()
|
|
594
|
+
.getValue()
|
|
595
|
+
.split(',')
|
|
596
|
+
.map((role) => role.trim())
|
|
597
|
+
: [];
|
|
598
|
+
})
|
|
599
|
+
.def([]),
|
|
600
|
+
sections: $.val(sections),
|
|
601
|
+
})),
|
|
602
|
+
],
|
|
603
|
+
}),
|
|
604
|
+
};
|
|
605
|
+
},
|
|
606
|
+
toFile(form, { config, descendants, database }) {
|
|
607
|
+
if (!form.has('name')) {
|
|
608
|
+
return { success: false };
|
|
609
|
+
}
|
|
610
|
+
// For DELETE records, output a simple delete XML
|
|
611
|
+
if (form.getAction() === 'DELETE') {
|
|
612
|
+
const xml = (0, xmlbuilder2_1.create)().ele('record_update', { table: 'sys_ui_form' });
|
|
613
|
+
const child = xml.ele('sys_ui_form', { action: 'DELETE' });
|
|
614
|
+
child.ele('sys_id').txt(form.getId().getValue());
|
|
615
|
+
child.ele('sys_scope', { display_value: config.scope }).txt(config.scopeId);
|
|
616
|
+
child.ele('sys_update_name').txt(`sys_ui_form_sections_${form.getId().getValue()}`);
|
|
617
|
+
return {
|
|
618
|
+
success: true,
|
|
619
|
+
value: {
|
|
620
|
+
source: form,
|
|
621
|
+
name: `sys_ui_form_sections_${form.getId().getValue()}.xml`,
|
|
622
|
+
category: form.getInstallCategory(),
|
|
623
|
+
content: xml.end({ prettyPrint: true }),
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
const formName = form.get('name').asString().getValue();
|
|
628
|
+
const formSysDomain = getOptionalString(form, 'sys_domain', 'global');
|
|
629
|
+
const { sysId: formViewName, name: formViewDisplayName } = resolveView(form.get('view'), database);
|
|
630
|
+
const xml = (0, xmlbuilder2_1.create)().ele('record_update');
|
|
631
|
+
const root = xml.ele('sys_ui_form_sections', {
|
|
632
|
+
form_id: form.getId().getValue(),
|
|
633
|
+
sys_domain: formSysDomain,
|
|
634
|
+
table: formName,
|
|
635
|
+
});
|
|
636
|
+
// Add all sys_ui_form_section records (excluding deleted ones)
|
|
637
|
+
const formSections = sortByPosition(descendants
|
|
638
|
+
.query('sys_ui_form_section')
|
|
639
|
+
.filter((formSection) => formSection.getAction() !== 'DELETE'));
|
|
640
|
+
for (const formSection of formSections) {
|
|
641
|
+
const child = root.ele('sys_ui_form_section', { action: 'INSERT_OR_UPDATE' });
|
|
642
|
+
child.ele('position').txt(formSection.get('position').toNumber().getValue().toString());
|
|
643
|
+
child.ele('sys_id').txt(formSection.getId().getValue());
|
|
644
|
+
child
|
|
645
|
+
.ele('sys_ui_form', {
|
|
646
|
+
display_value: formName,
|
|
647
|
+
name: formName,
|
|
648
|
+
sys_domain: formSysDomain,
|
|
649
|
+
view: formViewName,
|
|
650
|
+
})
|
|
651
|
+
.txt(form.getId().getValue());
|
|
652
|
+
// Add section reference
|
|
653
|
+
const sectionId = formSection.get('sys_ui_section');
|
|
654
|
+
const section = descendants.query('sys_ui_section').find((s) => s.getId().equals(sectionId));
|
|
655
|
+
if (section) {
|
|
656
|
+
const sectionCaption = getOptionalString(section, 'caption');
|
|
657
|
+
const sectionViewName = resolveViewName(section.get('view'), database);
|
|
658
|
+
child
|
|
659
|
+
.ele('sys_ui_section', {
|
|
660
|
+
caption: sectionCaption,
|
|
661
|
+
display_value: sectionCaption,
|
|
662
|
+
name: formName,
|
|
663
|
+
sys_domain: getOptionalString(section, 'sys_domain', 'global'),
|
|
664
|
+
view: sectionViewName,
|
|
665
|
+
})
|
|
666
|
+
.txt(section.getId().getValue());
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// Add the sys_ui_form record itself
|
|
670
|
+
const formChild = root.ele('sys_ui_form', { action: 'INSERT_OR_UPDATE' });
|
|
671
|
+
formChild.ele('name').txt(formName);
|
|
672
|
+
formChild.ele('roles').txt(getOptionalString(form, 'roles'));
|
|
673
|
+
formChild.ele('sys_id').txt(form.getId().getValue());
|
|
674
|
+
formChild.ele('sys_scope', { display_value: config.scope }).txt(config.scopeId);
|
|
675
|
+
formChild.ele('sys_user').txt(getOptionalString(form, 'sys_user'));
|
|
676
|
+
formChild.ele('view', formViewDisplayName ? { name: formViewDisplayName } : {}).txt(formViewName);
|
|
677
|
+
formChild.ele('view_name');
|
|
678
|
+
return {
|
|
679
|
+
success: true,
|
|
680
|
+
value: {
|
|
681
|
+
source: form,
|
|
682
|
+
name: `sys_ui_form_sections_${form.getId().getValue()}.xml`,
|
|
683
|
+
category: form.getInstallCategory(),
|
|
684
|
+
content: xml.end({ prettyPrint: true }),
|
|
685
|
+
},
|
|
686
|
+
};
|
|
687
|
+
},
|
|
688
|
+
async diff(existingDB, incomingDB, _, { factory }) {
|
|
689
|
+
/**
|
|
690
|
+
* Exclude sys_ui_section and sys_ui_element records from this diff to handle incremental updates correctly.
|
|
691
|
+
*
|
|
692
|
+
* Why: If a user modifies only a UI section during incremental transformation, we don't want that change
|
|
693
|
+
* to trigger a diff in sys_ui_form that would incorrectly remove form_sections. Instead, sys_ui_section
|
|
694
|
+
* and sys_ui_element records are compared separately using their own dedicated diff function.
|
|
695
|
+
*/
|
|
696
|
+
const incoming = incomingDB.query().filter((r) => FORM_XML_TABLES.includes(r.getTable()));
|
|
697
|
+
const existing = existingDB.query().filter((r) => FORM_XML_TABLES.includes(r.getTable()));
|
|
698
|
+
// If either database is empty, return the incoming as-is or empty database
|
|
699
|
+
if (incoming.length === 0 || existing.length === 0) {
|
|
700
|
+
return {
|
|
701
|
+
success: true,
|
|
702
|
+
value: incoming.length === 0 ? new sdk_build_core_1.Database() : new sdk_build_core_1.Database(incoming),
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
const changeDatabase = new sdk_build_core_1.Database();
|
|
706
|
+
let hasChanges = false;
|
|
707
|
+
// Get the main records
|
|
708
|
+
const existingForm = existing.filter((r) => r.getTable() === 'sys_ui_form')[0];
|
|
709
|
+
const incomingForm = incoming.filter((r) => r.getTable() === 'sys_ui_form')[0];
|
|
710
|
+
const existingFormSections = existing.filter((r) => r.getTable() === 'sys_ui_form_section');
|
|
711
|
+
const incomingFormSections = incoming.filter((r) => r.getTable() === 'sys_ui_form_section');
|
|
712
|
+
// 1. Compare and merge the main form record
|
|
713
|
+
if (incomingForm && existingForm) {
|
|
714
|
+
if (!existingForm.strictEquals(incomingForm)) {
|
|
715
|
+
changeDatabase.insert(existingForm.merge(incomingForm));
|
|
716
|
+
hasChanges = true;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
else if (incomingForm) {
|
|
720
|
+
changeDatabase.insert(incomingForm);
|
|
721
|
+
hasChanges = true;
|
|
722
|
+
}
|
|
723
|
+
// 2. Compare and merge sys_ui_form_section records (join table)
|
|
724
|
+
const markFormSectionsForRemoval = [];
|
|
725
|
+
for (const formSection of existingFormSections) {
|
|
726
|
+
const match = incomingDB.resolve(formSection.getId());
|
|
727
|
+
if (!match) {
|
|
728
|
+
hasChanges = true;
|
|
729
|
+
markFormSectionsForRemoval.push(formSection);
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
hasChanges = hasChanges || !formSection.strictEquals(match);
|
|
733
|
+
changeDatabase.insert(formSection.merge(match));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
incomingFormSections.forEach((formSection) => {
|
|
737
|
+
const match = changeDatabase.resolve(formSection.getId());
|
|
738
|
+
if (!match) {
|
|
739
|
+
changeDatabase.insert(formSection);
|
|
740
|
+
hasChanges = true;
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
// Delete removed form sections
|
|
744
|
+
for (const formSection of markFormSectionsForRemoval) {
|
|
745
|
+
const deleteRecord = await factory.createRecord({
|
|
746
|
+
source: formSection.getSource(),
|
|
747
|
+
table: 'sys_ui_form_section',
|
|
748
|
+
explicitId: formSection.getId(),
|
|
749
|
+
properties: formSection.properties(),
|
|
750
|
+
action: 'DELETE',
|
|
751
|
+
});
|
|
752
|
+
changeDatabase.insert(deleteRecord);
|
|
753
|
+
}
|
|
754
|
+
return {
|
|
755
|
+
success: true,
|
|
756
|
+
value: hasChanges ? changeDatabase : new sdk_build_core_1.Database(),
|
|
757
|
+
};
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
shapes: [
|
|
762
|
+
{
|
|
763
|
+
shape: sdk_build_core_1.CallExpressionShape,
|
|
764
|
+
fileTypes: ['fluent'],
|
|
765
|
+
async toRecord(callExpression, { factory, diagnostics }) {
|
|
766
|
+
if (callExpression.getCallee() !== 'Form') {
|
|
767
|
+
return { success: false };
|
|
768
|
+
}
|
|
769
|
+
const arg = callExpression.getArgument(0).asObject();
|
|
770
|
+
const tableArg = arg.get('table');
|
|
771
|
+
const tableName = tableArg.ifString();
|
|
772
|
+
// View handling: supports Record, RecordId, and string (aligned with ListPlugin)
|
|
773
|
+
const viewArg = arg.get('view');
|
|
774
|
+
let viewReference;
|
|
775
|
+
if (viewArg.isRecord()) {
|
|
776
|
+
viewReference = viewArg.asRecord();
|
|
777
|
+
}
|
|
778
|
+
else if (viewArg.isRecordId()) {
|
|
779
|
+
viewReference = viewArg.asRecordId();
|
|
780
|
+
}
|
|
781
|
+
else if (viewArg.isString()) {
|
|
782
|
+
const stringValue = viewArg.asString().getValue();
|
|
783
|
+
// Diagnostic 1: 'Default view' string literal footgun
|
|
784
|
+
if (stringValue === DEFAULT_VIEW) {
|
|
785
|
+
diagnostics.error(viewArg, `Do not use the hard-coded string '${DEFAULT_VIEW}' as the view. ` +
|
|
786
|
+
`Use the exported 'default_view' identifier instead: view: default_view`);
|
|
787
|
+
return { success: false };
|
|
788
|
+
}
|
|
789
|
+
viewReference = await factory.createReference({
|
|
790
|
+
source: callExpression,
|
|
791
|
+
table: 'sys_ui_view',
|
|
792
|
+
keys: { name: stringValue },
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
// Default for any other case
|
|
797
|
+
viewReference = DEFAULT_VIEW;
|
|
798
|
+
}
|
|
799
|
+
// Process roles if they exist as an array
|
|
800
|
+
let rolesValue = '';
|
|
801
|
+
const rolesArray = arg.get('roles').ifArray()?.getElements() ?? [];
|
|
802
|
+
if (rolesArray.length > 0) {
|
|
803
|
+
rolesValue = rolesArray
|
|
804
|
+
.map((role) => {
|
|
805
|
+
if (role.isString()) {
|
|
806
|
+
return role.asString().getValue();
|
|
807
|
+
}
|
|
808
|
+
else if (role.isObject()) {
|
|
809
|
+
return role.get('name')?.ifString()?.getValue() ?? '';
|
|
810
|
+
}
|
|
811
|
+
return '';
|
|
812
|
+
})
|
|
813
|
+
.filter((name) => name !== '')
|
|
814
|
+
.join(',');
|
|
815
|
+
}
|
|
816
|
+
// Create the main form record
|
|
817
|
+
const form = await factory.createRecord({
|
|
818
|
+
source: callExpression,
|
|
819
|
+
table: 'sys_ui_form',
|
|
820
|
+
properties: arg.transform(({ $ }) => ({
|
|
821
|
+
name: $.val(tableName),
|
|
822
|
+
view: $.val(viewReference),
|
|
823
|
+
sys_user: $.from('user'),
|
|
824
|
+
sys_domain: $.val('global'),
|
|
825
|
+
roles: $.val(rolesValue).def(''),
|
|
826
|
+
})),
|
|
827
|
+
});
|
|
828
|
+
// Process all sections
|
|
829
|
+
const sections = arg.get('sections').ifArray()?.getElements() || [];
|
|
830
|
+
if (sections.length === 0) {
|
|
831
|
+
diagnostics.error(arg.get('sections'), `Form does not have any sections. Add at least one section.`);
|
|
832
|
+
}
|
|
833
|
+
const sectionRecords = [];
|
|
834
|
+
const formSectionRecords = [];
|
|
835
|
+
const elementRecords = [];
|
|
836
|
+
const seenCaptions = new Map();
|
|
837
|
+
const globalSeenFields = new Set();
|
|
838
|
+
for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
|
|
839
|
+
const sectionObj = sections[sectionIndex]?.asObject();
|
|
840
|
+
if (!sectionObj) {
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
const caption = sectionObj.get('caption').ifString()?.getValue() ?? '';
|
|
844
|
+
// Diagnostic 2: Empty section caption
|
|
845
|
+
if (!caption.trim()) {
|
|
846
|
+
diagnostics.warn(sectionObj.get('caption'), `Section caption cannot be empty.`);
|
|
847
|
+
}
|
|
848
|
+
// Diagnostic 3: Duplicate section captions
|
|
849
|
+
// Note: When two sections share the same caption, fluent merges them into
|
|
850
|
+
// a single section on the instance due to coalesce keys. This may be unexpected if captions collide by accident.
|
|
851
|
+
if (caption.trim()) {
|
|
852
|
+
const prevIndex = seenCaptions.get(caption);
|
|
853
|
+
if (prevIndex !== undefined) {
|
|
854
|
+
diagnostics.error(sectionObj.get('caption'), `Duplicate section caption '${caption}'. Each section must have a unique caption.`);
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
seenCaptions.set(caption, sectionIndex);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
// Create the section record
|
|
861
|
+
const section = await factory.createRecord({
|
|
862
|
+
source: callExpression,
|
|
863
|
+
table: 'sys_ui_section',
|
|
864
|
+
properties: sectionObj.transform(({ $ }) => ({
|
|
865
|
+
name: $.val(tableName),
|
|
866
|
+
caption: $.val(caption),
|
|
867
|
+
title: $.def(false),
|
|
868
|
+
header: $.def(false),
|
|
869
|
+
view: $.val(viewReference),
|
|
870
|
+
sys_domain: $.val('global'),
|
|
871
|
+
})),
|
|
872
|
+
});
|
|
873
|
+
sectionRecords.push(section);
|
|
874
|
+
// Create the form-section linking record
|
|
875
|
+
const formSection = await factory.createRecord({
|
|
876
|
+
source: callExpression,
|
|
877
|
+
table: 'sys_ui_form_section',
|
|
878
|
+
properties: arg.transform(({ $ }) => ({
|
|
879
|
+
sys_ui_form: $.val(form),
|
|
880
|
+
sys_ui_section: $.val(section.getId()),
|
|
881
|
+
position: $.val(sectionIndex),
|
|
882
|
+
})),
|
|
883
|
+
});
|
|
884
|
+
formSectionRecords.push(formSection);
|
|
885
|
+
// Process content layout blocks
|
|
886
|
+
await processContentBlocks(arg, sectionObj, callExpression, factory, section, elementRecords, diagnostics, caption, globalSeenFields);
|
|
887
|
+
}
|
|
888
|
+
return {
|
|
889
|
+
success: true,
|
|
890
|
+
value: form.with(...sectionRecords, ...formSectionRecords, ...elementRecords),
|
|
891
|
+
};
|
|
892
|
+
},
|
|
893
|
+
},
|
|
894
|
+
],
|
|
895
|
+
});
|
|
896
|
+
/**
|
|
897
|
+
* Creates a sys_ui_element record
|
|
898
|
+
*/
|
|
899
|
+
async function createUiElement(arg, callExpression, factory, section, element, position, type = '', formatter = '') {
|
|
900
|
+
return await factory.createRecord({
|
|
901
|
+
source: callExpression,
|
|
902
|
+
table: 'sys_ui_element',
|
|
903
|
+
properties: arg.transform(({ $ }) => ({
|
|
904
|
+
sys_ui_section: $.val(section.getId()),
|
|
905
|
+
element: $.val(element),
|
|
906
|
+
position: $.val(position),
|
|
907
|
+
type: $.val(type),
|
|
908
|
+
sys_ui_formatter: $.val(formatter),
|
|
909
|
+
})),
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Processes the content layout blocks in a form section and creates UI element records.
|
|
914
|
+
* Reads the `content` array of layout blocks (one-column / two-column) and flattens them
|
|
915
|
+
* into sys_ui_element records with proper positioning and split markers.
|
|
916
|
+
*/
|
|
917
|
+
async function processContentBlocks(arg, sectionObj, callExpression, factory, section, elementRecords, diagnostics, caption, globalSeenFields) {
|
|
918
|
+
const contentBlocks = sectionObj.get('content').ifArray()?.getElements() || [];
|
|
919
|
+
let position = 0;
|
|
920
|
+
const seenFields = new Set();
|
|
921
|
+
// Diagnostic 6: Empty content blocks
|
|
922
|
+
if (contentBlocks.length === 0) {
|
|
923
|
+
diagnostics.warn(sectionObj.get('content'), `Section '${caption}' has no content blocks. The section will be empty on the form.`);
|
|
924
|
+
}
|
|
925
|
+
/** Processes an array of element shapes, reporting errors for non-object entries. */
|
|
926
|
+
async function processElements(elements, context) {
|
|
927
|
+
for (const elem of elements) {
|
|
928
|
+
if (!elem || !elem.isObject()) {
|
|
929
|
+
diagnostics.error(elem ?? sectionObj, `Invalid element in ${context} of section '${caption}'. Each element must be an object.`);
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
position = await processFormElement(arg, callExpression, factory, section, elementRecords, elem.asObject(), position, diagnostics, seenFields, caption, globalSeenFields);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
for (let blockIdx = 0; blockIdx < contentBlocks.length; blockIdx++) {
|
|
936
|
+
const block = contentBlocks[blockIdx];
|
|
937
|
+
if (!block || !block.isObject()) {
|
|
938
|
+
diagnostics.error(block ?? sectionObj, `Content block at index ${blockIdx} in section '${caption}' must be an object.`);
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
const blockObj = block.asObject();
|
|
942
|
+
const layout = blockObj.get('layout').ifString()?.getValue();
|
|
943
|
+
if (!layout || (layout !== 'one-column' && layout !== 'two-column')) {
|
|
944
|
+
diagnostics.error(blockObj.get('layout'), `Layout block requires 'layout' property set to 'one-column' or 'two-column'.`);
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
if (layout === 'one-column') {
|
|
948
|
+
await processElements(blockObj.get('elements').ifArray()?.getElements() || [], 'one-column block');
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
// Diagnostic 5: Empty two-column sides
|
|
952
|
+
const leftElements = blockObj.get('leftElements').ifArray()?.getElements() || [];
|
|
953
|
+
const rightElements = blockObj.get('rightElements').ifArray()?.getElements() || [];
|
|
954
|
+
if (leftElements.length === 0) {
|
|
955
|
+
diagnostics.warn(blockObj.get('leftElements'), `Two-column layout block at index ${blockIdx} in section '${caption}' has empty 'leftElements'. Consider using a one-column layout instead.`);
|
|
956
|
+
}
|
|
957
|
+
if (rightElements.length === 0) {
|
|
958
|
+
diagnostics.warn(blockObj.get('rightElements'), `Two-column layout block at index ${blockIdx} in section '${caption}' has empty 'rightElements'. Consider using a one-column layout instead.`);
|
|
959
|
+
}
|
|
960
|
+
// two-column: emit .begin_split, left elements, .split, right elements, .end_split
|
|
961
|
+
elementRecords.push(await createUiElement(arg, callExpression, factory, section, SPLIT_TYPE.BEGIN, position++, SPLIT_TYPE.BEGIN));
|
|
962
|
+
await processElements(leftElements, 'two-column leftElements');
|
|
963
|
+
elementRecords.push(await createUiElement(arg, callExpression, factory, section, SPLIT_TYPE.MIDDLE, position++, SPLIT_TYPE.MIDDLE));
|
|
964
|
+
await processElements(rightElements, 'two-column rightElements');
|
|
965
|
+
elementRecords.push(await createUiElement(arg, callExpression, factory, section, SPLIT_TYPE.END, position++, SPLIT_TYPE.END));
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Processes a single form element object and creates the appropriate record(s).
|
|
971
|
+
* Handles table_field, annotation, formatter, and list element types.
|
|
972
|
+
* Returns the next position index.
|
|
973
|
+
*/
|
|
974
|
+
async function processFormElement(arg, callExpression, factory, section, elementRecords, field, position, diagnostics, seenFields, caption, globalSeenFields) {
|
|
975
|
+
const typeField = field.get('type').ifString()?.getValue() ?? '';
|
|
976
|
+
const validTypes = ['table_field', 'annotation', 'formatter', 'list'];
|
|
977
|
+
if (!validTypes.includes(typeField)) {
|
|
978
|
+
diagnostics.error(field.get('type'), `Invalid type '${typeField}'. Valid types are: ${validTypes.map((t) => `'${t}'`).join(', ')}`);
|
|
979
|
+
return position;
|
|
980
|
+
}
|
|
981
|
+
// ── table_field ──
|
|
982
|
+
if (typeField === 'table_field') {
|
|
983
|
+
const fieldName = field.get('field').ifString()?.getValue() ?? '';
|
|
984
|
+
if (!fieldName) {
|
|
985
|
+
diagnostics.error(field.get('field'), `Table field element requires a 'field' property.`);
|
|
986
|
+
return position;
|
|
987
|
+
}
|
|
988
|
+
// Duplicate field check within the same section
|
|
989
|
+
if (seenFields.has(fieldName)) {
|
|
990
|
+
diagnostics.error(field, `Duplicate field '${fieldName}' in section '${caption}'. Each field should only appear once per section.`);
|
|
991
|
+
}
|
|
992
|
+
seenFields.add(fieldName);
|
|
993
|
+
// Diagnostic 4: Duplicate field across sections
|
|
994
|
+
if (globalSeenFields.has(fieldName)) {
|
|
995
|
+
diagnostics.warn(field, `Field '${fieldName}' already appears in another section. Duplicate fields across sections may cause unexpected behavior on the form.`);
|
|
996
|
+
}
|
|
997
|
+
globalSeenFields.add(fieldName);
|
|
998
|
+
elementRecords.push(await createUiElement(arg, callExpression, factory, section, fieldName, position));
|
|
999
|
+
return position + 1;
|
|
1000
|
+
}
|
|
1001
|
+
// ── annotation ──
|
|
1002
|
+
if (typeField === 'annotation') {
|
|
1003
|
+
const annotationIdField = field.get('annotationId');
|
|
1004
|
+
if (!(annotationIdField instanceof sdk_build_core_1.ElementAccessExpressionShape && annotationIdField.getCallee() === 'Now.ID')) {
|
|
1005
|
+
diagnostics.error(annotationIdField, `'annotationId' must be a Now.ID['...'] reference.`);
|
|
1006
|
+
return position;
|
|
1007
|
+
}
|
|
1008
|
+
const explicitId = annotationIdField;
|
|
1009
|
+
const textValue = field.get('text').ifString()?.getValue() ?? '';
|
|
1010
|
+
const isPlainText = field.get('isPlainText').ifBoolean()?.getValue() ?? true;
|
|
1011
|
+
// Handle annotationType - can be AnnotationType record or string GUID
|
|
1012
|
+
const annotationTypeArg = field.get('annotationType');
|
|
1013
|
+
let annotationTypeValue = DEFAULT_ANNOTATION_TYPE;
|
|
1014
|
+
if (annotationTypeArg) {
|
|
1015
|
+
if (annotationTypeArg.isRecord()) {
|
|
1016
|
+
annotationTypeValue = annotationTypeArg.asRecord().getId().getValue();
|
|
1017
|
+
}
|
|
1018
|
+
else if (annotationTypeArg.isString()) {
|
|
1019
|
+
const annotationTypeStr = annotationTypeArg.asString().getValue();
|
|
1020
|
+
if (annotationTypeStr in AnnotationType) {
|
|
1021
|
+
annotationTypeValue = AnnotationType[annotationTypeStr];
|
|
1022
|
+
}
|
|
1023
|
+
else if ((0, sdk_build_core_1.isGUID)(annotationTypeStr)) {
|
|
1024
|
+
annotationTypeValue = annotationTypeStr;
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
(0, utils_1.showGuidFieldDiagnostic)(annotationTypeArg, 'annotationType', 'sys_ui_annotation_type', diagnostics);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
// Create sys_ui_annotation record
|
|
1032
|
+
const annotationRecord = await factory.createRecord({
|
|
1033
|
+
source: callExpression,
|
|
1034
|
+
table: 'sys_ui_annotation',
|
|
1035
|
+
explicitId: explicitId,
|
|
1036
|
+
properties: arg.transform(({ $ }) => ({
|
|
1037
|
+
text: $.val(textValue),
|
|
1038
|
+
is_plain_text: $.val(isPlainText).def(true),
|
|
1039
|
+
type: $.val(annotationTypeValue),
|
|
1040
|
+
})),
|
|
1041
|
+
});
|
|
1042
|
+
elementRecords.push(annotationRecord);
|
|
1043
|
+
// Create sys_ui_element with element = annotation sys_id
|
|
1044
|
+
elementRecords.push(await createUiElement(arg, callExpression, factory, section, annotationRecord.getId().getValue(), position, 'annotation'));
|
|
1045
|
+
return position + 1;
|
|
1046
|
+
}
|
|
1047
|
+
// ── formatter ──
|
|
1048
|
+
if (typeField === 'formatter') {
|
|
1049
|
+
const formatterRefArg = field.get('formatterRef');
|
|
1050
|
+
let formatterRefValue = '';
|
|
1051
|
+
let recordFormatterField = '';
|
|
1052
|
+
if (formatterRefArg) {
|
|
1053
|
+
if (formatterRefArg.isRecord()) {
|
|
1054
|
+
const record = formatterRefArg.asRecord();
|
|
1055
|
+
formatterRefValue = record.getId().getValue();
|
|
1056
|
+
// Try to read the `formatter` column from the Record data
|
|
1057
|
+
recordFormatterField = record.get('formatter')?.ifString()?.getValue() ?? '';
|
|
1058
|
+
}
|
|
1059
|
+
else if (formatterRefArg.isString()) {
|
|
1060
|
+
const formatterStr = formatterRefArg.asString().getValue();
|
|
1061
|
+
if (formatterStr in Formatter) {
|
|
1062
|
+
formatterRefValue = Formatter[formatterStr];
|
|
1063
|
+
}
|
|
1064
|
+
else if ((0, sdk_build_core_1.isGUID)(formatterStr)) {
|
|
1065
|
+
formatterRefValue = formatterStr;
|
|
1066
|
+
}
|
|
1067
|
+
else {
|
|
1068
|
+
(0, utils_1.showGuidFieldDiagnostic)(formatterRefArg, 'formatterRef', 'sys_ui_formatter', diagnostics);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
// formatterName is optional — derive in order: explicit > Record `formatter` field > FORMATTER_ELEMENT_MAP > empty
|
|
1073
|
+
let formatterName = field.get('formatterName').ifString()?.getValue() ?? '';
|
|
1074
|
+
if (!formatterName && recordFormatterField) {
|
|
1075
|
+
formatterName = recordFormatterField;
|
|
1076
|
+
}
|
|
1077
|
+
if (!formatterName && formatterRefValue) {
|
|
1078
|
+
formatterName = FORMATTER_ELEMENT_MAP.get(formatterRefValue) ?? '';
|
|
1079
|
+
}
|
|
1080
|
+
elementRecords.push(await createUiElement(arg, callExpression, factory, section, formatterName, position, 'formatter', formatterRefValue));
|
|
1081
|
+
return position + 1;
|
|
1082
|
+
}
|
|
1083
|
+
// ── list ──
|
|
1084
|
+
if (typeField === 'list') {
|
|
1085
|
+
const listType = field.get('listType').ifString()?.getValue() ?? '';
|
|
1086
|
+
if (!listType || !['12M', 'M2M', 'custom'].includes(listType)) {
|
|
1087
|
+
diagnostics.error(field.get('listType'), `List element requires 'listType' set to '12M', 'M2M', or 'custom'.`);
|
|
1088
|
+
return position;
|
|
1089
|
+
}
|
|
1090
|
+
let elementValue = '';
|
|
1091
|
+
const tableName = arg.get('table').asString().getValue();
|
|
1092
|
+
const listRefArg = field.get('listRef');
|
|
1093
|
+
if (listType === 'custom') {
|
|
1094
|
+
// Custom lists use Record<'sys_relationship'> reference or string GUID via 'listRef' key
|
|
1095
|
+
if (listRefArg.isRecord()) {
|
|
1096
|
+
const relSysId = listRefArg.asRecord().getId().getValue();
|
|
1097
|
+
elementValue = `REL.${tableName}.REL:${relSysId}`;
|
|
1098
|
+
}
|
|
1099
|
+
else if (listRefArg.isString()) {
|
|
1100
|
+
const relStr = listRefArg.asString().getValue();
|
|
1101
|
+
if ((0, sdk_build_core_1.isGUID)(relStr)) {
|
|
1102
|
+
elementValue = `REL.${tableName}.REL:${relStr}`;
|
|
1103
|
+
}
|
|
1104
|
+
else {
|
|
1105
|
+
(0, utils_1.showGuidFieldDiagnostic)(listRefArg, 'listRef', 'sys_relationship', diagnostics);
|
|
1106
|
+
return position;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
else {
|
|
1110
|
+
diagnostics.error(listRefArg, `Custom list requires 'listRef' with a Record<'sys_relationship'> reference or a sys_relationship sys_id string (GUID).`);
|
|
1111
|
+
return position;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
else {
|
|
1115
|
+
// 12M and M2M use listRef as 'table.column' dot-notation string
|
|
1116
|
+
if (!listRefArg.isString()) {
|
|
1117
|
+
diagnostics.error(field, `List element requires 'listRef' as a dot-notation string '<table_name>.<column_name>' for '${listType}' type.`);
|
|
1118
|
+
return position;
|
|
1119
|
+
}
|
|
1120
|
+
const listRefStr = listRefArg.asString().getValue();
|
|
1121
|
+
const parts = listRefStr.split('.');
|
|
1122
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
1123
|
+
diagnostics.error(listRefArg, `Invalid 'listRef' format '${listRefStr}'. Expected '<table_name>.<column_name>' (e.g., 'task_sla.task').`);
|
|
1124
|
+
return position;
|
|
1125
|
+
}
|
|
1126
|
+
const [listTable, listColumn] = parts;
|
|
1127
|
+
elementValue = `${listType}.${tableName}.${listTable}.${listColumn}`;
|
|
1128
|
+
}
|
|
1129
|
+
elementRecords.push(await createUiElement(arg, callExpression, factory, section, elementValue, position, 'list'));
|
|
1130
|
+
return position + 1;
|
|
1131
|
+
}
|
|
1132
|
+
return position;
|
|
1133
|
+
}
|
|
1134
|
+
//# sourceMappingURL=form-plugin.js.map
|