diff --git a/Client-Side Components/Client Scripts/Modern JavaScript Patterns/README.md b/Client-Side Components/Client Scripts/Modern JavaScript Patterns/README.md new file mode 100644 index 0000000000..b9d46d23e3 --- /dev/null +++ b/Client-Side Components/Client Scripts/Modern JavaScript Patterns/README.md @@ -0,0 +1,200 @@ +# Modern JavaScript Patterns for ServiceNow Client Scripts + +This collection demonstrates modern JavaScript ES6+ patterns and best practices specifically adapted for ServiceNow client scripts, providing clean, maintainable, and performance-optimized code for form interactions. + +## 📋 Table of Contents + +- [Async/Await API Integration](#asyncawait-api-integration) +- [Promise-Based Form Operations](#promise-based-form-operations) +- [ES6+ Form Field Management](#es6-form-field-management) +- [Modern Event Handling](#modern-event-handling) +- [Advanced State Management](#advanced-state-management) + +## 🚀 Modern JavaScript Features for ServiceNow + +### Async/Await for GlideAjax +Replace callback-heavy GlideAjax patterns with modern async/await syntax for cleaner, more readable code. + +### Promise-Based Operations +Implement Promise patterns for form operations, field updates, and user interactions. + +### ES6+ Syntax Enhancements +- Template literals for dynamic string building +- Destructuring for clean data extraction +- Arrow functions for concise callback handling +- Classes for reusable form components + +### Modern Event Handling +- Event delegation patterns +- Debounced input handling +- Custom event systems + +## 🎯 Pattern Categories + +### API Integration Patterns +- **Async GlideAjax**: Modern promise-based server communication +- **Fetch-Style Operations**: Consistent API interaction patterns +- **Error Handling**: Comprehensive error management with try/catch + +### Form Interaction Patterns +- **Reactive Fields**: Field dependencies with modern observers +- **State Management**: Form state tracking and management +- **Validation**: Real-time validation with debouncing + +### User Experience Patterns +- **Progressive Enhancement**: Graceful degradation strategies +- **Loading States**: User feedback during async operations +- **Responsive Design**: Mobile-friendly form interactions + +### Performance Patterns +- **Debouncing**: Optimize frequent operations +- **Memoization**: Cache expensive calculations +- **Lazy Loading**: Load data and components on demand + +## 🔧 Implementation Guidelines + +### Modern JavaScript in ServiceNow +- Use ES6+ features available in modern browsers +- Implement fallbacks for legacy browser support +- Leverage ServiceNow's client-side APIs effectively + +### Performance Best Practices +- Minimize DOM manipulations +- Use efficient event handling patterns +- Implement proper cleanup for memory management + +### Code Organization +- Modular function design +- Reusable component patterns +- Clear separation of concerns + +## 📊 Pattern Examples + +### Before (Traditional) +```javascript +function onChange(control, oldValue, newValue, isLoading) { + if (isLoading || newValue == '') { + return; + } + + var ga = new GlideAjax('MyScriptInclude'); + ga.addParam('sysparm_name', 'getData'); + ga.addParam('sysparm_value', newValue); + ga.getXML(function(response) { + var answer = response.responseXML.documentElement.getAttribute("answer"); + if (answer) { + var data = JSON.parse(answer); + g_form.setValue('field1', data.value1); + g_form.setValue('field2', data.value2); + } + }); +} +``` + +### After (Modern) +```javascript +async function onChange(control, oldValue, newValue, isLoading) { + if (isLoading || !newValue) return; + + try { + const data = await fetchData(newValue); + updateFormFields(data); + } catch (error) { + handleError(error); + } +} + +const fetchData = (value) => { + return new Promise((resolve, reject) => { + const ga = new GlideAjax('MyScriptInclude'); + ga.addParam('sysparm_name', 'getData'); + ga.addParam('sysparm_value', value); + ga.getXML(response => { + try { + const answer = response.responseXML.documentElement.getAttribute("answer"); + resolve(JSON.parse(answer)); + } catch (error) { + reject(error); + } + }); + }); +}; + +const updateFormFields = ({ value1, value2 }) => { + g_form.setValue('field1', value1); + g_form.setValue('field2', value2); +}; +``` + +## 🛡️ Error Handling Patterns + +Modern error handling with comprehensive logging and user feedback: + +```javascript +const withErrorHandling = (fn) => { + return async (...args) => { + try { + return await fn(...args); + } catch (error) { + console.error('Operation failed:', error); + g_form.addErrorMessage('An error occurred. Please try again.'); + throw error; + } + }; +}; +``` + +## 🔄 State Management Patterns + +Implement reactive state management for complex form interactions: + +```javascript +class FormStateManager { + constructor() { + this.state = new Proxy({}, { + set: this.handleStateChange.bind(this) + }); + } + + handleStateChange(target, property, value) { + target[property] = value; + this.notifySubscribers(property, value); + return true; + } +} +``` + +## 📈 Performance Optimization + +### Debouncing Pattern +```javascript +const debounce = (func, delay) => { + let timeoutId; + return (...args) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => func.apply(this, args), delay); + }; +}; +``` + +### Memoization Pattern +```javascript +const memoize = (fn) => { + const cache = new Map(); + return (...args) => { + const key = JSON.stringify(args); + if (cache.has(key)) { + return cache.get(key); + } + const result = fn(...args); + cache.set(key, result); + return result; + }; +}; +``` + +## 🔗 Related Documentation + +- [ServiceNow Client Scripts Documentation](https://developer.servicenow.com/dev.do#!/learn/learning-plans/tokyo/new_to_servicenow/app_store_learnv2_automatingapps_tokyo_client_scripts) +- [Modern JavaScript (ES6+) Guide](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide) +- [ServiceNow GlideForm API](https://developer.servicenow.com/dev.do#!/reference/api/tokyo/client/c_GlideFormAPI) diff --git a/Client-Side Components/Client Scripts/Modern JavaScript Patterns/async_glideajax_patterns.js b/Client-Side Components/Client Scripts/Modern JavaScript Patterns/async_glideajax_patterns.js new file mode 100644 index 0000000000..367ffcc106 --- /dev/null +++ b/Client-Side Components/Client Scripts/Modern JavaScript Patterns/async_glideajax_patterns.js @@ -0,0 +1,626 @@ +/** + * Modern Async/Await GlideAjax Patterns + * + * This client script demonstrates modern promise-based and async/await patterns + * for ServiceNow GlideAjax operations, providing cleaner and more maintainable + * code compared to traditional callback approaches. + * + * Type: Client Script (All types: onLoad, onChange, onSubmit, onCellEdit) + * Table: Any table + * + * @author: ServiceNow Community + * @version: 1.0 + * @category: Client Scripts/Modern JavaScript + */ + +/** + * Modern GlideAjax wrapper with Promise support + */ +class ModernGlideAjax { + constructor(scriptInclude) { + this.scriptInclude = scriptInclude; + this.defaultTimeout = 30000; // 30 seconds + this.retryAttempts = 3; + this.retryDelay = 1000; // 1 second + } + + /** + * Execute server-side function with promise support + * @param {string} functionName - Server-side function name + * @param {Object} params - Parameters to pass + * @param {Object} options - Execution options + * @returns {Promise} Promise resolving to server response + */ + async execute(functionName, params = {}, options = {}) { + const config = { + timeout: options.timeout || this.defaultTimeout, + retry: options.retry !== false, + showLoading: options.showLoading !== false, + ...options + }; + + if (config.showLoading) { + this._showLoadingIndicator(); + } + + try { + const result = await this._executeWithRetry(functionName, params, config); + return result; + } finally { + if (config.showLoading) { + this._hideLoadingIndicator(); + } + } + } + + /** + * Execute with retry logic + * @param {string} functionName - Function name + * @param {Object} params - Parameters + * @param {Object} config - Configuration + * @returns {Promise} Promise resolving to result + * @private + */ + async _executeWithRetry(functionName, params, config) { + let lastError; + + for (let attempt = 1; attempt <= (config.retry ? this.retryAttempts : 1); attempt++) { + try { + return await this._executeRequest(functionName, params, config.timeout); + } catch (error) { + lastError = error; + + if (attempt < this.retryAttempts && this._isRetryableError(error)) { + console.warn(`GlideAjax attempt ${attempt} failed, retrying...`, error); + await this._delay(this.retryDelay * attempt); + } else { + break; + } + } + } + + throw lastError; + } + + /** + * Execute single GlideAjax request + * @param {string} functionName - Function name + * @param {Object} params - Parameters + * @param {number} timeout - Timeout in milliseconds + * @returns {Promise} Promise resolving to parsed response + * @private + */ + _executeRequest(functionName, params, timeout) { + return new Promise((resolve, reject) => { + const ga = new GlideAjax(this.scriptInclude); + ga.addParam('sysparm_name', functionName); + + // Add all parameters + Object.keys(params).forEach(key => { + ga.addParam(key, params[key]); + }); + + // Set up timeout + const timeoutId = setTimeout(() => { + reject(new Error(`Request timeout after ${timeout}ms`)); + }, timeout); + + ga.getXML(response => { + clearTimeout(timeoutId); + + try { + const answer = response.responseXML.documentElement.getAttribute('answer'); + + if (!answer) { + reject(new Error('Empty response from server')); + return; + } + + // Try to parse as JSON, fallback to string + let parsedResponse; + try { + parsedResponse = JSON.parse(answer); + } catch (e) { + parsedResponse = answer; + } + + // Check for server-side errors + if (parsedResponse && parsedResponse.error) { + reject(new Error(parsedResponse.error)); + return; + } + + resolve(parsedResponse); + + } catch (error) { + reject(new Error(`Failed to parse response: ${error.message}`)); + } + }); + }); + } + + /** + * Check if error is retryable + * @param {Error} error - Error to check + * @returns {boolean} True if retryable + * @private + */ + _isRetryableError(error) { + const retryableErrors = [ + 'timeout', + 'network', + 'connection', + 'server error' + ]; + + const errorMessage = error.message.toLowerCase(); + return retryableErrors.some(retryable => errorMessage.includes(retryable)); + } + + /** + * Delay execution + * @param {number} ms - Milliseconds to delay + * @returns {Promise} Promise resolving after delay + * @private + */ + _delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + /** + * Show loading indicator + * @private + */ + _showLoadingIndicator() { + if (typeof g_form !== 'undefined') { + // ServiceNow form loading indicator + g_form.addInfoMessage('Loading...'); + } + } + + /** + * Hide loading indicator + * @private + */ + _hideLoadingIndicator() { + if (typeof g_form !== 'undefined') { + g_form.clearMessages(); + } + } +} + +/** + * Async form field manager with modern patterns + */ +class AsyncFormManager { + constructor() { + this.ajax = new ModernGlideAjax('YourScriptInclude'); + this.cache = new Map(); + this.pendingRequests = new Map(); + this.observers = new Map(); + } + + /** + * Get data with caching and deduplication + * @param {string} key - Cache key + * @param {Function} dataFetcher - Function to fetch data + * @param {Object} options - Options + * @returns {Promise} Promise resolving to data + */ + async getCachedData(key, dataFetcher, options = {}) { + const { ttl = 300000, force = false } = options; // 5 minutes default TTL + + // Check cache first + if (!force && this.cache.has(key)) { + const cached = this.cache.get(key); + if (Date.now() - cached.timestamp < ttl) { + return cached.data; + } + } + + // Check for pending request + if (this.pendingRequests.has(key)) { + return await this.pendingRequests.get(key); + } + + // Create new request + const request = dataFetcher().then(data => { + this.cache.set(key, { + data, + timestamp: Date.now() + }); + this.pendingRequests.delete(key); + return data; + }).catch(error => { + this.pendingRequests.delete(key); + throw error; + }); + + this.pendingRequests.set(key, request); + return await request; + } + + /** + * Update form field with validation + * @param {string} fieldName - Field name + * @param {any} value - Field value + * @param {Object} options - Update options + * @returns {Promise} Promise resolving when update complete + */ + async updateField(fieldName, value, options = {}) { + const { validate = true, cascade = true } = options; + + try { + // Pre-validation + if (validate) { + await this.validateFieldValue(fieldName, value); + } + + // Update field + g_form.setValue(fieldName, value); + + // Trigger cascading updates if enabled + if (cascade) { + await this.processCascadingUpdates(fieldName, value); + } + + // Notify observers + this.notifyObservers(fieldName, value); + + } catch (error) { + console.error(`Failed to update field ${fieldName}:`, error); + g_form.addErrorMessage(`Failed to update ${fieldName}: ${error.message}`); + throw error; + } + } + + /** + * Validate field value against server rules + * @param {string} fieldName - Field name + * @param {any} value - Value to validate + * @returns {Promise} Promise resolving if valid + */ + async validateFieldValue(fieldName, value) { + const validationResult = await this.ajax.execute('validateField', { + table: g_form.getTableName(), + field: fieldName, + value: value, + record_id: g_form.getUniqueValue() + }); + + if (!validationResult.valid) { + throw new Error(validationResult.message || 'Validation failed'); + } + + return validationResult; + } + + /** + * Process cascading field updates + * @param {string} triggerField - Field that triggered the update + * @param {any} value - New value + * @returns {Promise} Promise resolving when cascading complete + */ + async processCascadingUpdates(triggerField, value) { + try { + const cascadeRules = await this.getCachedData( + `cascade_rules_${g_form.getTableName()}`, + () => this.ajax.execute('getCascadeRules', { + table: g_form.getTableName(), + trigger_field: triggerField + }) + ); + + if (cascadeRules && cascadeRules.length > 0) { + const updates = await Promise.allSettled( + cascadeRules.map(rule => this.applyCascadeRule(rule, value)) + ); + + // Handle any failed updates + const failures = updates.filter(result => result.status === 'rejected'); + if (failures.length > 0) { + console.warn('Some cascade updates failed:', failures); + } + } + + } catch (error) { + console.error('Failed to process cascading updates:', error); + // Don't throw - cascading failures shouldn't block the main update + } + } + + /** + * Apply individual cascade rule + * @param {Object} rule - Cascade rule + * @param {any} triggerValue - Value that triggered the cascade + * @returns {Promise} Promise resolving when rule applied + */ + async applyCascadeRule(rule, triggerValue) { + const { target_field, calculation_type, parameters } = rule; + + let newValue; + + switch (calculation_type) { + case 'lookup': + newValue = await this.performLookup(parameters, triggerValue); + break; + case 'calculation': + newValue = await this.performCalculation(parameters, triggerValue); + break; + case 'clear': + newValue = ''; + break; + default: + throw new Error(`Unknown calculation type: ${calculation_type}`); + } + + g_form.setValue(target_field, newValue); + return { field: target_field, value: newValue }; + } + + /** + * Perform lookup operation + * @param {Object} parameters - Lookup parameters + * @param {any} triggerValue - Trigger value + * @returns {Promise} Promise resolving to lookup result + */ + async performLookup(parameters, triggerValue) { + return await this.ajax.execute('performLookup', { + ...parameters, + trigger_value: triggerValue + }); + } + + /** + * Add field observer + * @param {string} fieldName - Field to observe + * @param {Function} callback - Callback function + */ + addObserver(fieldName, callback) { + if (!this.observers.has(fieldName)) { + this.observers.set(fieldName, new Set()); + } + this.observers.get(fieldName).add(callback); + } + + /** + * Remove field observer + * @param {string} fieldName - Field name + * @param {Function} callback - Callback to remove + */ + removeObserver(fieldName, callback) { + if (this.observers.has(fieldName)) { + this.observers.get(fieldName).delete(callback); + } + } + + /** + * Notify field observers + * @param {string} fieldName - Field name + * @param {any} value - New value + */ + notifyObservers(fieldName, value) { + if (this.observers.has(fieldName)) { + this.observers.get(fieldName).forEach(callback => { + try { + callback(fieldName, value); + } catch (error) { + console.error('Observer callback failed:', error); + } + }); + } + } +} + +// Create global instances for use in form scripts +const modernAjax = new ModernGlideAjax('YourScriptInclude'); +const formManager = new AsyncFormManager(); + +/** + * Modern onChange handler example + */ +async function modernOnChange(control, oldValue, newValue, isLoading, isTemplate) { + if (isLoading || isTemplate || newValue === oldValue) { + return; + } + + const fieldName = control.name; + + try { + // Example: Update related fields based on selection + if (fieldName === 'category') { + await updateCategoryDependentFields(newValue); + } + + // Example: Validate field value + if (fieldName === 'priority') { + await validatePrioritySelection(newValue); + } + + // Example: Load additional data + if (fieldName === 'assignment_group') { + await loadAssignmentGroupDetails(newValue); + } + + } catch (error) { + console.error(`onChange failed for ${fieldName}:`, error); + g_form.addErrorMessage(`Failed to process ${fieldName} change: ${error.message}`); + } +} + +/** + * Update category-dependent fields + * @param {string} categoryId - Selected category ID + */ +async function updateCategoryDependentFields(categoryId) { + if (!categoryId) { + g_form.clearValue('subcategory'); + g_form.clearValue('u_category_description'); + return; + } + + const categoryData = await formManager.getCachedData( + `category_${categoryId}`, + () => modernAjax.execute('getCategoryDetails', { category_id: categoryId }) + ); + + if (categoryData) { + // Clear and repopulate subcategory choices + g_form.clearOptions('subcategory'); + categoryData.subcategories.forEach(sub => { + g_form.addOption('subcategory', sub.value, sub.label); + }); + + // Update description field + g_form.setValue('u_category_description', categoryData.description); + + // Set field requirements based on category + const isRequired = categoryData.requires_subcategory; + g_form.setMandatory('subcategory', isRequired); + } +} + +/** + * Validate priority selection + * @param {string} priority - Selected priority + */ +async function validatePrioritySelection(priority) { + if (!priority) return; + + const validation = await modernAjax.execute('validatePriority', { + priority: priority, + caller_id: g_form.getValue('caller_id'), + category: g_form.getValue('category') + }); + + if (!validation.valid) { + g_form.addWarningMessage(validation.message); + + if (validation.suggested_priority) { + g_form.setValue('priority', validation.suggested_priority); + } + } +} + +/** + * Load assignment group details + * @param {string} groupId - Assignment group ID + */ +async function loadAssignmentGroupDetails(groupId) { + if (!groupId) { + g_form.clearValue('assigned_to'); + return; + } + + const groupDetails = await formManager.getCachedData( + `group_${groupId}`, + () => modernAjax.execute('getGroupDetails', { group_id: groupId }) + ); + + if (groupDetails) { + // Update assigned_to choices with group members + g_form.clearOptions('assigned_to'); + g_form.addOption('assigned_to', '', '-- None --'); + + groupDetails.members.forEach(member => { + g_form.addOption('assigned_to', member.sys_id, member.name); + }); + + // Set default assignee if available + if (groupDetails.default_assignee) { + g_form.setValue('assigned_to', groupDetails.default_assignee); + } + } +} + +/** + * Modern onLoad handler example + */ +async function modernOnLoad() { + try { + // Initialize form enhancements + await initializeFormEnhancements(); + + // Load initial data + await loadInitialFormData(); + + // Set up field observers + setupFieldObservers(); + + } catch (error) { + console.error('Form initialization failed:', error); + g_form.addErrorMessage('Form initialization failed. Some features may not work properly.'); + } +} + +/** + * Initialize form enhancements + */ +async function initializeFormEnhancements() { + // Example: Load user preferences + const userPrefs = await modernAjax.execute('getUserPreferences', { + user_id: g_user.userID, + table: g_form.getTableName() + }); + + if (userPrefs) { + // Apply user preferences + Object.keys(userPrefs).forEach(fieldName => { + if (g_form.isFieldVisible(fieldName)) { + g_form.setValue(fieldName, userPrefs[fieldName]); + } + }); + } +} + +/** + * Setup field observers for reactive behavior + */ +function setupFieldObservers() { + // Add observers for related field updates + formManager.addObserver('priority', (field, value) => { + console.log(`Priority changed to: ${value}`); + // Additional reactive logic here + }); + + formManager.addObserver('impact', (field, value) => { + console.log(`Impact changed to: ${value}`); + // Update urgency calculation + updateUrgencyCalculation(); + }); +} + +/** + * Utility function with debouncing + */ +const debouncedSearch = (() => { + let timeoutId; + return (searchTerm, callback, delay = 300) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + callback(searchTerm); + }, delay); + }; +})(); + +/** + * Example usage in onChange for search field + */ +async function onSearchFieldChange(control, oldValue, newValue, isLoading) { + if (isLoading || newValue === oldValue) return; + + debouncedSearch(newValue, async (term) => { + if (term.length >= 3) { + try { + const results = await modernAjax.execute('searchRecords', { + term: term, + table: 'cmdb_ci' + }); + + displaySearchResults(results); + } catch (error) { + console.error('Search failed:', error); + } + } + }); +} diff --git a/Client-Side Components/Client Scripts/Modern JavaScript Patterns/es6_field_management.js b/Client-Side Components/Client Scripts/Modern JavaScript Patterns/es6_field_management.js new file mode 100644 index 0000000000..1b2e636733 --- /dev/null +++ b/Client-Side Components/Client Scripts/Modern JavaScript Patterns/es6_field_management.js @@ -0,0 +1,822 @@ +/** + * ES6+ Form Field Management Patterns + * + * This client script demonstrates modern ES6+ JavaScript features for + * form field management, including destructuring, template literals, + * arrow functions, and class-based field controllers. + * + * Type: Client Script (All types) + * Table: Any table + * + * @author: ServiceNow Community + * @version: 1.0 + * @category: Client Scripts/ES6+ Patterns + */ + +/** + * Modern form field controller using ES6+ features + */ +class FormFieldController { + constructor(formRef = g_form) { + this.form = formRef; + this.fieldStates = new Map(); + this.validators = new Map(); + this.watchers = new Map(); + this.cache = new Map(); + + // Bind methods to preserve context + this.handleFieldChange = this.handleFieldChange.bind(this); + this.validateField = this.validateField.bind(this); + } + + /** + * Initialize field controller with configuration + * @param {Object} config - Controller configuration + */ + initialize(config = {}) { + const { + autoValidation = true, + cacheEnabled = true, + debugMode = false + } = config; + + this.config = { autoValidation, cacheEnabled, debugMode }; + + if (debugMode) { + console.log('FormFieldController initialized with config:', config); + } + } + + /** + * Set field value with ES6+ features + * @param {string} fieldName - Field name + * @param {any} value - Field value + * @param {Object} options - Set options + * @returns {Object} Operation result + */ + setFieldValue(fieldName, value, options = {}) { + const { + displayValue = false, + triggerEvents = true, + validate = false, + metadata = {} + } = options; + + try { + // Store previous value for comparison + const previousValue = this.getFieldValue(fieldName); + + // Set the value + if (displayValue) { + this.form.setDisplayValue(fieldName, value); + } else { + this.form.setValue(fieldName, value, triggerEvents); + } + + // Update field state + this.updateFieldState(fieldName, { + value, + previousValue, + displayValue, + timestamp: new Date().toISOString(), + metadata + }); + + // Trigger validation if enabled + if (validate && this.config.autoValidation) { + this.validateField(fieldName, value); + } + + return { + success: true, + field: fieldName, + value, + previousValue, + metadata + }; + + } catch (error) { + this.logError(`Failed to set field ${fieldName}:`, error); + return { + success: false, + field: fieldName, + error: error.message + }; + } + } + + /** + * Get field value with enhanced information + * @param {string} fieldName - Field name + * @param {Object} options - Get options + * @returns {Object} Field value information + */ + getFieldValue(fieldName, options = {}) { + const { + includeDisplayValue = false, + includeMetadata = false, + fromCache = this.config.cacheEnabled + } = options; + + // Check cache first + const cacheKey = `${fieldName}_${JSON.stringify(options)}`; + if (fromCache && this.cache.has(cacheKey)) { + return this.cache.get(cacheKey); + } + + const result = { + field: fieldName, + value: this.form.getValue(fieldName), + exists: this.form.isFieldVisible(fieldName) + }; + + if (includeDisplayValue) { + result.displayValue = this.form.getDisplayValue(fieldName); + } + + if (includeMetadata) { + result.metadata = this.getFieldMetadata(fieldName); + } + + // Cache result if enabled + if (this.config.cacheEnabled) { + this.cache.set(cacheKey, result); + + // Clear cache after 30 seconds + setTimeout(() => this.cache.delete(cacheKey), 30000); + } + + return result; + } + + /** + * Bulk field operations with destructuring + * @param {Object} fieldOperations - Field operations configuration + * @returns {Array} Array of operation results + */ + bulkFieldOperations(fieldOperations) { + const { + set: setOperations = {}, + clear: clearFields = [], + hide: hideFields = [], + show: showFields = [], + disable: disableFields = [], + enable: enableFields = [] + } = fieldOperations; + + const results = []; + + // Set field values + Object.entries(setOperations).forEach(([fieldName, value]) => { + results.push(this.setFieldValue(fieldName, value)); + }); + + // Clear fields + clearFields.forEach(fieldName => { + results.push(this.setFieldValue(fieldName, '')); + }); + + // Show/hide fields + [...showFields, ...hideFields].forEach(fieldName => { + const visible = showFields.includes(fieldName); + this.form.setVisible(fieldName, visible); + results.push({ + success: true, + field: fieldName, + operation: visible ? 'show' : 'hide' + }); + }); + + // Enable/disable fields + [...enableFields, ...disableFields].forEach(fieldName => { + const enabled = enableFields.includes(fieldName); + this.form.setReadOnly(fieldName, !enabled); + results.push({ + success: true, + field: fieldName, + operation: enabled ? 'enable' : 'disable' + }); + }); + + return results; + } + + /** + * Create field watcher with modern syntax + * @param {string} fieldName - Field to watch + * @param {Function} callback - Callback function + * @param {Object} options - Watcher options + */ + watchField(fieldName, callback, options = {}) { + const { + immediate = false, + debounce = 0, + condition = () => true + } = options; + + // Create debounced callback if specified + const actualCallback = debounce > 0 ? + this.debounce(callback, debounce) : callback; + + // Wrapper function with condition check + const watcherCallback = (field, oldValue, newValue) => { + if (condition(newValue, oldValue, field)) { + actualCallback(field, oldValue, newValue); + } + }; + + // Store watcher + if (!this.watchers.has(fieldName)) { + this.watchers.set(fieldName, new Set()); + } + this.watchers.get(fieldName).add(watcherCallback); + + // Execute immediately if requested + if (immediate) { + const currentValue = this.getFieldValue(fieldName).value; + watcherCallback(fieldName, null, currentValue); + } + + return { + unwatch: () => this.unwatchField(fieldName, watcherCallback) + }; + } + + /** + * Remove field watcher + * @param {string} fieldName - Field name + * @param {Function} callback - Callback to remove + */ + unwatchField(fieldName, callback) { + if (this.watchers.has(fieldName)) { + this.watchers.get(fieldName).delete(callback); + } + } + + /** + * Handle field change with watcher notification + * @param {string} fieldName - Field that changed + * @param {any} oldValue - Previous value + * @param {any} newValue - New value + */ + handleFieldChange(fieldName, oldValue, newValue) { + // Update field state + this.updateFieldState(fieldName, { + value: newValue, + previousValue: oldValue, + timestamp: new Date().toISOString() + }); + + // Notify watchers + if (this.watchers.has(fieldName)) { + this.watchers.get(fieldName).forEach(callback => { + try { + callback(fieldName, oldValue, newValue); + } catch (error) { + this.logError(`Watcher callback failed for ${fieldName}:`, error); + } + }); + } + + // Clear related cache entries + this.clearFieldCache(fieldName); + } + + /** + * Create field validator with ES6+ patterns + * @param {string} fieldName - Field to validate + * @param {Function|Array} validatorFn - Validator function(s) + * @param {Object} options - Validator options + */ + addValidator(fieldName, validatorFn, options = {}) { + const { + message = 'Validation failed', + severity = 'error', + async = false + } = options; + + const validator = { + fn: Array.isArray(validatorFn) ? validatorFn : [validatorFn], + message, + severity, + async + }; + + if (!this.validators.has(fieldName)) { + this.validators.set(fieldName, []); + } + + this.validators.get(fieldName).push(validator); + } + + /** + * Validate field with registered validators + * @param {string} fieldName - Field to validate + * @param {any} value - Value to validate + * @returns {Object} Validation result + */ + async validateField(fieldName, value = null) { + const fieldValue = value !== null ? value : this.getFieldValue(fieldName).value; + const validators = this.validators.get(fieldName) || []; + + const results = []; + + for (const validator of validators) { + for (const validatorFn of validator.fn) { + try { + const result = validator.async ? + await validatorFn(fieldValue, fieldName) : + validatorFn(fieldValue, fieldName); + + if (!result.valid) { + results.push({ + field: fieldName, + valid: false, + message: result.message || validator.message, + severity: validator.severity + }); + } + } catch (error) { + this.logError(`Validator failed for ${fieldName}:`, error); + results.push({ + field: fieldName, + valid: false, + message: 'Validation error occurred', + severity: 'error' + }); + } + } + } + + const isValid = results.length === 0; + + // Display validation messages + if (!isValid) { + const errorMessages = results + .filter(r => r.severity === 'error') + .map(r => r.message); + + if (errorMessages.length > 0) { + this.form.showFieldMsg(fieldName, errorMessages.join('; '), 'error'); + } + } else { + this.form.hideFieldMsg(fieldName); + } + + return { + valid: isValid, + results, + field: fieldName, + value: fieldValue + }; + } + + /** + * Get comprehensive field metadata + * @param {string} fieldName - Field name + * @returns {Object} Field metadata + */ + getFieldMetadata(fieldName) { + return { + visible: this.form.isFieldVisible(fieldName), + mandatory: this.form.isMandatory(fieldName), + readOnly: this.form.isReadOnly(fieldName), + newRecord: this.form.isNewRecord(), + fieldType: this.form.getField(fieldName)?.type || 'unknown', + tableName: this.form.getTableName(), + hasChoice: this.form.hasField(fieldName) && + this.form.getField(fieldName).choices !== undefined + }; + } + + /** + * Update field state with modern syntax + * @param {string} fieldName - Field name + * @param {Object} stateUpdate - State update + */ + updateFieldState(fieldName, stateUpdate) { + const existingState = this.fieldStates.get(fieldName) || {}; + + // Merge state using spread operator + const newState = { + ...existingState, + ...stateUpdate, + lastUpdated: new Date().toISOString() + }; + + this.fieldStates.set(fieldName, newState); + } + + /** + * Get field state + * @param {string} fieldName - Field name + * @returns {Object} Field state + */ + getFieldState(fieldName) { + return this.fieldStates.get(fieldName) || {}; + } + + /** + * Debounce utility function + * @param {Function} func - Function to debounce + * @param {number} delay - Delay in milliseconds + * @returns {Function} Debounced function + */ + debounce(func, delay) { + let timeoutId; + return (...args) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => func.apply(this, args), delay); + }; + } + + /** + * Clear field-related cache entries + * @param {string} fieldName - Field name + */ + clearFieldCache(fieldName) { + const keysToDelete = []; + + for (const [key] of this.cache) { + if (key.startsWith(fieldName)) { + keysToDelete.push(key); + } + } + + keysToDelete.forEach(key => this.cache.delete(key)); + } + + /** + * Log error with context + * @param {string} message - Error message + * @param {Error} error - Error object + */ + logError(message, error) { + console.error(`FormFieldController: ${message}`, error); + + if (this.config.debugMode) { + this.form.addErrorMessage(`Debug: ${message} - ${error.message}`); + } + } + + /** + * Cleanup resources + */ + destroy() { + this.fieldStates.clear(); + this.validators.clear(); + this.watchers.clear(); + this.cache.clear(); + } +} + +// Create global instance with modern syntax +const fieldController = new FormFieldController(); + +/** + * Modern field validation patterns + */ +class ModernFieldValidators { + /** + * Email validator using modern regex + * @param {string} value - Email value + * @returns {Object} Validation result + */ + static email(value) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + const valid = !value || emailRegex.test(value); + + return { + valid, + message: valid ? '' : 'Please enter a valid email address' + }; + } + + /** + * Phone number validator with modern formatting + * @param {string} value - Phone value + * @returns {Object} Validation result + */ + static phoneNumber(value) { + const phoneRegex = /^[\+]?[1-9][\d]{0,15}$/; + const cleanValue = value?.replace(/[\s\-\(\)]/g, '') || ''; + const valid = !value || phoneRegex.test(cleanValue); + + return { + valid, + message: valid ? '' : 'Please enter a valid phone number' + }; + } + + /** + * URL validator + * @param {string} value - URL value + * @returns {Object} Validation result + */ + static url(value) { + try { + const valid = !value || Boolean(new URL(value)); + return { + valid, + message: valid ? '' : 'Please enter a valid URL' + }; + } catch { + return { + valid: false, + message: 'Please enter a valid URL' + }; + } + } + + /** + * Required field validator + * @param {any} value - Field value + * @param {string} fieldName - Field name + * @returns {Object} Validation result + */ + static required(value, fieldName) { + const valid = value !== null && value !== undefined && + value.toString().trim() !== ''; + + return { + valid, + message: valid ? '' : `${fieldName} is required` + }; + } + + /** + * Length validator with options + * @param {Object} options - Length options + * @returns {Function} Validator function + */ + static length(options = {}) { + const { min = 0, max = Infinity, exact } = options; + + return (value) => { + const length = value ? value.toString().length : 0; + let valid = true; + let message = ''; + + if (exact !== undefined) { + valid = length === exact; + message = `Must be exactly ${exact} characters`; + } else { + if (length < min) { + valid = false; + message = `Must be at least ${min} characters`; + } else if (length > max) { + valid = false; + message = `Must not exceed ${max} characters`; + } + } + + return { valid, message }; + }; + } + + /** + * Custom regex validator + * @param {RegExp} regex - Regular expression + * @param {string} message - Error message + * @returns {Function} Validator function + */ + static regex(regex, message = 'Invalid format') { + return (value) => { + const valid = !value || regex.test(value); + return { + valid, + message: valid ? '' : message + }; + }; + } +} + +/** + * Modern onChange handler with ES6+ features + */ +const modernOnChange = async (control, oldValue, newValue, isLoading, isTemplate) => { + if (isLoading || isTemplate || newValue === oldValue) return; + + const { name: fieldName } = control; + + try { + // Notify field controller of change + fieldController.handleFieldChange(fieldName, oldValue, newValue); + + // Handle specific field logic with modern patterns + await handleFieldSpecificLogic(fieldName, newValue, oldValue); + + } catch (error) { + console.error(`Modern onChange failed for ${fieldName}:`, error); + g_form.addErrorMessage(`Field processing failed: ${error.message}`); + } +}; + +/** + * Handle field-specific logic with pattern matching + * @param {string} fieldName - Field name + * @param {any} newValue - New value + * @param {any} oldValue - Old value + */ +const handleFieldSpecificLogic = async (fieldName, newValue, oldValue) => { + // Modern switch-case equivalent using object mapping + const fieldHandlers = { + priority: () => handlePriorityChange(newValue), + category: () => handleCategoryChange(newValue), + caller_id: () => handleCallerChange(newValue), + impact: () => handleImpactChange(newValue), + urgency: () => handleUrgencyChange(newValue) + }; + + const handler = fieldHandlers[fieldName]; + if (handler) { + await handler(); + } +}; + +/** + * Handle priority change with modern patterns + * @param {string} priority - Priority value + */ +const handlePriorityChange = async (priority) => { + // Destructuring with default values + const priorityMappings = { + '1': { urgency: '1', escalate: true, notify: true }, + '2': { urgency: '2', escalate: false, notify: true }, + '3': { urgency: '3', escalate: false, notify: false } + }; + + const { urgency = '3', escalate = false, notify = false } = + priorityMappings[priority] || {}; + + // Bulk field operations with modern syntax + const operations = { + set: { urgency }, + ...(escalate && { set: { ...operations.set, u_escalated: 'true' } }) + }; + + fieldController.bulkFieldOperations(operations); + + if (notify) { + console.log(`Priority ${priority} set - notifications enabled`); + } +}; + +/** + * Setup modern field patterns on form load + */ +const setupModernFieldPatterns = () => { + // Initialize field controller + fieldController.initialize({ + autoValidation: true, + cacheEnabled: true, + debugMode: false + }); + + // Add modern validators using arrow functions + fieldController.addValidator('email', ModernFieldValidators.email); + fieldController.addValidator('phone', ModernFieldValidators.phoneNumber); + fieldController.addValidator('priority', ModernFieldValidators.required); + + // Add length validator for description + fieldController.addValidator( + 'short_description', + ModernFieldValidators.length({ min: 10, max: 160 }) + ); + + // Setup field watchers with modern syntax + fieldController.watchField('priority', (field, oldVal, newVal) => { + console.log(`Priority changed: ${oldVal} → ${newVal}`); + }, { debounce: 300 }); + + // Watch multiple related fields + ['impact', 'urgency'].forEach(field => { + fieldController.watchField(field, () => updatePriorityCalculation(), { + debounce: 200, + condition: (value) => value !== '' + }); + }); +}; + +/** + * Modern form state management + */ +class FormStateManager { + constructor() { + this.state = new Proxy({}, { + set: (target, property, value) => { + const oldValue = target[property]; + target[property] = value; + this.notifyStateChange(property, value, oldValue); + return true; + } + }); + + this.subscribers = new Map(); + } + + /** + * Subscribe to state changes + * @param {string} property - Property to watch + * @param {Function} callback - Callback function + */ + subscribe(property, callback) { + if (!this.subscribers.has(property)) { + this.subscribers.set(property, new Set()); + } + this.subscribers.get(property).add(callback); + + return () => this.unsubscribe(property, callback); + } + + /** + * Unsubscribe from state changes + * @param {string} property - Property name + * @param {Function} callback - Callback to remove + */ + unsubscribe(property, callback) { + if (this.subscribers.has(property)) { + this.subscribers.get(property).delete(callback); + } + } + + /** + * Notify subscribers of state change + * @param {string} property - Property that changed + * @param {any} newValue - New value + * @param {any} oldValue - Old value + */ + notifyStateChange(property, newValue, oldValue) { + if (this.subscribers.has(property)) { + this.subscribers.get(property).forEach(callback => { + try { + callback(newValue, oldValue, property); + } catch (error) { + console.error('State change callback failed:', error); + } + }); + } + } + + /** + * Update state with object spread + * @param {Object} updates - State updates + */ + updateState(updates) { + Object.assign(this.state, updates); + } + + /** + * Get current state + * @returns {Object} Current state + */ + getState() { + return { ...this.state }; + } +} + +// Create global state manager +const formState = new FormStateManager(); + +/** + * Modern template literal helpers + */ +const Templates = { + /** + * Create error message template + * @param {string} field - Field name + * @param {string} error - Error message + * @returns {string} Formatted error message + */ + errorMessage: (field, error) => + `⚠️ ${field.replace(/_/g, ' ').toUpperCase()}: ${error}`, + + /** + * Create info message template + * @param {string} action - Action performed + * @param {string} field - Field name + * @returns {string} Formatted info message + */ + infoMessage: (action, field) => + `✅ ${action} completed for ${field.replace(/_/g, ' ')}`, + + /** + * Create field label template + * @param {string} field - Field name + * @returns {string} Formatted field label + */ + fieldLabel: (field) => + field.split('_').map(word => + word.charAt(0).toUpperCase() + word.slice(1) + ).join(' ') +}; + +// Export for use in other scripts +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + FormFieldController, + ModernFieldValidators, + FormStateManager, + Templates + }; +} diff --git a/Client-Side Components/Client Scripts/Modern JavaScript Patterns/promise_based_operations.js b/Client-Side Components/Client Scripts/Modern JavaScript Patterns/promise_based_operations.js new file mode 100644 index 0000000000..07e58ff7db --- /dev/null +++ b/Client-Side Components/Client Scripts/Modern JavaScript Patterns/promise_based_operations.js @@ -0,0 +1,740 @@ +/** + * Promise-Based Form Operations + * + * This client script demonstrates promise-based patterns for form operations, + * providing consistent error handling, better composability, and improved + * code organization for complex form interactions. + * + * Type: Client Script (onLoad, onChange, onSubmit) + * Table: Any table + * + * @author: ServiceNow Community + * @version: 1.0 + * @category: Client Scripts/Promise Patterns + */ + +/** + * Promise-based form operation utilities + */ +class PromiseFormOperations { + constructor() { + this.operationQueue = []; + this.isProcessing = false; + this.defaultTimeout = 15000; + } + + /** + * Set field value with promise-based validation + * @param {string} fieldName - Field name + * @param {any} value - New value + * @param {Object} options - Operation options + * @returns {Promise} Promise resolving when operation completes + */ + setValue(fieldName, value, options = {}) { + return new Promise(async (resolve, reject) => { + try { + const { validate = false, trigger = true, displayValue = false } = options; + + // Pre-validation if requested + if (validate) { + const isValid = await this.validateFieldValue(fieldName, value); + if (!isValid.valid) { + reject(new Error(isValid.message)); + return; + } + } + + // Set the value + if (displayValue) { + g_form.setDisplayValue(fieldName, value); + } else { + g_form.setValue(fieldName, value, trigger); + } + + // Wait for any triggered events to complete + if (trigger) { + await this.waitForFieldUpdates(fieldName); + } + + resolve({ + field: fieldName, + value: value, + success: true + }); + + } catch (error) { + reject(error); + } + }); + } + + /** + * Get field value with promise interface + * @param {string} fieldName - Field name + * @param {Object} options - Get options + * @returns {Promise} Promise resolving to field value + */ + getValue(fieldName, options = {}) { + return new Promise((resolve, reject) => { + try { + const { displayValue = false, reference = false } = options; + + let value; + if (displayValue) { + value = g_form.getDisplayValue(fieldName); + } else if (reference && g_form.isReferenceField(fieldName)) { + value = { + value: g_form.getValue(fieldName), + displayValue: g_form.getDisplayValue(fieldName), + tableName: g_form.getTableName(fieldName) + }; + } else { + value = g_form.getValue(fieldName); + } + + resolve(value); + } catch (error) { + reject(error); + } + }); + } + + /** + * Submit form with comprehensive validation + * @param {Object} options - Submit options + * @returns {Promise} Promise resolving when submit completes + */ + submitForm(options = {}) { + return new Promise(async (resolve, reject) => { + try { + const { validate = true, showProgress = true } = options; + + if (showProgress) { + this.showProgress('Submitting form...'); + } + + // Pre-submit validation + if (validate) { + const validation = await this.validateAllFields(); + if (!validation.valid) { + reject(new Error('Form validation failed: ' + validation.errors.join(', '))); + return; + } + } + + // Attempt form submission + const submitResult = await this.performSubmit(); + + if (submitResult.success) { + resolve(submitResult); + } else { + reject(new Error(submitResult.message || 'Form submission failed')); + } + + } catch (error) { + reject(error); + } finally { + if (options.showProgress) { + this.hideProgress(); + } + } + }); + } + + /** + * Load related records with promise interface + * @param {string} table - Table name + * @param {Object} query - Query parameters + * @param {Object} options - Load options + * @returns {Promise} Promise resolving to records + */ + loadRelatedRecords(table, query, options = {}) { + return new Promise((resolve, reject) => { + const { limit = 100, orderBy = 'sys_created_on' } = options; + + const ga = new GlideAjax('ClientScriptHelper'); + ga.addParam('sysparm_name', 'getRelatedRecords'); + ga.addParam('table', table); + ga.addParam('query', JSON.stringify(query)); + ga.addParam('limit', limit); + ga.addParam('orderBy', orderBy); + + const timeout = setTimeout(() => { + reject(new Error('Request timeout')); + }, this.defaultTimeout); + + ga.getXML((response) => { + clearTimeout(timeout); + + try { + const answer = response.responseXML.documentElement.getAttribute('answer'); + const result = JSON.parse(answer); + + if (result.error) { + reject(new Error(result.error)); + } else { + resolve(result.records || []); + } + } catch (error) { + reject(new Error('Failed to parse response: ' + error.message)); + } + }); + }); + } + + /** + * Update multiple fields atomically + * @param {Object} fieldValues - Field name/value pairs + * @param {Object} options - Update options + * @returns {Promise} Promise resolving when all updates complete + */ + updateMultipleFields(fieldValues, options = {}) { + return new Promise(async (resolve, reject) => { + try { + const { atomic = true, validate = false } = options; + const updates = []; + const rollbackValues = {}; + + // Store current values for potential rollback + if (atomic) { + for (const fieldName of Object.keys(fieldValues)) { + rollbackValues[fieldName] = await this.getValue(fieldName); + } + } + + // Process all updates + for (const [fieldName, value] of Object.entries(fieldValues)) { + try { + const updateResult = await this.setValue(fieldName, value, { + validate, + trigger: false // Prevent cascading during batch update + }); + updates.push(updateResult); + } catch (error) { + if (atomic) { + // Rollback previous updates + await this.rollbackUpdates(rollbackValues); + reject(new Error(`Atomic update failed at ${fieldName}: ${error.message}`)); + return; + } else { + console.warn(`Failed to update ${fieldName}: ${error.message}`); + updates.push({ + field: fieldName, + value: value, + success: false, + error: error.message + }); + } + } + } + + // Trigger onChange events for all updated fields + Object.keys(fieldValues).forEach(fieldName => { + if (g_form.isFieldVisible(fieldName)) { + g_form.fieldChanged(fieldName, true); + } + }); + + resolve({ + updates: updates, + success: true, + totalUpdated: updates.filter(u => u.success).length + }); + + } catch (error) { + reject(error); + } + }); + } + + /** + * Clear multiple fields with options + * @param {Array} fieldNames - Fields to clear + * @param {Object} options - Clear options + * @returns {Promise} Promise resolving when clearing completes + */ + clearFields(fieldNames, options = {}) { + const clearValues = {}; + fieldNames.forEach(fieldName => { + clearValues[fieldName] = ''; + }); + + return this.updateMultipleFields(clearValues, options); + } + + /** + * Validate field value against business rules + * @param {string} fieldName - Field name + * @param {any} value - Value to validate + * @returns {Promise} Promise resolving to validation result + */ + validateFieldValue(fieldName, value) { + return new Promise((resolve, reject) => { + const ga = new GlideAjax('FieldValidationHelper'); + ga.addParam('sysparm_name', 'validateField'); + ga.addParam('table', g_form.getTableName()); + ga.addParam('field', fieldName); + ga.addParam('value', value); + ga.addParam('record_id', g_form.getUniqueValue()); + + ga.getXML((response) => { + try { + const answer = response.responseXML.documentElement.getAttribute('answer'); + const result = JSON.parse(answer); + resolve(result); + } catch (error) { + reject(error); + } + }); + }); + } + + /** + * Validate all form fields + * @returns {Promise} Promise resolving to overall validation result + */ + validateAllFields() { + return new Promise(async (resolve) => { + const errors = []; + const warnings = []; + + // Get all visible fields + const fields = this.getVisibleFields(); + + // Validate each field + for (const fieldName of fields) { + try { + const value = await this.getValue(fieldName); + const validation = await this.validateFieldValue(fieldName, value); + + if (!validation.valid) { + if (validation.severity === 'error') { + errors.push(`${fieldName}: ${validation.message}`); + } else { + warnings.push(`${fieldName}: ${validation.message}`); + } + } + } catch (error) { + errors.push(`${fieldName}: Validation failed - ${error.message}`); + } + } + + resolve({ + valid: errors.length === 0, + errors: errors, + warnings: warnings + }); + }); + } + + /** + * Wait for field updates to complete + * @param {string} fieldName - Field name + * @param {number} timeout - Timeout in milliseconds + * @returns {Promise} Promise resolving when updates complete + */ + waitForFieldUpdates(fieldName, timeout = 5000) { + return new Promise((resolve) => { + let checks = 0; + const maxChecks = timeout / 100; + + const checkForUpdates = () => { + checks++; + + // Check if any async operations are pending + if (checks >= maxChecks || !this.hasOpenAjaxRequests()) { + resolve(); + } else { + setTimeout(checkForUpdates, 100); + } + }; + + setTimeout(checkForUpdates, 100); + }); + } + + /** + * Perform actual form submission + * @returns {Promise} Promise resolving to submit result + * @private + */ + performSubmit() { + return new Promise((resolve) => { + // Override the default submit action + const originalAction = window.gsftSubmit; + + window.gsftSubmit = function(action) { + // Restore original function + window.gsftSubmit = originalAction; + + // Perform submission and resolve promise + try { + const result = originalAction.call(this, action); + resolve({ success: true, result: result }); + } catch (error) { + resolve({ success: false, message: error.message }); + } + }; + + // Trigger form submission + g_form.submit(); + }); + } + + /** + * Rollback field updates + * @param {Object} rollbackValues - Values to restore + * @returns {Promise} Promise resolving when rollback completes + * @private + */ + async rollbackUpdates(rollbackValues) { + for (const [fieldName, value] of Object.entries(rollbackValues)) { + try { + await this.setValue(fieldName, value, { validate: false, trigger: false }); + } catch (error) { + console.error(`Failed to rollback ${fieldName}: ${error.message}`); + } + } + } + + /** + * Get all visible fields on the form + * @returns {Array} Array of field names + * @private + */ + getVisibleFields() { + const fields = []; + const sections = g_form.getSections(); + + sections.forEach(section => { + const sectionFields = g_form.getSectionFields(section); + sectionFields.forEach(field => { + if (g_form.isFieldVisible(field)) { + fields.push(field); + } + }); + }); + + return fields; + } + + /** + * Check if there are pending AJAX requests + * @returns {boolean} True if requests are pending + * @private + */ + hasOpenAjaxRequests() { + // Check ServiceNow's internal AJAX queue if available + if (typeof CustomEvent !== 'undefined' && window.g_ajax_processors) { + return Object.keys(window.g_ajax_processors).length > 0; + } + return false; + } + + /** + * Show progress indicator + * @param {string} message - Progress message + * @private + */ + showProgress(message) { + g_form.addInfoMessage(message); + } + + /** + * Hide progress indicator + * @private + */ + hideProgress() { + g_form.clearMessages(); + } +} + +// Create global instance +const promiseFormOps = new PromiseFormOperations(); + +/** + * Promise-based field dependency manager + */ +class FieldDependencyManager { + constructor() { + this.dependencies = new Map(); + this.isProcessing = false; + } + + /** + * Add field dependency + * @param {string} sourceField - Source field that triggers the dependency + * @param {string} targetField - Target field that gets updated + * @param {Function} calculator - Function to calculate new value + * @param {Object} options - Dependency options + */ + addDependency(sourceField, targetField, calculator, options = {}) { + if (!this.dependencies.has(sourceField)) { + this.dependencies.set(sourceField, []); + } + + this.dependencies.get(sourceField).push({ + targetField, + calculator, + options + }); + } + + /** + * Process dependencies for a field change + * @param {string} sourceField - Field that changed + * @param {any} newValue - New value + * @returns {Promise} Promise resolving when all dependencies processed + */ + async processDependencies(sourceField, newValue) { + if (this.isProcessing) { + return; // Prevent recursive processing + } + + this.isProcessing = true; + + try { + const dependencies = this.dependencies.get(sourceField) || []; + + // Process dependencies in parallel + const updates = await Promise.allSettled( + dependencies.map(dep => this.processSingleDependency(dep, newValue)) + ); + + // Report any failures + const failures = updates.filter(result => result.status === 'rejected'); + if (failures.length > 0) { + console.warn('Some dependency updates failed:', failures); + } + + } finally { + this.isProcessing = false; + } + } + + /** + * Process single dependency + * @param {Object} dependency - Dependency configuration + * @param {any} sourceValue - Source field value + * @returns {Promise} Promise resolving when dependency processed + * @private + */ + async processSingleDependency(dependency, sourceValue) { + const { targetField, calculator, options } = dependency; + const { condition, delay = 0 } = options; + + // Check condition if specified + if (condition && !condition(sourceValue)) { + return; + } + + // Add delay if specified + if (delay > 0) { + await new Promise(resolve => setTimeout(resolve, delay)); + } + + // Calculate new value + const newValue = await calculator(sourceValue, targetField); + + // Update target field + if (newValue !== undefined) { + await promiseFormOps.setValue(targetField, newValue); + } + } +} + +// Create global dependency manager +const dependencyManager = new FieldDependencyManager(); + +/** + * Promise-based onChange handler example + */ +async function promiseOnChange(control, oldValue, newValue, isLoading, isTemplate) { + if (isLoading || isTemplate || newValue === oldValue) { + return; + } + + const fieldName = control.name; + + try { + // Process field dependencies + await dependencyManager.processDependencies(fieldName, newValue); + + // Handle specific field logic + switch (fieldName) { + case 'priority': + await handlePriorityChange(newValue); + break; + case 'category': + await handleCategoryChange(newValue); + break; + case 'caller_id': + await handleCallerChange(newValue); + break; + } + + } catch (error) { + console.error(`Promise-based onChange failed for ${fieldName}:`, error); + g_form.addErrorMessage(`Failed to process ${fieldName} change: ${error.message}`); + } +} + +/** + * Handle priority change with promise pattern + * @param {string} priority - New priority value + * @returns {Promise} Promise resolving when handling completes + */ +async function handlePriorityChange(priority) { + const updates = {}; + + // Set urgency based on priority + const urgencyMap = { '1': '1', '2': '2', '3': '3', '4': '3', '5': '3' }; + updates.urgency = urgencyMap[priority] || '3'; + + // Update impact if priority is critical + if (priority === '1') { + updates.impact = '1'; + updates.u_escalated = 'true'; + } + + await promiseFormOps.updateMultipleFields(updates); +} + +/** + * Handle category change with promise pattern + * @param {string} category - New category value + * @returns {Promise} Promise resolving when handling completes + */ +async function handleCategoryChange(category) { + if (!category) { + await promiseFormOps.clearFields(['subcategory', 'assignment_group']); + return; + } + + try { + // Load category data + const categoryData = await promiseFormOps.loadRelatedRecords('incident_category', { + name: category + }); + + if (categoryData.length > 0) { + const cat = categoryData[0]; + const updates = {}; + + if (cat.default_assignment_group) { + updates.assignment_group = cat.default_assignment_group; + } + + if (cat.default_priority) { + updates.priority = cat.default_priority; + } + + await promiseFormOps.updateMultipleFields(updates); + } + + } catch (error) { + console.error('Failed to load category data:', error); + } +} + +/** + * Promise-based onSubmit handler + */ +async function promiseOnSubmit() { + try { + // Perform comprehensive form validation + const validation = await promiseFormOps.validateAllFields(); + + if (!validation.valid) { + g_form.addErrorMessage('Please correct the following errors:\n' + + validation.errors.join('\n')); + return false; + } + + // Show warnings if any + if (validation.warnings.length > 0) { + g_form.addWarningMessage('Please review:\n' + + validation.warnings.join('\n')); + } + + // Additional business logic validation + const businessValidation = await validateBusinessRules(); + + if (!businessValidation.valid) { + g_form.addErrorMessage(businessValidation.message); + return false; + } + + return true; + + } catch (error) { + console.error('Form submission validation failed:', error); + g_form.addErrorMessage('Validation failed: ' + error.message); + return false; + } +} + +/** + * Validate business rules before submission + * @returns {Promise} Promise resolving to validation result + */ +async function validateBusinessRules() { + const priority = await promiseFormOps.getValue('priority'); + const category = await promiseFormOps.getValue('category'); + const assignmentGroup = await promiseFormOps.getValue('assignment_group'); + + // Example business rule: High priority incidents must have assignment group + if ((priority === '1' || priority === '2') && !assignmentGroup) { + return { + valid: false, + message: 'High priority incidents must have an assignment group' + }; + } + + // Example business rule: Certain categories require approval + const approvalRequiredCategories = ['security', 'data_breach']; + if (approvalRequiredCategories.includes(category)) { + const approval = await promiseFormOps.getValue('u_approval_status'); + if (!approval || approval === 'pending') { + return { + valid: false, + message: 'This category requires approval before submission' + }; + } + } + + return { valid: true }; +} + +/** + * Setup field dependencies on form load + */ +function setupPromiseBasedDependencies() { + // Priority affects urgency + dependencyManager.addDependency('priority', 'urgency', async (priority) => { + const urgencyMap = { '1': '1', '2': '2', '3': '3', '4': '3', '5': '3' }; + return urgencyMap[priority] || '3'; + }); + + // Category affects assignment group + dependencyManager.addDependency('category', 'assignment_group', async (category) => { + if (!category) return ''; + + const categoryData = await promiseFormOps.loadRelatedRecords('incident_category', { + name: category + }); + + return categoryData.length > 0 ? categoryData[0].default_assignment_group : ''; + }); + + // Caller affects location and company + dependencyManager.addDependency('caller_id', 'location', async (callerId) => { + if (!callerId) return ''; + + const userData = await promiseFormOps.loadRelatedRecords('sys_user', { + sys_id: callerId + }); + + return userData.length > 0 ? userData[0].location : ''; + }); +}