@servicenow/sdk-build-plugins 4.5.0 → 4.6.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 (144) hide show
  1. package/dist/column-plugin.js +3 -7
  2. package/dist/column-plugin.js.map +1 -1
  3. package/dist/flow/flow-logic/flow-logic-diagnostics.js +5 -5
  4. package/dist/flow/flow-logic/flow-logic-diagnostics.js.map +1 -1
  5. package/dist/flow/plugins/flow-action-definition-plugin.js +1229 -54
  6. package/dist/flow/plugins/flow-action-definition-plugin.js.map +1 -1
  7. package/dist/flow/plugins/flow-data-pill-plugin.js +5 -2
  8. package/dist/flow/plugins/flow-data-pill-plugin.js.map +1 -1
  9. package/dist/flow/plugins/flow-definition-plugin.js +16 -42
  10. package/dist/flow/plugins/flow-definition-plugin.js.map +1 -1
  11. package/dist/flow/plugins/flow-diagnostics-plugin.d.ts +2 -2
  12. package/dist/flow/plugins/flow-diagnostics-plugin.js +2 -2
  13. package/dist/flow/plugins/flow-instance-plugin.js +68 -22
  14. package/dist/flow/plugins/flow-instance-plugin.js.map +1 -1
  15. package/dist/flow/plugins/step-definition-plugin.js +2 -1
  16. package/dist/flow/plugins/step-definition-plugin.js.map +1 -1
  17. package/dist/flow/plugins/step-instance-plugin.d.ts +9 -1
  18. package/dist/flow/plugins/step-instance-plugin.js +649 -136
  19. package/dist/flow/plugins/step-instance-plugin.js.map +1 -1
  20. package/dist/flow/plugins/wfa-datapill-plugin.js +20 -5
  21. package/dist/flow/plugins/wfa-datapill-plugin.js.map +1 -1
  22. package/dist/flow/post-install.js +1 -0
  23. package/dist/flow/post-install.js.map +1 -1
  24. package/dist/flow/utils/complex-object-resolver.js +4 -1
  25. package/dist/flow/utils/complex-object-resolver.js.map +1 -1
  26. package/dist/flow/utils/complex-objects.js +1 -1
  27. package/dist/flow/utils/complex-objects.js.map +1 -1
  28. package/dist/flow/utils/flow-constants.d.ts +66 -2
  29. package/dist/flow/utils/flow-constants.js +402 -6
  30. package/dist/flow/utils/flow-constants.js.map +1 -1
  31. package/dist/flow/utils/flow-io-to-record.d.ts +1 -1
  32. package/dist/flow/utils/flow-io-to-record.js +37 -16
  33. package/dist/flow/utils/flow-io-to-record.js.map +1 -1
  34. package/dist/flow/utils/flow-shapes.js +4 -0
  35. package/dist/flow/utils/flow-shapes.js.map +1 -1
  36. package/dist/flow/utils/label-cache-parser.d.ts +9 -2
  37. package/dist/flow/utils/label-cache-parser.js +32 -4
  38. package/dist/flow/utils/label-cache-parser.js.map +1 -1
  39. package/dist/flow/utils/pill-shape-helpers.d.ts +15 -0
  40. package/dist/flow/utils/pill-shape-helpers.js +35 -0
  41. package/dist/flow/utils/pill-shape-helpers.js.map +1 -0
  42. package/dist/flow/utils/pill-string-parser.js +1 -0
  43. package/dist/flow/utils/pill-string-parser.js.map +1 -1
  44. package/dist/flow/utils/schema-to-flow-object.d.ts +6 -1
  45. package/dist/flow/utils/schema-to-flow-object.js +131 -15
  46. package/dist/flow/utils/schema-to-flow-object.js.map +1 -1
  47. package/dist/flow/utils/utils.d.ts +1 -0
  48. package/dist/flow/utils/utils.js +6 -1
  49. package/dist/flow/utils/utils.js.map +1 -1
  50. package/dist/form-plugin.js +7 -9
  51. package/dist/form-plugin.js.map +1 -1
  52. package/dist/inbound-email-action-plugin.d.ts +10 -0
  53. package/dist/inbound-email-action-plugin.js +128 -0
  54. package/dist/inbound-email-action-plugin.js.map +1 -0
  55. package/dist/index.d.ts +4 -0
  56. package/dist/index.js +4 -0
  57. package/dist/index.js.map +1 -1
  58. package/dist/instance-scan-plugin.js +0 -5
  59. package/dist/instance-scan-plugin.js.map +1 -1
  60. package/dist/property-plugin.js +1 -1
  61. package/dist/property-plugin.js.map +1 -1
  62. package/dist/record-plugin.d.ts +7 -0
  63. package/dist/record-plugin.js +10 -2
  64. package/dist/record-plugin.js.map +1 -1
  65. package/dist/rest-api-plugin.js +8 -1
  66. package/dist/rest-api-plugin.js.map +1 -1
  67. package/dist/schedule-script/scheduled-script-plugin.js +8 -3
  68. package/dist/schedule-script/scheduled-script-plugin.js.map +1 -1
  69. package/dist/service-catalog/service-catalog-base.d.ts +18 -18
  70. package/dist/service-catalog/service-catalog-base.js +22 -22
  71. package/dist/service-catalog/service-catalog-base.js.map +1 -1
  72. package/dist/service-portal/header-footer-plugin.d.ts +2 -0
  73. package/dist/service-portal/header-footer-plugin.js +50 -0
  74. package/dist/service-portal/header-footer-plugin.js.map +1 -0
  75. package/dist/service-portal/menu-plugin.js +3 -22
  76. package/dist/service-portal/menu-plugin.js.map +1 -1
  77. package/dist/service-portal/page-plugin.js +3 -24
  78. package/dist/service-portal/page-plugin.js.map +1 -1
  79. package/dist/service-portal/page-route-map-plugin.d.ts +2 -0
  80. package/dist/service-portal/page-route-map-plugin.js +114 -0
  81. package/dist/service-portal/page-route-map-plugin.js.map +1 -0
  82. package/dist/service-portal/portal-plugin.js +21 -8
  83. package/dist/service-portal/portal-plugin.js.map +1 -1
  84. package/dist/service-portal/utils.d.ts +40 -2
  85. package/dist/service-portal/utils.js +283 -2
  86. package/dist/service-portal/utils.js.map +1 -1
  87. package/dist/service-portal/widget-plugin.js +9 -218
  88. package/dist/service-portal/widget-plugin.js.map +1 -1
  89. package/dist/static-content-plugin.js +4 -0
  90. package/dist/static-content-plugin.js.map +1 -1
  91. package/dist/table-plugin.js +190 -26
  92. package/dist/table-plugin.js.map +1 -1
  93. package/dist/ui-action-plugin.js +1 -4
  94. package/dist/ui-action-plugin.js.map +1 -1
  95. package/dist/ui-page-plugin.js +68 -13
  96. package/dist/ui-page-plugin.js.map +1 -1
  97. package/dist/view-plugin.js +8 -3
  98. package/dist/view-plugin.js.map +1 -1
  99. package/dist/workspace-plugin.js +39 -36
  100. package/dist/workspace-plugin.js.map +1 -1
  101. package/package.json +5 -4
  102. package/src/column-plugin.ts +3 -8
  103. package/src/flow/flow-logic/flow-logic-diagnostics.ts +5 -6
  104. package/src/flow/plugins/flow-action-definition-plugin.ts +1581 -61
  105. package/src/flow/plugins/flow-data-pill-plugin.ts +5 -2
  106. package/src/flow/plugins/flow-definition-plugin.ts +12 -47
  107. package/src/flow/plugins/flow-diagnostics-plugin.ts +2 -2
  108. package/src/flow/plugins/flow-instance-plugin.ts +98 -22
  109. package/src/flow/plugins/step-definition-plugin.ts +2 -1
  110. package/src/flow/plugins/step-instance-plugin.ts +772 -156
  111. package/src/flow/plugins/wfa-datapill-plugin.ts +25 -5
  112. package/src/flow/post-install.ts +1 -0
  113. package/src/flow/utils/complex-object-resolver.ts +4 -1
  114. package/src/flow/utils/complex-objects.ts +1 -1
  115. package/src/flow/utils/flow-constants.ts +421 -5
  116. package/src/flow/utils/flow-io-to-record.ts +43 -17
  117. package/src/flow/utils/flow-shapes.ts +4 -0
  118. package/src/flow/utils/label-cache-parser.ts +33 -4
  119. package/src/flow/utils/pill-shape-helpers.ts +42 -0
  120. package/src/flow/utils/pill-string-parser.ts +1 -0
  121. package/src/flow/utils/schema-to-flow-object.ts +183 -15
  122. package/src/flow/utils/utils.ts +12 -1
  123. package/src/form-plugin.ts +1 -3
  124. package/src/inbound-email-action-plugin.ts +145 -0
  125. package/src/index.ts +4 -0
  126. package/src/instance-scan-plugin.ts +0 -5
  127. package/src/property-plugin.ts +4 -1
  128. package/src/record-plugin.ts +14 -4
  129. package/src/rest-api-plugin.ts +7 -1
  130. package/src/schedule-script/scheduled-script-plugin.ts +14 -3
  131. package/src/service-catalog/service-catalog-base.ts +22 -22
  132. package/src/service-portal/header-footer-plugin.ts +57 -0
  133. package/src/service-portal/menu-plugin.ts +1 -23
  134. package/src/service-portal/page-plugin.ts +3 -28
  135. package/src/service-portal/page-route-map-plugin.ts +124 -0
  136. package/src/service-portal/portal-plugin.ts +33 -10
  137. package/src/service-portal/utils.ts +404 -3
  138. package/src/service-portal/widget-plugin.ts +14 -290
  139. package/src/static-content-plugin.ts +3 -0
  140. package/src/table-plugin.ts +226 -36
  141. package/src/ui-action-plugin.ts +1 -8
  142. package/src/ui-page-plugin.ts +76 -13
  143. package/src/view-plugin.ts +10 -4
  144. package/src/workspace-plugin.ts +43 -43
