@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.
Files changed (287) hide show
  1. package/dist/acl-plugin.js +54 -4
  2. package/dist/acl-plugin.js.map +1 -1
  3. package/dist/applicability-plugin.js +2 -0
  4. package/dist/applicability-plugin.js.map +1 -1
  5. package/dist/application-menu-plugin.js +2 -0
  6. package/dist/application-menu-plugin.js.map +1 -1
  7. package/dist/arrow-function-plugin.d.ts +6 -1
  8. package/dist/arrow-function-plugin.js +105 -12
  9. package/dist/arrow-function-plugin.js.map +1 -1
  10. package/dist/atf/test-plugin.js +2 -0
  11. package/dist/atf/test-plugin.js.map +1 -1
  12. package/dist/basic-syntax-plugin.js +20 -0
  13. package/dist/basic-syntax-plugin.js.map +1 -1
  14. package/dist/call-expression-plugin.js +1 -0
  15. package/dist/call-expression-plugin.js.map +1 -1
  16. package/dist/claims-plugin.js +1 -0
  17. package/dist/claims-plugin.js.map +1 -1
  18. package/dist/client-script-plugin.js +1 -0
  19. package/dist/client-script-plugin.js.map +1 -1
  20. package/dist/column-plugin.js +1 -0
  21. package/dist/column-plugin.js.map +1 -1
  22. package/dist/cross-scope-privilege-plugin.js +1 -0
  23. package/dist/cross-scope-privilege-plugin.js.map +1 -1
  24. package/dist/dashboard/dashboard-plugin.js +2 -0
  25. package/dist/dashboard/dashboard-plugin.js.map +1 -1
  26. package/dist/data-plugin.js +1 -0
  27. package/dist/data-plugin.js.map +1 -1
  28. package/dist/email-notification-plugin.js +9 -13
  29. package/dist/email-notification-plugin.js.map +1 -1
  30. package/dist/flow/constants/flow-plugin-constants.d.ts +1 -1
  31. package/dist/flow/constants/flow-plugin-constants.js +1 -1
  32. package/dist/flow/constants/flow-plugin-constants.js.map +1 -1
  33. package/dist/flow/flow-logic/flow-logic-plugin-helpers.d.ts +82 -2
  34. package/dist/flow/flow-logic/flow-logic-plugin-helpers.js +48 -40
  35. package/dist/flow/flow-logic/flow-logic-plugin-helpers.js.map +1 -1
  36. package/dist/flow/flow-logic/flow-logic-plugin.js +1 -0
  37. package/dist/flow/flow-logic/flow-logic-plugin.js.map +1 -1
  38. package/dist/flow/plugins/approval-rules-plugin.js +1 -0
  39. package/dist/flow/plugins/approval-rules-plugin.js.map +1 -1
  40. package/dist/flow/plugins/flow-action-definition-plugin.js +4 -2
  41. package/dist/flow/plugins/flow-action-definition-plugin.js.map +1 -1
  42. package/dist/flow/plugins/flow-data-pill-plugin.js +1 -0
  43. package/dist/flow/plugins/flow-data-pill-plugin.js.map +1 -1
  44. package/dist/flow/plugins/flow-definition-plugin.js +8 -3
  45. package/dist/flow/plugins/flow-definition-plugin.js.map +1 -1
  46. package/dist/flow/plugins/flow-diagnostics-plugin.js +1 -0
  47. package/dist/flow/plugins/flow-diagnostics-plugin.js.map +1 -1
  48. package/dist/flow/plugins/flow-instance-plugin.js +68 -12
  49. package/dist/flow/plugins/flow-instance-plugin.js.map +1 -1
  50. package/dist/flow/plugins/flow-trigger-instance-plugin.js +1 -0
  51. package/dist/flow/plugins/flow-trigger-instance-plugin.js.map +1 -1
  52. package/dist/flow/plugins/inline-script-plugin.js +1 -0
  53. package/dist/flow/plugins/inline-script-plugin.js.map +1 -1
  54. package/dist/flow/plugins/step-definition-plugin.js +3 -2
  55. package/dist/flow/plugins/step-definition-plugin.js.map +1 -1
  56. package/dist/flow/plugins/step-instance-plugin.js +1 -0
  57. package/dist/flow/plugins/step-instance-plugin.js.map +1 -1
  58. package/dist/flow/plugins/trigger-plugin.js +2 -0
  59. package/dist/flow/plugins/trigger-plugin.js.map +1 -1
  60. package/dist/flow/plugins/wfa-datapill-plugin.js +1 -0
  61. package/dist/flow/plugins/wfa-datapill-plugin.js.map +1 -1
  62. package/dist/flow/post-install.d.ts +2 -0
  63. package/dist/flow/post-install.js +58 -0
  64. package/dist/flow/post-install.js.map +1 -0
  65. package/dist/flow/utils/complex-objects.js +4 -2
  66. package/dist/flow/utils/complex-objects.js.map +1 -1
  67. package/dist/flow/utils/flow-constants.d.ts +24 -0
  68. package/dist/flow/utils/flow-constants.js +29 -2
  69. package/dist/flow/utils/flow-constants.js.map +1 -1
  70. package/dist/flow/utils/flow-to-xml.d.ts +3 -2
  71. package/dist/flow/utils/flow-to-xml.js +3 -4
  72. package/dist/flow/utils/flow-to-xml.js.map +1 -1
  73. package/dist/flow/utils/label-cache-processor.d.ts +5 -0
  74. package/dist/flow/utils/label-cache-processor.js +14 -2
  75. package/dist/flow/utils/label-cache-processor.js.map +1 -1
  76. package/dist/flow/utils/service-catalog.js +5 -1
  77. package/dist/flow/utils/service-catalog.js.map +1 -1
  78. package/dist/form-plugin.d.ts +2 -0
  79. package/dist/form-plugin.js +1134 -0
  80. package/dist/form-plugin.js.map +1 -0
  81. package/dist/html-import-plugin.js +1 -0
  82. package/dist/html-import-plugin.js.map +1 -1
  83. package/dist/import-sets-plugin.js +2 -0
  84. package/dist/import-sets-plugin.js.map +1 -1
  85. package/dist/index.d.ts +9 -0
  86. package/dist/index.js +13 -1
  87. package/dist/index.js.map +1 -1
  88. package/dist/instance-scan-plugin.d.ts +2 -0
  89. package/dist/instance-scan-plugin.js +298 -0
  90. package/dist/instance-scan-plugin.js.map +1 -0
  91. package/dist/json-plugin.js +1 -0
  92. package/dist/json-plugin.js.map +1 -1
  93. package/dist/list-plugin.js +1 -0
  94. package/dist/list-plugin.js.map +1 -1
  95. package/dist/now-attach-plugin.js +1 -0
  96. package/dist/now-attach-plugin.js.map +1 -1
  97. package/dist/now-config-plugin.js +659 -51
  98. package/dist/now-config-plugin.js.map +1 -1
  99. package/dist/now-id-plugin.js +1 -0
  100. package/dist/now-id-plugin.js.map +1 -1
  101. package/dist/now-include-plugin.js +1 -0
  102. package/dist/now-include-plugin.js.map +1 -1
  103. package/dist/now-ref-plugin.js +1 -0
  104. package/dist/now-ref-plugin.js.map +1 -1
  105. package/dist/now-unresolved-plugin.js +1 -0
  106. package/dist/now-unresolved-plugin.js.map +1 -1
  107. package/dist/package-json-plugin.js +1 -0
  108. package/dist/package-json-plugin.js.map +1 -1
  109. package/dist/property-plugin.js +3 -1
  110. package/dist/property-plugin.js.map +1 -1
  111. package/dist/record-plugin.d.ts +30 -0
  112. package/dist/record-plugin.js +37 -1
  113. package/dist/record-plugin.js.map +1 -1
  114. package/dist/repack/lint/Rules.d.ts +11 -2
  115. package/dist/repack/lint/Rules.js +160 -16
  116. package/dist/repack/lint/Rules.js.map +1 -1
  117. package/dist/repack/lint/index.d.ts +10 -5
  118. package/dist/repack/lint/index.js +76 -50
  119. package/dist/repack/lint/index.js.map +1 -1
  120. package/dist/rest-api-plugin.js +14 -0
  121. package/dist/rest-api-plugin.js.map +1 -1
  122. package/dist/role-plugin.js +1 -0
  123. package/dist/role-plugin.js.map +1 -1
  124. package/dist/schedule-script/index.d.ts +1 -0
  125. package/dist/schedule-script/index.js +18 -0
  126. package/dist/schedule-script/index.js.map +1 -0
  127. package/dist/schedule-script/scheduled-script-plugin.d.ts +2 -0
  128. package/dist/schedule-script/scheduled-script-plugin.js +551 -0
  129. package/dist/schedule-script/scheduled-script-plugin.js.map +1 -0
  130. package/dist/schedule-script/timeZoneConverter.d.ts +61 -0
  131. package/dist/schedule-script/timeZoneConverter.js +170 -0
  132. package/dist/schedule-script/timeZoneConverter.js.map +1 -0
  133. package/dist/script-action-plugin.js +2 -0
  134. package/dist/script-action-plugin.js.map +1 -1
  135. package/dist/script-include-plugin.js +2 -0
  136. package/dist/script-include-plugin.js.map +1 -1
  137. package/dist/server-module-plugin/index.js +13 -2
  138. package/dist/server-module-plugin/index.js.map +1 -1
  139. package/dist/service-catalog/catalog-clientscript-plugin.js +2 -0
  140. package/dist/service-catalog/catalog-clientscript-plugin.js.map +1 -1
  141. package/dist/service-catalog/catalog-item-plugin.js +2 -0
  142. package/dist/service-catalog/catalog-item-plugin.js.map +1 -1
  143. package/dist/service-catalog/catalog-ui-policy-plugin.js +2 -0
  144. package/dist/service-catalog/catalog-ui-policy-plugin.js.map +1 -1
  145. package/dist/service-catalog/sc-record-producer-plugin.js +2 -0
  146. package/dist/service-catalog/sc-record-producer-plugin.js.map +1 -1
  147. package/dist/service-catalog/service-catalog-diagnostics.d.ts +6 -0
  148. package/dist/service-catalog/service-catalog-diagnostics.js +20 -0
  149. package/dist/service-catalog/service-catalog-diagnostics.js.map +1 -1
  150. package/dist/service-catalog/shape-to-record.js +7 -2
  151. package/dist/service-catalog/shape-to-record.js.map +1 -1
  152. package/dist/service-catalog/variable-set-plugin.js +2 -0
  153. package/dist/service-catalog/variable-set-plugin.js.map +1 -1
  154. package/dist/service-portal/angular-provider-plugin.js +2 -0
  155. package/dist/service-portal/angular-provider-plugin.js.map +1 -1
  156. package/dist/service-portal/dependency-plugin.js +5 -31
  157. package/dist/service-portal/dependency-plugin.js.map +1 -1
  158. package/dist/service-portal/menu-plugin.d.ts +2 -0
  159. package/dist/service-portal/menu-plugin.js +353 -0
  160. package/dist/service-portal/menu-plugin.js.map +1 -0
  161. package/dist/service-portal/page-plugin.d.ts +2 -0
  162. package/dist/service-portal/page-plugin.js +702 -0
  163. package/dist/service-portal/page-plugin.js.map +1 -0
  164. package/dist/service-portal/portal-plugin.d.ts +2 -0
  165. package/dist/service-portal/portal-plugin.js +296 -0
  166. package/dist/service-portal/portal-plugin.js.map +1 -0
  167. package/dist/service-portal/theme-plugin.d.ts +2 -0
  168. package/dist/service-portal/theme-plugin.js +112 -0
  169. package/dist/service-portal/theme-plugin.js.map +1 -0
  170. package/dist/service-portal/utils.d.ts +8 -0
  171. package/dist/service-portal/utils.js +50 -0
  172. package/dist/service-portal/utils.js.map +1 -0
  173. package/dist/service-portal/widget-plugin.js +45 -8
  174. package/dist/service-portal/widget-plugin.js.map +1 -1
  175. package/dist/sla-plugin.js +2 -0
  176. package/dist/sla-plugin.js.map +1 -1
  177. package/dist/static-content-plugin.js +1 -0
  178. package/dist/static-content-plugin.js.map +1 -1
  179. package/dist/table-plugin.js +1 -0
  180. package/dist/table-plugin.js.map +1 -1
  181. package/dist/ui-action-plugin.js +2 -0
  182. package/dist/ui-action-plugin.js.map +1 -1
  183. package/dist/ui-page-plugin.js +33 -8
  184. package/dist/ui-page-plugin.js.map +1 -1
  185. package/dist/ui-policy-plugin.js +1 -0
  186. package/dist/ui-policy-plugin.js.map +1 -1
  187. package/dist/user-preference-plugin.js +2 -0
  188. package/dist/user-preference-plugin.js.map +1 -1
  189. package/dist/utils.d.ts +20 -2
  190. package/dist/utils.js +34 -3
  191. package/dist/utils.js.map +1 -1
  192. package/dist/ux-list-menu-config-plugin.js +2 -0
  193. package/dist/ux-list-menu-config-plugin.js.map +1 -1
  194. package/dist/view-plugin.js +1 -0
  195. package/dist/view-plugin.js.map +1 -1
  196. package/dist/workspace-plugin.js +2 -0
  197. package/dist/workspace-plugin.js.map +1 -1
  198. package/package.json +10 -11
  199. package/src/_types/eslint-community-eslint-utils.d.ts +15 -0
  200. package/src/acl-plugin.ts +97 -8
  201. package/src/applicability-plugin.ts +2 -0
  202. package/src/application-menu-plugin.ts +2 -0
  203. package/src/arrow-function-plugin.ts +128 -13
  204. package/src/atf/test-plugin.ts +2 -0
  205. package/src/basic-syntax-plugin.ts +21 -0
  206. package/src/call-expression-plugin.ts +1 -0
  207. package/src/claims-plugin.ts +1 -0
  208. package/src/client-script-plugin.ts +2 -1
  209. package/src/column-plugin.ts +1 -0
  210. package/src/cross-scope-privilege-plugin.ts +2 -1
  211. package/src/dashboard/dashboard-plugin.ts +2 -0
  212. package/src/data-plugin.ts +1 -0
  213. package/src/email-notification-plugin.ts +3 -23
  214. package/src/flow/constants/flow-plugin-constants.ts +1 -1
  215. package/src/flow/flow-logic/flow-logic-plugin-helpers.ts +47 -45
  216. package/src/flow/flow-logic/flow-logic-plugin.ts +1 -0
  217. package/src/flow/plugins/approval-rules-plugin.ts +1 -0
  218. package/src/flow/plugins/flow-action-definition-plugin.ts +4 -2
  219. package/src/flow/plugins/flow-data-pill-plugin.ts +1 -0
  220. package/src/flow/plugins/flow-definition-plugin.ts +10 -4
  221. package/src/flow/plugins/flow-diagnostics-plugin.ts +1 -0
  222. package/src/flow/plugins/flow-instance-plugin.ts +103 -14
  223. package/src/flow/plugins/flow-trigger-instance-plugin.ts +1 -0
  224. package/src/flow/plugins/inline-script-plugin.ts +1 -0
  225. package/src/flow/plugins/step-definition-plugin.ts +3 -2
  226. package/src/flow/plugins/step-instance-plugin.ts +1 -0
  227. package/src/flow/plugins/trigger-plugin.ts +2 -0
  228. package/src/flow/plugins/wfa-datapill-plugin.ts +1 -0
  229. package/src/flow/post-install.ts +92 -0
  230. package/src/flow/utils/complex-objects.ts +10 -2
  231. package/src/flow/utils/flow-constants.ts +30 -1
  232. package/src/flow/utils/flow-to-xml.ts +4 -4
  233. package/src/flow/utils/label-cache-processor.ts +14 -2
  234. package/src/flow/utils/service-catalog.ts +5 -2
  235. package/src/form-plugin.ts +1411 -0
  236. package/src/html-import-plugin.ts +1 -0
  237. package/src/import-sets-plugin.ts +2 -0
  238. package/src/index.ts +9 -0
  239. package/src/instance-scan-plugin.ts +318 -0
  240. package/src/json-plugin.ts +1 -0
  241. package/src/list-plugin.ts +2 -1
  242. package/src/now-attach-plugin.ts +1 -0
  243. package/src/now-config-plugin.ts +833 -53
  244. package/src/now-id-plugin.ts +1 -0
  245. package/src/now-include-plugin.ts +1 -0
  246. package/src/now-ref-plugin.ts +1 -0
  247. package/src/now-unresolved-plugin.ts +1 -0
  248. package/src/package-json-plugin.ts +1 -0
  249. package/src/property-plugin.ts +3 -1
  250. package/src/record-plugin.ts +42 -2
  251. package/src/repack/lint/Rules.ts +171 -22
  252. package/src/repack/lint/index.ts +80 -56
  253. package/src/rest-api-plugin.ts +21 -1
  254. package/src/role-plugin.ts +2 -1
  255. package/src/schedule-script/index.ts +1 -0
  256. package/src/schedule-script/scheduled-script-plugin.ts +679 -0
  257. package/src/schedule-script/timeZoneConverter.ts +188 -0
  258. package/src/script-action-plugin.ts +2 -0
  259. package/src/script-include-plugin.ts +2 -0
  260. package/src/server-module-plugin/index.ts +14 -2
  261. package/src/service-catalog/catalog-clientscript-plugin.ts +2 -0
  262. package/src/service-catalog/catalog-item-plugin.ts +2 -0
  263. package/src/service-catalog/catalog-ui-policy-plugin.ts +2 -0
  264. package/src/service-catalog/sc-record-producer-plugin.ts +2 -0
  265. package/src/service-catalog/service-catalog-diagnostics.ts +30 -0
  266. package/src/service-catalog/shape-to-record.ts +8 -2
  267. package/src/service-catalog/variable-set-plugin.ts +2 -0
  268. package/src/service-portal/angular-provider-plugin.ts +2 -0
  269. package/src/service-portal/dependency-plugin.ts +6 -53
  270. package/src/service-portal/menu-plugin.ts +435 -0
  271. package/src/service-portal/page-plugin.ts +830 -0
  272. package/src/service-portal/portal-plugin.ts +319 -0
  273. package/src/service-portal/theme-plugin.ts +135 -0
  274. package/src/service-portal/utils.ts +69 -0
  275. package/src/service-portal/widget-plugin.ts +79 -9
  276. package/src/sla-plugin.ts +2 -0
  277. package/src/static-content-plugin.ts +1 -0
  278. package/src/table-plugin.ts +2 -1
  279. package/src/ui-action-plugin.ts +2 -0
  280. package/src/ui-page-plugin.ts +34 -8
  281. package/src/ui-policy-plugin.ts +2 -1
  282. package/src/user-preference-plugin.ts +2 -0
  283. package/src/utils.ts +42 -2
  284. package/src/ux-list-menu-config-plugin.ts +2 -0
  285. package/src/view-plugin.ts +1 -0
  286. package/src/workspace-plugin.ts +2 -0
  287. 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