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

1,691 lines (1,522 loc) 46.3 kB
"use strict"; /** * ServiceNow Widget Template Generator * Generates functional ServiceNow Service Portal widget templates based on requirements */ Object.defineProperty(exports, "__esModule", { value: true }); exports.widgetTemplateGenerator = exports.ServiceNowWidgetTemplateGenerator = void 0; class ServiceNowWidgetTemplateGenerator { /** * Generate complete widget components based on instruction */ generateWidget(options) { const { instruction = '', title = 'Widget', type = 'auto' } = options; const detectedType = type === 'auto' ? this.detectWidgetType(instruction) : type; const widgetTitle = title || this.extractTitleFromInstruction(instruction); return { template: this.generateTemplate(detectedType, widgetTitle, instruction), css: this.generateCss(detectedType, options), serverScript: this.generateServerScript(detectedType, instruction), clientScript: this.generateClientScript(detectedType, instruction), optionSchema: this.generateOptionSchema(detectedType) }; } /** * Detect widget type from instruction */ detectWidgetType(instruction) { const lower = instruction.toLowerCase(); if (lower.includes('chart') || lower.includes('graph') || lower.includes('analytics')) { return 'chart'; } if (lower.includes('dashboard') || lower.includes('metrics') || lower.includes('kpi')) { return 'dashboard'; } if (lower.includes('table') || lower.includes('list') || lower.includes('records')) { return 'table'; } if (lower.includes('form') || lower.includes('input') || lower.includes('create') || lower.includes('submit')) { return 'form'; } return 'info'; // Default to info card widget } /** * Extract widget title from instruction */ extractTitleFromInstruction(instruction) { // Simple extraction - look for patterns like "create X widget" or "X dashboard" const patterns = [ /create\s+(.+?)\s+widget/i, /(.+?)\s+dashboard/i, /(.+?)\s+chart/i, /(.+?)\s+table/i, /(.+?)\s+form/i ]; for (const pattern of patterns) { const match = instruction.match(pattern); if (match) { return match[1].replace(/\b\w/g, l => l.toUpperCase()); } } // Fallback: use first few words const words = instruction.split(' ').slice(0, 3); return words.map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '); } /** * Generate HTML template based on widget type */ generateTemplate(type, title, instruction) { switch (type) { case 'chart': return this.generateChartTemplate(title); case 'dashboard': return this.generateDashboardTemplate(title); case 'table': return this.generateTableTemplate(title); case 'form': return this.generateFormTemplate(title); case 'info': default: return this.generateInfoTemplate(title, instruction); } } /** * Chart widget template with Chart.js integration */ generateChartTemplate(title) { return ` <div class="panel panel-default widget-chart"> <div class="panel-heading"> <h3 class="panel-title">{{data.title || '${title}'}}</h3> <div class="panel-actions" ng-if="options.show_refresh"> <button class="btn btn-sm btn-default" ng-click="c.refreshData()" title="Refresh Chart"> <i class="fa fa-refresh" ng-class="{'fa-spin': data.loading}"></i> </button> </div> </div> <div class="panel-body"> <div ng-if="data.loading" class="text-center loading-state"> <i class="fa fa-spinner fa-spin fa-2x"></i> <p class="text-muted">Loading chart data...</p> </div> <div ng-if="!data.loading && !data.error" class="chart-container"> <canvas id="chart-{{::widget.id}}" width="400" height="300"></canvas> <!-- Chart Legend --> <div ng-if="data.chart_legend" class="chart-legend"> <div class="legend-item" ng-repeat="item in data.chart_legend"> <span class="legend-color" style="background-color: {{item.color}}"></span> <span class="legend-label">{{item.label}}: {{item.value}}</span> </div> </div> </div> <div ng-if="data.error" class="alert alert-danger"> <i class="fa fa-exclamation-triangle"></i> <strong>Chart Error:</strong> {{data.error}} </div> <div ng-if="!data.loading && !data.error && (!data.chart_data || data.chart_data.length === 0)" class="empty-state text-center"> <i class="fa fa-bar-chart fa-3x text-muted"></i> <h4>No Data Available</h4> <p class="text-muted">{{data.empty_message || 'No chart data to display'}}</p> </div> </div> </div>`.trim(); } /** * Dashboard widget template with metrics */ generateDashboardTemplate(title) { return ` <div class="widget-dashboard"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">{{data.title || '${title}'}}</h3> <span class="panel-subtitle" ng-if="data.last_updated"> Last updated: {{data.last_updated | date:'medium'}} </span> </div> <div class="panel-body"> <div ng-if="data.loading" class="text-center loading-state"> <i class="fa fa-spinner fa-spin fa-2x"></i> <p class="text-muted">Loading dashboard data...</p> </div> <div ng-if="!data.loading && data.metrics && data.metrics.length > 0" class="metrics-grid"> <div class="metric-item" ng-repeat="metric in data.metrics track by $index" ng-class="'metric-col-' + (12/data.metrics.length)"> <div class="metric-card" ng-class="{ 'metric-critical': metric.status === 'critical', 'metric-warning': metric.status === 'warning', 'metric-success': metric.status === 'success', 'metric-info': metric.status === 'info' }"> <div class="metric-icon" ng-if="metric.icon"> <i class="fa fa-{{metric.icon}}"></i> </div> <h4 class="metric-label">{{metric.label}}</h4> <div class="metric-value-container"> <h2 class="metric-value"> {{metric.value}} <small ng-if="metric.unit" class="metric-unit">{{metric.unit}}</small> </h2> <div class="metric-trend" ng-if="metric.trend !== undefined"> <i class="fa" ng-class="{ 'fa-arrow-up text-success': metric.trend > 0, 'fa-arrow-down text-danger': metric.trend < 0, 'fa-minus text-muted': metric.trend === 0 }"></i> <span class="trend-text">{{metric.trend_text || (metric.trend > 0 ? '+' + metric.trend + '%' : metric.trend + '%')}}</span> </div> </div> <div class="metric-description" ng-if="metric.description"> {{metric.description}} </div> </div> </div> </div> <div ng-if="!data.loading && (!data.metrics || data.metrics.length === 0)" class="empty-state text-center"> <i class="fa fa-dashboard fa-3x text-muted"></i> <h4>No Metrics Available</h4> <p class="text-muted">{{data.empty_message || 'Dashboard metrics will appear here'}}</p> </div> </div> </div> </div>`.trim(); } /** * Table widget template with sorting and filtering */ generateTableTemplate(title) { return ` <div class="panel panel-default widget-table"> <div class="panel-heading"> <h3 class="panel-title">{{data.title || '${title}'}}</h3> <div class="panel-actions"> <div class="input-group input-group-sm search-container" ng-if="options.enable_search"> <input type="text" class="form-control" placeholder="Search records..." ng-model="c.searchTerm" ng-change="c.onSearchChange()"> <span class="input-group-btn"> <button class="btn btn-default" ng-click="c.clearSearch()" ng-if="c.searchTerm"> <i class="fa fa-times"></i> </button> </span> </div> <button class="btn btn-sm btn-primary" ng-click="c.refreshData()" title="Refresh Data"> <i class="fa fa-refresh" ng-class="{'fa-spin': data.loading}"></i> </button> </div> </div> <div class="panel-body"> <div ng-if="data.loading" class="text-center loading-state"> <i class="fa fa-spinner fa-spin fa-2x"></i> <p class="text-muted">Loading records...</p> </div> <div ng-if="!data.loading && data.records && data.records.length > 0" class="table-container"> <table class="table table-striped table-hover table-responsive"> <thead> <tr> <th ng-repeat="column in data.columns" ng-click="c.sortBy(column.field)" class="sortable-header" ng-class="{'sorted': c.sortField === column.field}"> {{column.label}} <span class="sort-indicator"> <i class="fa fa-sort" ng-if="c.sortField !== column.field"></i> <i class="fa fa-sort-up" ng-if="c.sortField === column.field && !c.sortReverse"></i> <i class="fa fa-sort-down" ng-if="c.sortField === column.field && c.sortReverse"></i> </span> </th> <th ng-if="options.show_actions" class="actions-column">Actions</th> </tr> </thead> <tbody> <tr ng-repeat="record in data.records | filter:c.searchTerm | orderBy:c.sortField:c.sortReverse | limitTo:options.page_size track by record.sys_id" ng-class="{'row-highlighted': record.highlighted}"> <td ng-repeat="column in data.columns" ng-class="'column-' + column.type"> <!-- Link fields --> <a ng-if="column.type === 'link'" href="{{record[column.field + '_link'] || '#'}}" target="_blank" class="record-link"> {{record[column.field] || '-'}} </a> <!-- Date fields --> <span ng-if="column.type === 'date'"> {{record[column.field] | date:'short'}} </span> <!-- Status/State fields --> <span ng-if="column.type === 'status'" class="label" ng-class="'label-' + (record[column.field + '_class'] || 'default')"> {{record[column.field] || '-'}} </span> <!-- Default text fields --> <span ng-if="!column.type || (column.type !== 'link' && column.type !== 'date' && column.type !== 'status')"> {{record[column.field] || '-'}} </span> </td> <td ng-if="options.show_actions" class="actions-column"> <button class="btn btn-xs btn-default" ng-click="c.viewRecord(record)" title="View"> <i class="fa fa-eye"></i> </button> <button class="btn btn-xs btn-primary" ng-click="c.editRecord(record)" title="Edit" ng-if="options.allow_edit"> <i class="fa fa-edit"></i> </button> </td> </tr> </tbody> </table> <!-- Pagination --> <div ng-if="data.total_records > options.page_size" class="table-pagination text-center"> <button class="btn btn-sm btn-default" ng-click="c.previousPage()" ng-disabled="c.currentPage <= 1"> <i class="fa fa-chevron-left"></i> Previous </button> <span class="pagination-info"> Page {{c.currentPage}} of {{c.totalPages}} ({{data.total_records}} total) </span> <button class="btn btn-sm btn-default" ng-click="c.nextPage()" ng-disabled="c.currentPage >= c.totalPages"> Next <i class="fa fa-chevron-right"></i> </button> </div> </div> <div ng-if="!data.loading && (!data.records || data.records.length === 0)" class="empty-state text-center"> <i class="fa fa-table fa-3x text-muted"></i> <h4>No Records Found</h4> <p class="text-muted">{{data.empty_message || 'No records match your criteria'}}</p> </div> </div> </div>`.trim(); } /** * Form widget template with validation */ generateFormTemplate(title) { return ` <div class="panel panel-default widget-form"> <div class="panel-heading"> <h3 class="panel-title">{{data.title || '${title}'}}</h3> </div> <div class="panel-body"> <div ng-if="data.success_message" class="alert alert-success alert-dismissible"> <button type="button" class="close" ng-click="data.success_message = null"> <span>&times;</span> </button> <i class="fa fa-check-circle"></i> {{data.success_message}} </div> <div ng-if="data.error_message" class="alert alert-danger alert-dismissible"> <button type="button" class="close" ng-click="data.error_message = null"> <span>&times;</span> </button> <i class="fa fa-exclamation-triangle"></i> {{data.error_message}} </div> <form name="widgetForm" ng-submit="c.submitForm()" novalidate class="widget-form"> <div class="form-group" ng-repeat="field in data.form_fields" ng-class="{'has-error': widgetForm[field.name].$invalid && widgetForm[field.name].$touched}"> <label class="control-label" for="{{field.name}}"> {{field.label}} <span class="text-danger" ng-if="field.required">*</span> <span class="help-icon" ng-if="field.help_text" title="{{field.help_text}}"> <i class="fa fa-question-circle"></i> </span> </label> <!-- Text Input --> <input ng-if="field.type === 'text' || !field.type" type="text" class="form-control" id="{{field.name}}" name="{{field.name}}" ng-model="c.formData[field.name]" ng-required="field.required" placeholder="{{field.placeholder}}" maxlength="{{field.max_length}}"> <!-- Email Input --> <input ng-if="field.type === 'email'" type="email" class="form-control" id="{{field.name}}" name="{{field.name}}" ng-model="c.formData[field.name]" ng-required="field.required" placeholder="{{field.placeholder}}"> <!-- Textarea --> <textarea ng-if="field.type === 'textarea'" class="form-control" id="{{field.name}}" name="{{field.name}}" ng-model="c.formData[field.name]" ng-required="field.required" placeholder="{{field.placeholder}}" rows="{{field.rows || 3}}" maxlength="{{field.max_length}}"></textarea> <!-- Select Dropdown --> <select ng-if="field.type === 'select'" class="form-control" id="{{field.name}}" name="{{field.name}}" ng-model="c.formData[field.name]" ng-required="field.required"> <option value="">{{field.placeholder || 'Select ' + field.label}}</option> <option ng-repeat="option in field.options" value="{{option.value}}">{{option.label}}</option> </select> <!-- Checkbox --> <div ng-if="field.type === 'checkbox'" class="checkbox"> <label> <input type="checkbox" id="{{field.name}}" name="{{field.name}}" ng-model="c.formData[field.name]" ng-required="field.required"> {{field.checkbox_label || field.label}} </label> </div> <!-- Date Input --> <input ng-if="field.type === 'date'" type="date" class="form-control" id="{{field.name}}" name="{{field.name}}" ng-model="c.formData[field.name]" ng-required="field.required"> <!-- Validation Messages --> <div class="form-field-errors" ng-if="widgetForm[field.name].$invalid && widgetForm[field.name].$touched"> <small class="text-danger" ng-if="widgetForm[field.name].$error.required"> {{field.label}} is required </small> <small class="text-danger" ng-if="widgetForm[field.name].$error.email"> Please enter a valid email address </small> <small class="text-danger" ng-if="widgetForm[field.name].$error.maxlength"> {{field.label}} is too long (max {{field.max_length}} characters) </small> </div> <div class="field-help-text" ng-if="field.help_text"> <small class="text-muted">{{field.help_text}}</small> </div> </div> <div class="form-actions"> <button type="submit" class="btn btn-primary" ng-disabled="widgetForm.$invalid || c.submitting"> <i class="fa fa-spinner fa-spin" ng-if="c.submitting"></i> <i class="fa fa-check" ng-if="!c.submitting"></i> {{c.submitting ? (options.submit_text_loading || 'Submitting...') : (options.submit_text || 'Submit')}} </button> <button type="button" class="btn btn-default" ng-click="c.resetForm()" ng-if="options.show_reset"> <i class="fa fa-undo"></i> Reset </button> <button type="button" class="btn btn-link" ng-click="c.cancelForm()" ng-if="options.show_cancel"> Cancel </button> </div> </form> </div> </div>`.trim(); } /** * Info card widget template (default/generic) */ generateInfoTemplate(title, instruction) { return ` <div class="panel panel-default widget-info"> <div class="panel-heading"> <h3 class="panel-title">{{data.title || '${title}'}}</h3> <div class="panel-actions" ng-if="options.show_refresh"> <button class="btn btn-sm btn-default" ng-click="c.refreshData()" title="Refresh"> <i class="fa fa-refresh" ng-class="{'fa-spin': data.loading}"></i> </button> </div> </div> <div class="panel-body"> <div ng-if="data.loading" class="text-center loading-state"> <i class="fa fa-spinner fa-spin fa-2x"></i> <p class="text-muted">Loading information...</p> </div> <div ng-if="!data.loading"> <!-- Primary Content --> <div ng-if="data.content" class="widget-content"> <div ng-if="data.content.icon" class="content-icon text-center"> <i class="fa fa-{{data.content.icon}} fa-3x" ng-class="'text-' + (data.content.icon_color || 'primary')"></i> </div> <div ng-if="data.content.title" class="content-title"> <h4>{{data.content.title}}</h4> </div> <div ng-if="data.content.description" class="content-description"> <p ng-bind-html="data.content.description"></p> </div> <div ng-if="data.content.value" class="content-value text-center"> <h2 class="value-display" ng-class="'text-' + (data.content.value_color || 'info')"> {{data.content.value}} <small ng-if="data.content.unit">{{data.content.unit}}</small> </h2> </div> </div> <!-- List Items --> <div ng-if="data.items && data.items.length > 0" class="info-items"> <div class="list-group"> <div class="list-group-item info-item" ng-repeat="item in data.items track by $index" ng-class="'item-' + (item.type || 'default')"> <div class="item-header" ng-if="item.title || item.icon"> <div class="item-icon" ng-if="item.icon"> <i class="fa fa-{{item.icon}}" ng-class="'text-' + (item.icon_color || 'default')"></i> </div> <div class="item-title" ng-if="item.title"> <strong>{{item.title}}</strong> </div> </div> <div class="item-content"> <div class="item-description" ng-if="item.description"> {{item.description}} </div> <div class="item-value" ng-if="item.value"> <span class="value" ng-class="'text-' + (item.value_color || 'default')"> {{item.value}} <small ng-if="item.unit">{{item.unit}}</small> </span> </div> </div> <div class="item-meta" ng-if="item.timestamp || item.meta"> <small class="text-muted"> <span ng-if="item.timestamp">{{item.timestamp | date:'medium'}}</span> <span ng-if="item.meta">{{item.meta}}</span> </small> </div> <div class="item-actions" ng-if="item.actions && item.actions.length > 0"> <button ng-repeat="action in item.actions" class="btn btn-xs" ng-class="'btn-' + (action.type || 'default')" ng-click="c.executeAction(action, item)"> <i class="fa fa-{{action.icon}}" ng-if="action.icon"></i> {{action.label}} </button> </div> </div> </div> </div> <!-- Empty State --> <div ng-if="!data.content && (!data.items || data.items.length === 0)" class="empty-state text-center"> <i class="fa fa-info-circle fa-3x text-muted"></i> <h4>{{data.empty_title || 'Information Widget'}}</h4> <p class="text-muted">{{data.empty_message || 'Widget data will appear here when available'}}</p> </div> </div> </div> </div>`.trim(); } /** * Generate CSS styles based on widget type and options */ generateCss(type, options) { const { theme = 'default', responsive = true } = options; let css = ` /* Base Panel Styles */ .panel { margin-bottom: 20px; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border: 1px solid #ddd; } .panel-heading { background: linear-gradient(to bottom, #f8f9fa 0%, #e9ecef 100%); border-bottom: 1px solid #dee2e6; position: relative; padding: 15px; border-radius: 5px 5px 0 0; } .panel-title { margin: 0; font-size: 16px; font-weight: 600; color: #495057; } .panel-subtitle { font-size: 12px; color: #6c757d; display: block; margin-top: 4px; } .panel-actions { position: absolute; right: 15px; top: 50%; transform: translateY(-50%); display: flex; gap: 8px; align-items: center; } .panel-body { padding: 15px; } /* Loading States */ .loading-state { padding: 40px 20px; } .loading-state .fa-spinner { color: #007bff; margin-bottom: 10px; } /* Empty States */ .empty-state { padding: 40px 20px; } .empty-state .fa { margin-bottom: 15px; opacity: 0.5; } `; // Add type-specific styles switch (type) { case 'chart': css += this.getChartCss(); break; case 'dashboard': css += this.getDashboardCss(); break; case 'table': css += this.getTableCss(); break; case 'form': css += this.getFormCss(); break; case 'info': css += this.getInfoCss(); break; } // Add theme styles if (theme === 'dark') { css += this.getDarkThemeCss(); } else if (theme === 'minimal') { css += this.getMinimalThemeCss(); } // Add responsive styles if (responsive) { css += this.getResponsiveCss(); } return css; } getChartCss() { return ` /* Chart Widget Styles */ .widget-chart canvas { max-width: 100%; height: auto; } .chart-container { position: relative; } .chart-legend { margin-top: 15px; display: flex; flex-wrap: wrap; gap: 15px; justify-content: center; } .legend-item { display: flex; align-items: center; gap: 5px; } .legend-color { width: 12px; height: 12px; border-radius: 2px; } .legend-label { font-size: 12px; color: #495057; } `; } getDashboardCss() { return ` /* Dashboard Widget Styles */ .metrics-grid { display: flex; flex-wrap: wrap; gap: 15px; } .metric-item { flex: 1; min-width: 200px; } .metric-card { background: #fff; border: 1px solid #e9ecef; border-radius: 6px; padding: 20px; text-align: center; transition: all 0.3s ease; height: 100%; } .metric-card:hover { box-shadow: 0 4px 8px rgba(0,0,0,0.1); transform: translateY(-2px); } .metric-card.metric-critical { border-left: 4px solid #dc3545; } .metric-card.metric-warning { border-left: 4px solid #ffc107; } .metric-card.metric-success { border-left: 4px solid #28a745; } .metric-card.metric-info { border-left: 4px solid #17a2b8; } .metric-icon { font-size: 24px; color: #6c757d; margin-bottom: 10px; } .metric-label { font-size: 14px; color: #6c757d; margin-bottom: 8px; font-weight: 500; } .metric-value { font-size: 28px; font-weight: 700; margin-bottom: 8px; line-height: 1; } .metric-unit { font-size: 14px; font-weight: 400; color: #6c757d; } .metric-trend { font-size: 12px; display: flex; align-items: center; justify-content: center; gap: 4px; } .trend-text { font-weight: 500; } .metric-description { font-size: 12px; color: #6c757d; margin-top: 8px; } `; } getTableCss() { return ` /* Table Widget Styles */ .search-container { max-width: 200px; margin-right: 8px; } .table-container { overflow-x: auto; } .table { margin-bottom: 0; } .sortable-header { cursor: pointer; user-select: none; position: relative; } .sortable-header:hover { background-color: #f8f9fa; } .sort-indicator { margin-left: 5px; opacity: 0.5; } .sorted .sort-indicator { opacity: 1; } .actions-column { width: 100px; text-align: center; } .record-link { color: #007bff; text-decoration: none; } .record-link:hover { text-decoration: underline; } .row-highlighted { background-color: #fff3cd !important; } .table-pagination { margin-top: 15px; padding-top: 15px; border-top: 1px solid #dee2e6; } .pagination-info { margin: 0 15px; color: #6c757d; font-size: 14px; } .column-status .label { font-size: 11px; padding: 3px 8px; } .label-critical { background-color: #dc3545; } .label-warning { background-color: #ffc107; color: #212529; } .label-success { background-color: #28a745; } .label-info { background-color: #17a2b8; } .label-default { background-color: #6c757d; } `; } getFormCss() { return ` /* Form Widget Styles */ .widget-form { max-width: 600px; } .form-group { margin-bottom: 20px; } .control-label { font-weight: 500; margin-bottom: 5px; display: block; } .help-icon { color: #6c757d; cursor: help; margin-left: 4px; } .form-control { border-radius: 4px; border: 1px solid #ced4da; padding: 8px 12px; transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } .form-control:focus { border-color: #80bdff; box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25); } .has-error .form-control { border-color: #dc3545; } .has-error .control-label { color: #dc3545; } .form-field-errors { margin-top: 5px; } .field-help-text { margin-top: 5px; } .form-actions { margin-top: 30px; padding-top: 20px; border-top: 1px solid #dee2e6; display: flex; gap: 10px; align-items: center; } .checkbox label { font-weight: normal; margin-bottom: 0; cursor: pointer; } `; } getInfoCss() { return ` /* Info Widget Styles */ .widget-content { text-align: center; margin-bottom: 20px; } .content-icon { margin-bottom: 15px; } .content-title h4 { margin-bottom: 10px; color: #495057; } .content-description { color: #6c757d; margin-bottom: 15px; } .content-value { margin-bottom: 15px; } .value-display { font-weight: 700; margin-bottom: 0; } .info-items { margin-top: 20px; } .info-item { border: 1px solid #e9ecef; margin-bottom: 8px; padding: 12px; border-radius: 4px; } .item-header { display: flex; align-items: center; margin-bottom: 8px; gap: 8px; } .item-icon { font-size: 16px; } .item-title { flex: 1; } .item-content { margin-bottom: 8px; } .item-description { color: #6c757d; margin-bottom: 5px; } .item-value .value { font-weight: 600; } .item-meta { margin-top: 8px; } .item-actions { margin-top: 8px; display: flex; gap: 5px; } `; } getDarkThemeCss() { return ` /* Dark Theme */ .panel { background-color: #343a40; border-color: #495057; } .panel-heading { background: linear-gradient(to bottom, #495057 0%, #343a40 100%); border-bottom-color: #495057; } .panel-title { color: #fff; } .panel-subtitle { color: #adb5bd; } .panel-body { background-color: #343a40; color: #fff; } .metric-card { background-color: #495057; border-color: #6c757d; color: #fff; } .table { color: #fff; } .table-striped tbody tr:nth-of-type(odd) { background-color: rgba(255,255,255,.05); } `; } getMinimalThemeCss() { return ` /* Minimal Theme */ .panel { border: none; box-shadow: none; background: transparent; } .panel-heading { background: transparent; border-bottom: 1px solid #dee2e6; padding: 10px 0; } .panel-title { font-size: 18px; font-weight: 300; } .panel-body { padding: 20px 0; } .metric-card { background: transparent; border: 1px solid #dee2e6; box-shadow: none; } `; } getResponsiveCss() { return ` /* Responsive Styles */ @media (max-width: 768px) { .panel-actions { position: static; transform: none; margin-top: 10px; text-align: right; } .metrics-grid { flex-direction: column; } .metric-item { min-width: auto; } .search-container { max-width: none; margin-bottom: 10px; margin-right: 0; } .table-pagination { text-align: center; } .pagination-info { display: block; margin: 10px 0; } .form-actions { flex-direction: column; align-items: stretch; } .form-actions .btn { margin-bottom: 5px; } } @media (max-width: 480px) { .panel-body { padding: 10px; } .metric-value { font-size: 24px; } .table-container { font-size: 14px; } .actions-column { width: 80px; } .btn-xs { padding: 2px 5px; font-size: 10px; } } `; } /** * Generate server script based on widget type */ generateServerScript(type, instruction) { const baseScript = ` (function() { // Widget server script - runs server-side to fetch data var widgetType = '${type}'; var instruction = '${instruction.replace(/'/g, "\\'")}'; // Set widget title data.title = options.title || '${this.extractTitleFromInstruction(instruction)}'; data.loading = false; data.error = null; try { // Widget-specific data loading switch(widgetType) {`; switch (type) { case 'chart': return baseScript + ` case 'chart': loadChartData(); break; case 'dashboard': loadDashboardData(); break; case 'table': loadTableData(); break; case 'form': loadFormData(); break; default: loadInfoData(); } } catch (e) { data.error = 'Server error: ' + e.message; gs.error('Widget server script error: ' + e.message); } function loadChartData() { // Sample chart data - replace with actual ServiceNow queries data.chart_data = [ { label: 'Open', value: 25, color: '#dc3545' }, { label: 'In Progress', value: 45, color: '#ffc107' }, { label: 'Resolved', value: 30, color: '#28a745' } ]; data.chart_legend = data.chart_data; data.chart_type = options.chart_type || 'doughnut'; // Example: Load incident data // var gr = new GlideRecord('incident'); // gr.addQuery('state', 'IN', '1,2,3'); // gr.query(); // data.chart_data = processIncidentData(gr); } function loadDashboardData() { data.metrics = [ { label: 'Total Items', value: 150, unit: '', status: 'info', icon: 'list', trend: 5, trend_text: '+5%' }, { label: 'Critical Issues', value: 3, unit: '', status: 'critical', icon: 'exclamation-triangle', trend: -2, trend_text: '-2 from yesterday' }, { label: 'Success Rate', value: 98.5, unit: '%', status: 'success', icon: 'check-circle', trend: 1, trend_text: '+1%' } ]; data.last_updated = new GlideDateTime().getDisplayValue(); } function loadTableData() { data.columns = [ { field: 'number', label: 'Number', type: 'link' }, { field: 'short_description', label: 'Description', type: 'text' }, { field: 'state', label: 'State', type: 'status' }, { field: 'opened_at', label: 'Opened', type: 'date' } ]; data.records = []; data.total_records = 0; // Example: Load incident records // var gr = new GlideRecord('incident'); // gr.orderByDesc('opened_at'); // gr.setLimit(options.page_size || 10); // gr.query(); // // while (gr.next()) { // data.records.push({ // sys_id: gr.getUniqueValue(), // number: gr.getDisplayValue('number'), // number_link: '/' + gr.getTableName() + '.do?sys_id=' + gr.getUniqueValue(), // short_description: gr.getDisplayValue('short_description'), // state: gr.getDisplayValue('state'), // state_class: getStateClass(gr.getValue('state')), // opened_at: gr.getDisplayValue('opened_at') // }); // } // // data.total_records = gr.getRowCount(); } function loadFormData() { data.form_fields = [ { name: 'title', label: 'Title', type: 'text', required: true, placeholder: 'Enter title...' }, { name: 'description', label: 'Description', type: 'textarea', required: false, placeholder: 'Enter description...', rows: 3 }, { name: 'priority', label: 'Priority', type: 'select', required: true, options: [ { value: '1', label: 'Critical' }, { value: '2', label: 'High' }, { value: '3', label: 'Medium' }, { value: '4', label: 'Low' } ] } ]; } function loadInfoData() { data.content = { icon: 'info-circle', icon_color: 'primary', title: 'Information Widget', description: 'This widget displays important information and updates.', value: null }; data.items = [ { title: 'Sample Information', description: 'This is sample information displayed in the widget.', icon: 'info', icon_color: 'info', timestamp: new GlideDateTime().getDisplayValue() } ]; } })();`.trim(); default: return baseScript + ` default: loadInfoData(); } } catch (e) { data.error = 'Server error: ' + e.message; gs.error('Widget server script error: ' + e.message); } function loadInfoData() { data.content = { icon: 'info-circle', icon_color: 'primary', title: 'Information Widget', description: 'This widget displays important information and updates.', value: null }; data.items = [ { title: 'Sample Information', description: 'This is sample information displayed in the widget.', icon: 'info', icon_color: 'info', timestamp: new GlideDateTime().getDisplayValue() } ]; } })();`.trim(); } } /** * Generate client script based on widget type */ generateClientScript(type, instruction) { const baseScript = ` function($scope, $http, spUtil, $timeout) { var c = this; var widgetType = '${type}'; // Initialize controller c.init = function() { c.widgetType = widgetType; c.loading = false; // Type-specific initialization switch(widgetType) {`; switch (type) { case 'chart': return baseScript + ` case 'chart': c.initChart(); break; case 'dashboard': c.initDashboard(); break; case 'table': c.initTable(); break; case 'form': c.initForm(); break; default: c.initInfo(); } }; // Chart-specific functions c.initChart = function() { $timeout(function() { if (c.data.chart_data && c.data.chart_data.length > 0) { c.renderChart(); } }, 100); }; c.renderChart = function() { var canvas = document.getElementById('chart-' + c.widget.id); if (!canvas) return; var ctx = canvas.getContext('2d'); var chartType = c.data.chart_type || 'doughnut'; // Simple chart rendering (requires Chart.js) if (typeof Chart !== 'undefined') { new Chart(ctx, { type: chartType, data: { labels: c.data.chart_data.map(function(item) { return item.label; }), datasets: [{ data: c.data.chart_data.map(function(item) { return item.value; }), backgroundColor: c.data.chart_data.map(function(item) { return item.color; }) }] }, options: { responsive: true, maintainAspectRatio: false, legend: { display: false } } }); } }; c.refreshData = function() { c.loading = true; c.server.refresh().then(function() { c.loading = false; if (widgetType === 'chart') { $timeout(function() { c.renderChart(); }, 100); } }); }; // Dashboard functions c.initDashboard = function() { // Dashboard initialization }; // Table functions c.initTable = function() { c.currentPage = 1; c.sortField = null; c.sortReverse = false; c.searchTerm = ''; c.calculatePagination(); }; c.sortBy = function(field) { if (c.sortField === field) { c.sortReverse = !c.sortReverse; } else { c.sortField = field; c.sortReverse = false; } }; c.onSearchChange = function() { c.currentPage = 1; c.calculatePagination(); }; c.clearSearch = function() { c.searchTerm = ''; c.onSearchChange(); }; c.calculatePagination = function() { var pageSize = c.options.page_size || 10; c.totalPages = Math.ceil((c.data.total_records || 0) / pageSize); }; c.previousPage = function() { if (c.currentPage > 1) { c.currentPage--; } }; c.nextPage = function() { if (c.currentPage < c.totalPages) { c.currentPage++; } }; c.viewRecord = function(record) { // Navigate to record window.open('/' + record.sys_table + '.do?sys_id=' + record.sys_id, '_blank'); }; c.editRecord = function(record) { // Navigate to record form window.open('/' + record.sys_table + '.do?sys_id=' + record.sys_id, '_blank'); }; // Form functions c.initForm = function() { c.formData = {}; c.submitting = false; c.resetForm(); }; c.submitForm = function() { if ($scope.widgetForm.$valid) { c.submitting = true; // Submit form data to server c.server.update().then(function(response) { c.submitting = false; if (response && response.success) { c.data.success_message = response.message || 'Form submitted successfully!'; c.data.error_message = null; c.resetForm(); } else { c.data.error_message = response.error || 'Form submission failed. Please try again.'; c.data.success_message = null; } }).catch(function(error) { c.submitting = false; c.data.error_message = 'Network error. Please try again.'; c.data.success_message = null; }); } }; c.resetForm = function() { c.formData = {}; if ($scope.widgetForm) { $scope.widgetForm.$setPristine(); $scope.widgetForm.$setUntouched(); } }; c.cancelForm = function() { c.resetForm(); // Additional cancel logic }; // Info widget functions c.initInfo = function() { // Info widget initialization }; c.executeAction = function(action, item) { switch(action.type) { case 'link': window.open(action.url, '_blank'); break; case 'refresh': c.refreshData(); break; default: console.log('Action executed:', action, item); } }; // Initialize widget c.init(); }`.trim(); default: return baseScript + ` default: c.initInfo(); } }; c.initInfo = function() { // Info widget initialization }; c.refreshData = function() { c.loading = true; c.server.refresh().then(function() { c.loading = false; }); }; c.executeAction = function(action, item) { switch(action.type) { case 'link': window.open(action.url, '_blank'); break; case 'refresh': c.refreshData(); break; default: console.log('Action executed:', action, item); } }; // Initialize widget c.init(); }`.trim(); } } /** * Generate option schema based on widget type */ generateOptionSchema(type) { const baseOptions = [ { "name": "title", "label": "Widget Title", "type": "string", "value": "" }, { "name": "show_refresh", "label": "Show Refresh Button", "type": "boolean", "value": true } ]; let typeSpecificOptions = []; switch (type) { case 'chart': typeSpecificOptions = [ { "name": "chart_type", "label": "Chart Type", "type": "choice", "choices": [ { "label": "Doughnut", "value": "doughnut" }, { "label": "Bar", "value": "bar" }, { "label": "Line", "value": "line" }, { "label": "Pie", "value": "pie" } ], "value": "doughnut" } ]; break; case 'table': typeSpecificOptions = [ { "name": "enable_search", "label": "Enable Search", "type": "boolean", "value": true }, { "name": "page_size", "label": "Records Per Page", "type": "integer", "value": 10 }, { "name": "show_actions", "label": "Show Action Buttons", "type": "boolean", "value": true }, { "name": "allow_edit", "label": "Allow Edit Action", "type": "boolean", "value": false } ]; break; case 'form': typeSpecificOptions = [ { "name": "submit_text", "label": "Submit Button Text", "type": "string", "value": "Submit" }, { "name": "submit_text_loading", "label": "Submit Button Loading Text", "type": "string", "value": "Submitting..." }, { "name": "show_reset", "label": "Show Reset Button", "type": "boolean", "value": true }, { "name": "show_cancel", "label": "Show Cancel Button", "type": "boolean", "value": false } ]; break; } const allOptions = [...baseOptions, ...typeSpecificOptions]; return JSON.stringify(allOptions, null, 2); } } exports.ServiceNowWidgetTemplateGenerator = ServiceNowWidgetTemplateGenerator; // Export singleton instance exports.widgetTemplateGenerator = new ServiceNowWidgetTemplateGenerator(); //# sourceMappingURL=widget-template-generator.js.map