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), } }, }, ], })