UNPKG

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
{ "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": "" } }