mcpField
U
ui-actions

ui-actions

github.com/happy-technologies-llc开发4142 次安装2 天前更新
代码仓库

关于 ui-actions

ui-actions 是面向 ServiceNow 开发者和管理员的技能,指导创建和配置表单及列表 UI Action。内容涵盖脚本执行、可见性条件、角色安全、测试与调试。

主要功能

  1. 01配置表单按钮和菜单项
  2. 02创建列表操作和批量操作
  3. 03设置客户端与服务器脚本
  4. 04配置可见性条件和角色限制
  5. 05测试与调试 UI Action

适用场景

  1. 01为表单添加自定义操作按钮
  2. 02为列表创建上下文菜单或批量操作
  3. 03按状态或角色控制操作可见性

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/happy-technologies-llc/happy-platform-skills --skill ui-actions

安装后在 Claude Code 中输入/ui-actions确认已加载。

详细介绍

UI 操作

概述

本技能介绍如何在 ServiceNow 中创建和配置 UI 操作:

  • 表单按钮(页眉/页脚)和表单上下文菜单项
  • 列表按钮、列表上下文菜单和列表选择操作
  • 客户端脚本、服务器端脚本和组合执行
  • 可见性条件和基于角色的安全性
  • 常见模式:批准、拒绝、克隆、升级等
  • 测试和调试 UI 操作

适用场景: 需要向表单和列表添加可执行客户端或服务器端逻辑的自定义按钮或菜单项时。

适用对象: 需要通过自定义功能扩展 ServiceNow 表单和列表的开发者和管理员。

前提条件

  • 角色: admin、personalize(用于基本 UI 操作)或特定应用角色
  • 访问权限: sys_ui_action 表,以及用于测试的相关表
  • 知识: JavaScript、GlideRecord API、客户端 g_form API
  • 相关技能: 用于服务器端模式的 admin/script-execution

了解 UI 操作

UI 操作类型
┌─────────────────────────────────────────────────────────────────────────────┐
│                           UI Action Locations                                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────── FORM ────────────┐    ┌─────────── LIST ────────────┐        │
│  │                             │    │                             │        │
│  │  [Form Button] [Button]     │    │  [List Button] [Button]     │        │
│  │  ─────────────────────────  │    │  ─────────────────────────  │        │
│  │  ┌─────────────────────┐    │    │  ☐ Number    Description    │        │
│  │  │                     │    │    │  ☐ INC001   Server down     │        │
│  │  │   Form Content      │    │    │  ☐ INC002   Email issue     │        │
│  │  │                     │    │    │                             │        │
│  │  │   Right-click →     │    │    │   Right-click →             │        │
│  │  │   [Context Menu]    │    │    │   [Context Menu]            │        │
│  │  │                     │    │    │                             │        │
│  │  └─────────────────────┘    │    │  Actions Dropdown:          │        │
│  │                             │    │   ☐ List Choice Option      │        │
│  │  [Footer Button] [Button]   │    │                             │        │
│  └─────────────────────────────┘    └─────────────────────────────┘        │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
UI 操作执行流程
User Clicks UI Action
        │
        ▼
┌───────────────────┐
│ Check Conditions  │
│ (if defined)      │
└────────┬──────────┘
         │
    Condition
      True?
    ┌───┴───┐
   Yes     No
    │       │
    ▼       ▼
┌───────┐ [Action
│Client │  Hidden]
│Script?│
└───┬───┘
    │
   Yes ──► Run Client Script (onclick)
    │           │
    │       gsftSubmit()?
    │       ┌───┴───┐
    │      Yes     No
    │       │       │
    │       ▼       ▼
    │   ┌───────┐ [Client
    │   │Submit │  Only]
    │   │ Form  │
    │   └───┬───┘
    │       │
    ▼       ▼
┌───────────────────┐
│ Run Server Script │◄── No Client Script (direct server)
└───────────────────┘
         │
         ▼
    ┌─────────┐
    │Redirect?│
    └────┬────┘
     ┌───┴───┐
    Yes     No
     │       │
     ▼       ▼
[Custom   [Form
 URL]     Reload]

操作步骤

阶段 1:了解 UI 操作字段
步骤 1.1:获取表架构

查询 sys_ui_action 架构:

Tool: SN-Get-Table-Schema
Parameters:
  table_name: sys_ui_action
