import {
CallExpressionShape,
Plugin,
type Diagnostics,
type Factory,
type Record,
type Shape,
type Transform,
} from '@servicenow/sdk-build-core'
import { NowIdShape } from '../now-id-plugin'
import { NowIncludeShape } from '../now-include-plugin'
import { toReference, getFieldAsNumber, noThrow } from '../utils'
import { getRolesString, serializeWidgetParametersForMenu } from './utils'
const default_placeholder_dimensions = `{
"mobile": {
"height": "250px",
"width": "100%"
},
"desktop": {
"height": "250px",
"width": "100%"
},
"tablet": {
"height": "250px",
"width": "100%"
}
}`
const default_placeholder_template = `
`
const default_placeholder_script = `function evaluateConfig(options) { return {
"mobile": {
"height": "250px",
"width": "100%"
},
"desktop": {
"height": "250px",
"width": "100%"
},
"tablet": {
"height": "250px",
"width": "100%"
}
}; }`
const defaultValues = {
menuItem: {
type: 'page',
order: 100,
active: true,
color: 'default',
size: 'md',
},
menu: {
active: true,
color: 'default',
size: 'md',
asyncLoadDeviceType: 'desktop,tablet,mobile',
},
}
const addProperty = (obj: { [key: string]: unknown }, key: string, value: unknown) => {
if (
value === undefined ||
(Array.isArray(value) && value.length === 0) ||
(typeof value === 'string' && value === '')
) {
return
}
obj[key] = value
}
async function getMenuItem(item: Record, transform: Transform) {
const obj: { [key: string]: unknown } = {}
addProperty(obj, '$id', NowIdShape.from(item))
addProperty(obj, 'label', item.get('label'))
addProperty(obj, 'order', getFieldAsNumber(item, 'order', 100))
addProperty(obj, 'active', item.get('active').toBoolean())
addProperty(obj, 'glyph', item.get('glyph')?.ifString()?.getValue())
addProperty(obj, 'color', item.get('color')?.ifString()?.getValue())
addProperty(obj, 'hint', item.get('hint')?.ifString()?.getValue())
addProperty(obj, 'shortDescription', item.get('short_description')?.ifString()?.getValue())
addProperty(obj, 'condition', item.get('condition')?.ifString()?.getValue())
addProperty(obj, 'type', item.get('type'))
const roles = item.get('roles')
const rolesStr = roles.ifString()?.getValue() ?? ''
if (rolesStr) {
addProperty(obj, 'roles', rolesStr.split(','))
}
// Convert all type-specific fields unconditionally to ensure round-trip data preservation
const spPage = item.get('sp_page')
if (spPage?.ifString()?.getValue()) {
addProperty(obj, 'page', toReference(spPage))
}
addProperty(obj, 'url', item.get('url')?.ifString()?.getValue())
addProperty(obj, 'urlTarget', item.get('url_target')?.ifString()?.getValue())
const scCategory = item.get('sc_category')
if (scCategory?.ifString()?.getValue()) {
addProperty(obj, 'scCategory', toReference(scCategory))
}
const scCatItem = item.get('sc_cat_item')
if (scCatItem?.ifString()?.getValue()) {
addProperty(obj, 'catItem', toReference(scCatItem))
}
addProperty(obj, 'kbTopic', item.get('kb_topic')?.ifString()?.getValue())
const kbArticle = item.get('kb_article')
if (kbArticle?.ifString()?.getValue()) {
addProperty(obj, 'kbArticle', toReference(kbArticle))
}
const kbCategory = item.get('kb_category')
if (kbCategory?.ifString()?.getValue()) {
addProperty(obj, 'kbCategory', toReference(kbCategory))
}
addProperty(obj, 'table', item.get('table')?.ifString()?.getValue())
addProperty(obj, 'filter', item.get('filter')?.ifString()?.getValue())
addProperty(obj, 'display1', item.get('display_1')?.ifString()?.getValue())
addProperty(obj, 'display2', item.get('display_2')?.ifString()?.getValue())
addProperty(obj, 'displayDate', item.get('display_date')?.ifString()?.getValue())
// Handle Now.include support for script field
const recordScript = item.get('record_script')?.ifString()?.getValue()
if (recordScript && recordScript.trim() !== '') {
const script = await NowIncludeShape.fromRecord(item, item.get('record_script'), transform)
addProperty(obj, 'script', script)
}
return obj
}
async function createItemRecords(
itemsShape: Shape,
menuId: Shape | undefined,
parentItemId: Shape | undefined,
factory: Factory,
diagnostics: Diagnostics
): Promise {
const items = itemsShape?.ifArray()?.getElements() ?? []
const created: Record[] = []
for (const item of items) {
const obj = item.asObject()
const type = obj.get('type').asString().getValue()
const itemRecord = await factory.createRecord({
source: item,
table: 'sp_rectangle_menu_item',
explicitId: obj.get('$id'),
properties: {
label: obj.get('label')?.ifString()?.getValue() ?? '',
order: obj.get('order').ifNumber()?.getValue() ?? defaultValues.menuItem.order,
active: obj.get('active')?.ifBoolean()?.getValue() ?? defaultValues.menuItem.active,
glyph: obj.get('glyph')?.ifString()?.getValue() ?? 'empty',
color: obj.get('color')?.ifString()?.getValue() ?? defaultValues.menuItem.color,
hint: obj.get('hint')?.ifString()?.getValue() ?? '',
short_description: obj.get('shortDescription')?.ifString()?.getValue() ?? '',
condition: obj.get('condition')?.ifString()?.getValue() ?? '',
type,
sp_rectangle_menu: menuId ?? '',
sp_rectangle_menu_item: parentItemId ?? '',
sp_page: obj.get('page') ? toReference(obj.get('page')) : '',
url: obj.get('url')?.ifString()?.getValue() ?? '',
url_target: obj.get('urlTarget')?.ifString()?.getValue() ?? '',
sc_category: obj.get('scCategory') ? toReference(obj.get('scCategory')) : '',
sc_cat_item: obj.get('catItem') ? toReference(obj.get('catItem')) : '',
kb_topic: obj.get('kbTopic')?.ifString()?.getValue() ?? '',
kb_article: obj.get('kbArticle') ? toReference(obj.get('kbArticle')) : '',
kb_category: obj.get('kbCategory') ? toReference(obj.get('kbCategory')) : '',
table: obj.get('table')?.ifString()?.getValue() ?? '',
filter: obj.get('filter')?.ifString()?.getValue() ?? '',
display_1: obj.get('display1')?.ifString()?.getValue() ?? '',
display_2: obj.get('display2')?.ifString()?.getValue() ?? '',
display_date: obj.get('displayDate')?.ifString()?.getValue() ?? '',
record_script: obj.get('script')?.ifString()?.getValue() ?? '',
roles: getRolesString(obj.get('roles'), diagnostics),
},
})
created.push(itemRecord)
const childItems = obj.get('childItems')
if (childItems?.isDefined()) {
const childRecords = await createItemRecords(
childItems,
undefined,
itemRecord.getId(),
factory,
diagnostics
)
created.push(...childRecords)
}
}
return created
}
export const SPMenuPlugin = Plugin.create({
name: 'SPMenuPlugin',
records: {
sp_instance_menu: {
relationships: {
sp_rectangle_menu_item: {
via: 'sp_rectangle_menu',
descendant: true,
relationships: {
sp_rectangle_menu_item: {
via: 'sp_rectangle_menu_item',
descendant: true,
},
},
},
},
async toShape(record, { descendants, transform }) {
const items = descendants.query('sp_rectangle_menu_item', { sp_rectangle_menu: record.getId() })
const menuItems = await Promise.all(
items.map(async (item) => {
const itemId = item.getId()
const childItems = descendants.query('sp_rectangle_menu_item', {
sp_rectangle_menu_item: itemId,
})
const childMenuItems = await Promise.all(
childItems.map(async (childItem) => {
return await getMenuItem(childItem, transform)
})
)
const menuItem = {
...(await getMenuItem(item, transform)),
}
if (childMenuItems.length > 0) {
menuItem['childItems'] = childMenuItems
}
return menuItem
})
)
const rolesField = record.get('roles')
const roleStr = rolesField?.ifString()?.getValue()
let rolesArray: string[] | undefined
if (roleStr && roleStr !== '') {
const filteredRoles = roleStr.split(',').filter((r) => r !== '')
if (filteredRoles.length > 0) {
rolesArray = filteredRoles
}
}
const asyncLoadTrigger =
record.get('async_load_trigger')?.ifString()?.ifNotEmpty()?.getValue() ?? 'viewport'
const asyncLoadDeviceType = record.get('async_load_device_type')?.ifString()?.getValue() ?? ''
return {
success: true,
value: new CallExpressionShape({
source: record,
callee: 'SPMenu',
args: [
record.transform(({ $ }) => ({
$id: $.val(NowIdShape.from(record)),
title: $,
id: $.from('id').def(''),
widget: $.from('sp_widget'),
column: $.from('sp_column'),
active: $.toBoolean().def(true),
widgetParameters: $.from('widget_parameters')
.map((d) => {
const json = d.ifString()?.getValue()
if (!json) {
return ''
}
const parsed = noThrow(() => JSON.parse(json))
return parsed instanceof Error ? json : parsed
})
.def(''),
css: $.def(''),
url: $.def(''),
glyph: $.def(''),
size: $.def('md'),
color: $.def('default'),
cssClass: $.from('class_name').def(''),
order: $.from('order').toNumber().def(1),
roles: $.val(rolesArray),
shortDescription: $.from('short_description').def(''),
asyncLoad: $.from('async_load').toBoolean().def(false),
preservePlaceholderSize: $.from('preserve_placeholder_size').toBoolean().def(false),
advancedPlaceholderDimensions: $.from('advanced_placeholder_dimensions')
.toBoolean()
.def(false),
placeholderDimensions: $.from('placeholder_dimensions')
.map((d) => {
const json = d.ifString()?.getValue()
const parsed = noThrow(() => json && JSON.parse(json))
return parsed instanceof Error ? json : parsed
})
.def(''),
placeholderConfigurationScript: $.from('placeholder_dimensions_script').def(
default_placeholder_script
),
placeholderTemplate: $.from('placeholder_template').def(default_placeholder_template),
items: $.val(menuItems),
asyncLoadTrigger: $.val(asyncLoadTrigger),
asyncLoadDeviceType: $.val(asyncLoadDeviceType),
})),
],
}),
}
},
},
},
shapes: [
{
shape: CallExpressionShape,
fileTypes: ['fluent'],
async toRecord(callExpression, { factory, diagnostics }) {
if (callExpression.getCallee() !== 'SPMenu') {
return { success: false }
}
const menu = callExpression.getArgument(0).asObject()
// Process roles if they exist as an array
const roles = menu.get('roles')
const rolesString = getRolesString(roles, diagnostics)
const menuRecord = await factory.createRecord({
source: callExpression,
table: 'sp_instance_menu',
explicitId: menu.get('$id'),
properties: menu.transform(({ $ }) => ({
title: $,
id: $,
sp_widget: $.from('widget').map(toReference).def(''),
sp_column: $.from('column').map(toReference).def(''),
widget_parameters: $.from('widgetParameters').map(serializeWidgetParametersForMenu).def(''),
css: $.def(''),
url: $.def(''),
glyph: $.def(''),
size: $.def('md'),
color: $.def('default'),
class_name: $.from('cssClass').def(''),
active: $.toBoolean().def(true),
order: $.toNumber().def(1),
roles: $.val(rolesString),
short_description: $.from('shortDescription').def(''),
async_load: $.from('asyncLoad').toBoolean().def(false),
async_load_trigger: $.from('asyncLoadTrigger').def('viewport'),
async_load_device_type: $.from('asyncLoadDeviceType').def(
defaultValues.menu.asyncLoadDeviceType
),
preserve_placeholder_size: $.from('preservePlaceholderSize').toBoolean().def(false),
placeholder_dimensions: $.from('placeholderDimensions')
.map((v) =>
v.ifString() || v instanceof NowIncludeShape ? v : JSON.stringify(v.getValue())
)
.def(default_placeholder_dimensions),
advanced_placeholder_dimensions: $.from('advancedPlaceholderDimensions').toBoolean().def(false),
placeholder_dimensions_script: $.from('placeholderConfigurationScript').def(
default_placeholder_script
),
placeholder_template: $.from('placeholderTemplate').def(default_placeholder_template),
})),
})
const itemRecords = await createItemRecords(
menu.get('items'),
menuRecord.getId(),
undefined,
factory,
diagnostics
)
return {
success: true,
value: menuRecord.with(...itemRecords),
}
},
},
],
})