snow-flow
Version:
Snow-Flow v3.2.0: Complete ServiceNow Enterprise Suite with 180+ MCP Tools. ATF Testing, Knowledge Management, Service Catalog, Change Management with CAB scheduling, Virtual Agent chatbots with NLU, Performance Analytics KPIs, Flow Designer automation, A
292 lines • 16.9 kB
JSON
{
"type": "widget",
"name": "Data Table Widget Template",
"description": "Template for creating data table widgets with sorting, filtering, and pagination",
"category": "patterns/datatable",
"config": {
"name": "{{WIDGET_NAME|data_table}}",
"id": "{{WIDGET_ID|data_table}}",
"instance_id": "{{INSTANCE_ID|table_}}",
"data": {
"title": "{{WIDGET_TITLE|Data Table}}",
"short_description": "{{WIDGET_DESCRIPTION|Interactive data table with advanced features}}",
"roles": "{{WIDGET_ROLES|}}"
},
"options": [
{
"name": "table_name",
"section": "Data",
"label": "Table",
"type": "reference",
"value": "{{TABLE_NAME|incident}}",
"ed": "sys_db_object",
"help": "Select the table to display data from"
},
{
"name": "max_records",
"section": "Data",
"label": "Maximum Records",
"type": "integer",
"value": "100",
"help": "Maximum number of records to display"
},
{
"name": "enable_filter",
"section": "Features",
"label": "Enable Filtering",
"type": "boolean",
"value": "true"
},
{
"name": "enable_export",
"section": "Features",
"label": "Enable Export",
"type": "boolean",
"value": "true"
},
{
"name": "enable_inline_edit",
"section": "Features",
"label": "Enable Inline Editing",
"type": "boolean",
"value": "false"
},
{
"name": "row_actions",
"section": "Features",
"label": "Row Actions",
"type": "json",
"value": "[{\"label\":\"View\",\"action\":\"view\",\"icon\":\"eye\"},{\"label\":\"Edit\",\"action\":\"edit\",\"icon\":\"pencil\"}]"
}
],
"html": [
"<div class=\"data-table-widget\">",
" <div class=\"table-header\">",
" <h2 class=\"table-title\">{{data.title}}</h2>",
" <div class=\"table-controls\">",
" <div class=\"search-box\" ng-if=\"options.enable_filter\">",
" <i class=\"fa fa-search\"></i>",
" <input type=\"text\" ng-model=\"c.searchTerm\" ng-change=\"c.filterData()\" placeholder=\"Search...\">",
" </div>",
" <button class=\"btn btn-primary\" ng-if=\"options.enable_export\" ng-click=\"c.exportData()\">",
" <i class=\"fa fa-download\"></i> Export",
" </button>",
" </div>",
" </div>",
" ",
" <div class=\"table-container\" ng-if=\"!c.loading\">",
" <table class=\"table table-striped table-hover\">",
" <thead>",
" <tr>",
" <th ng-repeat=\"column in c.columns\" ng-click=\"c.sort(column.field)\" class=\"sortable\">",
" {{column.label}}",
" <i class=\"fa fa-sort\" ng-if=\"c.sortField !== column.field\"></i>",
" <i class=\"fa fa-sort-asc\" ng-if=\"c.sortField === column.field && c.sortDirection === 'asc'\"></i>",
" <i class=\"fa fa-sort-desc\" ng-if=\"c.sortField === column.field && c.sortDirection === 'desc'\"></i>",
" </th>",
" <th ng-if=\"c.rowActions.length > 0\">Actions</th>",
" </tr>",
" </thead>",
" <tbody>",
" <tr ng-repeat=\"row in c.displayData\">",
" <td ng-repeat=\"column in c.columns\">",
" <span ng-if=\"!c.isEditing(row, column)\" ng-click=\"c.startEdit(row, column)\">",
" {{c.getCellValue(row, column)}}",
" </span>",
" <input ng-if=\"c.isEditing(row, column)\" ",
" type=\"text\" ",
" ng-model=\"row[column.field]\" ",
" ng-blur=\"c.saveEdit(row, column)\"",
" ng-keypress=\"c.handleEditKeypress($event, row, column)\">",
" </td>",
" <td ng-if=\"c.rowActions.length > 0\" class=\"actions-cell\">",
" <button ng-repeat=\"action in c.rowActions\" ",
" class=\"btn btn-sm btn-default\" ",
" ng-click=\"c.performAction(action, row)\"",
" title=\"{{action.label}}\">",
" <i class=\"fa fa-{{action.icon}}\"></i>",
" </button>",
" </td>",
" </tr>",
" </tbody>",
" </table>",
" </div>",
" ",
" <div class=\"table-pagination\" ng-if=\"c.totalPages > 1\">",
" <button class=\"btn btn-sm\" ng-click=\"c.previousPage()\" ng-disabled=\"c.currentPage === 1\">",
" <i class=\"fa fa-chevron-left\"></i>",
" </button>",
" <span class=\"page-info\">Page {{c.currentPage}} of {{c.totalPages}}</span>",
" <button class=\"btn btn-sm\" ng-click=\"c.nextPage()\" ng-disabled=\"c.currentPage === c.totalPages\">",
" <i class=\"fa fa-chevron-right\"></i>",
" </button>",
" </div>",
" ",
" <div class=\"table-loading\" ng-if=\"c.loading\">",
" <i class=\"fa fa-spinner fa-spin fa-3x\"></i>",
" <p>Loading data...</p>",
" </div>",
"</div>"
],
"css": [
".data-table-widget {",
" background: #fff;",
" border-radius: 8px;",
" box-shadow: 0 2px 4px rgba(0,0,0,0.1);",
" overflow: hidden;",
"}",
"",
".table-header {",
" display: flex;",
" justify-content: space-between;",
" align-items: center;",
" padding: 20px;",
" border-bottom: 1px solid #e0e0e0;",
"}",
"",
".table-title {",
" margin: 0;",
" font-size: 20px;",
" font-weight: 600;",
"}",
"",
".table-controls {",
" display: flex;",
" align-items: center;",
" gap: 15px;",
"}",
"",
".search-box {",
" position: relative;",
" display: flex;",
" align-items: center;",
"}",
"",
".search-box i {",
" position: absolute;",
" left: 10px;",
" color: #666;",
"}",
"",
".search-box input {",
" padding: 8px 8px 8px 35px;",
" border: 1px solid #ddd;",
" border-radius: 4px;",
" width: 250px;",
"}",
"",
".table-container {",
" overflow-x: auto;",
"}",
"",
"table {",
" width: 100%;",
" border-collapse: collapse;",
"}",
"",
"th {",
" background: #f5f5f5;",
" padding: 12px;",
" text-align: left;",
" font-weight: 600;",
" color: #333;",
" white-space: nowrap;",
"}",
"",
"th.sortable {",
" cursor: pointer;",
" user-select: none;",
"}",
"",
"th.sortable:hover {",
" background: #e8e8e8;",
"}",
"",
"th i {",
" margin-left: 5px;",
" color: #999;",
"}",
"",
"td {",
" padding: 12px;",
" border-bottom: 1px solid #f0f0f0;",
"}",
"",
"td input {",
" width: 100%;",
" padding: 4px 8px;",
" border: 1px solid #007bff;",
" border-radius: 4px;",
"}",
"",
"tr:hover {",
" background: #f9f9f9;",
"}",
"",
".actions-cell {",
" white-space: nowrap;",
"}",
"",
".actions-cell button {",
" margin-right: 5px;",
"}",
"",
".table-pagination {",
" display: flex;",
" justify-content: center;",
" align-items: center;",
" padding: 20px;",
" gap: 15px;",
" border-top: 1px solid #e0e0e0;",
"}",
"",
".page-info {",
" color: #666;",
"}",
"",
".table-loading {",
" text-align: center;",
" padding: 60px 20px;",
" color: #666;",
"}",
"",
"@media (max-width: 768px) {",
" .table-header {",
" flex-direction: column;",
" align-items: flex-start;",
" gap: 15px;",
" }",
" ",
" .search-box input {",
" width: 200px;",
" }",
"}"
],
"client_controller": "{{CLIENT_CONTROLLER|datatable_controller}}",
"script": "{{SERVER_SCRIPT|datatable_server_script}}",
"link_function": "{{LINK_FUNCTION|}}",
"demo_data": {
"columns": [
{"field": "number", "label": "Number"},
{"field": "short_description", "label": "Description"},
{"field": "priority", "label": "Priority"},
{"field": "state", "label": "State"}
],
"data": [
{"number": "INC0001", "short_description": "Sample incident", "priority": "High", "state": "Open"}
]
}
},
"variables": {
"WIDGET_NAME": "data_table",
"WIDGET_ID": "data_table",
"INSTANCE_ID": "table_",
"WIDGET_TITLE": "Data Table",
"WIDGET_DESCRIPTION": "Interactive data table with advanced features",
"WIDGET_ROLES": "",
"TABLE_NAME": "incident",
"CLIENT_CONTROLLER": "function($scope, $http, $timeout) {\n var c = this;\n \n c.loading = true;\n c.data = [];\n c.displayData = [];\n c.columns = [];\n c.searchTerm = '';\n c.sortField = null;\n c.sortDirection = 'asc';\n c.currentPage = 1;\n c.pageSize = 20;\n c.totalPages = 1;\n c.editingCell = null;\n c.rowActions = [];\n \n // Parse row actions from options\n try {\n c.rowActions = JSON.parse(c.options.row_actions || '[]');\n } catch(e) {\n c.rowActions = [];\n }\n \n c.getData = function() {\n c.loading = true;\n c.server.get({\n action: 'get_table_data',\n table: c.options.table_name,\n max_records: c.options.max_records\n }).then(function(response) {\n c.columns = response.data.columns || [];\n c.data = response.data.data || [];\n c.filterData();\n c.loading = false;\n });\n };\n \n c.filterData = function() {\n var filtered = c.data;\n \n if (c.searchTerm) {\n filtered = c.data.filter(function(row) {\n return Object.values(row).some(function(value) {\n return String(value).toLowerCase().includes(c.searchTerm.toLowerCase());\n });\n });\n }\n \n // Apply sorting\n if (c.sortField) {\n filtered.sort(function(a, b) {\n var aVal = a[c.sortField] || '';\n var bVal = b[c.sortField] || '';\n var result = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;\n return c.sortDirection === 'desc' ? -result : result;\n });\n }\n \n // Apply pagination\n c.totalPages = Math.ceil(filtered.length / c.pageSize);\n var start = (c.currentPage - 1) * c.pageSize;\n c.displayData = filtered.slice(start, start + c.pageSize);\n };\n \n c.sort = function(field) {\n if (c.sortField === field) {\n c.sortDirection = c.sortDirection === 'asc' ? 'desc' : 'asc';\n } else {\n c.sortField = field;\n c.sortDirection = 'asc';\n }\n c.filterData();\n };\n \n c.previousPage = function() {\n if (c.currentPage > 1) {\n c.currentPage--;\n c.filterData();\n }\n };\n \n c.nextPage = function() {\n if (c.currentPage < c.totalPages) {\n c.currentPage++;\n c.filterData();\n }\n };\n \n c.getCellValue = function(row, column) {\n var value = row[column.field];\n // Format based on field type\n if (column.type === 'date') {\n return value ? new Date(value).toLocaleDateString() : '';\n }\n return value || '';\n };\n \n c.isEditing = function(row, column) {\n return c.options.enable_inline_edit && \n c.editingCell && \n c.editingCell.row === row && \n c.editingCell.column === column;\n };\n \n c.startEdit = function(row, column) {\n if (c.options.enable_inline_edit) {\n c.editingCell = {row: row, column: column};\n }\n };\n \n c.saveEdit = function(row, column) {\n c.editingCell = null;\n // Save to server\n c.server.get({\n action: 'update_record',\n table: c.options.table_name,\n sys_id: row.sys_id,\n field: column.field,\n value: row[column.field]\n });\n };\n \n c.handleEditKeypress = function(event, row, column) {\n if (event.keyCode === 13) { // Enter key\n c.saveEdit(row, column);\n }\n };\n \n c.performAction = function(action, row) {\n switch(action.action) {\n case 'view':\n window.open('/' + c.options.table_name + '.do?sys_id=' + row.sys_id, '_blank');\n break;\n case 'edit':\n window.open('/' + c.options.table_name + '.do?sys_id=' + row.sys_id + '&sysparm_view=edit', '_blank');\n break;\n default:\n // Custom action\n $scope.$emit('widget-action', {action: action, row: row});\n }\n };\n \n c.exportData = function() {\n var csv = c.columns.map(function(col) { return col.label; }).join(',') + '\\n';\n c.displayData.forEach(function(row) {\n csv += c.columns.map(function(col) {\n var val = row[col.field] || '';\n return '\"' + String(val).replace(/\"/g, '\"\"') + '\"';\n }).join(',') + '\\n';\n });\n \n var blob = new Blob([csv], {type: 'text/csv'});\n var url = window.URL.createObjectURL(blob);\n var a = document.createElement('a');\n a.href = url;\n a.download = c.data.title + '.csv';\n a.click();\n };\n \n // Initial load\n c.getData();\n}",
"SERVER_SCRIPT": "(function() {\n try {\n data.columns = [];\n data.data = [];\n gs.log('DataTable widget server script started', 'datatable_widget');\n \n if (input && input.action === 'get_table_data') {\n var tableName = input.table || 'incident';\n var maxRecords = parseInt(input.max_records) || 100;\n \n gs.log('Getting table data: table=' + tableName + ', maxRecords=' + maxRecords, 'datatable_widget');\n \n // Get table metadata\n var fields = [];\n var gd = new GlideRecord('sys_dictionary');\n gd.addQuery('name', tableName);\n gd.addQuery('internal_type', '!=', 'collection');\n gd.orderBy('column_label');\n gd.setLimit(10); // Limit columns for performance\n gd.query();\n \n while (gd.next()) {\n if (gd.element && !gd.element.startsWith('sys_')) {\n fields.push({\n field: gd.element.toString(),\n label: gd.column_label.toString() || gd.element.toString(),\n type: gd.internal_type.toString()\n });\n }\n }\n \n // Default fields if none found\n if (fields.length === 0) {\n fields = [\n {field: 'number', label: 'Number', type: 'string'},\n {field: 'short_description', label: 'Short Description', type: 'string'},\n {field: 'sys_created_on', label: 'Created', type: 'glide_date_time'}\n ];\n gs.log('Using default fields for table: ' + tableName, 'datatable_widget');\n }\n \n data.columns = fields;\n gs.log('Found ' + fields.length + ' columns for table', 'datatable_widget');\n \n // Get table data\n var gr = new GlideRecord(tableName);\n gr.orderByDesc('sys_created_on');\n gr.setLimit(maxRecords);\n gr.query();\n \n var recordCount = 0;\n while (gr.next()) {\n var row = {sys_id: gr.getUniqueValue()};\n fields.forEach(function(field) {\n row[field.field] = gr.getDisplayValue(field.field) || '';\n });\n data.data.push(row);\n recordCount++;\n }\n \n gs.log('Retrieved ' + recordCount + ' records from ' + tableName, 'datatable_widget');\n \n } else if (input && input.action === 'update_record') {\n // ✅ FIXED: Enhanced update with validation and error handling\n if (!input.table || !input.sys_id || !input.field) {\n throw new Error('Missing required parameters for update: table, sys_id, field');\n }\n \n gs.log('Updating record: table=' + input.table + ', sys_id=' + input.sys_id + ', field=' + input.field, 'datatable_widget');\n \n var gr = new GlideRecord(input.table);\n if (gr.get(input.sys_id)) {\n gr.setValue(input.field, input.value || '');\n gr.update();\n data.success = true;\n data.message = 'Record updated successfully';\n gs.log('Record updated successfully', 'datatable_widget');\n } else {\n data.success = false;\n data.error = 'Record not found: ' + input.sys_id;\n gs.warn('Record not found for update: ' + input.sys_id, 'datatable_widget');\n }\n }\n } catch (error) {\n gs.error('DataTable widget error: ' + error.message, 'datatable_widget');\n data.error = 'Error processing request: ' + error.message;\n data.success = false;\n }\n})()",
"LINK_FUNCTION": ""
}
}