步骤 1.2:关键字段参考
字段类型说明
name字符串按钮/菜单项标签
table字符串目标表(例如 incident)
action_name字符串内部名称(不含空格,用于 URL)
order整数显示顺序(数值越小,越靠前)
active布尔值启用/禁用此操作
client布尔值运行客户端脚本(onclick)
script脚本服务器端 GlideRecord 脚本
onclick脚本客户端 JavaScript(g_form)
condition脚本服务器端可见性条件
comments字符串说明/文档
步骤 1.3:位置字段
字段类型说明
form_button布尔值显示为表单页眉按钮
form_button_v2布尔值显示为表单页脚按钮(v2 表单)
form_link布尔值显示在表单上下文菜单中
list_banner_button布尔值显示为列表页眉按钮
list_button布尔值显示在列表操作中(旧版)
list_choice布尔值显示在列表操作下拉菜单中
list_context_menu布尔值显示在列表右键菜单中
list_link布尔值显示在列表相关链接中
阶段 2:创建表单按钮 UI 操作
步骤 2.1:基本表单按钮

创建一个在服务器端运行的简单表单按钮:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Escalate to Management"
    table: incident
    action_name: escalate_to_management
    order: 100
    active: true
    form_button: true
    client: false
    script: |
      // Server-side script
      current.escalation = 1;
      current.priority = 1;
      current.work_notes = "Escalated to management by " + gs.getUserDisplayName();
      current.update();
      action.setRedirectURL(current);
    comments: "Escalates incident to management with P1 priority"

结果: 返回所创建 UI 操作的 sys_id。

步骤 2.2:带客户端脚本的表单按钮

创建带客户端确认的按钮:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Close Incident"
    table: incident
    action_name: close_incident
    order: 200
    active: true
    form_button: true
    client: true
    onclick: |
      // Client-side script
      var closeCode = g_form.getValue('close_code');
      var closeNotes = g_form.getValue('close_notes');

      // Validate required fields
      if (!closeCode) {
        g_form.showFieldMsg('close_code', 'Close code is required', 'error');
        return false;  // Prevent submission
      }

      if (!closeNotes) {
        g_form.showFieldMsg('close_notes', 'Close notes are required', 'error');
        return false;
      }

      // Confirm with user
      if (!confirm('Are you sure you want to close this incident?')) {
        return false;
      }

      // Submit the form
      gsftSubmit(null, g_form.getFormElement(), 'close_incident');
    script: |
      // Server-side script (runs after form submit)
      current.state = 7;  // Closed
      current.closed_at = new GlideDateTime();
      current.closed_by = gs.getUserID();
      current.update();
      action.setRedirectURL(current);
    comments: "Closes incident with validation and confirmation"
步骤 2.3:表单页眉按钮与页脚按钮

表单页眉按钮(form_button):

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Quick Assign"
    table: incident
    action_name: quick_assign
    order: 50
    active: true
    form_button: true
    form_button_v2: false
    # ... rest of configuration

表单页脚按钮(form_button_v2):

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Save and Return"
    table: incident
    action_name: save_and_return
    order: 300
    active: true
    form_button: false
    form_button_v2: true
    # ... rest of configuration
阶段 3:创建列表 UI 操作
步骤 3.1:列表横幅按钮

显示在列表顶部的按钮:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Export Selected"
    table: incident
    action_name: export_selected
    order: 100
    active: true
    list_banner_button: true
    client: true
    onclick: |
      // Get selected records
      var list = GlideList2.get(g_form.getTableName());
      var checked = list.getChecked();

      if (!checked) {
        alert('Please select at least one record');
        return false;
      }

      // Redirect to export with selected sys_ids
      var url = 'export_selected.do?sysparm_sys_ids=' + checked;
      window.location = url;
    comments: "Exports selected incidents to CSV"
步骤 3.2:列表上下文菜单

列表行的右键菜单项:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Assign to Me"
    table: incident
    action_name: assign_to_me_list
    order: 100
    active: true
    list_context_menu: true
    client: false
    script: |
      // Server-side script
      // current is the right-clicked record
      current.assigned_to = gs.getUserID();
      current.assignment_group = gs.getUser().getMyGroups()[0];
      current.work_notes = "Self-assigned via list context menu";
      current.update();
    comments: "Assigns the right-clicked incident to current user"
步骤 3.3:列表选择操作(操作下拉菜单)

