@servicenow/sdk-build-plugins 4.5.0 → 4.6.1

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 (175) hide show
  1. package/dist/acl-plugin.js +3 -1
  2. package/dist/acl-plugin.js.map +1 -1
  3. package/dist/atf/test-plugin.js +6 -8
  4. package/dist/atf/test-plugin.js.map +1 -1
  5. package/dist/basic-syntax-plugin.js +10 -3
  6. package/dist/basic-syntax-plugin.js.map +1 -1
  7. package/dist/column-plugin.js +99 -49
  8. package/dist/column-plugin.js.map +1 -1
  9. package/dist/flow/flow-logic/flow-logic-diagnostics.js +5 -5
  10. package/dist/flow/flow-logic/flow-logic-diagnostics.js.map +1 -1
  11. package/dist/flow/plugins/flow-action-definition-plugin.js +1229 -54
  12. package/dist/flow/plugins/flow-action-definition-plugin.js.map +1 -1
  13. package/dist/flow/plugins/flow-data-pill-plugin.js +5 -2
  14. package/dist/flow/plugins/flow-data-pill-plugin.js.map +1 -1
  15. package/dist/flow/plugins/flow-definition-plugin.js +16 -42
  16. package/dist/flow/plugins/flow-definition-plugin.js.map +1 -1
  17. package/dist/flow/plugins/flow-diagnostics-plugin.d.ts +2 -2
  18. package/dist/flow/plugins/flow-diagnostics-plugin.js +2 -2
  19. package/dist/flow/plugins/flow-instance-plugin.js +68 -22
  20. package/dist/flow/plugins/flow-instance-plugin.js.map +1 -1
  21. package/dist/flow/plugins/step-definition-plugin.js +2 -1
  22. package/dist/flow/plugins/step-definition-plugin.js.map +1 -1
  23. package/dist/flow/plugins/step-instance-plugin.d.ts +9 -1
  24. package/dist/flow/plugins/step-instance-plugin.js +649 -136
  25. package/dist/flow/plugins/step-instance-plugin.js.map +1 -1
  26. package/dist/flow/plugins/wfa-datapill-plugin.js +20 -5
  27. package/dist/flow/plugins/wfa-datapill-plugin.js.map +1 -1
  28. package/dist/flow/post-install.js +1 -0
  29. package/dist/flow/post-install.js.map +1 -1
  30. package/dist/flow/utils/complex-object-resolver.js +4 -1
  31. package/dist/flow/utils/complex-object-resolver.js.map +1 -1
  32. package/dist/flow/utils/complex-objects.js +1 -1
  33. package/dist/flow/utils/complex-objects.js.map +1 -1
  34. package/dist/flow/utils/flow-constants.d.ts +66 -2
  35. package/dist/flow/utils/flow-constants.js +402 -6
  36. package/dist/flow/utils/flow-constants.js.map +1 -1
  37. package/dist/flow/utils/flow-io-to-record.d.ts +1 -1
  38. package/dist/flow/utils/flow-io-to-record.js +37 -16
  39. package/dist/flow/utils/flow-io-to-record.js.map +1 -1
  40. package/dist/flow/utils/flow-shapes.js +4 -0
  41. package/dist/flow/utils/flow-shapes.js.map +1 -1
  42. package/dist/flow/utils/label-cache-parser.d.ts +9 -2
  43. package/dist/flow/utils/label-cache-parser.js +32 -4
  44. package/dist/flow/utils/label-cache-parser.js.map +1 -1
  45. package/dist/flow/utils/pill-shape-helpers.d.ts +15 -0
  46. package/dist/flow/utils/pill-shape-helpers.js +35 -0
  47. package/dist/flow/utils/pill-shape-helpers.js.map +1 -0
  48. package/dist/flow/utils/pill-string-parser.js +1 -0
  49. package/dist/flow/utils/pill-string-parser.js.map +1 -1
  50. package/dist/flow/utils/schema-to-flow-object.d.ts +6 -1
  51. package/dist/flow/utils/schema-to-flow-object.js +131 -15
  52. package/dist/flow/utils/schema-to-flow-object.js.map +1 -1
  53. package/dist/flow/utils/utils.d.ts +1 -0
  54. package/dist/flow/utils/utils.js +6 -1
  55. package/dist/flow/utils/utils.js.map +1 -1
  56. package/dist/form-plugin.js +7 -9
  57. package/dist/form-plugin.js.map +1 -1
  58. package/dist/inbound-email-action-plugin.d.ts +10 -0
  59. package/dist/inbound-email-action-plugin.js +128 -0
  60. package/dist/inbound-email-action-plugin.js.map +1 -0
  61. package/dist/index.d.ts +4 -0
  62. package/dist/index.js +4 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/instance-scan-plugin.js +0 -5
  65. package/dist/instance-scan-plugin.js.map +1 -1
  66. package/dist/now-config-plugin.js +1 -0
  67. package/dist/now-config-plugin.js.map +1 -1
  68. package/dist/property-plugin.js +1 -1
  69. package/dist/property-plugin.js.map +1 -1
  70. package/dist/record-plugin.d.ts +7 -0
  71. package/dist/record-plugin.js +13 -4
  72. package/dist/record-plugin.js.map +1 -1
  73. package/dist/rest-api-plugin.js +8 -1
  74. package/dist/rest-api-plugin.js.map +1 -1
  75. package/dist/schedule-script/scheduled-script-plugin.js +8 -3
  76. package/dist/schedule-script/scheduled-script-plugin.js.map +1 -1
  77. package/dist/script-include-plugin.js +4 -0
  78. package/dist/script-include-plugin.js.map +1 -1
  79. package/dist/service-catalog/catalog-clientscript-plugin.js +2 -2
  80. package/dist/service-catalog/catalog-clientscript-plugin.js.map +1 -1
  81. package/dist/service-catalog/catalog-ui-policy-plugin.js +2 -2
  82. package/dist/service-catalog/catalog-ui-policy-plugin.js.map +1 -1
  83. package/dist/service-catalog/service-catalog-base.d.ts +20 -20
  84. package/dist/service-catalog/service-catalog-base.js +24 -24
  85. package/dist/service-catalog/service-catalog-base.js.map +1 -1
  86. package/dist/service-catalog/utils.js +1 -1
  87. package/dist/service-catalog/utils.js.map +1 -1
  88. package/dist/service-portal/header-footer-plugin.d.ts +2 -0
  89. package/dist/service-portal/header-footer-plugin.js +50 -0
  90. package/dist/service-portal/header-footer-plugin.js.map +1 -0
  91. package/dist/service-portal/menu-plugin.js +3 -22
  92. package/dist/service-portal/menu-plugin.js.map +1 -1
  93. package/dist/service-portal/page-plugin.js +3 -24
  94. package/dist/service-portal/page-plugin.js.map +1 -1
  95. package/dist/service-portal/page-route-map-plugin.d.ts +2 -0
  96. package/dist/service-portal/page-route-map-plugin.js +114 -0
  97. package/dist/service-portal/page-route-map-plugin.js.map +1 -0
  98. package/dist/service-portal/portal-plugin.js +21 -8
  99. package/dist/service-portal/portal-plugin.js.map +1 -1
  100. package/dist/service-portal/utils.d.ts +40 -2
  101. package/dist/service-portal/utils.js +283 -2
  102. package/dist/service-portal/utils.js.map +1 -1
  103. package/dist/service-portal/widget-plugin.js +9 -218
  104. package/dist/service-portal/widget-plugin.js.map +1 -1
  105. package/dist/static-content-plugin.js +4 -0
  106. package/dist/static-content-plugin.js.map +1 -1
  107. package/dist/table-plugin.js +377 -67
  108. package/dist/table-plugin.js.map +1 -1
  109. package/dist/ui-action-plugin.js +1 -4
  110. package/dist/ui-action-plugin.js.map +1 -1
  111. package/dist/ui-page-plugin.js +68 -13
  112. package/dist/ui-page-plugin.js.map +1 -1
  113. package/dist/ui-policy-plugin.js +28 -96
  114. package/dist/ui-policy-plugin.js.map +1 -1
  115. package/dist/utils.d.ts +5 -1
  116. package/dist/utils.js +41 -0
  117. package/dist/utils.js.map +1 -1
  118. package/dist/view-plugin.js +8 -3
  119. package/dist/view-plugin.js.map +1 -1
  120. package/dist/workspace-plugin.js +39 -36
  121. package/dist/workspace-plugin.js.map +1 -1
  122. package/package.json +5 -4
  123. package/src/acl-plugin.ts +3 -1
  124. package/src/atf/test-plugin.ts +6 -9
  125. package/src/basic-syntax-plugin.ts +11 -3
  126. package/src/column-plugin.ts +137 -75
  127. package/src/flow/flow-logic/flow-logic-diagnostics.ts +5 -6
  128. package/src/flow/plugins/flow-action-definition-plugin.ts +1581 -61
  129. package/src/flow/plugins/flow-data-pill-plugin.ts +5 -2
  130. package/src/flow/plugins/flow-definition-plugin.ts +12 -47
  131. package/src/flow/plugins/flow-diagnostics-plugin.ts +2 -2
  132. package/src/flow/plugins/flow-instance-plugin.ts +98 -22
  133. package/src/flow/plugins/step-definition-plugin.ts +2 -1
  134. package/src/flow/plugins/step-instance-plugin.ts +772 -156
  135. package/src/flow/plugins/wfa-datapill-plugin.ts +25 -5
  136. package/src/flow/post-install.ts +1 -0
  137. package/src/flow/utils/complex-object-resolver.ts +4 -1
  138. package/src/flow/utils/complex-objects.ts +1 -1
  139. package/src/flow/utils/flow-constants.ts +421 -5
  140. package/src/flow/utils/flow-io-to-record.ts +43 -17
  141. package/src/flow/utils/flow-shapes.ts +4 -0
  142. package/src/flow/utils/label-cache-parser.ts +33 -4
  143. package/src/flow/utils/pill-shape-helpers.ts +42 -0
  144. package/src/flow/utils/pill-string-parser.ts +1 -0
  145. package/src/flow/utils/schema-to-flow-object.ts +183 -15
  146. package/src/flow/utils/utils.ts +12 -1
  147. package/src/form-plugin.ts +1 -3
  148. package/src/inbound-email-action-plugin.ts +145 -0
  149. package/src/index.ts +4 -0
  150. package/src/instance-scan-plugin.ts +0 -5
  151. package/src/now-config-plugin.ts +1 -0
  152. package/src/property-plugin.ts +4 -1
  153. package/src/record-plugin.ts +25 -7
  154. package/src/rest-api-plugin.ts +7 -1
  155. package/src/schedule-script/scheduled-script-plugin.ts +14 -3
  156. package/src/script-include-plugin.ts +8 -0
  157. package/src/service-catalog/catalog-clientscript-plugin.ts +2 -2
  158. package/src/service-catalog/catalog-ui-policy-plugin.ts +2 -2
  159. package/src/service-catalog/service-catalog-base.ts +24 -24
  160. package/src/service-catalog/utils.ts +1 -1
  161. package/src/service-portal/header-footer-plugin.ts +57 -0
  162. package/src/service-portal/menu-plugin.ts +1 -23
  163. package/src/service-portal/page-plugin.ts +3 -28
  164. package/src/service-portal/page-route-map-plugin.ts +124 -0
  165. package/src/service-portal/portal-plugin.ts +33 -10
  166. package/src/service-portal/utils.ts +404 -3
  167. package/src/service-portal/widget-plugin.ts +14 -290
  168. package/src/static-content-plugin.ts +3 -0
  169. package/src/table-plugin.ts +466 -99
  170. package/src/ui-action-plugin.ts +1 -8
  171. package/src/ui-page-plugin.ts +76 -13
  172. package/src/ui-policy-plugin.ts +32 -128
  173. package/src/utils.ts +52 -0
  174. package/src/view-plugin.ts +10 -4
  175. package/src/workspace-plugin.ts +43 -43