@@ -16,6 +16,7 @@ import {
16
16
  import {
17
17
  ServicePortal,
18
18
  SPWidget,
19
+ SPHeaderFooter,
19
20
  SPAngularProvider,
20
21
  SPWidgetDependency,
21
22
  CssInclude,
@@ -23,6 +24,7 @@ import {
23
24
  SPPage,
24
25
  SPTheme,
25
26
  SPMenu,
27
+ SPPageRouteMap,
26
28
  } from '@servicenow/sdk-core/runtime/service-portal'
27
29
  import {
28
30
  CatalogItem,
@@ -33,11 +35,12 @@ import {
33
35
  } from '@servicenow/sdk-core/runtime/service-catalog'
34
36
  import { ClientScript } from '@servicenow/sdk-core/runtime/clientscript'
35
37
  import { ScriptAction, ScriptInclude, ScheduledScript } from '@servicenow/sdk-core/runtime/sys'
36
- import { List } from '@servicenow/sdk-core/runtime/ui'
38
+ import { List, Form } from '@servicenow/sdk-core/runtime/ui'
37
39
  import { Table } from '@servicenow/sdk-core/runtime/db'
38
40
  import { RestApi } from '@servicenow/sdk-core/runtime/rest'
39
- import { EmailNotification } from '@servicenow/sdk-core/runtime/notification'
41
+ import { EmailNotification, InboundEmailAction } from '@servicenow/sdk-core/runtime/notification'
40
42
  import { UiAction, UiPage, UiPolicy } from '@servicenow/sdk-core/runtime/ui'
43
+ import { ActionStepDefinition } from '@servicenow/sdk-core/runtime/flow'
41
44
  import { Applicability, UxListMenuConfig, Workspace } from '@servicenow/sdk-core/runtime/uxf'
42
45
  import { Dashboard } from '@servicenow/sdk-core/runtime/dashboard'
43
46
  import { ColumnTypeCheck, LinterCheck, ScriptOnlyCheck, TableCheck } from '@servicenow/sdk-core/runtime/instancescan'
@@ -223,6 +226,10 @@ export const TableOwnership = {
223
226
  sys_script: BusinessRule.name,
224
227
  sys_script_client: ClientScript.name,
225
228
  sys_scope_privilege: CrossScopePrivilege.name,
229
+ sys_ui_form: Form.name,
230
+ sys_ui_section: Form.name,
231
+ sys_ui_form_section: Form.name,
232
+ sys_ui_element: Form.name,
226
233
  sys_ui_list_element: List.name,
227
234
  sys_ui_list: List.name,
228
235
  sys_properties: Property.name,
@@ -230,6 +237,7 @@ export const TableOwnership = {
230
237
  sys_user_role_contains: Role.name,
231
238
  sys_script_include: ScriptInclude.name,
232
239
  sp_widget: SPWidget.name,
240
+ sp_header_footer: SPHeaderFooter.name,
233
241
  m2m_sp_widget_dependency: SPWidget.name,
234
242
  m2m_sp_ng_pro_sp_widget: SPWidget.name,
235
243
  sp_portal: ServicePortal.name,
@@ -248,6 +256,7 @@ export const TableOwnership = {
248
256
  m2m_sp_ng_pro_sp_ng_pro: SPAngularProvider.name,
249
257
  sp_instance_menu: SPMenu.name,
250
258
  sp_rectangle_menu_item: SPMenu.name,
259
+ sp_page_route_map: SPPageRouteMap.name,
251
260
  sys_atf_test: Test.name,
252
261
  sys_ws_header_map: RestApi.name,
253
262
  sys_ws_query_parameter_map: RestApi.name,
@@ -267,6 +276,7 @@ export const TableOwnership = {
267
276
  sysevent_script_action: ScriptAction.name,
268
277
  sysauto_script: ScheduledScript.name,
269
278
  sysevent_email_action: EmailNotification.name,
279
+ sysevent_in_email_action: InboundEmailAction.name,
270
280
  sys_db_object: Table.name,
271
281
  sys_dictionary: Table.name,
272
282
  sys_hub_flow: 'Flow',
@@ -274,10 +284,10 @@ export const TableOwnership = {
274
284
  sys_hub_action_instance_v2: 'wfa.action',
275
285
  sys_hub_sub_flow_instance_v2: 'wfa.subflow',
276
286
  sys_hub_flow_logic_instance_v2: 'wfa.flowLogic',
277
- sys_flow_step_definition: 'ActionStepDefinition',
287
+ sys_flow_step_definition: ActionStepDefinition.name,
278
288
  sys_decision: 'DecisionTablePlugin',
279
289
  sys_hub_action_type_definition: 'Action',
280
- sys_hub_step_instance: 'wfa.action_step',
290
+ sys_hub_step_instance: 'wfa.actionStep',
281
291
  sp_theme: SPTheme.name,
282
292
  m2m_sp_theme_css_include: SPTheme.name,
283
293
  m2m_sp_theme_js_include: SPTheme.name,
@@ -183,6 +183,7 @@ async function generateRouteAttributeRecords(
183
183
  installCategory: restDef.getInstallCategory(),
184
184
  })
185
185
 
186
+ const isShared = attrMap.has(attrRecord.getId().getValue())
186
187
  checkForDuplicateRecords(attrMap, attrRecord, attr.asObject(), diagnostics)
187
188
 
188
189
  const attributeMappingRecord = await factory.createRecord({
@@ -195,7 +196,12 @@ async function generateRouteAttributeRecords(
195
196
  installCategory: restDef.getInstallCategory(),
196
197
  })
197
198
 
198
- records.push(attrRecord.with(attributeMappingRecord))
199
+ if (isShared) {
200
+ // The attrRecord was already emitted by a previous route — only emit the mapping
201
+ records.push(attributeMappingRecord)
202
+ } else {
203
+ records.push(attrRecord.with(attributeMappingRecord))
204
+ }
199
205
  }
200
206
 
201
207
  return records
@@ -9,6 +9,7 @@ import {
9
9
  } from '@servicenow/sdk-build-core'
10
10
  import { NowIdShape } from '../now-id-plugin'
11
11
  import { NowIncludeShape } from '../now-include-plugin'
12
+ import { ModuleFunctionShape } from '../server-module-plugin'
12
13
  import { createSdkDocEntry, toReference } from '../utils'
13
14
  import { dateTimeFieldToXML, convertXMLToDateTime, formatTimeDataToDateTime, formatToUTC } from './timeZoneConverter'
14
15
 
@@ -444,13 +445,21 @@ export const ScheduledScriptPlugin = Plugin.create({
444
445
  {
445
446
  shape: CallExpressionShape,
446
447
  fileTypes: ['fluent'],
447
- async toRecord(callExpression, { factory, diagnostics }) {
448
+ async toRecord(callExpression, { factory, diagnostics, config }) {
448
449
  if (callExpression.getCallee() !== 'ScheduledScript') {
449
450
  return { success: false }
450
451
  }
451
452
 
452
453
  const args = callExpression.getArgument(0).asObject()
453
454
 
455
+ // Check if script is unresolved module
456
+ if (args.get('script').isUnresolved()) {
457
+ diagnostics.error(
458
+ args.get('script').getOriginalNode(),
459
+ `Unable to resolve the script reference, ensure the imported module is within the ${config.serverModulesDir} directory.`
460
+ )
461
+ }
462
+
454
463
  // Validate executionInterval is defined when frequency is 'periodically'
455
464
  const frequency = args.get('frequency').ifString()?.getValue()
456
465
  if (frequency === 'periodically' && args.get('executionInterval').isUndefined()) {
@@ -667,8 +676,10 @@ export const ScheduledScriptPlugin = Plugin.create({
667
676
  // Protection policy
668
677
  sys_policy: $.from('protectionPolicy'),
669
678
 
670
- // Script
671
- script: $.toCdata(),
679
+ // Script - handle both module functions and strings
680
+ script: $.map(
681
+ (v) => v.if(ModuleFunctionShape)?.toString((n) => `${n}({{PARAMS}})`, []) ?? v
682
+ ).toCdata(),
672
683
  })),
673
684
  })
674
685
 
@@ -255,6 +255,17 @@ export function transformCatalogBaseFieldsToShape(record: Record, $: ShapeTransf
255
255
  visibleBundle: $.from('visible_bundle').toBoolean().def(true),
256
256
  visibleGuide: $.from('visible_guide').toBoolean().def(true),
257
257
  visibleStandalone: $.from('visible_standalone').toBoolean().def(true),
258
+ hideAddToCart: $.from('no_cart_v2').toBoolean().def(false),
259
+ hideAddToWishList: $.from('no_wishlist_v2').toBoolean().def(false),
260
+ hideDeliveryTime: $.from('no_delivery_time_v2').toBoolean().def(false),
261
+ hideQuantitySelector: $.from('no_quantity_v2').toBoolean().def(false),
262
+ hideSP: $.from('hide_sp').toBoolean().def(false),
263
+ image: $.def(''),
264
+ model: $.def(''),
265
+ mobilePictureType: $.from('mobile_picture_type')
266
+ .map((v: Shape) => getMobilePictureTypeFromDb(v.toString().getValue()))
267
+ .def('desktopPicture'),
268
+ makeItemNonConversational: $.from('make_item_non_conversational').toBoolean().def(false),
258
269
  }
259
270
  }
260
271
 
@@ -327,6 +338,17 @@ export function transformCatalogItemBaseFieldsToRecord($: ShapeTransform) {
327
338
  visible_bundle: $.from('visibleBundle').def(true),
328
339
  visible_guide: $.from('visibleGuide').def(true),
329
340
  visible_standalone: $.from('visibleStandalone').def(true),
341
+ no_cart_v2: $.from('hideAddToCart').def(false),
342
+ no_wishlist_v2: $.from('hideAddToWishList').def(false),
343
+ no_delivery_time_v2: $.from('hideDeliveryTime').def(false),
344
+ no_quantity_v2: $.from('hideQuantitySelector').def(false),
345
+ hide_sp: $.from('hideSP').def(false),
346
+ image: $.def(''),
347
+ model: $.def(''),
348
+ mobile_picture_type: $.from('mobilePictureType')
349
+ .map((v: Shape) => getMobilePictureTypeToDb(v.toString().getValue()))
350
+ .def('use_desktop_picture'),
351
+ make_item_non_conversational: $.from('makeItemNonConversational').def(false),
330
352
  }
331
353
  }
332
354
 
@@ -375,9 +397,6 @@ export function transformCatalogItemSpecificFieldsToShape(record: Record, $: Sha
375
397
  requestMethod: $.from('request_method')
376
398
  .map((v: Shape) => (v.ifString()?.isEmpty() ? undefined : v.getValue()))
377
399
  .def('order'),
378
- hideAddToCart: $.from('no_cart_v2').toBoolean().def(false),
379
- hideQuantitySelector: $.from('no_quantity_v2').toBoolean().def(false),
380
- hideDeliveryTime: $.from('no_delivery_time_v2').toBoolean().def(false),
381
400
  customCart: $.from('custom_cart').def(''),
382
401
  recurringFrequency: $.from('recurring_frequency').def(''),
383
402
 
@@ -388,16 +407,8 @@ export function transformCatalogItemSpecificFieldsToShape(record: Record, $: Sha
388
407
  noProceedCheckout: $.from('no_proceed_checkout').toBoolean().def(false),
389
408
  noQuantity: $.from('no_quantity').toBoolean().def(false),
390
409
  noSearch: $.from('no_search').toBoolean().def(false),
391
- hideAddToWishList: $.from('no_wishlist_v2').toBoolean().def(false),
392
410
 
393
411
  // Additional fields
394
- hideSP: $.from('hide_sp').toBoolean().def(false),
395
- image: $.def(''),
396
- makeItemNonConversational: $.from('make_item_non_conversational').toBoolean().def(false),
397
- mobilePictureType: $.from('mobile_picture_type')
398
- .map((v: Shape) => getMobilePictureTypeFromDb(v.toString().getValue()))
399
- .def('desktopPicture'),
400
- model: $.def(''),
401
412
  vendor: $.def(''),
402
413
  location: $.def(''),
403
414
  }
@@ -448,9 +459,6 @@ export function transformCatalogItemSpecificFieldsToRecord(arg: ObjectShape, $:
448
459
  request_method: $.from('requestMethod')
449
460
  .map((v: Shape) => (v.ifString()?.getValue() === 'order' ? '' : v.getValue()))
450
461
  .def(''),
451
- no_cart_v2: $.from('hideAddToCart').def(false),
452
- no_quantity_v2: $.from('hideQuantitySelector').def(false),
453
- no_delivery_time_v2: $.from('hideDeliveryTime').def(false),
454
462
  custom_cart: $.from('customCart').def(''),
455
463
  recurring_frequency: $.from('recurringFrequency').def(''),
456
464
 
@@ -461,16 +469,8 @@ export function transformCatalogItemSpecificFieldsToRecord(arg: ObjectShape, $:
461
469
  no_proceed_checkout: $.from('noProceedCheckout').def(false),
462
470
  no_quantity: $.from('noQuantity').def(false),
463
471
  no_search: $.from('noSearch').def(false),
464
- no_wishlist_v2: $.from('hideAddToWishList').def(false),
465
472
 
466
473
  // Additional
467
- hide_sp: $.from('hideSP').def(false),
468
- image: $.def(''),
469
- make_item_non_conversational: $.from('makeItemNonConversational').def(false),
470
- mobile_picture_type: $.from('mobilePictureType')
471
- .map((v: Shape) => getMobilePictureTypeToDb(v.toString().getValue()))
472
- .def('use_desktop_picture'),
473
- model: $.def(''),
474
474
  vendor: $.def(''),
475
475
  location: $.def(''),
476
476
  }
@@ -0,0 +1,57 @@
1
+ import { CallExpressionShape, Plugin } from '@servicenow/sdk-build-core'
2
+ import { createSdkDocEntry } from '../utils'
3
+ import { createWidgetToRecord, createWidgetToShape } from './utils'
4
+ import {
5
+ getDefaultClientScript,
6
+ SP_WIDGET_DEFAULT_HTML_TEMPLATE,
7
+ SP_WIDGET_DEFAULT_LINK_SCRIPT,
8
+ SP_WIDGET_DEFAULT_SERVER_SCRIPT,
9
+ } from './widget-plugin'
10
+
11
+ export const SPHeaderFooterPlugin = Plugin.create({
12
+ name: 'SPHeaderFooterPlugin',
13
+ docs: [createSdkDocEntry('SPHeaderFooter', ['sp_header_footer'])],
14
+ records: {
15
+ sp_header_footer: {
16
+ relationships: {
17
+ m2m_sp_widget_dependency: { via: 'sp_widget', descendant: true },
18
+ m2m_sp_ng_pro_sp_widget: { via: 'sp_widget', descendant: true },
19
+ sp_ng_template: { via: 'sp_widget', descendant: true },
20
+ },
21
+ async toShape(record, { descendants }) {
22
+ return createWidgetToShape(record, descendants, 'SPHeaderFooter', 'sp_header_footer', true)
23
+ },
24
+ },
25
+ m2m_sp_widget_dependency: {
26
+ coalesce: ['sp_widget', 'sp_dependency'],
27
+ },
28
+ m2m_sp_ng_pro_sp_widget: {
29
+ coalesce: ['sp_widget', 'sp_angular_provider'],
30
+ },
31
+ },
32
+ shapes: [
33
+ {
34
+ shape: CallExpressionShape,
35
+ fileTypes: ['fluent'],
36
+ async toRecord(callExpression, { diagnostics, factory, config }) {
37
+ if (callExpression.getCallee() !== 'SPHeaderFooter') {
38
+ return { success: false }
39
+ }
40
+
41
+ return createWidgetToRecord(
42
+ callExpression,
43
+ { diagnostics, factory, config },
44
+ {
45
+ callee: 'SPHeaderFooter',
46
+ table: 'sp_header_footer',
47
+ includeStaticField: true,
48
+ getDefaultClientScript,
49
+ defaultServerScript: SP_WIDGET_DEFAULT_SERVER_SCRIPT,
50
+ defaultLinkScript: SP_WIDGET_DEFAULT_LINK_SCRIPT,
51
+ defaultHtmlTemplate: SP_WIDGET_DEFAULT_HTML_TEMPLATE,
52
+ }
53
+ )
54
+ },
55
+ },
56
+ ],
57
+ })
@@ -10,6 +10,7 @@ import {
10
10
  import { NowIdShape } from '../now-id-plugin'
11
11
  import { NowIncludeShape } from '../now-include-plugin'
12
12
  import { toReference, getFieldAsNumber, noThrow, createSdkDocEntry } from '../utils'
13
+ import { getRolesString } from './utils'
13
14
 
14
15
  const default_placeholder_dimensions = `{
15
16
  "mobile": {
@@ -73,29 +74,6 @@ const defaultValues = {
73
74
  },
74
75
  }
75
76
 
76
- function getRolesString(rolesShape: Shape, diagnostics: Diagnostics): string {
77
- const roles = rolesShape
78
- ?.ifArray()
79
- ?.map((role) => {
80
- if (role.isString()) {
81
- return role.getValue()
82
- }
83
- if (role.isRecord()) {
84
- const name = role.get('name')
85
- if (name?.isString()) {
86
- return name.getValue()
87
- }
88
- return role.getId()
89
- }
90
-
91
- diagnostics.error(role, `roles must be strings or role records`)
92
- return undefined
93
- })
94
- .filter((r) => r) as string[] | undefined
95
-
96
- return roles?.toString() ?? ''
97
- }
98
-
99
77
  const addProperty = (obj: { [key: string]: unknown }, key: string, value: unknown) => {
100
78
  if (
101
79
  value === undefined ||
@@ -6,11 +6,11 @@ import {
6
6
  type Record,
7
7
  type RecordId,
8
8
  type Shape,
9
- type ArrayShape,
10
9
  type ObjectShape,
11
10
  } from '@servicenow/sdk-build-core'
12
11
  import isEmpty from 'lodash/isEmpty'
13
12
  import { toReference, getFieldAsNumber, createSdkDocEntry } from '../utils'
13
+ import { getRolesString } from './utils'
14
14
 
15
15
  type Dict = { [key: string]: unknown }
16
16
 
@@ -292,29 +292,6 @@ function getInstanceObject(instance: Record): object | undefined {
292
292
  return instanceObject
293
293
  }
294
294
 
295
- /**
296
- * Converts an ArrayShape of roles to a comma-separated string.
297
- * Handles both role record objects and string values, removes duplicates and empty values.
298
- * @param roles - ArrayShape containing role records or strings
299
- * @returns Comma-separated string of unique role names or undefined if no roles
300
- */
301
- function getRolesString(roles: ArrayShape | undefined): string | undefined {
302
- if (!roles) {
303
- return
304
- }
305
- const uniqueRoles = [
306
- ...new Set(
307
- roles.getElements().map((role) => {
308
- const roleName = role.isRecord()
309
- ? role.get('name')?.ifString()?.getValue()
310
- : role.ifString()?.getValue()
311
- return (roleName ?? '').trim()
312
- })
313
- ),
314
- ]
315
- return uniqueRoles.filter((role) => role !== '').join(',')
316
- }
317
-
318
295
  /**
319
296
  * Recursively processes nested rows within a column
320
297
  * @param columnRows - Rows that belong to the column
@@ -548,8 +525,7 @@ async function createInstanceRecords(instancesArray: Shape[], columnId: RecordId
548
525
  const instance = instanceShape.asObject()
549
526
 
550
527
  // Process roles if they exist as an array
551
- const rolesArray = instance.get('roles').ifArray()
552
- const rolesString = getRolesString(rolesArray)
528
+ const rolesString = getRolesString(instance.get('roles'))
553
529
 
554
530
  const instanceRecord = await factory.createRecord({
555
531
  source: instanceShape,
@@ -749,8 +725,7 @@ export const SPPagePlugin = Plugin.create({
749
725
  }
750
726
 
751
727
  // Process roles if they exist as an array
752
- const rolesArray = page.get('roles').ifArray()
753
- const rolesString = getRolesString(rolesArray)
728
+ const rolesString = getRolesString(page.get('roles'))
754
729
 
755
730
  const pageIdShape = page.get('pageId')
756
731
  const pageId = pageIdShape.asString().getValue()
@@ -0,0 +1,124 @@
1
+ import { CallExpressionShape, Plugin, type Shape } from '@servicenow/sdk-build-core'
2
+ import { NowIdShape } from '../now-id-plugin'
3
+ import { toReference, createSdkDocEntry } from '../utils'
4
+ import { getRolesString } from './utils'
5
+
6
+ function getPortalsString(portalsShape: Shape): string {
7
+ const portals = portalsShape
8
+ .ifArray()
9
+ ?.getElements()
10
+ .map((p) => {
11
+ const ref = toReference(p)
12
+ return typeof ref === 'string' ? ref : ref.getValue()
13
+ })
14
+ .filter((id) => id !== '')
15
+
16
+ if (!portals || portals.length === 0) {
17
+ return ''
18
+ }
19
+ return [...new Set(portals)].join(',')
20
+ }
21
+
22
+ export const SPPageRouteMapPlugin = Plugin.create({
23
+ name: 'SPPageRouteMapPlugin',
24
+ docs: [createSdkDocEntry('SPPageRouteMap', ['sp_page_route_map'])],
25
+ records: {
26
+ sp_page_route_map: {
27
+ coalesce: ['route_from_page', 'portals'],
28
+ toShape(record) {
29
+ const portalsStr = record.get('portals').ifString()?.getValue() ?? ''
30
+ const portalsArray = portalsStr
31
+ ? portalsStr
32
+ .split(',')
33
+ .map((p) => p.trim())
34
+ .filter((p) => p !== '')
35
+ : []
36
+
37
+ const rolesStr = record.get('roles').ifString()?.getValue() ?? ''
38
+ const rolesArray = rolesStr
39
+ ? rolesStr
40
+ .split(',')
41
+ .map((r) => r.trim())
42
+ .filter((r) => r !== '')
43
+ : []
44
+
45
+ return {
46
+ success: true,
47
+ value: new CallExpressionShape({
48
+ source: record,
49
+ callee: 'SPPageRouteMap',
50
+ args: [
51
+ record.transform(({ $ }) => {
52
+ const config: { [key: string]: typeof $ | undefined } = {
53
+ $id: $.val(NowIdShape.from(record)),
54
+ routeFromPage: $.from('route_from_page'),
55
+ routeToPage: $.from('route_to_page'),
56
+ shortDescription: $.from('short_description').def(''),
57
+ active: $.from('active').toBoolean().def(true),
58
+ order: $.from('order').toNumber().def(10),
59
+ }
60
+ if (portalsArray.length > 0) {
61
+ config['portals'] = $.val(portalsArray)
62
+ }
63
+ if (rolesArray.length > 0) {
64
+ config['roles'] = $.val(rolesArray)
65
+ }
66
+ return config
67
+ }),
68
+ ],
69
+ }),
70
+ }
71
+ },
72
+ },
73
+ },
74
+ shapes: [
75
+ {
76
+ shape: CallExpressionShape,
77
+ fileTypes: ['fluent'],
78
+ async toRecord(callExpression, { factory, diagnostics }) {
79
+ if (callExpression.getCallee() !== 'SPPageRouteMap') {
80
+ return { success: false }
81
+ }
82
+
83
+ const args = callExpression.getArgument(0).asObject()
84
+
85
+ const routeFromShape = args.get('routeFromPage')
86
+ const routeToShape = args.get('routeToPage')
87
+ const routeFromRef = toReference(routeFromShape)
88
+ const routeToRef = toReference(routeToShape)
89
+ const routeFromStr = typeof routeFromRef === 'string' ? routeFromRef : routeFromRef.getValue()
90
+ const routeToStr = typeof routeToRef === 'string' ? routeToRef : routeToRef.getValue()
91
+
92
+ if (routeFromStr !== '' && routeToStr !== '' && routeFromStr === routeToStr) {
93
+ diagnostics.error(
94
+ routeToShape.getOriginalNode(),
95
+ `Invalid SPPageRouteMap: "routeToPage" must be different from "routeFromPage".`
96
+ )
97
+ }
98
+
99
+ const portalsString = getPortalsString(args.get('portals'))
100
+ const rolesString = getRolesString(args.get('roles'), diagnostics)
101
+
102
+ const routeMapRecord = await factory.createRecord({
103
+ source: callExpression,
104
+ table: 'sp_page_route_map',
105
+ explicitId: args.get('$id'),
106
+ properties: args.transform(({ $ }) => ({
107
+ route_from_page: $.from('routeFromPage').map(toReference).def(''),
108
+ route_to_page: $.from('routeToPage').map(toReference).def(''),
109
+ short_description: $.from('shortDescription').def(''),
110
+ portals: $.val(portalsString).def(''),
111
+ roles: $.val(rolesString).def(''),
112
+ active: $.def(true),
113
+ order: $.def(10),
114
+ })),
115
+ })
116
+
117
+ return {
118
+ success: true,
119
+ value: routeMapRecord,
120
+ }
121
+ },
122
+ },
123
+ ],
124
+ })
@@ -173,17 +173,40 @@ export const ServicePortalPlugin = Plugin.create({
173
173
  )
174
174
  }
175
175
  }
176
- // Extract first catalog and knowledgeBase from arrays to populate portal fields
177
176
  const catalogsForM2M = portalArgs.get('catalogs')?.ifArray()?.getElements() ?? []
178
177
  const knowledgeBasesForM2M = portalArgs.get('knowledgeBases')?.ifArray()?.getElements() ?? []
179
- const firstCatalogFromArray =
180
- catalogsForM2M.length > 0 ? catalogsForM2M[0]?.asObject()?.get('catalog') : undefined
181
- const firstKnowledgeBaseFromArray =
182
- knowledgeBasesForM2M.length > 0
183
- ? knowledgeBasesForM2M[0]?.asObject()?.get('knowledgeBase')
184
- : undefined
185
178
 
186
- // Build portal properties with conditional catalog and knowledgeBase
179
+ const deprecatedKnowledgeBase = portalArgs.get('knowledgeBase')
180
+ if (deprecatedKnowledgeBase?.isDefined()) {
181
+ if (knowledgeBasesForM2M.length > 0) {
182
+ diagnostics.error(
183
+ deprecatedKnowledgeBase.getOriginalNode(),
184
+ `Cannot use both 'knowledgeBase' and 'knowledgeBases'. Remove 'knowledgeBase' and use only the 'knowledgeBases' array instead.`
185
+ )
186
+ } else {
187
+ diagnostics.warn(
188
+ deprecatedKnowledgeBase.getOriginalNode(),
189
+ `'knowledgeBase' is deprecated. Use the 'knowledgeBases' array instead.`
190
+ )
191
+ }
192
+ }
193
+
194
+ const deprecatedCatalog = portalArgs.get('catalog')
195
+ if (deprecatedCatalog?.isDefined()) {
196
+ if (catalogsForM2M.length > 0) {
197
+ diagnostics.error(
198
+ deprecatedCatalog.getOriginalNode(),
199
+ `Cannot use both 'catalog' and 'catalogs'. Remove 'catalog' and use only the 'catalogs' array instead.`
200
+ )
201
+ } else {
202
+ diagnostics.warn(
203
+ deprecatedCatalog.getOriginalNode(),
204
+ `'catalog' is deprecated. Use the 'catalogs' array instead.`
205
+ )
206
+ }
207
+ }
208
+
209
+ // Build portal properties
187
210
  const baseProperties = portalArgs.transform(({ $ }) => ({
188
211
  title: $,
189
212
  url_suffix: $.from('urlSuffix').def(''),
@@ -203,13 +226,13 @@ export const ServicePortalPlugin = Plugin.create({
203
226
  search_application: $.from('searchApplication'),
204
227
  alternate_portal: $.from('alternatePortal'),
205
228
  sc_catalog_page: $.from('catalogHomePage'),
206
- sc_catalog: $.val(firstCatalogFromArray),
207
- kb_knowledge_base: $.val(firstKnowledgeBaseFromArray),
208
229
  login_page: $.from('loginPage'),
209
230
  kb_knowledge_page: $.from('knowledgeHomePage'),
210
231
  sp_chat_queue: $.from('chatQueue'),
211
232
  notfound_page: $.from('notFoundPage'),
212
233
  ts_index_group: $.from('textIndexGroup'),
234
+ ...(knowledgeBasesForM2M.length === 0 ? { kb_knowledge_base: $.from('knowledgeBase') } : {}),
235
+ ...(catalogsForM2M.length === 0 ? { sc_catalog: $.from('catalog') } : {}),
213
236
  enable_ais: $.from('enableAiSearch').def(false),
214
237
  enable_certificate_based_authentication: $.from('enableCertificateBasedAuthentication').def(false),
215
238
  default: $.from('defaultPortal').def(false),