操作下拉菜单中的批量操作:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Bulk Close"
    table: incident
    action_name: bulk_close
    order: 200
    active: true
    list_choice: true
    client: true
    onclick: |
      // Get checked records
      var list = GlideList2.get(g_form.getTableName());
      var checked = list.getChecked();

      if (!checked) {
        alert('Please select at least one incident to close');
        return false;
      }

      var count = checked.split(',').length;
      if (!confirm('Close ' + count + ' selected incident(s)?')) {
        return false;
      }

      // Submit to server
      gsftSubmit(null, g_form.getFormElement(), 'bulk_close');
    script: |
      // Server-side - process all checked records
      var sysIds = RP.getParameterValue('sysparm_checked_items');
      if (sysIds) {
        var ids = sysIds.split(',');
        for (var i = 0; i < ids.length; i++) {
          var gr = new GlideRecord('incident');
          if (gr.get(ids[i])) {
            gr.state = 7;  // Closed
            gr.close_code = 'Solved (Permanently)';
            gr.close_notes = 'Bulk closed by ' + gs.getUserDisplayName();
            gr.update();
          }
        }
      }
    comments: "Closes all selected incidents in bulk"
阶段 4:上下文菜单(表单右键菜单)
步骤 4.1:表单上下文菜单项

向表单右键菜单添加菜单项:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "View Audit History"
    table: incident
    action_name: view_audit_history
    order: 500
    active: true
    form_link: true
    client: true
    onclick: |
      // Open audit history in new window
      var sysId = g_form.getUniqueValue();
      var tableName = g_form.getTableName();
      var url = 'sys_audit_list.do?sysparm_query=tablename=' + tableName +
                '^documentkey=' + sysId + '&sysparm_view=';
      window.open(url, '_blank');
    comments: "Opens audit history for current record in new tab"
步骤 4.2:带服务器端脚本的表单链接

在服务器端运行的上下文菜单:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Create Child Incident"
    table: incident
    action_name: create_child_incident
    order: 600
    active: true
    form_link: true
    client: false
    script: |
      // Create child incident
      var child = new GlideRecord('incident');
      child.initialize();
      child.parent_incident = current.sys_id;
      child.short_description = 'Child of: ' + current.short_description;
      child.description = 'Child incident created from ' + current.number;
      child.caller_id = current.caller_id;
      child.category = current.category;
      child.subcategory = current.subcategory;
      child.priority = current.priority;
      var childId = child.insert();

      // Redirect to new child incident
      action.setRedirectURL(child);
    comments: "Creates a child incident linked to current record"
阶段 5:可见性条件
步骤 5.1:简单条件

仅在特定状态下显示按钮:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Reopen Incident"
    table: incident
    action_name: reopen_incident
    order: 100
    active: true
    form_button: true
    condition: "current.state == 7"
    script: |
      // Reopen logic
      current.state = 2;  // In Progress
      current.work_notes = "Reopened by " + gs.getUserDisplayName();
      current.update();
      action.setRedirectURL(current);
    comments: "Visible only when incident is Closed (state=7)"

常见条件模式:

条件含义
current.state == 1状态为 New
current.active == true记录处于活动状态
current.assigned_to == gs.getUserID()分配给当前用户
current.assignment_group.manager == gs.getUserID()当前用户是组经理
current.isNewRecord()新记录(尚未保存)
!current.isNewRecord()仅限现有记录
current.priority <= 2优先级为 1 或 2
current.caller_id.vip == true呼叫者是 VIP
步骤 5.2:基于角色的条件

仅对特定角色显示按钮:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Override SLA"
    table: incident
    action_name: override_sla
    order: 100
    active: true
    form_button: true
    condition: "gs.hasRole('incident_manager') || gs.hasRole('admin')"
    script: |
      // SLA override logic
      current.sla_due = '';
      current.work_notes = "SLA overridden by manager";
      current.update();
    comments: "Visible only to incident managers and admins"
步骤 5.3:复杂条件

组合多个条件:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Emergency Escalation"
    table: incident
    action_name: emergency_escalation
    order: 50
    active: true
    form_button: true
    condition: |
      // Only show for P1/P2 active incidents assigned to current user's group
      current.state != 6 && current.state != 7 &&
      current.priority <= 2 &&
      current.assignment_group.getDisplayValue() != '' &&
      gs.getUser().isMemberOf(current.assignment_group)
    script: |
      // Emergency escalation logic
      current.escalation = 3;
      current.work_notes = "[EMERGENCY ESCALATION] " + gs.getUserDisplayName();
      current.update();
    comments: "Emergency escalation for P1/P2 incidents in user's group"
步骤 5.4:UI Action 可见性最佳实践
Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Approve Request"
    table: sc_req_item
    action_name: approve_request
    order: 100
    active: true
    form_button: true
    condition: |
      // Best practice: Check multiple conditions
      var canApprove = false;

      // 1. Check record state
      if (current.approval == 'requested') {
        // 2. Check if user is an approver
        var approver = new GlideRecord('sysapproval_approver');
        approver.addQuery('sysapproval', current.sys_id);
        approver.addQuery('approver', gs.getUserID());
        approver.addQuery('state', 'requested');
        approver.query();

        if (approver.next()) {
          canApprove = true;
        }

        // 3. Or check if user has override role
        if (gs.hasRole('approval_admin')) {
          canApprove = true;
        }
      }

      canApprove;
    script: |
      // Approval logic
    comments: "Visible only when user can approve this request"