@@ -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),
@@ -1,5 +1,163 @@
1
1
  import { NowIncludeShape } from '../now-include-plugin'
2
- import { type Record, Shape, type RecordId, type Diagnostics, type Factory } from '@servicenow/sdk-build-core'
2
+ import {
3
+ type Record,
4
+ Shape,
5
+ type RecordId,
6
+ type Diagnostics,
7
+ type Factory,
8
+ CallExpressionShape,
9
+ isGUID,
10
+ } from '@servicenow/sdk-build-core'
11
+ import { noThrow, reverseObject } from '../utils'
12
+ import { WidgetCategories } from '@servicenow/sdk-core/runtime/service-portal'
13
+ import { NowIdShape } from '../now-id-plugin'
14
+
15
+ function convertOptionSchemaKeys(optionSchema: unknown, convertKey: (key: string) => string): unknown {
16
+ if (!Array.isArray(optionSchema)) {
17
+ return optionSchema
18
+ }
19
+ return optionSchema.map((option: globalThis.Record<string, unknown>) =>
20
+ Object.fromEntries(Object.entries(option).map(([k, v]) => [convertKey(k), v]))
21
+ )
22
+ }
23
+
24
+ const toCamelCase = (key: string) => key.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase())
25
+
26
+ /**
27
+ * Shared toShape implementation for SPWidget and SPHeaderFooter
28
+ */
29
+ export async function createWidgetToShape(
30
+ record: Record,
31
+ descendants: { query: (table: string) => Record[] },
32
+ callee: 'SPWidget' | 'SPHeaderFooter',
33
+ tablePrefix: string = 'sp_widget',
34
+ includeStaticField: boolean = false
35
+ ) {
36
+ const folderName =
37
+ record.get('id')?.asString()?.getValue()?.replace(/[\s-]/g, '_') ||
38
+ record.get('name')?.asString()?.getValue()?.toLowerCase().replace(/[\s-]/g, '_')
39
+ const dependencies = descendants.query('m2m_sp_widget_dependency')
40
+ const angularProviders = descendants.query('m2m_sp_ng_pro_sp_widget')
41
+ const templates = descendants.query('sp_ng_template').map((template) => {
42
+ return {
43
+ $id: template.getId(),
44
+ id: template.get('id'),
45
+ htmlTemplate: template.get('template'),
46
+ }
47
+ })
48
+
49
+ const category = reverseObject(WidgetCategories)[record.get('category').ifString()?.getValue() || '']
50
+ const serverScript = await createIncludeShape(
51
+ record,
52
+ record.get('script'),
53
+ 'server_script',
54
+ 'js',
55
+ folderName,
56
+ tablePrefix
57
+ )
58
+ const clientScript = await createIncludeShape(
59
+ record,
60
+ record.get('client_script'),
61
+ 'client_script',
62
+ 'js',
63
+ folderName,
64
+ tablePrefix
65
+ )
66
+ const css = await createIncludeShape(record, record.get('css'), 'style', 'scss', folderName, tablePrefix)
67
+ const htmlTemplate = await createIncludeShape(
68
+ record,
69
+ record.get('template'),
70
+ 'template',
71
+ 'html',
72
+ folderName,
73
+ tablePrefix
74
+ )
75
+ const linkScript = await createIncludeShape(
76
+ record,
77
+ record.get('link'),
78
+ 'link-script',
79
+ 'js',
80
+ folderName,
81
+ tablePrefix
82
+ )
83
+
84
+ return {
85
+ success: true,
86
+ value: new CallExpressionShape({
87
+ source: record,
88
+ callee,
89
+ args: [
90
+ record.transform(({ $ }) => {
91
+ const baseTransform = {
92
+ $id: $.val(NowIdShape.from(record)),
93
+ name: $,
94
+ category: $.val(category).def('custom'),
95
+ clientScript: $.val(clientScript).def(''),
96
+ serverScript: $.val(serverScript).def(''),
97
+ controllerAs: $.from('controller_as').def('c'),
98
+ htmlTemplate: $.val(htmlTemplate).def(''),
99
+ customCss: $.val(css).def(''),
100
+ dataTable: $.from('data_table')
101
+ .map((v: Shape) => {
102
+ const val = v.ifString()?.getValue()
103
+ return val && val.trim() !== '' ? val : 'sp_instance'
104
+ })
105
+ .def('sp_instance'),
106
+ demoData: $.from('demo_data')
107
+ .map((d: Shape) => {
108
+ const json = d.ifString()?.getValue()
109
+
110
+ const parsed = noThrow(() => json && JSON.parse(json))
111
+ return parsed instanceof Error ? json : parsed
112
+ })
113
+ .def(''),
114
+ description: $.def(''),
115
+ docs: $.def(''),
116
+ fields: $.from('field_list')
117
+ .map((fields: Shape) => {
118
+ const fieldStr = fields.ifString()?.getValue()
119
+ const fieldArray = fieldStr?.split(',').filter((f: string) => f.trim())
120
+ return fieldArray && fieldArray.length > 0 ? fieldArray : undefined
121
+ })
122
+ .def(undefined),
123
+ hasPreview: $.from('has_preview').toBoolean().def(false),
124
+ id: $.def(''),
125
+ internal: $.toBoolean().def(false),
126
+ linkScript: $.val(linkScript).def(''),
127
+ roles: $.map((role: Shape) => {
128
+ const roleStr = role.ifString()?.getValue()
129
+ const roleArray = roleStr?.split(',').filter((r: string) => r.trim())
130
+ return roleArray && roleArray.length > 0 ? roleArray : undefined
131
+ }).def(undefined),
132
+ servicenow: $.toBoolean().def(false),
133
+ optionSchema: $.from('option_schema')
134
+ .map((v: Shape) => {
135
+ const json = v.ifString()?.getValue()
136
+ const parsed = noThrow(() => json && JSON.parse(json))
137
+ const result = parsed instanceof Error ? json : parsed
138
+ return convertOptionSchemaKeys(result, toCamelCase)
139
+ })
140
+ .def(''),
141
+ public: $.toBoolean().def(false),
142
+ dependencies: $.val(
143
+ dependencies.length > 0 ? dependencies.map((dep) => dep.get('sp_dependency')) : undefined
144
+ ),
145
+ angularProviders: $.val(
146
+ angularProviders.length > 0
147
+ ? angularProviders.map((ap) => ap.get('sp_angular_provider'))
148
+ : undefined
149
+ ),
150
+ templates: $.val(templates.length > 0 ? templates : undefined),
151
+ }
152
+
153
+ return includeStaticField
154
+ ? { ...baseTransform, static: $.from('static').toBoolean().def(false) }
155
+ : baseTransform
156
+ }),
157
+ ],
158
+ }),
159
+ }
160
+ }
3
161
 
