import { CallExpressionShape, Plugin } from '@servicenow/sdk-build-core' import { NowIdShape } from './now-id-plugin' import { generateDeprecatedDiagnostics, validateClientSideScript } from './utils' import { NowIncludeShape } from './now-include-plugin' enum UITypeMapping { desktop = 0, mobile_or_service_portal = 1, all = 10, } const clientScriptAliases = { appliesExtended: ['applies_extended'], isolateScript: ['isolate_script'], uiType: ['ui_type'], } /** * Creates a Client Script (`sys_script_client`). * * @see https://docs.servicenow.com/csh?topicname=client-script-api-now-ts.html&version=latest * * @param config - an object containing the following properties: * * **$id** - unique id for the record, typically using `Now.ID["value"]` * * **name** - name of the client script * * **table** - name of the table on which the client script runs * * **type** - type of client script, which defines when it runs * * **uiType** - user interface to which the client script applies * * **active**? - whether the record is enabled * * **appliesExtended**? - indicates whether the client script applies to tables extended from the specified table * * **description**? - description of the functionality and purpose of the client script * * **field**? - field on the table that the client script applies to. * Takes effect only when the type property is set to `onChange` or `onCellEdit`; a warning is emitted at build time if set with other event types. * * **global**? - indicates which views of the table the client script runs. * `true`: the script runs on all views * `false`: the script runs only on specified views * * **isolateScript**? - indicates whether scripts run in strict mode, with * access to direct DOM, `jQuery`, `prototype`, and the `window` object turned off * * **messages**? - strings that are available to the client script as localized messages using `getmessage('[message]')`. * For more information, see [Translate a client script message](https://docs.servicenow.com/csh?topicname=t_TranslateAClientScriptMessage.html&version=latest) * * **order**? - the execution order of the client script (lower numbers execute first) * * **script**? - inline script preceded by a `script` tagged template literal. * * **view**? - views of the table on which the client script runs. This * property applies only when the `global` property is set to `false` */ export const ClientScriptPlugin = Plugin.create({ name: 'ClientScriptPlugin', records: { sys_script_client: { async toShape(record, { transform }) { const script = await NowIncludeShape.fromRecord(record, record.get('script'), transform) return { success: true, value: new CallExpressionShape({ source: record, callee: 'ClientScript', args: [ record .transform(({ $, merge }) => ({ $id: $.val(NowIdShape.from(record)), type: $.def(''), table: $, appliesExtended: $.from('applies_extended').toBoolean().def(false), isolateScript: $.from('isolate_script').toBoolean().def(false), script: $.val(script), name: $, description: $.def(''), messages: $.def(''), global: $.toBoolean().def(true), active: $.toBoolean().def(true), // Unlike sys_script.order (business rules, default 100), the sys_script_client // dictionary defines `order` with no default attribute, so there is no // platform default to fall back to here. order: $.map((v) => v.ifString()?.ifNotEmpty()?.toNumber()), view: $.def(''), uiType: $.from('ui_type') .map((v) => getUITypeFromId(v.toNumber().getValue())) .def('desktop'), [merge]: $.from('field').map((field) => { if (!field.ifString()?.isEmpty()) { return { field } } return {} }), })) .withAliasedKeys(clientScriptAliases), ], }), } }, }, }, shapes: [ { shape: CallExpressionShape, fileTypes: ['fluent'], async toRecord(callExpression, { diagnostics, factory, compiler }) { if (callExpression.getCallee() !== 'ClientScript') { return { success: false } } const clientScript = callExpression.getArgument(0).asObject().withAliasedKeys(clientScriptAliases) generateDeprecatedDiagnostics(clientScript, diagnostics) const script = clientScript.get('script').ifDefined()?.toString().getValue() const type = clientScript.get('type').ifString()?.getValue() const field = clientScript.get('field').ifString()?.getValue() if (field && (!type || !['onChange', 'onCellEdit'].includes(type))) { diagnostics.warn( clientScript.get('field'), `Field value takes effect only when type is set to onChange or onCellEdit` ) } if (script && !validateClientSideScript(script, compiler)) { diagnostics.error( clientScript.get('script'), `Client side scripts cannot import or require modules.` ) } return { success: true, value: await factory.createRecord({ source: callExpression, table: 'sys_script_client', explicitId: clientScript.get('$id'), properties: clientScript.transform(({ $ }) => ({ table: $, global: $.def(true), view: $, field: $, script: $.toCdata(), type: $, name: $, active: $.def(true), applies_extended: $.from('appliesExtended').def(false), description: $, messages: $, isolate_script: $.from('isolateScript').def(false), ui_type: $.from('uiType') .map((v) => (v.isString() ? getUITypeId(v.getValue()) : undefined)) .def(0), sys_name: $.from('name'), order: $.map((v) => v.omitFromXmlWhen((s) => !s.isNumber())), })), }), } }, }, ], }) function getUITypeFromId(id: number) { const type = UITypeMapping[id] if (!type) { throw Error('Invalid UI Type encountered, check XML data before transforming again.') } return type } function getUITypeId(value: string) { if (value in UITypeMapping) { return UITypeMapping[value as keyof typeof UITypeMapping] } throw Error('Invalid ui_type found in xml') }