阶段 6:客户端脚本与服务器端脚本
步骤 6.1:仅客户端(不提交)

仅在客户端运行的操作:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Copy to Clipboard"
    table: incident
    action_name: copy_to_clipboard
    order: 100
    active: true
    form_button: true
    client: true
    onclick: |
      // Pure client-side - no server round trip
      var number = g_form.getValue('number');
      var shortDesc = g_form.getValue('short_description');
      var text = number + ': ' + shortDesc;

      // Copy to clipboard (modern browsers)
      navigator.clipboard.writeText(text).then(function() {
        g_form.addInfoMessage('Copied to clipboard: ' + text);
      }).catch(function(err) {
        alert('Failed to copy: ' + err);
      });

      // Return false to prevent form submission
      return false;
    script: ""
    comments: "Copies incident number and description to clipboard"
步骤 6.2:仅服务器端

仅在服务器端运行的操作:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Recalculate Priority"
    table: incident
    action_name: recalculate_priority
    order: 100
    active: true
    form_button: true
    client: false
    script: |
      // Server-side only
      var calculator = new PriorityCalculator();
      var newPriority = calculator.calculate(current.impact, current.urgency);

      current.priority = newPriority;
      current.work_notes = "Priority recalculated to " + newPriority;
      current.update();

      action.setRedirectURL(current);
    comments: "Recalculates priority based on impact and urgency matrix"
步骤 6.3:组合客户端和服务器端

客户端验证/确认,然后由服务器执行:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Transfer to Team"
    table: incident
    action_name: transfer_to_team
    order: 100
    active: true
    form_button: true
    client: true
    onclick: |
      // Client-side validation and data collection
      var group = g_form.getValue('assignment_group');
      if (!group) {
        g_form.showFieldMsg('assignment_group', 'Please select a group first', 'error');
        return false;
      }

      var reason = prompt('Enter transfer reason (required):');
      if (!reason || reason.trim() === '') {
        alert('Transfer reason is required');
        return false;
      }

      // Store reason in a hidden field or use sysparm
      g_form.setValue('work_notes', 'Transfer Reason: ' + reason);

      // Submit to server
      gsftSubmit(null, g_form.getFormElement(), 'transfer_to_team');
    script: |
      // Server-side execution
      var targetGroup = current.assignment_group.getDisplayValue();

      // Clear individual assignee
      current.assigned_to = '';

      // Add transfer audit trail
      current.work_notes = 'Transferred to ' + targetGroup + '\n' +
                           'Transferred by: ' + gs.getUserDisplayName();
      current.update();

      gs.addInfoMessage('Incident transferred to ' + targetGroup);
      action.setRedirectURL(current);
    comments: "Transfers incident with reason capture"
阶段 7:在 UI Actions 中使用 g_form
步骤 7.1:常见 g_form 方法

读取字段值:

// In onclick script
var value = g_form.getValue('field_name');
var display = g_form.getDisplayBox('field_name').value;
var reference = g_form.getReference('caller_id', function(ref) {
  // Async callback for reference fields
  alert('Caller name: ' + ref.name);
});

设置字段值:

// In onclick script
g_form.setValue('priority', '1');
g_form.setValue('work_notes', 'Updated via UI Action');
g_form.setDisplay('field_name', true);  // Show field
g_form.setMandatory('field_name', true);  // Make mandatory
g_form.setReadOnly('field_name', true);  // Make read-only

用户反馈:

// Messages
g_form.addInfoMessage('Operation completed successfully');
g_form.addWarningMessage('Please review the changes');
g_form.addErrorMessage('An error occurred');
g_form.showFieldMsg('field_name', 'Message text', 'info|warning|error');