4
162
  /**
5
163
  * Creates a NowIncludeShape with a custom file path suffix and extension
@@ -9,9 +167,10 @@ export async function createIncludeShape(
9
167
  content: string | Shape,
10
168
  suffix: string,
11
169
  extension: 'js' | 'html' | 'scss',
12
- folderName: string
170
+ folderName: string,
171
+ tablePrefix: string = 'sp_widget'
13
172
  ): Promise<NowIncludeShape> {
14
- const baseName = `sp_widget_${folderName}`
173
+ const baseName = `${tablePrefix}_${folderName}`
15
174
  const includedText = content instanceof Shape ? content.toString().getValue() : content
16
175
 
17
176
  return new NowIncludeShape({
@@ -23,6 +182,248 @@ export async function createIncludeShape(
23
182
 
24
183
  export const DEFAULT_ORDER = 100
25
184
 
185
+ /**
186
+ * Shared toRecord implementation for SPWidget and SPHeaderFooter
187
+ */
188
+ export async function createWidgetToRecord(
189
+ callExpression: CallExpressionShape,
190
+ {
191
+ diagnostics,
192
+ factory,
193
+ config,
194
+ }: {
195
+ diagnostics: Diagnostics
196
+ factory: Factory
197
+ config: { scope: string }
198
+ },
199
+ options: {
200
+ callee: 'SPWidget' | 'SPHeaderFooter'
201
+ table: 'sp_widget' | 'sp_header_footer'
202
+ includeStaticField?: boolean
203
+ getDefaultClientScript: (controller: string) => string
204
+ defaultServerScript: string
205
+ defaultLinkScript: string
206
+ defaultHtmlTemplate: string
207
+ }
208
+ ) {
209
+ const { table, includeStaticField = false } = options
210
+ const widget = callExpression.getArgument(0).asObject()
211
+ const dependencies =
212
+ widget
213
+ .get('dependencies')
214
+ .ifArray()
215
+ ?.map((dep) => (dep.isString() ? dep.getValue() : dep.ifRecord()?.getId()))
216
+ .filter((dep) => dep) ?? []
217
+ const angularProviders =
218
+ widget
219
+ .get('angularProviders')
220
+ .ifArray()
221
+ ?.map((ap) => (ap.isString() ? ap.getValue() : ap.ifRecord()?.getId()))
222
+ .filter((ap) => ap) ?? []
223
+
224
+ const templates: Array<{ $id: string; id: unknown; htmlTemplate: unknown }> =
225
+ widget
226
+ .get('templates')
227
+ .ifArray()
228
+ ?.map((shp) => shp.getValue() as { $id: string; id: unknown; htmlTemplate: unknown }) ?? []
229
+ const widgetId = widget.get('id').ifString()
230
+ if (widgetId && !/^[a-zA-Z0-9_-]+$/g.test(widgetId.getValue())) {
231
+ diagnostics.error(
232
+ widgetId.getOriginalNode(),
233
+ `Invalid value: must contain only alphanumeric, -, or _ characters`
234
+ )
235
+ }
236
+
237
+ const clientScript = widget.get('clientScript')
238
+ const clientScriptValue =
239
+ clientScript instanceof NowIncludeShape ? clientScript.getValue() : clientScript.ifString()?.getValue()
240
+ if (clientScriptValue && clientScriptValue.trim().length > 0) {
241
+ const clientScriptPattern =
242
+ /^(function|api\.controller\s?=\s?function)\s?([$a-z_][$0-9a-z_]*)?\s?\(.*\)\s?\n?{/i
243
+ if (!clientScriptPattern.test(clientScriptValue.trim())) {
244
+ diagnostics.error(
245
+ clientScript.getOriginalNode(),
246
+ `Client controller must contain a JavaScript function. Example: api.controller = function($scope) { ... }`
247
+ )
248
+ }
249
+ }
250
+
251
+ const htmlTemplate = widget.get('htmlTemplate')
252
+ const htmlTemplateValue =
253
+ htmlTemplate instanceof NowIncludeShape ? htmlTemplate.getValue() : htmlTemplate.ifString()?.getValue()
254
+ if (htmlTemplateValue && htmlTemplateValue.length > 0 && htmlTemplateValue.indexOf('href="#"') > 0) {
255
+ diagnostics.error(
256
+ htmlTemplate.getOriginalNode(),
257
+ `Do not use href="#" in the Service Portal, use href="javascript:void(0)" instead`
258
+ )
259
+ }
260
+
261
+ const roles = widget
262
+ .get('roles')
263
+ .ifArray()
264
+ ?.map((role) => {
265
+ if (role.isString()) {
266
+ return role
267
+ }
268
+ if (role.isRecord()) {
269
+ return role.get('name')
270
+ }
271
+
272
+ return undefined
273
+ })
274
+ .filter((role) => role) as Shape[] | undefined
275
+
276
+ if (roles) {
277
+ roles
278
+ .filter((role) => isGUID(role.getValue() as string))
279
+ .forEach((role) =>
280
+ diagnostics.error(
281
+ role!.getOriginalNode(),
282
+ `expecting role names or role records created by the Role or Record plugins, not sys_ids`
283
+ )
284
+ )
285
+ }
286
+
287
+ const controller = widget.get('controllerAs').ifString()?.getValue() || 'c'
288
+ const servicenow =
289
+ (widget.get('servicenow').ifBoolean()?.getValue() &&
290
+ (config.scope.startsWith('sn_') || config.scope.startsWith('snc_'))) ||
291
+ false
292
+
293
+ const widgetRecord = await factory.createRecord({
294
+ source: callExpression,
295
+ table,
296
+ explicitId: widget.get('$id'),
297
+ properties: widget.transform(({ $ }) => {
298
+ const baseTransform = {
299
+ name: $,
300
+ category: $.map((v) => {
301
+ const catKey = v.ifString()?.getValue() || ''
302
+ return WidgetCategories[catKey as keyof typeof WidgetCategories]
303
+ }).def('custom'),
304
+ client_script: $.from('clientScript').def(options.getDefaultClientScript(controller)),
305
+ script: $.from('serverScript').def(options.defaultServerScript),
306
+ controller_as: $.from('controllerAs').def('c'),
307
+ template: $.from('htmlTemplate').def(options.defaultHtmlTemplate),
308
+ css: $.from('customCss').def(''),
309
+ data_table: $.from('dataTable').def('sp_instance'),
310
+ demo_data: $.from('demoData')
311
+ .map((v) => (v.ifString() || v instanceof NowIncludeShape ? v : JSON.stringify(v.getValue())))
312
+ .def(''),
313
+ description: $.def(''),
314
+ docs: $.map((v) => (v.isString() ? v : v.ifRecord()?.getId())),
315
+ field_list: $.from('fields')
316
+ .map((v) => v.getValue()?.toString())
317
+ .def(''),
318
+ has_preview: $.from('hasPreview').toBoolean().def(false),
319
+ id: $.def(''),
320
+ internal: $.def(false),
321
+ servicenow: $.val(servicenow),
322
+ link: $.from('linkScript').def(options.defaultLinkScript),
323
+ roles: $.val(roles?.map((r) => r.getValue()).toString()).def(''),
324
+ option_schema: $.from('optionSchema')
325
+ .map((v) => {
326
+ const json = v.getValue()
327
+ const converted = convertOptionSchemaKeys(json, (key: string) =>
328
+ key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`)
329
+ )
330
+ return converted && JSON.stringify(converted)
331
+ })
332
+ .def(''),
333
+ public: $.def(false),
334
+ }
335
+
336
+ return includeStaticField
337
+ ? { ...baseTransform, static: $.from('static').toBoolean().def(false) }
338
+ : baseTransform
339
+ }),
340
+ })
341
+
342
+ return {
343
+ success: true,
344
+ value: widgetRecord.with(
345
+ ...(await Promise.all(
346
+ templates.map(async (template) => {
347
+ return await factory.createRecord({
348
+ source: callExpression,
349
+ table: 'sp_ng_template',
350
+ explicitId: template.$id,
351
+ properties: {
352
+ id: template.id,
353
+ sp_widget: widgetRecord.getId(),
354
+ template: template.htmlTemplate,
355
+ },
356
+ })
357
+ })
358
+ )),
359
+ ...(await Promise.all(
360
+ dependencies.map(async (dep) => {
361
+ return await factory.createRecord({
362
+ source: callExpression,
363
+ table: 'm2m_sp_widget_dependency',
364
+ properties: {
365
+ sp_dependency: dep!,
366
+ sp_widget: widgetRecord.getId(),
367
+ },
368
+ })
369
+ })
370
+ )),
371
+ ...(await Promise.all(
372
+ angularProviders.map(async (ap) => {
373
+ return await factory.createRecord({
374
+ source: callExpression,
375
+ table: 'm2m_sp_ng_pro_sp_widget',
376
+ properties: {
377
+ sp_angular_provider: ap!,
378
+ sp_widget: widgetRecord.getId(),
379
+ },
380
+ })
381
+ })
382
+ ))
383
+ ),
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Converts a roles Shape (array of strings, Role() expressions, or Record references)
389
+ * to a deduplicated comma-separated string of role names.
390
+ * @param rolesShape - Shape containing the roles array
391
+ * @param diagnostics - Optional diagnostics for reporting invalid role entries
392
+ * @returns Comma-separated string of unique role names, or empty string if no roles
393
+ */
394
+ export function getRolesString(rolesShape: Shape, diagnostics?: Diagnostics): string {
395
+ const roles = rolesShape
396
+ .ifArray()
397
+ ?.getElements()
398
+ .map((role) => {
399
+ if (role.isString()) {
400
+ return role.getValue()
401
+ }
402
+ if (role instanceof CallExpressionShape) {
403
+ const name = role.getArgument(0).asObject().get('name')
404
+ if (name.isString()) {
405
+ return name.getValue()
406
+ }
407
+ }
408
+ if (role.isRecord()) {
409
+ const name = role.get('name')
410
+ if (name?.isString()) {
411
+ return name.getValue()
412
+ }
413
+ }
414
+ if (diagnostics) {
415
+ diagnostics.error(role, 'roles must be strings or role records')
416
+ }
417
+ return undefined
418
+ })
419
+ .filter((r): r is string => r !== undefined && r.trim() !== '')
420
+
421
+ if (!roles || roles.length === 0) {
422
+ return ''
423
+ }
424
+ return [...new Set(roles)].join(',')
425
+ }
426
+
26
427
  export async function getIncludeRecords(
27
428
  shape: Shape,
28
429
  parentId: Shape,