diff --git a/.github/scripts/validate-structure.js b/.github/scripts/validate-structure.js new file mode 100644 index 0000000000..ec4fb4541b --- /dev/null +++ b/.github/scripts/validate-structure.js @@ -0,0 +1,121 @@ +#!/usr/bin/env node + +const { execSync } = require('child_process'); + +const allowedCategories = new Set([ + 'Core ServiceNow APIs', + 'Server-Side Components', + 'Client-Side Components', + 'Modern Development', + 'Integration', + 'Specialized Areas' +]); + +function resolveDiffRange() { + if (process.argv[2]) { + return process.argv[2]; + } + + const inCI = process.env.GITHUB_ACTIONS === 'true'; + if (!inCI) { + return 'origin/main...HEAD'; + } + + const base = process.env.GITHUB_BASE_REF ? `origin/${process.env.GITHUB_BASE_REF}` : 'origin/main'; + const head = process.env.GITHUB_SHA || 'HEAD'; + return `${base}...${head}`; +} + +function getChangedFiles(diffRange) { + let output; + try { + output = execSync(`git diff --name-only --diff-filter=ACMR ${diffRange}`, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'] + }); + } catch (error) { + console.error('Failed to collect changed files. Ensure the base branch is fetched.'); + console.error(error.stderr?.toString() || error.message); + process.exit(1); + } + + return output + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); +} + +function validateFilePath(filePath) { + const normalized = filePath.replace(/\\/g, '/'); + const segments = normalized.split('/'); + + // Check for invalid characters that break local file systems + for (let i = 0; i < segments.length; i++) { + const segment = segments[i]; + + // Check for trailing periods (invalid on Windows) + if (segment.endsWith('.')) { + return `Invalid folder/file name '${segment}' in path '${normalized}': Names cannot end with a period (.) as this breaks local file system sync on Windows.`; + } + + // Check for trailing spaces (invalid on Windows) + if (segment.endsWith(' ')) { + return `Invalid folder/file name '${segment}' in path '${normalized}': Names cannot end with a space as this breaks local file system sync on Windows.`; + } + + // Check for reserved Windows names + const reservedNames = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9']; + const nameWithoutExt = segment.split('.')[0].toUpperCase(); + if (reservedNames.includes(nameWithoutExt)) { + return `Invalid folder/file name '${segment}' in path '${normalized}': '${nameWithoutExt}' is a reserved name on Windows and will break local file system sync.`; + } + + // Check for invalid characters (Windows and general file system restrictions) + const invalidChars = /[<>:"|?*\x00-\x1F]/; + if (invalidChars.test(segment)) { + return `Invalid folder/file name '${segment}' in path '${normalized}': Contains characters that are invalid on Windows file systems (< > : " | ? * or control characters).`; + } + } + + if (!allowedCategories.has(segments[0])) { + return null; + } + + // Files must live under: Category/Subcategory/SpecificUseCase/ + if (segments.length < 4) { + return `Move '${normalized}' under a valid folder hierarchy (Category/Subcategory/Use-Case/your-file). Files directly inside '${segments[0]}' or its subcategories are not allowed.`; + } + + return null; +} + +function main() { + const diffRange = resolveDiffRange(); + const changedFiles = getChangedFiles(diffRange); + + if (changedFiles.length === 0) { + console.log('No relevant file changes detected.'); + return; + } + + const problems = []; + + for (const filePath of changedFiles) { + const issue = validateFilePath(filePath); + if (issue) { + problems.push(issue); + } + } + + if (problems.length > 0) { + console.error('Folder structure violations found:'); + for (const msg of problems) { + console.error(` - ${msg}`); + } + process.exit(1); + } + + console.log('Folder structure looks good.'); +} + +main(); diff --git a/.github/workflows/pr-auto-unassign-stale.yml b/.github/workflows/pr-auto-unassign-stale.yml new file mode 100644 index 0000000000..5bc93bc475 --- /dev/null +++ b/.github/workflows/pr-auto-unassign-stale.yml @@ -0,0 +1,154 @@ +name: Auto-unassign stale PR assignees + +on: + schedule: + - cron: "*/15 * * * *" # run every 15 minutes + workflow_dispatch: + inputs: + enabled: + description: "Enable this automation" + type: boolean + default: true + max_age_minutes: + description: "Unassign if assigned longer than X minutes" + type: number + default: 60 + dry_run: + description: "Preview only; do not change assignees" + type: boolean + default: false + +permissions: + pull-requests: write + issues: write + +env: + # Defaults (can be overridden via workflow_dispatch inputs) + ENABLED: "true" + MAX_ASSIGN_AGE_MINUTES: "60" + DRY_RUN: "false" + +jobs: + sweep: + runs-on: ubuntu-latest + steps: + - name: Resolve inputs into env + run: | + # Prefer manual run inputs when present + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "ENABLED=${{ inputs.enabled }}" >> $GITHUB_ENV + echo "MAX_ASSIGN_AGE_MINUTES=${{ inputs.max_age_minutes }}" >> $GITHUB_ENV + echo "DRY_RUN=${{ inputs.dry_run }}" >> $GITHUB_ENV + fi + echo "Effective config: ENABLED=$ENABLED, MAX_ASSIGN_AGE_MINUTES=$MAX_ASSIGN_AGE_MINUTES, DRY_RUN=$DRY_RUN" + + - name: Exit if disabled + if: ${{ env.ENABLED != 'true' && env.ENABLED != 'True' && env.ENABLED != 'TRUE' }} + run: echo "Disabled via ENABLED=$ENABLED. Exiting." && exit 0 + + - name: Unassign stale assignees + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + + const MAX_MIN = parseInt(process.env.MAX_ASSIGN_AGE_MINUTES || "60", 10); + const DRY_RUN = ["true","True","TRUE","1","yes"].includes(String(process.env.DRY_RUN)); + const now = new Date(); + + core.info(`Scanning open PRs. Threshold = ${MAX_MIN} minutes. DRY_RUN=${DRY_RUN}`); + + // List all open PRs + const prs = await github.paginate(github.rest.pulls.list, { + owner, repo, state: "open", per_page: 100 + }); + + let totalUnassigned = 0; + + for (const pr of prs) { + if (!pr.assignees || pr.assignees.length === 0) continue; + + const number = pr.number; + core.info(`PR #${number}: "${pr.title}" β€” assignees: ${pr.assignees.map(a => a.login).join(", ")}`); + + // Pull reviews (to see if an assignee started a review) + const reviews = await github.paginate(github.rest.pulls.listReviews, { + owner, repo, pull_number: number, per_page: 100 + }); + + // Issue comments (general comments) + const issueComments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number: number, per_page: 100 + }); + + // Review comments (file-level) + const reviewComments = await github.paginate(github.rest.pulls.listReviewComments, { + owner, repo, pull_number: number, per_page: 100 + }); + + // Issue events (to find assignment timestamps) + const issueEvents = await github.paginate(github.rest.issues.listEvents, { + owner, repo, issue_number: number, per_page: 100 + }); + + for (const a of pr.assignees) { + const assignee = a.login; + + // Find the most recent "assigned" event for this assignee + const assignedEvents = issueEvents + .filter(e => e.event === "assigned" && e.assignee && e.assignee.login === assignee) + .sort((x, y) => new Date(y.created_at) - new Date(x.created_at)); + + if (assignedEvents.length === 0) { + core.info(` - @${assignee}: no 'assigned' event found; skipping.`); + continue; + } + + const assignedAt = new Date(assignedEvents[0].created_at); + const ageMin = (now - assignedAt) / 60000; + + // Has the assignee commented (issue or review comments) or reviewed? + const hasIssueComment = issueComments.some(c => c.user?.login === assignee); + const hasReviewComment = reviewComments.some(c => c.user?.login === assignee); + const hasReview = reviews.some(r => r.user?.login === assignee); + + const eligible = + ageMin >= MAX_MIN && + !hasIssueComment && + !hasReviewComment && + !hasReview && + pr.state === "open"; + + core.info(` - @${assignee}: assigned ${ageMin.toFixed(1)} min ago; commented=${hasIssueComment || hasReviewComment}; reviewed=${hasReview}; open=${pr.state==='open'} => ${eligible ? 'ELIGIBLE' : 'skip'}`); + + if (!eligible) continue; + + if (DRY_RUN) { + core.notice(`Would unassign @${assignee} from PR #${number}`); + } else { + try { + await github.rest.issues.removeAssignees({ + owner, repo, issue_number: number, assignees: [assignee] + }); + totalUnassigned += 1; + // Optional: leave a gentle heads-up comment + await github.rest.issues.createComment({ + owner, repo, issue_number: number, + body: `πŸ‘‹ Unassigning @${assignee} due to inactivity (> ${MAX_MIN} min without comments/reviews). This PR remains open for other reviewers.` + }); + core.info(` Unassigned @${assignee} from #${number}`); + } catch (err) { + core.warning(` Failed to unassign @${assignee} from #${number}: ${err.message}`); + } + } + } + } + + core.summary + .addHeading('Auto-unassign report') + .addRaw(`Threshold: ${MAX_MIN} minutes\n\n`) + .addRaw(`Total unassignments: ${totalUnassigned}\n`) + .write(); + + result-encoding: string diff --git a/.github/workflows/validate-structure.yml b/.github/workflows/validate-structure.yml new file mode 100644 index 0000000000..86979f70a7 --- /dev/null +++ b/.github/workflows/validate-structure.yml @@ -0,0 +1,134 @@ +ο»Ώname: Validate Folder Structure + +on: + pull_request_target: + branches: + - main + +permissions: + contents: read + pull-requests: write + +concurrency: + group: folder-structure-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +jobs: + structure: + runs-on: ubuntu-latest + steps: + - name: Checkout base repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Cache validation script + run: cp .github/scripts/validate-structure.js "$RUNNER_TEMP/validate-structure.js" + + - name: Fetch pull request head + id: fetch_head + env: + PR_REMOTE_URL: https://github.com/${{ github.event.pull_request.head.repo.full_name }}.git + PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} + run: | + git remote remove pr >/dev/null 2>&1 || true + git remote add pr "$PR_REMOTE_URL" + if git fetch pr "$PR_HEAD_REF":pr-head --no-tags; then + git checkout pr-head + git fetch origin "${{ github.event.pull_request.base.ref }}" + echo "fetched=true" >> "$GITHUB_OUTPUT" + else + echo "::warning::Unable to fetch fork repository. Skipping structure validation." + echo "fetched=false" >> "$GITHUB_OUTPUT" + fi + + - name: Use Node.js 18 + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Validate folder layout + if: ${{ steps.fetch_head.outputs.fetched == 'true' }} + id: validate + run: | + set -euo pipefail + + tmp_output=$(mktemp) + tmp_error=$(mktemp) + + set +e + node "$RUNNER_TEMP/validate-structure.js" origin/${{ github.event.pull_request.base.ref }}...HEAD >"$tmp_output" 2>"$tmp_error" + status=$? + set -e + + cat "$tmp_output" + cat "$tmp_error" >&2 + + if grep -q 'Folder structure violations found' "$tmp_output" "$tmp_error"; then + # Save validation output for use in PR comment + cat "$tmp_output" "$tmp_error" > "$RUNNER_TEMP/validation_output.txt" + echo "status=failed" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [ $status -ne 0 ]; then + echo "::warning::Structure validation skipped because the diff could not be evaluated (exit code $status)." + echo "status=skipped" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "status=passed" >> "$GITHUB_OUTPUT" + + - name: Close pull request on failure + if: ${{ steps.validate.outputs.status == 'failed' }} + uses: actions/github-script@v6 + with: + github-token: ${{ github.token }} + script: | + const pullNumber = context.payload.pull_request.number; + const owner = context.repo.owner; + const repo = context.repo.repo; + + const fs = require('fs'); + const output = fs.readFileSync(process.env.RUNNER_TEMP + '/validation_output.txt', 'utf8'); + + let commentBody = `Thank you for your contribution. However, it doesn't comply with our contributing guidelines.\n\n`; + + // Check if the error is about invalid file/folder names + if (output.includes('Names cannot end with a period') || + output.includes('Names cannot end with a space') || + output.includes('is a reserved name on Windows') || + output.includes('Contains characters that are invalid')) { + commentBody += `**❌ Invalid File/Folder Names Detected**\n\n`; + commentBody += `Your contribution contains file or folder names that will break when syncing to local file systems (especially Windows):\n\n`; + commentBody += `\`\`\`\n${output}\n\`\`\`\n\n`; + commentBody += `**Common issues:**\n`; + commentBody += `- Folder/file names ending with a period (.) - not allowed on Windows\n`; + commentBody += `- Folder/file names ending with spaces - not allowed on Windows\n`; + commentBody += `- Reserved names like CON, PRN, AUX, NUL, COM1-9, LPT1-9 - not allowed on Windows\n`; + commentBody += `- Invalid characters: < > : " | ? * or control characters\n\n`; + commentBody += `Please rename these files/folders to be compatible with all operating systems.\n\n`; + } else { + commentBody += `As a reminder, the general requirements (as outlined in the [CONTRIBUTING.md file](https://github.com/ServiceNowDevProgram/code-snippets/blob/main/CONTRIBUTING.md)) are the following: follow the folder+subfolder guidelines and include a README.md file explaining what the code snippet does.\n\n`; + commentBody += `**Validation errors:**\n\`\`\`\n${output}\n\`\`\`\n\n`; + } + + commentBody += `Review your contribution against the guidelines and make the necessary adjustments. Closing this for now. Once you make additional changes, feel free to re-open this Pull Request or create a new one.`; + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pullNumber, + body: commentBody.trim() + }); + + await github.rest.pulls.update({ + owner, + repo, + pull_number: pullNumber, + state: 'closed' + }); + + - name: Mark job as failed if validation failed + if: ${{ steps.validate.outputs.status == 'failed' }} + run: exit 1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..1bd1f57045 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,113 @@ +# How to Contribute + +We welcome contributions to the **ServiceNow Developer Program's Code Snippets Repository**! Follow these steps to get involved: + +## Steps to Contribute + +1. **Fork the Repository**: Click the "Fork" button on the top right of this page to create your own copy of the repository. + +2. **Create a New Branch**: + - Name your branch according to the functionality you are adding (e.g., `feature/new-snippet` or `bugfix/fix-issue`). + - Switch to your new branch from the main branch dropdown. + +3. **Add or Edit Code Snippets**: + - Navigate to the appropriate folders and files to add, edit, or reorganize code snippets. + - Commit your changes to your forked repository. + +4. **Submit a Pull Request**: + - Go to the original repository and click on the "Pull Requests" tab. + - Click "New Pull Request" and select your branch. + - Ensure your pull request has a descriptive title and comment that outlines what changes you made. + - Only include files relevant to the changes described in the pull request title and description. + - Avoid submitting XML exports of ServiceNow records. + +That's it! A Developer Advocate or a designated approver from the ServiceNow Dev Program will review your pull request. If approved, it will be merged into the main repository for everyone's benefit! + +### Note on Multiple Submissions +If you plan to submit another pull request while your original is still pending, make sure to create a new branch in your forked repository first. + +## General Requirements + +- **Descriptive Pull Request Titles**: Your pull request must have explicit and descriptive titles that accurately represent the changes made. +- **Scope Adherence**: Changes that fall outside the described scope will result in the entire pull request being rejected. +- **Quality Over Quantity**: Low-effort or spam pull requests will be marked accordingly. +- **Expanded Snippets**: Code snippets reused from the [ServiceNow Documentation](https://docs.servicenow.com/) or [API References](https://developer.servicenow.com/dev.do#!/reference/) are acceptable only if they are expanded in a meaningful way (e.g., with additional context, documentation, or variations). Remember: *"QUANTITY IS FUN, QUALITY IS KEY."* +- **Relevance**: Code should be relevant to ServiceNow Developers. +- **ES2021 Compatibility**: While ES2021 is allowed, we encourage you to disclose if your code is using ES2021 features, as not everyone may be working with ES2021-enabled applications. + +## Core Documentation File Changes + +**IMPORTANT**: For changes to core documentation files (README.md, CONTRIBUTING.md, LICENSE, etc.), contributors must: + +1. **Submit an Issue First**: Before making any changes to core documentation files, create an issue describing: + - What you intend to edit + - Why the change is needed + - Your proposed approach + +2. **Get Assignment**: Wait to be assigned to the issue by a maintainer before submitting a PR. + +3. **Reference the Issue**: Include the issue number in your PR title and description. + +This process helps prevent merge conflicts when multiple contributors want to update the same documentation files and ensures all changes align with the project's direction. + +## Repository Structure + +**IMPORTANT**: The repository has been reorganized into major categories. All new contributions MUST follow this structure for PR approval. + +Please follow this directory structure when organizing your code snippets: + +- **Top-Level Categories**: These are fixed categories that represent major areas of ServiceNow development: + - `Core ServiceNow APIs/` - GlideRecord, GlideAjax, GlideSystem, GlideDate, etc. + - `Server-Side Components/` - Background Scripts, Business Rules, Script Includes, etc. + - `Client-Side Components/` - Client Scripts, Catalog Client Scripts, UI Actions, etc. + - `Modern Development/` - Service Portal, NOW Experience, GraphQL, ECMAScript 2021 + - `Integration/` - RESTMessageV2, Import Sets, Mail Scripts, MIDServer, etc. + - `Specialized Areas/` - CMDB, ITOM, Performance Analytics, ATF Steps, etc. + +- **Sub-Categories**: Each top-level category contains sub-folders for specific ServiceNow technologies or use cases. +- **Snippet Folders**: Each sub-category contains folders for **each code snippet**. +- **Snippet Folder Contents**: Within each snippet folder, include: + - A `README.md` file that describes the code snippet. + - Individual files for each variant of the code snippet. + +### New Structure Example + +``` +Core ServiceNow APIs/ + β”œβ”€β”€ GlideRecord/ + β”‚ β”œβ”€β”€ Query Performance Optimization/ + β”‚ β”‚ β”œβ”€β”€ README.md # Description of the optimization snippet + β”‚ β”‚ β”œβ”€β”€ basic_query.js # Basic query example + β”‚ β”‚ └── optimized_query.js # Performance-optimized version + β”‚ └── Reference Field Handling/ + β”‚ β”œβ”€β”€ README.md # Description of reference handling + β”‚ └── reference_query.js # Reference field query example + └── GlideAjax/ + β”œβ”€β”€ Async Data Loading/ + β”‚ β”œβ”€β”€ README.md # Description of async loading + β”‚ β”œβ”€β”€ client_script.js # Client-side implementation + β”‚ └── script_include.js # Server-side Script Include +Server-Side Components/ + β”œβ”€β”€ Business Rules/ + β”‚ β”œβ”€β”€ Auto Assignment Logic/ + β”‚ β”‚ β”œβ”€β”€ README.md # Description of auto assignment + β”‚ β”‚ └── assignment_rule.js # Business rule implementation +``` + +### Category Placement Guidelines + +- **Core ServiceNow APIs**: All Glide* APIs and core ServiceNow JavaScript APIs +- **Server-Side Components**: Code that runs on the server (Business Rules, Background Scripts, etc.) +- **Client-Side Components**: Code that runs in the browser (Client Scripts, UI Actions, etc.) +- **Modern Development**: Modern ServiceNow development approaches and frameworks +- **Integration**: External system integrations, data import/export, and communication +- **Specialized Areas**: Domain-specific functionality (CMDB, ITOM, Testing, etc.) + +## Final Checklist + +Before submitting your pull request, ensure that: +- All code snippet files are in the appropriate folders. +- Each folder is correctly placed within its category. +- Your code snippet is accompanied by a `readme.md` file that describes it. + +Thank you for contributing! Your efforts help create a richer resource for the ServiceNow development community. diff --git a/Client-Side Components/Catalog Client Script/Auto Save Draft Feature/README.md b/Client-Side Components/Catalog Client Script/Auto Save Draft Feature/README.md new file mode 100644 index 0000000000..9b7aea40d3 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Auto Save Draft Feature/README.md @@ -0,0 +1,114 @@ +# Auto Save Draft Feature for Catalog Items + +This snippet provides automatic draft saving functionality for ServiceNow Catalog Items, helping prevent data loss by automatically saving form data at regular intervals. + +## Overview + +The feature includes two implementations: +1. Basic Implementation (`basic_implementation.js`) +2. Advanced Implementation (`advanced_implementation.js`) + +## Basic Implementation + +### Features +- Auto-saves form data every minute +- Stores single draft in sessionStorage +- Provides draft restoration on form load +- Basic error handling and user feedback + +### Usage +```javascript +// Apply in Catalog Client Script +// Select "onLoad" for "Client script runs" +// Copy content from basic_implementation.js +``` + +## Advanced Implementation + +### Enhanced Features +- Multiple draft support (keeps last 3 drafts) +- Advanced draft management +- Draft selection dialog +- Detailed metadata tracking +- Improved error handling +- User-friendly notifications + +### Usage +```javascript +// Apply in Catalog Client Script +// Select "onLoad" for "Client script runs" +// Copy content from advanced_implementation.js +``` + +## Technical Details + +### Dependencies +- ServiceNow Platform UI Framework +- GlideForm API +- GlideModal (advanced implementation only) + +### Browser Support +- Modern browsers with sessionStorage support +- ES5+ compatible + +### Security Considerations +- Uses browser's sessionStorage (cleared on session end) +- No sensitive data transmission +- Instance-specific storage + +## Implementation Guide + +1. Create a new Catalog Client Script: + - Table: Catalog Client Script [catalog_script_client] + - Type: onLoad + - Active: true + +2. Choose implementation: + - For basic needs: Copy `basic_implementation.js` + - For advanced features: Copy `advanced_implementation.js` + +3. Apply to desired Catalog Items: + - Select applicable Catalog Items + - Test in dev environment first + +## Best Practices + +1. Testing: + - Test with various form states + - Verify draft restoration + - Check browser storage limits + +2. Performance: + - Default 60-second interval is recommended + - Adjust based on form complexity + - Monitor browser memory usage + +3. User Experience: + - Clear feedback messages + - Confirmation dialogs + - Error notifications + +## Limitations + +- Browser session dependent +- Storage size limits +- Form field compatibility varies + +## Troubleshooting + +Common issues and solutions: +1. Draft not saving + - Check browser console for errors + - Verify sessionStorage availability + - Check form modification detection + +2. Restoration fails + - Validate stored data format + - Check browser storage permissions + - Verify form field compatibility + +## Version Information + +- Compatible with ServiceNow: Rome and later +- Browser Requirements: Modern browsers with ES5+ support +- Last Updated: October 2025 \ No newline at end of file diff --git a/Client-Side Components/Catalog Client Script/Auto Save Draft Feature/advanced_implementation.js b/Client-Side Components/Catalog Client Script/Auto Save Draft Feature/advanced_implementation.js new file mode 100644 index 0000000000..8ef56456ec --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Auto Save Draft Feature/advanced_implementation.js @@ -0,0 +1,125 @@ +/** + * Advanced Auto-save Draft Implementation with Enhanced Features + * This version adds multi-draft support and advanced error handling + */ + +function onLoad() { + var autosaveInterval = 60000; // 1 minute + var maxDrafts = 3; // Maximum number of drafts to keep + + // Initialize draft manager + initializeDraftManager(); + + // Try to restore previous draft + restoreLastDraft(); + + // Set up auto-save interval + setInterval(function() { + if (g_form.isModified()) { + saveAdvancedDraft(); + } + }, autosaveInterval); +} + +function initializeDraftManager() { + window.draftManager = { + maxDrafts: 3, + draftPrefix: 'catalogDraft_' + g_form.getUniqueValue() + '_', + + getAllDrafts: function() { + var drafts = []; + for (var i = 0; i < sessionStorage.length; i++) { + var key = sessionStorage.key(i); + if (key.startsWith(this.draftPrefix)) { + drafts.push({ + key: key, + data: JSON.parse(sessionStorage.getItem(key)) + }); + } + } + return drafts.sort((a, b) => b.data.timestamp - a.data.timestamp); + }, + + cleanup: function() { + var drafts = this.getAllDrafts(); + if (drafts.length > this.maxDrafts) { + drafts.slice(this.maxDrafts).forEach(function(draft) { + sessionStorage.removeItem(draft.key); + }); + } + } + }; +} + +function saveAdvancedDraft() { + try { + var draftData = {}; + g_form.serialize(draftData); + + // Add metadata + var draftKey = window.draftManager.draftPrefix + new Date().getTime(); + var draftInfo = { + timestamp: new Date().getTime(), + data: draftData, + user: g_user.userName, + catalog_item: g_form.getTableName(), + fields_modified: g_form.getModifiedFields() + }; + + sessionStorage.setItem(draftKey, JSON.stringify(draftInfo)); + window.draftManager.cleanup(); + + // Show success message with draft count + var remainingDrafts = window.draftManager.getAllDrafts().length; + g_form.addInfoMessage('Draft saved. You have ' + remainingDrafts + ' saved draft(s).'); + + } catch (e) { + console.error('Error saving draft: ' + e); + g_form.addErrorMessage('Failed to save draft: ' + e.message); + } +} + +function restoreLastDraft() { + try { + var drafts = window.draftManager.getAllDrafts(); + + if (drafts.length > 0) { + // If multiple drafts exist, show selection dialog + if (drafts.length > 1) { + showDraftSelectionDialog(drafts); + } else { + promptToRestoreDraft(drafts[0].data); + } + } + } catch (e) { + console.error('Error restoring draft: ' + e); + g_form.addErrorMessage('Failed to restore draft: ' + e.message); + } +} + +function showDraftSelectionDialog(drafts) { + var dialog = new GlideModal('select_draft_dialog'); + dialog.setTitle('Available Drafts'); + + var html = '
'; + drafts.forEach(function(draft, index) { + var date = new Date(draft.data.timestamp).toLocaleString(); + html += '
'; + html += 'Draft ' + (index + 1) + ' - ' + date; + html += '
Modified fields: ' + draft.data.fields_modified.join(', '); + html += '
'; + }); + html += '
'; + + dialog.renderWithContent(html); +} + +function promptToRestoreDraft(draftInfo) { + var timestamp = new Date(draftInfo.timestamp); + if (confirm('A draft from ' + timestamp.toLocaleString() + ' was found. Would you like to restore it?')) { + Object.keys(draftInfo.data).forEach(function(field) { + g_form.setValue(field, draftInfo.data[field]); + }); + g_form.addInfoMessage('Draft restored from ' + timestamp.toLocaleString()); + } +} \ No newline at end of file diff --git a/Client-Side Components/Catalog Client Script/Auto Save Draft Feature/basic_implementation.js b/Client-Side Components/Catalog Client Script/Auto Save Draft Feature/basic_implementation.js new file mode 100644 index 0000000000..8665dff034 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Auto Save Draft Feature/basic_implementation.js @@ -0,0 +1,58 @@ +/** + * Basic Auto-save Draft Implementation + * This version provides core functionality for auto-saving catalog item form data + */ + +function onLoad() { + var autosaveInterval = 60000; // 1 minute + + // Try to restore previous draft + restoreLastDraft(); + + // Set up auto-save interval + setInterval(function() { + if (g_form.isModified()) { + saveDraft(); + } + }, autosaveInterval); +} + +function saveDraft() { + try { + var draftData = {}; + g_form.serialize(draftData); + + var draftKey = 'catalogDraft_' + g_form.getUniqueValue(); + sessionStorage.setItem(draftKey, JSON.stringify({ + timestamp: new Date().getTime(), + data: draftData + })); + + g_form.addInfoMessage('Draft saved automatically'); + } catch (e) { + console.error('Error saving draft: ' + e); + } +} + +function restoreLastDraft() { + try { + var draftKey = 'catalogDraft_' + g_form.getUniqueValue(); + var savedDraft = sessionStorage.getItem(draftKey); + + if (savedDraft) { + var draftData = JSON.parse(savedDraft); + var timestamp = new Date(draftData.timestamp); + + if (confirm('A draft from ' + timestamp.toLocaleString() + ' was found. Would you like to restore it?')) { + Object.keys(draftData.data).forEach(function(field) { + g_form.setValue(field, draftData.data[field]); + }); + g_form.addInfoMessage('Draft restored from ' + timestamp.toLocaleString()); + } else { + sessionStorage.removeItem(draftKey); + } + } + } catch (e) { + console.error('Error restoring draft: ' + e); + } +} \ No newline at end of file diff --git a/Client-Side Components/Catalog Client Script/Auto Save Draft Feature/script.js b/Client-Side Components/Catalog Client Script/Auto Save Draft Feature/script.js new file mode 100644 index 0000000000..89cf714501 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Auto Save Draft Feature/script.js @@ -0,0 +1,66 @@ +/** + * Auto-save draft feature for Catalog Client Script + * + * This script automatically saves form data as a draft in the browser's sessionStorage + * every minute if changes are detected. It also provides functionality to restore + * the last saved draft when the form is loaded. + */ + +// Executes when the form loads +function onLoad() { + var autosaveInterval = 60000; // 1 minute + + // Try to restore previous draft + restoreLastDraft(); + + // Set up auto-save interval + setInterval(function() { + if (g_form.isModified()) { + saveDraft(); + } + }, autosaveInterval); +} + +// Saves the current form state as a draft +function saveDraft() { + try { + var draftData = {}; + g_form.serialize(draftData); + + var draftKey = 'catalogDraft_' + g_form.getUniqueValue(); + sessionStorage.setItem(draftKey, JSON.stringify({ + timestamp: new Date().getTime(), + data: draftData + })); + + g_form.addInfoMessage('Draft saved automatically'); + } catch (e) { + console.error('Error saving draft: ' + e); + } +} + +// Restores the last saved draft if available +function restoreLastDraft() { + try { + var draftKey = 'catalogDraft_' + g_form.getUniqueValue(); + var savedDraft = sessionStorage.getItem(draftKey); + + if (savedDraft) { + var draftData = JSON.parse(savedDraft); + var timestamp = new Date(draftData.timestamp); + + // Ask user if they want to restore the draft + if (confirm('A draft from ' + timestamp.toLocaleString() + ' was found. Would you like to restore it?')) { + Object.keys(draftData.data).forEach(function(field) { + g_form.setValue(field, draftData.data[field]); + }); + g_form.addInfoMessage('Draft restored from ' + timestamp.toLocaleString()); + } else { + // Clear the draft if user chooses not to restore + sessionStorage.removeItem(draftKey); + } + } + } catch (e) { + console.error('Error restoring draft: ' + e); + } +} \ No newline at end of file diff --git a/Client-Side Components/Catalog Client Script/Auto-populate field from URL/README.md b/Client-Side Components/Catalog Client Script/Auto-populate field from URL/README.md new file mode 100644 index 0000000000..6f9328b0de --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Auto-populate field from URL/README.md @@ -0,0 +1,17 @@ +This piece of code is designed for an usecase where you might want to populate a field value that you're passing as a query in the URL which redirects to a catalog item. +In this case, a custom field 'u_date' is chosen as an example to be shown: + +1. You open a catalog item record via a URL that carries a date in the query string. +Example: +https://your-instance.service-now.com/your_form.do?sysparm_u_date=2025-10-31 +-(This URL includes a parameter named sysparm_u_date with the value 2025-10-31.) + + +2. The catalog client script reads the page URL and extracts that specific parameter which returns the value "2025-10-31". + +3. If the parameter is present, the script populates the form field. +Calling g_form.setValue('u_date', '2025-10-31') sets the date field on the form to 31 October 2025. + + +Result: +The date field in the form is prefilled from the URL diff --git a/Client-Side Components/Catalog Client Script/Auto-populate field from URL/popdatefromurl.js b/Client-Side Components/Catalog Client Script/Auto-populate field from URL/popdatefromurl.js new file mode 100644 index 0000000000..431587d034 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Auto-populate field from URL/popdatefromurl.js @@ -0,0 +1,8 @@ +//Logic to fetch the u_date field value passed in the url and setting it in the actual field. + + +var fetchUrl = top.location.href; //get the URL + +var setDate = new URLSearchParams(gUrl).get("sysparm_u_date"); //fetch the value of date from the query parameter + +g_form.setValue('u_date', setDate); //set the value to the actual field diff --git a/Client-Side Components/Catalog Client Script/MRVS dependent ref qual 1st row/AccountUtils.js b/Client-Side Components/Catalog Client Script/MRVS dependent ref qual 1st row/AccountUtils.js new file mode 100644 index 0000000000..602bc20056 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/MRVS dependent ref qual 1st row/AccountUtils.js @@ -0,0 +1,19 @@ +var AccountUtils = Class.create(); +AccountUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, { + + //Populate the department name from the account in the session data for the reference qualifier to use: + + setSessionData: function() { + var acct = this.getParameter('sysparm_account'); + var dept = ''; + var acctGR = new GlideRecord('customer_account'); //reference table for Account variable + if (acctGR.get(acct)) { + dept = '^dept_name=' + acctGR.dept_name; //department field name on account table + } + + var session = gs.getSession().putClientData('selected_dept', dept); + return; + }, + + type: 'AccountUtils' +}); diff --git a/Client-Side Components/Catalog Client Script/MRVS dependent ref qual 1st row/README.md b/Client-Side Components/Catalog Client Script/MRVS dependent ref qual 1st row/README.md new file mode 100644 index 0000000000..d700088dc9 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/MRVS dependent ref qual 1st row/README.md @@ -0,0 +1,6 @@ +This Catalog Client Script and Script Include are used with a reference qualifier similar to +javascript: 'disable=false' + session.getClientData('selected_dept'); + +The scenario is a MRVS with a reference variable to the customer account table. When an (active) account is selected in the first row, subsequent rows should only be able to select active accounts in the same department as the first account. + +The Catalog Client Script will pass the first selected account (if there is one) to a Script Include each time a MRVS row is added or edited. The Script Include will pass the department name to the reference qualifier. diff --git a/Client-Side Components/Catalog Client Script/MRVS dependent ref qual 1st row/onLoad.js b/Client-Side Components/Catalog Client Script/MRVS dependent ref qual 1st row/onLoad.js new file mode 100644 index 0000000000..6c041330e2 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/MRVS dependent ref qual 1st row/onLoad.js @@ -0,0 +1,17 @@ +function onLoad() { + //applies to MRVS, not Catalog Item. This will pass the first selected account (if there is one) to a Script Include each time a MRVS row is added or edited + var mrvs = g_service_catalog.parent.getValue('my_mrvs'); //MRVS internal name + var acct = ''; + if (mrvs.length > 2) { //MRVS is not empty + var obj = JSON.parse(mrvs); + acct = obj[0].account_mrvs; + } + var ga = new GlideAjax('AccountUtils'); + ga.addParam('sysparm_name', 'setSessionData'); + ga.addParam('sysparm_account', acct); + ga.getXMLAnswer(getResponse); +} + +function getResponse(response) { + //do nothing +} diff --git a/Client-Side Components/Catalog Client Script/Return Date Validation/README.md b/Client-Side Components/Catalog Client Script/Return Date Validation/README.md new file mode 100644 index 0000000000..08b40e0c12 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Return Date Validation/README.md @@ -0,0 +1,23 @@ +This piece of code was written as a part of an usecase where the return date value validation was expected to be after start date as well as within 6 months from the start date. This code runs in an OnChange catalog client script for the field 'u_return_date' and validates the return date(u_return_date) in a ServiceNow Catalog item form to ensure: + +1. A start date(u_start_date) is entered before setting a return date(u_return_date). +2. The return date is within 6 months after the start date. +3. Return date must be after the start date, it can't be same as it or before it. + +Let’s say with an example: + +a) + u_start_date = 2025-10-01 + You enter u_return_date = 2026-04-15 + + Steps: + a)Difference = 196 days β†’ More than 180 days + b)Result: u_return_date is cleared and error shown: β€œSelect Return Date within 6 months from Start Date” + +b) + u_start_date = 2025-10-02 + You enter u_return_date = 2025-10-01 + + Steps: + a)Difference = -1 day β†’ Return date put as 1 day before start date + b)Result: u_return_date is cleared and error shown: β€œSelect Return Date in future than Start Date” diff --git a/Client-Side Components/Catalog Client Script/Return Date Validation/validateReturndate.js b/Client-Side Components/Catalog Client Script/Return Date Validation/validateReturndate.js new file mode 100644 index 0000000000..e9bcfa96fb --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Return Date Validation/validateReturndate.js @@ -0,0 +1,23 @@ +function onChange(control, oldValue, newValue, isLoading) { + if (isLoading || newValue == '') { + return; + } + var u_start_date = g_form.getValue('u_start_date'); //start date validation to check to see whether filled or not + if (!u_start_date) { + g_form.clearValue('u_return_date'); + g_form.showFieldMsg('u_return_date', 'Please enter start date', 'error'); + } else { + var startTime = getDateFromFormat(u_start_date, g_user_date_format); //converting to js date object + var returnTime = getDateFromFormat(newValue, g_user_date_format); + var selectedStartDate = new Date(startTime); + var returnDate = new Date(returnTime); + var returnDateDifference = (returnDate - selectedStartDate) / 86400000; //converting the diff between the dates to days by dividing by 86400000 + if (returnDateDifference > 180) { + g_form.clearValue('u_return_date'); + g_form.showFieldMsg('u_return_date', 'Select Return Date within 6 months from Start Date', 'error'); + } else if (returnDateDifference < 1) { + g_form.clearValue('u_return_date'); + g_form.showFieldMsg('u_return_date', 'Select Return Date in future than Start Date', 'error'); + } + } +} diff --git a/Client-Side Components/Catalog Client Script/spModal for Sweet Alerts/readme.md b/Client-Side Components/Catalog Client Script/spModal for Sweet Alerts/readme.md new file mode 100644 index 0000000000..52108efc00 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/spModal for Sweet Alerts/readme.md @@ -0,0 +1,27 @@ +In ServiceNow, Open catalog client Scripts [catalog_script_client] and paste the code snippet of [spModalSweetAlerts.js] file. + +Setup: +1. A catalog item having variable name Rewards[rewards] of type 'Select Box'(include none as true) and 2 choices(Yes and No) +2. A Single line type field named 'Reward Selected' [reward_selected] which will hold the value selected by user from the spModal popup. +3. The onLoad catalog client script setup as below: +4. Type: onChange +5. Variable: rewards (as per step 1) +6. Script: [[spModalSweetAlerts.js]] + + + +Screenshots: + + +image + + +Rewards selected as 'Yes' + +image + +From the spModal popup select anyone of the reward, it should be populated in the Reward Selected field. +Along with that a message shows the same of the selection. + +image + diff --git a/Client-Side Components/Catalog Client Script/spModal for Sweet Alerts/spModalSweetAlerts.js b/Client-Side Components/Catalog Client Script/spModal for Sweet Alerts/spModalSweetAlerts.js new file mode 100644 index 0000000000..47ebfddc3b --- /dev/null +++ b/Client-Side Components/Catalog Client Script/spModal for Sweet Alerts/spModalSweetAlerts.js @@ -0,0 +1,32 @@ +function onChange(control, oldValue, newValue) { + if (newValue == 'Yes') { + spModal.open({ + title: "Reward Type", + message: "Please select the category of Reward", + buttons: [{ + label: "Star Performer", + value: "Star Performer" + }, + { + label: "Emerging Player", + value: "Emerging Player" + }, + { + label: "High Five Award", + value: "High Five Award" + }, + { + label: "Rising Star", + value: "Rising Star" + } + ] + }).then(function(choice) { + if (choice && choice.value) { + g_form.addInfoMessage('Selected Reward: '+ choice.label); + g_form.setValue('reward_selected', choice.value); + } + }); + } else { + g_form.clearValue('reward_selected'); + } +} diff --git a/Client-Side Components/Client Scripts/Color-coded Priority field for improved UX/README.md b/Client-Side Components/Client Scripts/Color-coded Priority field for improved UX/README.md new file mode 100644 index 0000000000..92c842b04a --- /dev/null +++ b/Client-Side Components/Client Scripts/Color-coded Priority field for improved UX/README.md @@ -0,0 +1,19 @@ +# Field Color-Coding Based on Choice Values + +## Purpose +Dynamically change the background color of any choice field on a form based on the selected backend value. + +## How to Use +1. Create an OnChange client script on the desired choice field. +2. Replace `'your_field_name'` in the script with your actual field name. +3. Update the `colorMap` with relevant backend choice values and colors. +4. Save and test on the form. + +## Key Points +- Works with any choice field +- Uses backend values of choices for mapping colors. + +## Demo + +image + diff --git a/Client-Side Components/Client Scripts/Color-coded Priority field for improved UX/setColor.js b/Client-Side Components/Client Scripts/Color-coded Priority field for improved UX/setColor.js new file mode 100644 index 0000000000..9538d57461 --- /dev/null +++ b/Client-Side Components/Client Scripts/Color-coded Priority field for improved UX/setColor.js @@ -0,0 +1,15 @@ +function onChange(control, oldValue, newValue, isLoading) { + + var colorMap = { + '1': '#e74c3c', // Critical - strong red + '2': '#e67e22', // High - bright orange + '3': '#f1c40f', // Moderate - yellow + '4': '#3498db', // Low - blue + '5': '#27ae60' // Planning - green + }; + + var priorityField = g_form.getControl('priority'); + if (!priorityField) return; + + priorityField.style.backgroundColor = colorMap[newValue] || ''; +} diff --git a/Client-Side Components/Client Scripts/Detect oldValue newValue and Operation in Glide List Type Fields/detectOldValuenewValueOperation.js b/Client-Side Components/Client Scripts/Detect oldValue newValue and Operation in Glide List Type Fields/detectOldValuenewValueOperation.js new file mode 100644 index 0000000000..87cb6ded61 --- /dev/null +++ b/Client-Side Components/Client Scripts/Detect oldValue newValue and Operation in Glide List Type Fields/detectOldValuenewValueOperation.js @@ -0,0 +1,43 @@ +function onChange(control, oldValue, newValue, isLoading, isTemplate) { + if (isLoading) { + return; + } + + var prevValue; + if (g_scratchpad.prevValue == undefined) + prevValue = oldValue; + else { + prevValue = g_scratchpad.prevValue; + } + g_scratchpad.prevValue = newValue; + + + var oldGlideValue = prevValue.split(','); + var newGlideValue = newValue.split(','); + + var operation; + + if (oldGlideValue.length > newGlideValue.length || newValue == '') { + operation = 'remove'; + } else if (oldGlideValue.length < newGlideValue.length || oldGlideValue.length == newGlideValue.length) { + operation = 'add'; + } else { + operation = ''; + } + + var ajaxGetNames = new GlideAjax('watchListCandidatesUtil'); + ajaxGetNames.addParam('sysparm_name', 'getWatchListUsers'); + ajaxGetNames.addParam('sysparm_old_values', oldGlideValue); + ajaxGetNames.addParam('sysparm_new_values', newGlideValue); + ajaxGetNames.getXMLAnswer(function(response) { + + var result = JSON.parse(response); + + g_form.clearMessages(); + g_form.addSuccessMessage('Operation Performed : ' + operation); + g_form.addSuccessMessage('OldValue : ' + result.oldU); + g_form.addSuccessMessage('NewValue : ' + result.newU); + + }); + +} diff --git a/Client-Side Components/Client Scripts/Detect oldValue newValue and Operation in Glide List Type Fields/readme.md b/Client-Side Components/Client Scripts/Detect oldValue newValue and Operation in Glide List Type Fields/readme.md new file mode 100644 index 0000000000..cb2b0e5533 --- /dev/null +++ b/Client-Side Components/Client Scripts/Detect oldValue newValue and Operation in Glide List Type Fields/readme.md @@ -0,0 +1,39 @@ +In Client Scripts, oldValue will display the value of last value/record which is stored in that field. +For new records, it is generally empty and for existing records it displays the value which is stored after load. +If we will try to change the value in that field, it will still show oldValue the same value which was there during the load of form. + +So, In order to identify the oldValue on change(as it does in business rule(previous)), This script comes handy and also it will +detect the action performed. + + +This onChange Client script comes handy when dealing with Glide List type fields where we have to detect whether the value was added or removed and returns the display name of users who were added/removed along with name of operation performed. + +Setup details: + +onChange client Script on [incident] table +Field Name: [watch_list] +Script: Check [detectOldValuenewValueOperation.js] file +Script Include Name: watchListCandidatesUtil (client callable/GlideAjax Enabled - true), Check [watchListCandidatesUtil.js] file for script + + + +Output: + +Currently there is no one in the watchlist field: + +image + + +Adding [Bryan Rovell], It shows the operation was addition, oldValue as 'No record found' as there was no value earlier.New Value shows the name of the user (Bryan Rovell) +image + + +Adding 2 users one by one: + +image + + +Removing 2 at once: + +image + diff --git a/Client-Side Components/Client Scripts/Detect oldValue newValue and Operation in Glide List Type Fields/watchListCandidatesUtil.js b/Client-Side Components/Client Scripts/Detect oldValue newValue and Operation in Glide List Type Fields/watchListCandidatesUtil.js new file mode 100644 index 0000000000..26616d6049 --- /dev/null +++ b/Client-Side Components/Client Scripts/Detect oldValue newValue and Operation in Glide List Type Fields/watchListCandidatesUtil.js @@ -0,0 +1,37 @@ +var watchListCandidatesUtil = Class.create(); +watchListCandidatesUtil.prototype = Object.extendsObject(AbstractAjaxProcessor, { + + + getWatchListUsers: function() { + + var oldUsers = this.getParameter('sysparm_old_values'); + var newUsers = this.getParameter('sysparm_new_values'); + + var result = { + oldU: this._getUserNames(oldUsers), + newU: this._getUserNames(newUsers) + }; + + return JSON.stringify(result); + }, + + + _getUserNames: function(userList) { + var names = []; + + var grUserTab = new GlideRecord('sys_user'); + grUserTab.addQuery('sys_id', 'IN', userList); + grUserTab.query(); + if (grUserTab.hasNext()) { + while (grUserTab.next()) { + names.push(grUserTab.getDisplayValue('name')); + } + return names.toString(); + } else { + return 'No record found'; + } + }, + + + type: 'watchListCandidatesUtil' +}); diff --git a/Client-Side Components/Client Scripts/Get Logged in User Information/README.md b/Client-Side Components/Client Scripts/Get Logged in User Information/README.md new file mode 100644 index 0000000000..79c5c88b9b --- /dev/null +++ b/Client-Side Components/Client Scripts/Get Logged in User Information/README.md @@ -0,0 +1,16 @@ +# The Glide User (g_user) is a global object available within the client side. It provides information about the logged-in user. + +Property Description + +g_user.userID Sys ID of the currently logged-in user +g_user.name User's Full name +g_user.firstName User's First name +g_user.lastName User's Last name + +# It also has some methods available within the client side. + +Method Description + +g_user.hasRole() Determine whether the logged-in user has a specific role +g_user.hasRoleExactly() Do not consider the admin role while evaluating the method +g_user.hasRoles() You can pass two or more roles in a single method diff --git a/Client-Side Components/Client Scripts/Get Logged in User Information/script.js b/Client-Side Components/Client Scripts/Get Logged in User Information/script.js new file mode 100644 index 0000000000..2c35485db6 --- /dev/null +++ b/Client-Side Components/Client Scripts/Get Logged in User Information/script.js @@ -0,0 +1,18 @@ +if (g_user.hasRole('admin') || g_user.hasRole('itil')) { + // User has at least one of the roles + g_form.setDisplay('internal_notes', true); +} + +if (g_user.hasRole('admin') && g_user.hasRole('itil')) { + // User must have both roles + g_form.setDisplay('advanced_settings', true); +} + +//Using the parameters to set a field value +g_form.setValue('short_description', g_user.firstName); + +g_form.setValue('short_description', g_user.lastName); + +g_form.setValue('short_description', g_user.name); + +g_form.setValue('short_description', g_user.userID); diff --git a/Client-Side Components/Client Scripts/Hide Dependent Choice field if there no dependent choices/HideDepnedentField.js b/Client-Side Components/Client Scripts/Hide Dependent Choice field if there no dependent choices/HideDepnedentField.js new file mode 100644 index 0000000000..bb0f64875c --- /dev/null +++ b/Client-Side Components/Client Scripts/Hide Dependent Choice field if there no dependent choices/HideDepnedentField.js @@ -0,0 +1,18 @@ +function onChange(control, oldValue, newValue, isLoading, isTemplate) { + var fieldToHide = 'subcategory'; // I have taken subcategory as an example + if (newValue === '') { + g_form.setDisplay(fieldToHide, false); + return; + } + var ga = new GlideAjax('NumberOfDependentChoices'); + ga.addParam('sysparm_name', 'getCountOfDependentChoices'); + ga.addParam('sysparm_tableName', g_form.getTableName()); + ga.addParam('sysparm_element', fieldToHide); + ga.addParam('sysparm_choiceName', newValue); + ga.getXMLAnswer(callBack); + + function callBack(answer) { + g_form.setDisplay(fieldToHide, parseInt(answer) > 0 ? true : false); + } + +} diff --git a/Client-Side Components/Client Scripts/Hide Dependent Choice field if there no dependent choices/NumberOfDependentChoices.js b/Client-Side Components/Client Scripts/Hide Dependent Choice field if there no dependent choices/NumberOfDependentChoices.js new file mode 100644 index 0000000000..71417224fa --- /dev/null +++ b/Client-Side Components/Client Scripts/Hide Dependent Choice field if there no dependent choices/NumberOfDependentChoices.js @@ -0,0 +1,21 @@ +var NumberOfDependentChoices = Class.create(); +NumberOfDependentChoices.prototype = Object.extendsObject(AbstractAjaxProcessor, { + getCountOfDependentChoices: function() { + var dependentChoiceCount = 0; + var choiceName = this.getParameter('sysparm_choiceName'); + var tableName = this.getParameter('sysparm_tableName'); + var element = this.getParameter('sysparm_element'); + var choiceCountGa = new GlideAggregate('sys_choice'); + choiceCountGa.addAggregate('COUNT'); + choiceCountGa.addQuery('dependent_value',choiceName); + choiceCountGa.addQuery('inactive','false'); + choiceCountGa.addQuery('name',tableName); + choiceCountGa.addQuery('element',element); + choiceCountGa.query(); + while(choiceCountGa.next()){ + dependentChoiceCount = choiceCountGa.getAggregate('COUNT'); + } + return dependentChoiceCount; + }, + type: 'NumberOfDependentChoices' +}); diff --git a/Client-Side Components/Client Scripts/Hide Dependent Choice field if there no dependent choices/README.md b/Client-Side Components/Client Scripts/Hide Dependent Choice field if there no dependent choices/README.md new file mode 100644 index 0000000000..80b4531a8a --- /dev/null +++ b/Client-Side Components/Client Scripts/Hide Dependent Choice field if there no dependent choices/README.md @@ -0,0 +1,7 @@ +Hide the dependent choice field when there are no available options for the selected parent choice. + +For example, if a selected category on the incident form has no subcategories, then the subcategory field should be hidden. + +The file NumberOfDependentChoices.js is a client callable script include file which has a method which returns number of dependent choices for a selected choice of parent choice field. + +HideDepnedentField.js is client script which hides the dependent choice field(ex:subcategory field on incident form) if there are no active choices to show for a selected choices of it's dependent field (example: category on incident form) diff --git a/Client-Side Components/Client Scripts/field-character-counter/README.md b/Client-Side Components/Client Scripts/field-character-counter/README.md new file mode 100644 index 0000000000..d30b341b50 --- /dev/null +++ b/Client-Side Components/Client Scripts/field-character-counter/README.md @@ -0,0 +1,56 @@ +# Field Character Counter + +## Use Case +Provides real-time character count feedback for text fields in ServiceNow forms. Shows remaining characters with visual indicators to help users stay within field limits. + +## Requirements +- ServiceNow instance +- Client Script execution rights +- Text fields with character limits + +## Implementation +1. Create a new Client Script with Type "onChange" +2. Copy the script code from `script.js` +3. Configure the field name and character limit in the script +4. Apply to desired table/form +5. Save and test + +## Configuration +Edit these variables in the script: + +```javascript +var fieldName = 'short_description'; // Your field name +var maxLength = 160; // Your character limit +``` + +## Features +- Real-time character counting as user types +- Visual indicators: info (blue), warning (yellow), error (red) +- Shows "X characters remaining" or "Exceeds limit by X characters" +- Automatically clears previous messages + +## Common Examples +```javascript +// Short Description (160 chars) +var fieldName = 'short_description'; +var maxLength = 160; + +// Description (4000 chars) +var fieldName = 'description'; +var maxLength = 4000; + +// Work Notes (4000 chars) +var fieldName = 'work_notes'; +var maxLength = 4000; +``` + +## Message Thresholds +- **50+ remaining**: Info message (blue) +- **1-20 remaining**: Warning message (yellow) +- **Over limit**: Error message (red) + +## Notes +- Uses standard ServiceNow APIs: `g_form.showFieldMsg()` and `g_form.hideFieldMsg()` +- Create separate Client Scripts for multiple fields +- Works with all text fields and text areas +- Character count includes all characters (spaces, punctuation, etc.) \ No newline at end of file diff --git a/Client-Side Components/UI Pages/Resolve Incident UI Page/README.md b/Client-Side Components/UI Pages/Resolve Incident UI Page/README.md new file mode 100644 index 0000000000..d602d54f83 --- /dev/null +++ b/Client-Side Components/UI Pages/Resolve Incident UI Page/README.md @@ -0,0 +1,44 @@ +Use Case: + +Everytime user try to resolve the incident, the resolution codes and resolution notes are mandatory to be entered as it hidden in tabs,Since it is mandatory fields. So to ease the process we introduced a custom UI action will prompt the user +to enter resolution notes and resolution codes and automatically set the state to Resolved. + +How it Works: + +Navigate the Incident form and make sure the incident is not closed or active is false. +Click Resolve Incident UI action, it will open an modal with asking resolution notes and resolution code. +Provide the details and submit. incident is updated with above Resolution notes and codes and set state to be Resolved. + + +Below Action Need to Performed: + +1.Create UI action: + +Navigate to System UI > UI Actions. +Create a new UI Action with the following details: +Name: Resolve Incident (or a descriptive name of your choice). +Table: Incident [incident]. +Action name: resolve_incident_action (must be a unique, server-safe name). +Order: A number that determines the position of the button on the form. +Client: Check this box. This is crucial for running client-side JavaScript. +Form button: Check this box to display it on the form. +Onclick: ResolveIncident() (This must match the function name). +Condition: Set a condition to control when the button is visible (e.g., current.active == true). + +2.Create Script Include: + +Navigate to System Definition > Script Includes. +Click New. +Fill in the form: +Name: ResolutionProcessor +Client callable: Check the box. +Copy the provided script into the Script field. +Click Submit. + +3.Create UI page: + +Navigate to System Definition > UI pages +Fill the HTML and client script. +Click Submit. + + diff --git a/Client-Side Components/UI Pages/Resolve Incident UI Page/UI_action.js b/Client-Side Components/UI Pages/Resolve Incident UI Page/UI_action.js new file mode 100644 index 0000000000..75e25468f0 --- /dev/null +++ b/Client-Side Components/UI Pages/Resolve Incident UI Page/UI_action.js @@ -0,0 +1,18 @@ +function ResolveIncident() { + var dialog = new GlideModal("resolve_incident"); + dialog.setTitle("Resolve Incident"); + dialog.setPreference('sysparm_record_id', g_form.getUniqueValue()); + dialog.render(); //Open the dialog +} + + +// Navigate to System UI > UI Actions. +// Create a new UI Action with the following details: +// Name: Resolve Incident (or a descriptive name of your choice). +// Table: Incident [incident]. +// Action name: resolve_incident_action (must be a unique, server-safe name). +// Order: A number that determines the position of the button on the form. +// Client: Check this box. This is crucial for running client-side JavaScript. +// Form button: Check this box to display it on the form. +// Onclick: ResolveIncident() (This must match the function name). +// Condition: Set a condition to control when the button is visible (e.g., current.active == true). diff --git a/Client-Side Components/UI Pages/Resolve Incident UI Page/ui_page_client.js b/Client-Side Components/UI Pages/Resolve Incident UI Page/ui_page_client.js new file mode 100644 index 0000000000..73fc21706b --- /dev/null +++ b/Client-Side Components/UI Pages/Resolve Incident UI Page/ui_page_client.js @@ -0,0 +1,29 @@ +// Below code will be used in client script of UI page as mentioned in README.md file + +function ResolveIncidentOnsubmit(sysId) { //This function is called in UI page HTML section When user clicks the Submit button + var rejectionReason = document.getElementById('resolution_reason').value.trim(); + var resolutionCode = document.getElementById('resolution_code').value.trim(); + if (!rejectionReason || rejectionReason === ' ') { + alert('Resolution Notes is a mandatory field.'); + return false; + } + if (resolutionCode == 'None') { + alert('Resolution Code is a mandatory field.'); + return false; + } + var ga = new GlideAjax('ResolutionProcessor'); + ga.addParam('sysparm_name', 'updateRecord'); + ga.addParam('sysparm_record_id', sysId); + ga.addParam('sysparm_reason', rejectionReason); + ga.addParam('sysparm_resolution', resolutionCode); + ga.getXML(handleSuccessfulSubmit); + GlideDialogWindow.get().destroy(); + return false; + function handleSuccessfulSubmit(answer) { + window.location.reload(); + } + } + function closeDialog() { + GlideDialogWindow.get().destroy(); + return false; + } diff --git a/Client-Side Components/UI Pages/Resolve Incident UI Page/ui_page_html.html b/Client-Side Components/UI Pages/Resolve Incident UI Page/ui_page_html.html new file mode 100644 index 0000000000..829d4ff1cb --- /dev/null +++ b/Client-Side Components/UI Pages/Resolve Incident UI Page/ui_page_html.html @@ -0,0 +1,27 @@ + + + +
+

+ + +

+

+ + +

+ +
+