// Clear messages
g_form.clearMessages();
g_form.hideFieldMsg('field_name');
步骤 7.2:完整客户端脚本示例
Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Validate and Submit"
    table: incident
    action_name: validate_submit
    order: 100
    active: true
    form_button: true
    client: true
    onclick: |
      function validateAndSubmit() {
        // Clear previous messages
        g_form.clearMessages();

        var valid = true;
        var errors = [];

        // Validate short description
        var shortDesc = g_form.getValue('short_description');
        if (!shortDesc || shortDesc.length < 10) {
          g_form.showFieldMsg('short_description', 'Must be at least 10 characters', 'error');
          valid = false;
          errors.push('Short description');
        }

        // Validate caller
        var caller = g_form.getValue('caller_id');
        if (!caller) {
          g_form.showFieldMsg('caller_id', 'Caller is required', 'error');
          valid = false;
          errors.push('Caller');
        }

        // Validate category
        var category = g_form.getValue('category');
        if (!category) {
          g_form.showFieldMsg('category', 'Category is required', 'error');
          valid = false;
          errors.push('Category');
        }

        // Check P1 requirements
        var priority = g_form.getValue('priority');
        if (priority == '1') {
          var businessImpact = g_form.getValue('business_impact');
          if (!businessImpact) {
            g_form.showFieldMsg('business_impact', 'Required for P1 incidents', 'error');
            valid = false;
            errors.push('Business Impact');
          }
        }

        if (!valid) {
          g_form.addErrorMessage('Please fix validation errors: ' + errors.join(', '));
          return false;
        }

        // Confirmation
        if (confirm('Submit this incident?')) {
          gsftSubmit(null, g_form.getFormElement(), 'validate_submit');
          return true;
        }

        return false;
      }

      validateAndSubmit();
    script: |
      current.update();
      gs.addInfoMessage('Incident saved successfully');
      action.setRedirectURL(current);
    comments: "Validates required fields before submission"
阶段 8:操作后重定向
步骤 8.1:重定向方法

重定向到同一条记录:

// Server-side script
action.setRedirectURL(current);

重定向到另一条记录:

// Server-side script
var newRecord = new GlideRecord('incident');
newRecord.get('some_sys_id');
action.setRedirectURL(newRecord);

重定向到列表:

// Server-side script
action.setRedirectURL('incident_list.do?sysparm_query=active=true');

重定向到特定 URL:

// Server-side script
action.setRedirectURL('/nav_to.do?uri=incident.do?sys_id=' + current.sys_id);

阻止重定向(留在表单上):

// Server-side script
current.update();
action.setRedirectURL(current);  // Refresh current record

重定向到上一页:

// Client-side onclick
// After server processing, redirect back
history.back();
return false;
步骤 8.2:常见重定向模式

克隆并重定向到新记录:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Clone Incident"
    table: incident
    action_name: clone_incident
    order: 100
    active: true
    form_button: true
    condition: "!current.isNewRecord()"
    client: false
    script: |
      // Clone the incident
      var clone = new GlideRecord('incident');
      clone.initialize();

      // Copy fields
      clone.short_description = 'CLONE: ' + current.short_description;
      clone.description = current.description;
      clone.caller_id = current.caller_id;
      clone.category = current.category;
      clone.subcategory = current.subcategory;
      clone.priority = current.priority;
      clone.impact = current.impact;
      clone.urgency = current.urgency;

      // Reference original
      clone.work_notes = 'Cloned from ' + current.number;

      var cloneId = clone.insert();

      // Redirect to new cloned record
      action.setRedirectURL(clone);
    comments: "Creates a copy of the current incident"

保存并返回列表:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Save and Return to List"
    table: incident
    action_name: save_return_list
    order: 100
    active: true
    form_button: true
    client: false
    script: |
      current.update();
      action.setRedirectURL('incident_list.do');
    comments: "Saves current record and returns to incident list"
阶段 9:操作安全
步骤 9.1:基于角色的访问控制

限制特定角色使用(使用条件):

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Delete Incident"
    table: incident
    action_name: delete_incident
    order: 900
    active: true
    form_button: true
    condition: "gs.hasRole('incident_admin')"
    client: true
    onclick: |
      if (!confirm('Are you sure you want to DELETE this incident? This cannot be undone.')) {
        return false;
      }

      if (!confirm('FINAL WARNING: This will permanently delete ' + g_form.getValue('number'))) {
        return false;
      }

      gsftSubmit(null, g_form.getFormElement(), 'delete_incident');
    script: |
      var number = current.number;
      current.deleteRecord();
      gs.addInfoMessage('Incident ' + number + ' has been deleted');
      action.setRedirectURL('incident_list.do');
    comments: "Permanently deletes incident - restricted to incident_admin"
步骤 9.2:为 UI Action 创建 ACL

创建明确的 ACL 保护:

Tool: SN-Create-Record
Parameters:
  table_name: sys_security_acl
  data:
    name: "incident.delete_incident"
    type: ui_action
    operation: execute
    active: true
    admin_overrides: false
    advanced: false
    script: "gs.hasRole('incident_admin')"
    description: "ACL for delete incident UI action"
