UNPKG

@servicenow/sdk

Version:
600 lines (491 loc) 22.8 kB
--- tags: [platform-view, ui-action, ui-policy, ui-formatter, buttons, actions, field-visibility, form-layout] --- # Platform Views & UI Layout Control Configure UI controls for ServiceNow platform lists and forms. Covers UI Actions (sys_ui_action), UI Policies (sys_ui_policy), and UI Formatters (sys_ui_formatter). Use this guide when configuring buttons on forms or lists, field visibility/mandatory rules, formatting a form, adding sections, activity streams, process flows, dynamic field behavior, or role-restricted actions. ## Choosing the Right Approach | Need | Use | API | |------|-----|-----| | Button/link on form or list | UI Actions | `UiAction` | | Field visibility/mandatory/read-only based on condition | UI Policies | `UiPolicy` | | Non-field content on forms (activity, process flow) | UI Formatters | Record API | | Configure list columns and order | Lists | `List` | | Different form layout per role/group/persona | Views | Record API | | Auto-switch layout when condition met | View Rules | Record API | | Hide New/Edit buttons, role-based list actions | List Controls | Record API | ### Views vs View Rules vs UI Policies | Scenario | Use | |----------|-----| | Whole form layout changes per role/group (different fields/sections) | **View** | | Whole form layout switches automatically based on condition/device/state | **View Rule** | | Specific fields hide/show/mandatory/read-only when condition met | **UI Policy** | | Control list buttons (New/Edit) or disable pagination | **List Control** | ### Views vs ACLs | Intent | Use | |--------|-----| | Certain fields/sections should not appear in the form for some users | **Views** -- fields absent from the form entirely | | Restrict who can read/write/delete records or fields (data security) | **ACLs** -- security enforcement | ## Avoidance - **Never create `sys_ui_formatter` records for Activity or Attached Knowledge** -- they already exist globally. - **Never create custom formatters** -- not supported in Fluent. - **Never use `disabled` in UI Policy actions** -- use `readOnly` instead (the plugin maps it). - **Never place a formatter in a section with no other elements** -- it will not render. - **Never have a UI Action script return a value** -- the `script` field must never return anything. - **Never skip the view uniqueness check** -- `sys_ui_view` is global and `title` must be unique across all scopes. - **Never create lists for tables that don't exist yet** -- define the table first. - **Never use personal list preferences (sys_ui_list_user) in application code** -- those are user-specific. - **Never mix basic and advanced relationship fields in view configurations** -- use one pattern or the other. - **Never use Business Rules, Client Scripts, or UI Policies for view switching** -- always use View Rules (sysrule_view). - **Never combine `omit_*_button: true` with `*_roles` for the same capability** -- the omit flag overrides role permissions. --- ## UI Actions Use the `UiAction` API from `@servicenow/sdk/core`. Every UI Action must have `$id`, `table`, `name`, and `actionName`. ### Key Guidance 1. **Be explicit about placement:** Set `form.showButton`, `list.showButton`, etc. to control where the action appears. If blank, the action may not appear anywhere useful. 2. **Set visibility mode:** Use `showInsert: true` for new record forms, `showUpdate: true` for existing record forms and list views. 3. **Client vs. server scripts:** Set `client.isClient: true` for client-side execution. When `isClient` is true, use `client.onClick` for the trigger and `script` for the function definition. 4. **Set a `style`** on form/list objects -- `'primary'`, `'destructive'`, or `'unstyled'`. 5. **Use `condition`** to control when the action is visible (e.g., `current.canWrite()`). 6. **The `script` field must never return anything.** ### UI Action Properties | Property | Type | Required | Description | |----------|------|----------|-------------| | `$id` | `Now.ID[string]` | Yes | Unique identifier | | `table` | `string` | Yes | Table this UI Action is associated with | | `name` | `string` | Yes | Display name | | `actionName` | `string` | Yes | Unique identifier usable in scripts | | `active` | `boolean` | No | Whether the action is available | | `showInsert` | `boolean` | No | Show on form in insert mode (before save) | | `showUpdate` | `boolean` | No | Show on form in update mode (after save) | | `showQuery` | `boolean` | No | Show on list when a filter is applied | | `showMultipleUpdate` | `boolean` | No | Allow triggering on multiple selected records | | `condition` | `string` | No | Script/condition controlling visibility | | `script` | `string` | No | Script to execute when triggered | | `hint` | `string` | No | Tooltip text | | `order` | `number` | No | Button/link position | | `isolateScript` | `boolean` | No | Run script in isolated scope | | `roles` | `(string or Role)[]` | No | Roles that can see/execute the action | | `includeInViews` | `string[]` | No | Views where the action appears | | `excludeFromViews` | `string[]` | No | Views where the action is excluded | | `messages` | `string[]` | No | Messages for client scripts | #### Form Properties | Property | Type | Description | |----------|------|-------------| | `form.showButton` | `boolean` | Add button to form | | `form.showLink` | `boolean` | Add link to form | | `form.showContextMenu` | `boolean` | Add to right-click menu | | `form.style` | `string` | `'primary'`, `'destructive'`, or `'unstyled'` | #### List Properties | Property | Type | Description | |----------|------|-------------| | `list.showButton` | `boolean` | Add button to list | | `list.showLink` | `boolean` | Add link to list | | `list.showContextMenu` | `boolean` | Add to right-click menu | | `list.style` | `string` | `'primary'`, `'destructive'`, or `'unstyled'` | | `list.showListChoice` | `boolean` | Add to choice field dropdowns | | `list.showBannerButton` | `boolean` | Add button in list banner | | `list.showSaveWithFormButton` | `boolean` | Save form before executing | #### Client Properties | Property | Type | Description | |----------|------|-------------| | `client.isClient` | `boolean` | Script runs on client (true) or server (false) | | `client.isUi11compatible` | `boolean` | Compatible with UI11 | | `client.isUi16Compatible` | `boolean` | Compatible with UI16 | | `client.onClick` | `string` | JavaScript to run when clicked. Must be set if isClient is true to run on core UI | #### Workspace Properties | Property | Type | Description | |----------|------|-------------| | `workspace.isConfigurableWorkspace` | `boolean` | Enable for Configurable Workspace | | `workspace.showFormButtonV2` | `boolean` | V2 button rendering | | `workspace.showFormMenuButtonV2` | `boolean` | V2 menu rendering | | `workspace.clientScriptV2` | `string` | V2 client script model code | ### UI Action Examples #### Form button that refreshes the page ```typescript fluent import '@servicenow/sdk/global'; import { UiAction } from '@servicenow/sdk/core'; export const refreshAction = UiAction({ $id: Now.ID['refresh-page'], table: 'test_table', name: 'Refresh Page', actionName: 'refresh_page', active: true, hint: 'Refresh the current page', showUpdate: true, showInsert: true, form: { showButton: true, style: 'primary', }, script: `window.location.reload();`, }); ``` #### List button with info message ```typescript fluent import '@servicenow/sdk/global'; import { UiAction } from '@servicenow/sdk/core'; export const listAction = UiAction({ $id: Now.ID['list-info'], table: 'test_table', name: 'Show Info', actionName: 'show_info', active: true, showUpdate: true, list: { showBannerButton: true, showButton: true, style: 'primary', }, script: `gs.addInfoMessage('button pressed');`, }); ``` #### Conditional action with roles and multi-row update ```typescript fluent import '@servicenow/sdk/global'; import { UiAction } from '@servicenow/sdk/core'; export const adminAction = UiAction({ $id: Now.ID['admin-action'], table: 'test_table', name: 'Admin Action', actionName: 'admin_action', active: true, showUpdate: true, showInsert: true, showMultipleUpdate: true, list: { showButton: true, style: 'primary' }, form: { showButton: true, style: 'primary' }, condition: `current.canWrite();`, roles: ['admin'], }); ``` #### Client-side workspace-compatible action ```typescript fluent import '@servicenow/sdk/global'; import { UiAction } from '@servicenow/sdk/core'; export const workspaceAction = UiAction({ $id: Now.ID['workspace-action'], table: 'test_table', name: 'Workspace Action', actionName: 'workspace_action', active: true, showUpdate: true, showInsert: true, form: { showButton: true, style: 'primary' }, client: { isClient: true, isUi16Compatible: true }, roles: ['itil'], workspace: { showFormButtonV2: false, showFormMenuButtonV2: false, isConfigurableWorkspace: false, }, script: Now.include('../../client/test.js'), }); ``` --- ## UI Policies Use the `UiPolicy` API from `@servicenow/sdk/core`. Every UI Policy must have `$id`, `table`, and `shortDescription`. ### Key Guidance 1. **Use encoded query syntax for `conditions`** -- e.g., `"priority=1^state!=6"`. 2. **Action properties use `boolean | 'ignore'`** -- use `true`/`false` to set, `'ignore'` to leave unchanged. 3. **Use `readOnly` (not `disabled`)** -- it maps directly to ServiceNow's `disabled` field. 4. **Prefer declarative `actions` over scripts** -- better performance and less error-prone. 5. **`reverseIfFalse` defaults to `true`** -- when conditions are false, actions are automatically inverted. 6. **Choice fields use stored values** -- use the `Value` column, not the `Label` column. Right-click field > Show Choice List to find stored values. ### UI Policy Properties | Property | Type | Required | Default | Description | |----------|------|----------|---------|-------------| | `$id` | `Now.ID[string]` | Yes | | Unique identifier | | `table` | `string` | Yes | | Table the policy applies to | | `shortDescription` | `string` | Yes | | Brief description | | `active` | `boolean` | No | `true` | Whether policy is active | | `global` | `boolean` | No | `true` | Apply to all views | | `onLoad` | `boolean` | No | `false` | Run when form loads | | `reverseIfFalse` | `boolean` | No | `true` | Invert actions when condition is false | | `inherit` | `boolean` | No | `false` | Apply to extending tables | | `order` | `number` | No | `100` | Execution order (lower first) | | `conditions` | `string` | No | | Encoded query for when policy applies | | `runScripts` | `boolean` | No | `false` | Enable script execution | | `scriptTrue` | `string` | No | | JS when condition is true (wrap in `function onCondition() {}`) | | `scriptFalse` | `string` | No | | JS when condition is false | | `uiType` | `string` | No | `'desktop'` | `'desktop'`, `'mobile-service-portal'`, or `'all'` | | `isolateScript` | `boolean` | No | `false` | Run scripts in isolated scope | | `actions` | `array` | No | | Field actions (see below) | | `relatedListActions` | `array` | No | | Related list visibility controls (see below) | #### Field Action Properties | Property | Type | Required | Default | Description | |----------|------|----------|---------|-------------| | `field` | `string` | Yes | | Target field name | | `visible` | `boolean \| 'ignore'` | No | `'ignore'` | Show/hide field | | `readOnly` | `boolean \| 'ignore'` | No | `'ignore'` | Read-only/editable (maps to ServiceNow `disabled`) | | `mandatory` | `boolean \| 'ignore'` | No | `'ignore'` | Required/optional | | `cleared` | `boolean` | No | `false` | Clear field value when condition met | | `value` | `string` | No | | Set field to specific value | | `fieldMessage` | `string` | No | | Message to display near field | | `fieldMessageType` | `string` | No | `'none'` | `'info'`, `'warning'`, `'error'`, or `'none'` | #### Related List Action Properties | Property | Type | Required | Description | |----------|------|----------|-------------| | `$id` | `Now.ID[string]` | Yes | Unique identifier | | `list` | `string` | Yes | Related list ID: plain GUID or `table.field` format | | `visible` | `boolean \| 'ignore'` | No | Show/hide the related list | The plugin automatically adds/removes the `REL:` prefix for GUIDs when transforming to/from ServiceNow. ### Condition Syntax **AND operator (`^`)** -- all conditions must be true: ``` conditions: "priority=1^state=2" ``` **OR operator (`^OR`)** -- at least one must be true: ``` conditions: "priority=1^ORpriority=2" ``` **Common operators:** `=`, `!=`, `>`, `>=`, `<`, `<=`, `IN`, `LIKE`, `NOT LIKE`, `STARTSWITH`, `ANYTHING`, `EMPTYSTRING`, `SAMEAS`, `NSAMEAS`, `BETWEEN` **Choice field patterns:** | Pattern | Condition Example | |---------|-------------------| | Specific value | `category=hardware` | | Multiple values (OR) | `categoryINhardware,software` | | Not a value | `category!=hardware` | | Any value selected | `category!=NULL` | | No value selected | `category=NULL` | ### UI Policy Examples #### Progressive disclosure ```typescript fluent import { UiPolicy } from '@servicenow/sdk/core'; export const categoryPolicy = UiPolicy({ $id: Now.ID['incident_category_policy'], table: 'incident', shortDescription: 'Show subcategory when category is selected', onLoad: true, conditions: 'category!=NULL', actions: [ { field: 'subcategory', visible: true, mandatory: true }, ], }); ``` #### State-based read-only ```typescript fluent import { UiPolicy } from '@servicenow/sdk/core'; export const closedPolicy = UiPolicy({ $id: Now.ID['closed_incident_policy'], table: 'incident', shortDescription: 'Make fields read-only for closed incidents', onLoad: true, conditions: 'state=7', actions: [ { field: 'short_description', readOnly: true }, { field: 'description', readOnly: true }, { field: 'priority', readOnly: true }, { field: 'assignment_group', readOnly: true }, ], }); ``` #### Field clearing on condition change ```typescript fluent import { UiPolicy } from '@servicenow/sdk/core'; export const reopenPolicy = UiPolicy({ $id: Now.ID['incident_reopen_policy'], table: 'incident', shortDescription: 'Clear resolution fields when incident is reopened', onLoad: true, conditions: 'state!=6^state!=7', actions: [ { field: 'resolution_code', cleared: true }, { field: 'close_notes', cleared: true }, { field: 'resolved_at', cleared: true }, { field: 'resolved_by', cleared: true }, ], }); ``` #### Default values with field messages ```typescript fluent import { UiPolicy } from '@servicenow/sdk/core'; export const securityPolicy = UiPolicy({ $id: Now.ID['security_incident_policy'], table: 'incident', shortDescription: 'Set defaults for security incidents', onLoad: true, conditions: 'categoryLIKEsecurity', actions: [ { field: 'priority', value: '2', readOnly: true, fieldMessage: 'Priority automatically set to High for security incidents', fieldMessageType: 'info', }, { field: 'assignment_group', mandatory: true, fieldMessage: 'Security incidents must be assigned immediately', fieldMessageType: 'warning', }, ], }); ``` #### Combined field and related list control ```typescript fluent import { UiPolicy } from '@servicenow/sdk/core'; export const highPriorityPolicy = UiPolicy({ $id: Now.ID['high_priority_policy'], table: 'incident', shortDescription: 'Show additional details for high priority incidents', onLoad: true, conditions: 'priority=1^ORpriority=2', actions: [ { field: 'work_notes', visible: true, mandatory: true }, { field: 'impact', mandatory: true }, ], relatedListActions: [ { $id: Now.ID['show_tasks'], list: 'incident_task.parent', visible: true }, { $id: Now.ID['show_cis'], list: 'task_ci.task', visible: true }, ], }); ``` ### UI Policy Scripts Set `runScripts: true` to enable scripts. Scripts execute client-side and have access to `g_form`, `g_user`, `g_scratchpad`, and other client APIs. All scripts must be wrapped in `function onCondition() { ... }` format. **Best practices:** - Prefer field actions over scripts for simple behaviors. - Always provide both `scriptTrue` and `scriptFalse` to properly reverse behaviors. - Clear messages in `scriptFalse` to avoid stale messages. - Set `isolateScript: true` to avoid variable conflicts. **Common g_form methods:** ```javascript g_form.setVisible('field_name', true); g_form.setMandatory('field_name', true); g_form.setReadOnly('field_name', true); g_form.setValue('field_name', 'value'); g_form.getValue('field_name'); g_form.clearValue('field_name'); g_form.addInfoMessage('message'); g_form.addErrorMessage('message'); g_form.showFieldMsg('field_name', 'text', 'info'); g_form.hideFieldMsg('field_name'); g_form.clearMessages(); ``` **Example with scripts:** ```typescript fluent import { UiPolicy } from '@servicenow/sdk/core'; export const validationPolicy = UiPolicy({ $id: Now.ID['incident_validation_policy'], table: 'incident', shortDescription: 'Validate fields based on priority', onLoad: true, conditions: 'priority=1', runScripts: true, uiType: 'all', isolateScript: true, scriptTrue: `function onCondition() { g_form.setMandatory('justification', true); g_form.setMandatory('business_service', true); g_form.showFieldMsg('priority', 'High priority requires justification', 'info'); if (g_form.getValue('urgency') == '') { g_form.setValue('urgency', '1'); } }`, scriptFalse: `function onCondition() { g_form.setMandatory('justification', false); g_form.setMandatory('business_service', false); g_form.hideFieldMsg('priority'); }`, }); ``` ### Related List Visibility Related list actions control visibility of related lists on forms. The `list` property identifies which related list to control: - **GUID format** (system relationships): `"b9edf0ca0a0a0b010035de2d6b579a03"` -- plugin auto-adds `REL:` prefix. - **Table.Field format** (reference fields): `"incident.caller_id"` - **Table.Table format** (parent-child): `"change_request.change_task"` **Finding related list GUIDs:** ```javascript var gr = new GlideRecord('sys_ui_related_list'); gr.addQuery('name', 'CONTAINS', 'your_table_name'); gr.query(); while (gr.next()) { gs.print(gr.name + ' -> ' + gr.sys_id); } ``` --- ## UI Formatters Formatters add non-field content to forms (activity streams, process flows, checklists). Custom formatters are **not** supported in Fluent -- always use built-in formatters. ### Built-In Formatters | Formatter | Macro | When to Use | Position | |-----------|-------|-------------|----------| | Activity | `activity.xml` | Journal entries, comments, work notes | Last in section | | Process Flow | `process_flow` | Lifecycle stage visualization | First in section | | CI Relations | `ui_ng_relation_formatter.xml` | CMDB relationship maps | First in section | | Parent Breadcrumb | `parent_crumbs` | Parent hierarchy trail | First in section | | Contextual Search | `cxs_table_search.xml` | Auto-suggest knowledge articles | Below search context field | | Variables Editor | `com_glideapp_questionset_default_question_editor` | Record producer variables | -- | | Checklist | `inline_checklist_macro` | Sub-task tracking | Last in section | | Attached Knowledge | `attached_knowledge` | Linked knowledge articles | Last in section | ### Key Rules 1. **Activity and Attached Knowledge formatters already exist globally** -- never create `sys_ui_formatter` records for them. Skip straight to adding the `sys_ui_element`. 2. **A formatter requires a section** -- it must reside in a `sys_ui_section`, and the section must have at least one non-formatter element. 3. **Parent Breadcrumb requires a field named exactly `parent`** -- no variations like `parent_task` or `parent_record`. 4. **Process Flow requires stage configuration** -- verify `sys_process_flow` records exist for the target table. 5. **Position matters** -- Process Flow and Parent Breadcrumb go first; Activity and Checklist go last. ### Sequential Steps to Add a Formatter 1. **Check formatter exists** (`sys_ui_formatter`): For Activity and Attached Knowledge, skip to step 4. For others, query `sys_ui_formatter` for the target table, then global, then the extended-from table. 2. **If Process Flow**: Verify/create stage records in `sys_process_flow`. 3. **If Contextual Search**: Verify/create search config in `cxs_table_config`. 4. **Check section exists** (`sys_ui_section`): Query for the target table and view. Create if missing. 5. **Add formatter element** (`sys_ui_element`): Create with `type: 'formatter'` and reference to the section and formatter. 6. **Ensure at least one non-formatter element** exists in the section. ### Formatter Examples #### Add Activity Formatter (no formatter record needed) ```typescript fluent import { Record } from '@servicenow/sdk/core'; Record({ $id: Now.ID['activity_formatter_element'], table: 'sys_ui_element', data: { sys_ui_section: section.$id, element: 'activity.xml', type: 'formatter', position: 99, }, }); ``` #### Create Process Flow Formatter ```typescript fluent import { Record } from '@servicenow/sdk/core'; export const processFlowFormatter = Record({ $id: Now.ID['process_flow_formatter'], table: 'sys_ui_formatter', data: { name: 'Process Flow Formatter', type: 'formatter', formatter: 'process_flow.xml', table: 'table_name', active: true, }, }); ``` #### Add stages for Process Flow ```typescript fluent import { Record } from '@servicenow/sdk/core'; Record({ $id: Now.ID['flow-stage-new'], table: 'sys_process_flow', data: { active: true, condition: 'state=new^EQ', label: 'New', name: 'Task Flow - New State', order: '100', table: 'table_name', }, }); Record({ $id: Now.ID['flow-stage-progress'], table: 'sys_process_flow', data: { active: true, condition: 'state=in_progress^EQ', label: 'In Progress', name: 'Task Flow - In Progress', order: '200', table: 'table_name', }, }); ```