import { CallExpressionShape, Plugin, type Record, type RecordId, type Shape, type StringShape, } from '@servicenow/sdk-build-core' import type { UiAction } from '@servicenow/sdk-core/runtime/ui' import { NowIdShape } from './now-id-plugin' import { ModuleFunctionShape } from './server-module-plugin' export const UiActionPlugin = Plugin.create({ name: 'UiActionPlugin', records: { sys_ui_action: { relationships: { sys_ui_action_role: { via: 'sys_ui_action', descendant: true, relationships: { sys_user_role: { via: 'sys_user_role', inverse: true, }, }, }, sys_ui_action_view: { via: 'sys_ui_action', descendant: true, relationships: { sys_ui_view: { via: 'sys_ui_view', inverse: true, }, }, }, }, toShape(record, { descendants }) { const actionViews = descendants.query('sys_ui_action_view') const excludeFrom: (RecordId | string)[] = [], includeIn: (RecordId | string)[] = [] for (const actionView of actionViews) { const viewName = actionView.getId().getKey('sys_ui_view') if (!viewName) { continue } const visibility = actionView.get('visibility').ifString()?.getValue() visibility === 'exclude' ? excludeFrom.push(viewName) : includeIn.push(viewName) } const roles = descendants.query('sys_ui_action_role').map((m2m) => m2m.get('sys_user_role')) const getBoolean = (key: string): boolean | undefined => { return record.get(key)?.ifDefined()?.toBoolean().getValue() } const getString = (key: string): string | undefined => { return record.get(key)?.ifString()?.getValue() } const form = { showButton: getBoolean('form_button'), showLink: getBoolean('form_link'), showContextMenu: getBoolean('form_context_menu'), style: getString('form_style'), showIconOnly: getBoolean('show_form_icon_only'), iconName: getString('form_button_icon'), } const list = { showButton: getBoolean('list_button'), showLink: getBoolean('list_link'), showContextMenu: getBoolean('list_context_menu'), style: getString('list_style'), showListChoice: getBoolean('list_choice'), showBannerButton: getBoolean('list_banner_button'), showSaveWithFormButton: getBoolean('list_save_with_form_button'), showIconOnly: getBoolean('show_list_icon_only'), iconName: getString('list_button_icon'), } const client = { isClient: getBoolean('client'), isUi11Compatible: getBoolean('ui11_compatible'), onClick: getString('onclick'), isUi16Compatible: getBoolean('ui16_compatible'), } const workspace = { clientScriptV2: getString('client_script_v2'), showFormButtonV2: getBoolean('form_button_v2'), showFormMenuButtonV2: getBoolean('form_menu_button_v2'), isConfigurableWorkspace: getBoolean('format_for_configurable_workspace'), } // Extract messages from the record if available const messages: string[] = [] const messagesField = record.get('messages') if (messagesField?.isArray()) { const messageValues = messagesField.asArray().getValue() if (Array.isArray(messageValues)) { messageValues.forEach((msg) => { if (typeof msg === 'string') { messages.push(msg) } }) } } else if (messagesField?.ifString()) { // Handle messages as a string (from XML) const messageString = messagesField.ifString()?.getValue() if (messageString) { // Split by newlines and add each line as a separate message const messageLines = messageString.split('\n') messageLines.forEach((line) => { if (line.trim()) { messages.push(line) } }) } } return { success: true, value: new CallExpressionShape({ source: record, callee: 'UiAction', args: [ record.transform(({ $ }) => ({ $id: $.val(NowIdShape.from(record)), table: $, name: $, active: $.toBoolean().def(true), actionName: $.from('action_name').def(''), form: $.val(form).def({ showButton: false, showLink: false, showContextMenu: false, style: '', showIconOnly: false, iconName: '', }), list: $.val(list).def({ showButton: false, showLink: false, showContextMenu: false, style: '', showListChoice: false, showBannerButton: false, showSaveWithFormButton: false, showIconOnly: false, iconName: '', }), client: $.val(client).def({ isClient: false, isUi11Compatible: false, onClick: '', isUi16Compatible: false, }), workspace: $.val(workspace).def({ clientScriptV2: '', showFormButtonV2: false, showFormMenuButtonV2: false, isConfigurableWorkspace: false, }), comments: $.def(''), messages: $.val(messages.length > 0 ? messages : []), condition: $.def(''), script: $.def(''), hint: $.def(''), order: $.map((o) => (o.ifString()?.getValue() === '' ? 100 : o)) .toNumber() .def(100), overrides: $.from('sys_overrides').def(''), showQuery: $.from('show_query').toBoolean().def(false), showUpdate: $.from('show_update').toBoolean(), showInsert: $.from('show_insert').toBoolean(), showMultipleUpdate: $.from('show_multiple_update').toBoolean().def(false), isolateScript: $.from('isolate_script').toBoolean().def(false), roles: $.val(roles).def([]), includeInViews: $.val(includeIn).def([]), excludeFromViews: $.val(excludeFrom).def([]), })), ], }), } }, }, sys_ui_action_role: { coalesce: ['sys_ui_action', 'sys_user_role'], }, sys_ui_action_view: { coalesce: ['sys_ui_action', 'sys_ui_view'], }, }, shapes: [ { shape: CallExpressionShape, fileTypes: ['fluent'], async toRecord(callExpression, { diagnostics, factory }) { if (callExpression.getCallee() !== 'UiAction') { return { success: false } } const arg = callExpression.getArgument(0).asObject() const isClient = arg.get(['client', 'isClient']) const form = arg.get('form')?.getValue() as UiAction['form'] //showIconOnly doesn't influence formAction so we don't include it in the condition const isFormAction = form?.showButton || form?.showLink || form?.showContextMenu const list = arg.get('list').getValue() as UiAction['list'] //showIconOnly doesn't influence listAction so we don't include it in the condition const isListAction = list?.showButton || list?.showBannerButton || list?.showContextMenu || list?.showListChoice || list?.showLink || list?.showSaveWithFormButton const uiAction = await factory.createRecord({ source: callExpression, table: 'sys_ui_action', explicitId: arg.get('$id'), properties: arg.transform(({ $ }) => ({ name: $, table: $, action_name: $.from('actionName'), comments: $.def(''), active: $.def(true), client: $.val(isClient?.ifDefined()?.toBoolean().getValue() ?? false), onclick: $.val(arg.get(['client', 'onClick'])), messages: $.val( arg.get('messages')?.isArray() ? arg.get('messages').asArray().getValue().join('\n') : arg.get('messages') ).def([]), list_style: $.val(list?.style), form_style: $.val(form?.style), condition: $.def(''), script: $.map( (v) => v.if(ModuleFunctionShape)?.toString((n) => `${n}({{PARAMS}})`, ['current']) ?? v ) .def('') .toCdata(), hint: $.def(''), order: $.toNumber().def(100), sys_overrides: $.from('overrides').def(''), form_action: $.val(isFormAction).def(false), list_action: $.val(isListAction).def(false), form_button: $.val(form?.showButton).def(false), list_button: $.val(list?.showButton).def(false), form_link: $.val(form?.showLink).def(false), list_link: $.val(list?.showLink).def(false), show_form_icon_only: $.val(form?.showIconOnly).def(false), show_list_icon_only: $.val(list?.showIconOnly).def(false), form_context_menu: $.val(form?.showContextMenu).def(false), list_context_menu: $.val(list?.showContextMenu).def(false), show_query: $.from('showQuery').toBoolean().def(false), show_insert: $.from('showInsert').toBoolean().def(false).def(true), show_multiple_update: $.from('showMultipleUpdate').toBoolean().def(false), show_update: $.from('showUpdate').toBoolean().def(true), list_choice: $.val(list?.showListChoice).def(false), list_banner_button: $.val(list?.showBannerButton).def(false), list_save_with_form_button: $.val(list?.showSaveWithFormButton).def(false), isolate_script: $.from('isolateScript').toBoolean().def(false), ui11_compatible: $.val(arg.get(['client', 'isUi11Compatible'])).def(false), ui16_compatible: $.val(arg.get(['client', 'isUi16Compatible'])).def(false), client_script_v2: $.val(arg.get(['workspace', 'clientScriptV2'])).toCdata(), form_button_v2: $.val(arg.get(['workspace', 'showFormButtonV2'])).def(false), form_menu_button_v2: $.val(arg.get(['workspace', 'showFormMenuButtonV2'])).def(false), format_for_configurable_workspace: $.val(arg.get(['workspace', 'isConfigurableWorkspace'])).def( false ), sys_name: $.from('name'), form_button_icon: $.val(form?.iconName), list_button_icon: $.val(list?.iconName), })), }) // omitFromXmlIfUndefined is available only on shape which we dont get access to // during transform op when setting the value using $.val() so setting it here. uiAction.get('form_button_icon').omitFromXmlIfUndefined() uiAction.get('list_button_icon').omitFromXmlIfUndefined() if (arg.get('script').is(ModuleFunctionShape) && isClient.ifBoolean()?.getValue()) { diagnostics.error(isClient, 'Module scripts (sys_module) cannot be used on client-side UI Actions') } const isConfigurableWorkspace = arg .get(['workspace', 'isConfigurableWorkspace']) .ifBoolean() ?.getValue() const showFormButtonV2 = arg.get(['workspace', 'showFormButtonV2']).ifBoolean()?.getValue() const showFormMenuButtonV2 = arg.get(['workspace', 'showFormMenuButtonV2']).ifBoolean()?.getValue() if (isConfigurableWorkspace && !showFormButtonV2 && !showFormMenuButtonV2) { diagnostics.error( arg.get(['workspace', 'isConfigurableWorkspace']), 'workspace.showFormButtonV2 or workspace.showFormMenuButtonV2 must be true when workspace.isConfigurableWorkspace is true' ) } const excludeFromViews = (arg.get('excludeFromViews').ifArray()?.getElements() as Shape[]) ?? [] const includeInViews = (arg.get('includeInViews').ifArray()?.getElements() as Shape[]) ?? [] const excludeSet = new Set(excludeFromViews.map((v) => v.getValue())) includeInViews.forEach((x) => { if (excludeSet.has(x.getValue())) { diagnostics.error( arg.get('includeInViews'), 'The same view should not appear in both the `excludeFromViews` and `includeInViews`' ) } }) const uiActionViews: Record[] = [] for (const view of includeInViews) { uiActionViews.push( await factory.createRecord({ source: callExpression, table: 'sys_ui_action_view', properties: { sys_ui_action: uiAction.getId(), visibility: 'include', sys_ui_view: await factory.createReference({ source: view, table: 'sys_ui_view', keys: { name: view }, }), }, }) ) } for (const view of excludeFromViews) { uiActionViews.push( await factory.createRecord({ source: callExpression, table: 'sys_ui_action_view', properties: { sys_ui_action: uiAction.getId(), visibility: 'exclude', sys_ui_view: await factory.createReference({ source: view, table: 'sys_ui_view', keys: { name: view }, }), }, }) ) } const roles = arg.get('roles').ifArray()?.getElements() ?? [] const roleRecords: Record[] = [] for (const role of roles) { const roleReference = role.isString() ? await factory.createReference({ source: role, table: 'sys_user_role', keys: { name: role }, }) : role roleRecords.push( await factory.createRecord({ source: callExpression, table: 'sys_ui_action_role', properties: { sys_ui_action: uiAction.getId(), sys_user_role: roleReference, }, }) ) } return { success: true, value: uiAction.with(...roleRecords, ...uiActionViews), } }, }, ], })