步骤 9.3:安全最佳实践
  1. 始终在服务器端进行验证: 客户端检查可能被绕过
  2. 同时使用条件和 ACL: 纵深防御
  3. 记录敏感操作: 创建审计跟踪
  4. 限制破坏性操作: 要求确认并限制角色

完整的安全操作示例:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Override Approval"
    table: sc_req_item
    action_name: override_approval
    order: 100
    active: true
    form_button: true
    condition: |
      // Check role and record state
      gs.hasRole('approval_admin') &&
      current.approval == 'requested' &&
      !current.isNewRecord()
    client: true
    onclick: |
      // Client-side confirmation
      var reason = prompt('Enter override reason (required for audit):');
      if (!reason || reason.trim().length < 10) {
        alert('A detailed reason (min 10 chars) is required for approval override');
        return false;
      }

      if (!confirm('Override approval for ' + g_form.getValue('number') + '?')) {
        return false;
      }

      // Store reason
      g_form.setValue('work_notes', 'APPROVAL OVERRIDE REASON: ' + reason);
      gsftSubmit(null, g_form.getFormElement(), 'override_approval');
    script: |
      // Server-side validation (defense in depth)
      if (!gs.hasRole('approval_admin')) {
        gs.addErrorMessage('Unauthorized: approval_admin role required');
        action.setRedirectURL(current);
        return;
      }

      // Process override
      current.approval = 'approved';
      current.work_notes = 'APPROVAL OVERRIDE\n' +
                           'Overridden by: ' + gs.getUserDisplayName() + '\n' +
                           'Date: ' + new GlideDateTime().getDisplayValue();
      current.update();

      // Audit log
      gs.log('Approval override on ' + current.number + ' by ' + gs.getUserName(), 'ApprovalOverride');

      gs.addInfoMessage('Approval overridden successfully');
      action.setRedirectURL(current);
    comments: "Override approval with audit trail - approval_admin only"
阶段 10:常见用例
步骤 10.1:批准/拒绝模式

批准按钮:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Approve"
    table: sc_req_item
    action_name: approve_item
    order: 100
    active: true
    form_button: true
    condition: |
      current.approval == 'requested' &&
      new ApprovalHelper().isUserApprover(gs.getUserID(), current.sys_id)
    client: true
    onclick: |
      if (!confirm('Approve this request?')) {
        return false;
      }
      gsftSubmit(null, g_form.getFormElement(), 'approve_item');
    script: |
      var approver = new GlideRecord('sysapproval_approver');
      approver.addQuery('sysapproval', current.sys_id);
      approver.addQuery('approver', gs.getUserID());
      approver.addQuery('state', 'requested');
      approver.query();

      if (approver.next()) {
        approver.state = 'approved';
        approver.update();

        current.work_notes = 'Approved by ' + gs.getUserDisplayName();
        current.update();

        gs.addInfoMessage('Request approved');
      }
      action.setRedirectURL(current);
    comments: "Approve request item"

拒绝按钮:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Reject"
    table: sc_req_item
    action_name: reject_item
    order: 110
    active: true
    form_button: true
    condition: |
      current.approval == 'requested' &&
      new ApprovalHelper().isUserApprover(gs.getUserID(), current.sys_id)
    client: true
    onclick: |
      var reason = prompt('Enter rejection reason (required):');
      if (!reason || reason.trim() === '') {
        alert('Rejection reason is required');
        return false;
      }

      g_form.setValue('work_notes', 'REJECTION REASON: ' + reason);

      if (!confirm('Reject this request?')) {
        return false;
      }
      gsftSubmit(null, g_form.getFormElement(), 'reject_item');
    script: |
      var approver = new GlideRecord('sysapproval_approver');
      approver.addQuery('sysapproval', current.sys_id);
      approver.addQuery('approver', gs.getUserID());
      approver.addQuery('state', 'requested');
      approver.query();

      if (approver.next()) {
        approver.state = 'rejected';
        approver.update();

        current.work_notes = 'Rejected by ' + gs.getUserDisplayName();
        current.update();

        gs.addInfoMessage('Request rejected');
      }
      action.setRedirectURL(current);
    comments: "Reject request item with reason"
步骤 10.2:升级模式
Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Escalate"
    table: incident
    action_name: escalate_incident
    order: 100
    active: true
    form_button: true
    condition: "current.escalation < 3 && current.state != 6 && current.state != 7"
    client: true
    onclick: |
      var currentLevel = parseInt(g_form.getValue('escalation')) || 0;
      var levels = ['Normal', 'Moderate', 'High', 'Overdue'];
      var nextLevel = levels[currentLevel + 1] || 'Maximum';

      if (!confirm('Escalate to ' + nextLevel + ' level?')) {
        return false;
      }
      gsftSubmit(null, g_form.getFormElement(), 'escalate_incident');
    script: |
      var oldLevel = current.escalation;
      current.escalation = parseInt(current.escalation) + 1;
      current.work_notes = 'Escalated from level ' + oldLevel + ' to ' + current.escalation +
                           ' by ' + gs.getUserDisplayName();
      current.update();

      gs.addInfoMessage('Incident escalated to level ' + current.escalation);
      action.setRedirectURL(current);
    comments: "Escalates incident to next level"
步骤 10.3:克隆模式
Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Clone Record"
    table: incident
    action_name: clone_record
    order: 200
    active: true
    form_link: true
    condition: "!current.isNewRecord()"
    client: false
    script: |
      // Fields to copy
      var copyFields = [
        'short_description', 'description', 'caller_id',
        'category', 'subcategory', 'priority', 'impact',
        'urgency', 'assignment_group', 'contact_type'
      ];

      // Fields to exclude (system fields, unique identifiers)
      var excludeFields = [
        'sys_id', 'number', 'opened_at', 'opened_by',
        'closed_at', 'closed_by', 'resolved_at', 'resolved_by',
        'sys_created_on', 'sys_created_by', 'sys_updated_on', 'sys_updated_by'
      ];

      var clone = new GlideRecord('incident');
      clone.initialize();

      // Copy specified fields
      for (var i = 0; i < copyFields.length; i++) {
        var field = copyFields[i];
        if (current.getValue(field)) {
          clone.setValue(field, current.getValue(field));
        }
      }

      // Modify for clone
      clone.short_description = '[CLONE] ' + clone.short_description;
      clone.work_notes = 'Cloned from ' + current.number + ' by ' + gs.getUserDisplayName();

      var sysId = clone.insert();

      gs.addInfoMessage('Cloned incident ' + current.number + ' successfully');
      action.setRedirectURL(clone);
    comments: "Creates a copy of the incident"
阶段 11:测试 UI Actions
步骤 11.1:查询现有 UI Actions

查找表中的所有 UI Actions:

Tool: SN-Query-Table
Parameters:
  table_name: sys_ui_action
  query: table=incident^active=true
  fields: sys_id,name,action_name,order,form_button,list_banner_button,condition
  limit: 50

按名称查找 UI Actions:

Tool: SN-Query-Table
Parameters:
  table_name: sys_ui_action
  query: nameLIKEapprove^active=true
  fields: sys_id,name,table,condition,client
  limit: 20
步骤 11.2:测试清单

测试前:

  • UI Action 已启用
  • 表正确
  • 已设置位置(form_button、list_banner_button 等)
  • 在测试场景下条件返回 true
  • 脚本没有语法错误

功能测试:

  1. 可见性测试:

    • 条件为 true 时按钮是否显示?
    • 条件为 false 时按钮是否隐藏?
    • 按钮是否仅对正确的角色可见?
  2. 客户端脚本测试:

    • onclick 验证是否正常工作?
    • 是否显示错误消息?
    • 是否显示确认对话框?
    • 表单是否正确提交?
  3. 服务器端脚本测试:

    • 记录是否正确更新?
    • 重定向是否正常工作?
    • 是否添加了工作备注?
    • 是否创建了审计跟踪?
  4. 安全测试:

    • 未授权用户能否访问该操作?
    • 用户能否绕过客户端验证?
    • 是否设置了服务器端角色检查?
步骤 11.3:调试服务器端脚本

添加调试日志:

Tool: SN-Update-Record
Parameters:
  table_name: sys_ui_action
  sys_id: [action_sys_id]
  data:
    script: |
      // Debug logging
      gs.info('[DEBUG] UI Action: escalate_incident started');
      gs.info('[DEBUG] Current user: ' + gs.getUserName());
      gs.info('[DEBUG] Current record: ' + current.number);

      try {
        // Your logic here
        current.escalation = parseInt(current.escalation) + 1;
        current.update();

        gs.info('[DEBUG] Escalation updated to: ' + current.escalation);

      } catch (e) {
        gs.error('[DEBUG] Error: ' + e.message);
      }

      action.setRedirectURL(current);

检查日志:

Tool: SN-Query-Table
Parameters:
  table_name: syslog
  query: messageLIKE[DEBUG] UI Action^sys_created_on>javascript:gs.minutesAgo(10)
  fields: message,level,sys_created_on
  limit: 50
阶段 12:显式本地脚本开发
步骤 12.1:在本地拉取 UI Action 字段

同时获取脚本字段和新鲜度元数据:

Tool: SN-Get-Record
Parameters:
  table_name: sys_ui_action
  sys_id: [ui_action_sys_id]
  fields: sys_id,script,onclick,sys_updated_on,sys_mod_count
  instance: dev

将 script 和 onclick 分别写入本地文件。推送前,再次获取相同字段;如果 sys_updated_on、sys_mod_count 或任一远程字段发生变化,则停止操作。在本地合并任何冲突后,只更新已审核的字段:

Tool: SN-Update-Record
Parameters:
  table_name: sys_ui_action
  sys_id: [ui_action_sys_id]
  data:
    script: [reviewed_server_script]
    onclick: [reviewed_client_script]
  instance: dev
步骤 12.2:目录结构
servicenow/
└── ui_actions/
    ├── incident/
    │   ├── escalate_incident_server.js
    │   ├── escalate_incident_client.js
    │   ├── close_incident_server.js
    │   └── close_incident_client.js
    ├── sc_req_item/
    │   ├── approve_item_server.js
    │   └── reject_item_server.js
    └── README.md

工具使用摘要

操作MCP 工具说明
创建 UI ActionSN-Create-Recordsys_ui_action 表
更新 UI ActionSN-Update-Record修改 script、condition
查询 UI ActionSN-Query-Table按表、名称查找
获取架构SN-Get-Table-Schema了解可用字段
本地脚本周期SN-Get-Record + SN-Update-Record拉取、检查新鲜度、编辑、推送
执行测试脚本SN-Execute-Background-Script调试服务器逻辑

最佳实践

  • 使用描述性名称: 使用清晰、以动作为导向的名称(例如,“升级至管理层”)
  • 保持顺序一致: 使用标准化的顺序编号(100、200、300)
  • 始终进行验证: 同时验证客户端(用户体验)和服务器端(安全性)
  • 使用条件: 隐藏不相关的按钮,而不是显示禁用的按钮
  • 添加注释: 记录用途和任何依赖项
  • 基于角色的访问: 始终限制敏感操作
  • 审计跟踪: 为所有重要操作添加工作备注
  • 错误处理: 使用 try-catch 块封装服务器脚本
  • 全面测试: 测试所有条件、角色和边缘情况
  • 版本控制: 在本地同步脚本以进行版本控制

故障排除

按钮未显示

症状: UI Action 按钮未在表单/列表上显示 原因:

  1. UI Action 未激活
  2. 条件计算结果为 false
  3. 设置了错误的位置标志
  4. 用户缺少所需角色

解决方案:

Tool: SN-Query-Table
Parameters:
  table_name: sys_ui_action
  query: action_name=my_action
  fields: active,condition,form_button,table

检查 active=true,并验证条件逻辑。

客户端脚本未执行

症状: 点击按钮时没有任何反应 原因:

  1. JavaScript 语法错误
  2. client 字段未设为 true
  3. onclick 为空

解决方案:

  1. 检查浏览器控制台(F12)中的错误
  2. 验证 client: true
  3. 在浏览器控制台中测试 onclick 脚本
服务器脚本失败

症状: 表单已提交,但没有执行任何操作 原因:

  1. 服务器脚本语法错误
  2. 权限错误
  3. 缺少 current.update()

解决方案:

Tool: SN-Query-Table
Parameters:
  table_name: syslog
  query: levelINerror,warn^sys_created_on>javascript:gs.minutesAgo(5)
  fields: message,source,sys_created_on
  limit: 20
重定向无效

症状: 停留在表单页面或跳转到错误页面 原因:

  1. 缺少 action.setRedirectURL()
  2. 重定向目标无效
  3. 客户端在提交前返回 false

解决方案:

  • 验证服务器脚本末尾是否有 action.setRedirectURL(current)
  • 对于自定义 URL,确保格式有效
  • 检查客户端 onclick 是否返回 true 或提交表单
条件错误

症状: 按钮意外显示/消失 原因:

  1. 条件语法错误(不会显示错误)
  2. 未加载引用字段
  3. 类型不匹配(字符串与数字)

解决方案: 在后台脚本中测试条件:

Tool: SN-Execute-Background-Script
Parameters:
  script: |
    var current = new GlideRecord('incident');
    current.get('[sys_id]');

    // Test your condition
    var result = current.state == 7;  // Your condition
    gs.info('Condition result: ' + result);
  description: Test UI action condition

相关技能

  • admin/script-execution - 后台脚本模式
  • catalog/ui-policies - 目录表单行为
  • admin/workflow-creation - 工作流集成
  • security/acl-management - 访问控制列表
  • admin/script-sync - 本地脚本开发

参考资料

评论

支持 Markdown