diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index aa1f154ac4..0000000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(find:*)", - "Bash(ls:*)", - "Bash(mkdir:*)", - "Bash(mv:*)", - "Bash(rmdir:*)", - "Bash(curl:*)", - "Bash(node:*)" - ], - "deny": [] - } -} \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..96784d9c24 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,41 @@ +# PR Description: +replace this with your description + +# Pull Request Checklist + +## Overview +- [x] Put an x inside of the square brackets to check each item. +- [ ] I have read and understood the [CONTRIBUTING.md](CONTRIBUTING.md) guidelines +- [ ] My pull request has a descriptive title that accurately reflects the changes and the description has been filled in above. +- [ ] I've included only files relevant to the changes described in the PR title and description +- [ ] I've created a new branch in my forked repository for this contribution + +## Code Quality +- [ ] My code is relevant to ServiceNow developers +- [ ] My code snippets expand meaningfully on official ServiceNow documentation (if applicable) +- [ ] I've disclosed use of ES2021 features (if applicable) +- [ ] I've tested my code snippets in a ServiceNow environment (where possible) + +## Repository Structure Compliance +- [ ] I've placed my code snippet(s) in one of the required top-level categories: + - `Core ServiceNow APIs/` + - `Server-Side Components/` + - `Client-Side Components/` + - `Modern Development/` + - `Integration/` + - `Specialized Areas/` +- [ ] I've used appropriate sub-categories within the top-level categories +- [ ] Each code snippet has its own folder with a descriptive name + +## Documentation +- [ ] I've included a README.md file for each code snippet +- [ ] The README.md includes: + - Description of the code snippet functionality + - Usage instructions or examples + - Any prerequisites or dependencies + - (Optional) Screenshots or diagrams if helpful + +## Restrictions +- [ ] My PR does not include XML exports of ServiceNow records +- [ ] My PR does not contain sensitive information (passwords, API keys, tokens) +- [ ] My PR does not include changes that fall outside the described scope 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/pages.yml b/.github/workflows/pages.yml index 59ab31e474..c85758f3da 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -2,10 +2,6 @@ name: Deploy GitHub Pages on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages @@ -17,45 +13,74 @@ permissions: # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. concurrency: - group: "pages" - cancel-in-progress: false + group: pages-${{ github.ref }} + cancel-in-progress: true jobs: - # Build job + # Build and optimize job build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - - - name: Setup Ruby - uses: ruby/setup-ruby@v1 + + - name: Setup Node.js + uses: actions/setup-node@v4 with: - ruby-version: '3.1' - bundler-cache: true + node-version: '18' + cache: 'npm' + + - name: Install build dependencies + run: | + npm init -y + npm install --save-dev html-minifier-terser clean-css-cli terser html-validate + + - name: Validate HTML files + run: | + echo "Validating HTML files..." + npx html-validate *.html pages/*.html || echo "HTML validation completed with warnings" + + - name: Optimize assets + run: | + echo "Optimizing HTML files..." + # Create backup directory + mkdir -p .backup + + # Minify HTML files (preserve original structure) + find . -name "*.html" -not -path "./node_modules/*" -not -path "./.backup/*" | while read file; do + echo "Minifying: $file" + npx html-minifier-terser \ + --collapse-whitespace \ + --remove-comments \ + --remove-optional-tags \ + --remove-redundant-attributes \ + --remove-script-type-attributes \ + --remove-style-link-type-attributes \ + --minify-css \ + --minify-js \ + "$file" -o "$file.tmp" && mv "$file.tmp" "$file" + done + # Minify CSS files if any exist + if find . -name "*.css" -not -path "./node_modules/*" -not -path "./.backup/*" | grep -q .; then + echo "Optimizing CSS files..." + find . -name "*.css" -not -path "./node_modules/*" -not -path "./.backup/*" | while read file; do + echo "Minifying: $file" + npx cleancss "$file" -o "$file" + done + fi + + # Remove build dependencies from final artifact + rm -rf node_modules package*.json + - name: Setup Pages id: pages uses: actions/configure-pages@v4 - - - name: Install dependencies - run: | - gem install jekyll bundler - bundle init - echo 'gem "jekyll", "~> 4.3"' >> Gemfile - echo 'gem "minima", "~> 2.5"' >> Gemfile - echo 'gem "jekyll-feed"' >> Gemfile - echo 'gem "jekyll-sitemap"' >> Gemfile - echo 'gem "jekyll-seo-tag"' >> Gemfile - bundle install - - - name: Build with Jekyll - run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" - env: - JEKYLL_ENV: production - + - name: Upload artifact uses: actions/upload-pages-artifact@v3 + with: + path: '.' # Deployment job deploy: @@ -64,7 +89,6 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest needs: build - if: github.ref == 'refs/heads/main' steps: - name: Deploy to GitHub Pages id: deployment 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/.gitignore b/.gitignore index e43b0f9889..68a3cd2015 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -.DS_Store +.DS_Store + +# Claude Code settings +.claude/ +settings.local.json diff --git a/CLAUDE.md b/AGENTS.md similarity index 97% rename from CLAUDE.md rename to AGENTS.md index aea2dffa24..b5192f5742 100644 --- a/CLAUDE.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ -# CLAUDE.md +# AGENTS.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to AI Coding Agents when working with code in this repository. ## Repository Overview @@ -115,4 +115,4 @@ The repository is organized into **6 major categories**. All contributions MUST - Git-enabled IDEs like VS Code recommended for larger contributions - Standard .gitignore excludes only .DS_Store files -This repository serves as a comprehensive reference for ServiceNow developers at all skill levels, emphasizing practical, tested solutions for real-world development scenarios. \ No newline at end of file +This repository serves as a comprehensive reference for ServiceNow developers at all skill levels, emphasizing practical, tested solutions for real-world development scenarios. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 70f3215b6f..1bd1f57045 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,10 +31,25 @@ If you plan to submit another pull request while your original is still pending, - **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.”* +- **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. 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/Autofilling the request details from previous request/Auto fill script include.JS b/Client-Side Components/Catalog Client Script/Autofilling the request details from previous request/Auto fill script include.JS new file mode 100644 index 0000000000..c4a343c99e --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Autofilling the request details from previous request/Auto fill script include.JS @@ -0,0 +1,30 @@ +var GetRecentRequestValues = Class.create(); +GetRecentRequestValues.prototype = Object.extendsObject(AbstractAjaxProcessor, { + getValues: function() { + var userID = this.getParameter('sysparm_user'); + var itemID = this.getParameter('sysparm_item'); + var result = { found: false, values: {} }; + + var gr = new GlideRecord('sc_req_item'); + gr.addQuery('requested_for', userID); + gr.addQuery('cat_item', itemID); + gr.orderByDesc('sys_created_on'); + gr.setLimit(1); + gr.query(); + + if (gr.next()) { + result.found = true; + + + var vars = gr.variables; + result.values = { + 'requested_for': vars.requested_for + '', + 'location': vars.location + '', + 'department': vars.department + '' + }; + } + + return JSON.stringify(result); + } +}); + diff --git a/Client-Side Components/Catalog Client Script/Autofilling the request details from previous request/Client script Autofill.js b/Client-Side Components/Catalog Client Script/Autofilling the request details from previous request/Client script Autofill.js new file mode 100644 index 0000000000..b030ddcca1 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Autofilling the request details from previous request/Client script Autofill.js @@ -0,0 +1,27 @@ +function onLoad() { + var user = g_user.userID; + var itemID = g_form.getUniqueValue(); + + var ga = new GlideAjax('GetRecentRequestValues'); + ga.addParam('sysparm_name', 'getValues'); + ga.addParam('sysparm_user', user); + ga.addParam('sysparm_item', itemID); + ga.getXMLAnswer(function(response) { + var data = JSON.parse(response); + if (data && data.found) { + var confirmFill = confirm("We found a similar request. Do you want to autofill fields?"); + if (confirmFill) { + for (var field in data.values) { + if (g_form.getControl(field)) { + g_form.setValue(field, data.values[field]); + console.log("Set " + field + " to " + data.values[field]); + } else { + console.log("Field not found: " + field); + } + } + } + } else { + console.log("No previous request found."); + } + }); +} diff --git a/Client-Side Components/Catalog Client Script/Autofilling the request details from previous request/Readme.md b/Client-Side Components/Catalog Client Script/Autofilling the request details from previous request/Readme.md new file mode 100644 index 0000000000..a90213b53a --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Autofilling the request details from previous request/Readme.md @@ -0,0 +1,10 @@ +Recent Request Autofill for ServiceNow Catalog.it automatically offers to fill in fields based on the user's most recent similar request. + Features +- Detects previous requests for the same catalog item +- Prompts user to reuse values from their last submission +- Autofills fields like location, department, and justification + +image + + + diff --git a/Client-Side Components/Catalog Client Script/Autopopulate user information fields/ClientCallableScriptInclude.js b/Client-Side Components/Catalog Client Script/Autopopulate user information fields/ClientCallableScriptInclude.js new file mode 100644 index 0000000000..0e17bbe045 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Autopopulate user information fields/ClientCallableScriptInclude.js @@ -0,0 +1,25 @@ +/* +* The following is a client callable script include. This can be used with the onChange Client script to be able to gather the data on the server side +*/ + +var ReferenceQualifierAjaxHelper = Class.create(); +ReferenceQualifierAjaxHelper.prototype = Object.extendsObject(AbstractAjaxProcessor, { + getUserInformation : function() { + var userID = this.getParameter('sysparm_user'); + var userRec = new GlideRecord('sys_user'); + + if(userRec.get(userID)) { + var results = { + "email" : userRec.getValue('email'), + "department" : userRec.getValue('department'), + "title" : userRec.getValue('title'), + "phone" : userRec.getValue('phone') + }; + + return JSON.stringify(results) + } + + }, + + type: 'ReferenceQualifierAjaxHelper' +}); diff --git a/Client-Side Components/Catalog Client Script/Autopopulate user information fields/README.md b/Client-Side Components/Catalog Client Script/Autopopulate user information fields/README.md new file mode 100644 index 0000000000..0614b60044 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Autopopulate user information fields/README.md @@ -0,0 +1,6 @@ +## Overview +This onchange catalog client script and script inlcude work together autopopulate the user fields that might show up on a catalog item. In the +global scope you will have to create the client callable script include to be able to use the Ajax call that is in the on change client script. +In this example we use the OOB Requested For field that already auto populates the user that is logged in then we go to the server to get that +users information. The fields that are brough back are the ones that are in the code but you can modify to bring back more or less fields if needed. + diff --git a/Client-Side Components/Catalog Client Script/Autopopulate user information fields/onChangeClientScript.js b/Client-Side Components/Catalog Client Script/Autopopulate user information fields/onChangeClientScript.js new file mode 100644 index 0000000000..805547fe44 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Autopopulate user information fields/onChangeClientScript.js @@ -0,0 +1,23 @@ +/* +* In order for this to work make sure to have an onChange catalog client script on a variable that is type Requested For. This variable +* already autopopulates the logged in user with its OOB functionality. In the updateUserFields function you can add any other user fields +* that you might need. +*/ + +function onChange(control, oldValue, newValue, isLoading) { + //This variable will store the sys_id of the user that populates in your requested for variable + var userID = newValue; + + var ga = new GlideAjax(ReferenceQualifierAjaxHelper); + ga.addParam('sysparm_name', 'getUserInformation'); + ga.addParam('sysparm_user', userID); + ga.getXMLAnswer(updateUserFields); + + function updateUserFields(response) { + var returnedData = JSON.parse(response); + g_form.setValue("email", returnedData.email); + g_form.setValue("department", returnedData.department); + g_form.setValue("title", returnedData.title); + g_form.setValue("phone", returnedData.phone); + } +} diff --git a/Client-Side Components/Catalog Client Script/Catalog Approval/Readme.md b/Client-Side Components/Catalog Client Script/Catalog Approval/Readme.md new file mode 100644 index 0000000000..cb49be2676 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Catalog Approval/Readme.md @@ -0,0 +1 @@ +This project adds a dynamic preview feature to Service Catalog items, allowing users to see the full approval chain before submitting a request. It improves transparency, reduces confusion, and helps users understand who will be involved in the approval process based on their selections. diff --git a/Client-Side Components/Catalog Client Script/Catalog Approval/client script.js b/Client-Side Components/Catalog Client Script/Catalog Approval/client script.js new file mode 100644 index 0000000000..d5af4a33d6 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Catalog Approval/client script.js @@ -0,0 +1,10 @@ +function onLoad() { + var ga = new GlideAjax('ApprovalChainHelper'); + ga.addParam('sysparm_name', 'getApprovers'); + ga.addParam('sysparm_item_id', g_form.getUniqueValue()); + ga.getXMLAnswer(function(response) { + var approvers = JSON.parse(response); + var message = 'This request will be approved by: ' + approvers.join(', '); + g_form.showFieldMsg('requested_for', message, 'info'); + }); +} diff --git a/Client-Side Components/Catalog Client Script/Catalog Approval/script include.js b/Client-Side Components/Catalog Client Script/Catalog Approval/script include.js new file mode 100644 index 0000000000..db94b99438 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Catalog Approval/script include.js @@ -0,0 +1,20 @@ +var ApprovalChainHelper = Class.create(); +ApprovalChainHelper.prototype = Object.extendsObject(AbstractAjaxProcessor, { + getApprovers: function() { + var itemId = this.getParameter('sysparm_item_id'); + var userId = gs.getUserID(); + + var approvers = []; + + // Example logic: fetch approval rules based on item and user + var ruleGR = new GlideRecord('sysapproval_approver'); + ruleGR.addQuery('document_id', 80f8920bc3e4b2105219daec050131e3); + ruleGR.query(); + + while (ruleGR.next()) { + approvers.push(ruleGR.approver.name.toString()); + } + + return JSON.stringify(approvers); + } +}); diff --git a/Client-Side Components/Catalog Client Script/Clear all fields/README.md b/Client-Side Components/Catalog Client Script/Clear all fields/README.md index d058526e5f..0a84c1175e 100644 --- a/Client-Side Components/Catalog Client Script/Clear all fields/README.md +++ b/Client-Side Components/Catalog Client Script/Clear all fields/README.md @@ -1,13 +1,18 @@ # Clear all fields on a catalog item form -This works on both the native platform and service portal / mobile. Typically used with an OnChange catalog client script when you would like to reset all the fields after a certain variable is changed. +This function clears all editable fields on a form, except those explicitly excluded. +It works on both the native platform (Classic UI) and Service Portal / Mobile. +Typically used with an OnChange catalog client script when you want to clear all fields after a certain variable changes. -This function does support an exclusion list if there are fields you would like to exclude from being reset, typically you would want to add the field that triggered to the change to the exlusion +The function returns an array of the field names that were cleared, which can be used for logging or further processing. -### Example +### Exclusion Support -```js -clearFields(['field1', 'field2']); -``` +You can pass an array of field names to exclude from being cleared. +This is useful when you want to preserve the value of the field that triggered the change or other important fields. -All fields on the form **except** field1 and field2 will be cleared. \ No newline at end of file +### Example +``` +clearFields(['short_description', 'priority']); +``` +// Clears all fields except 'short_description' and 'priority' diff --git a/Client-Side Components/Catalog Client Script/Clear all fields/script.js b/Client-Side Components/Catalog Client Script/Clear all fields/script.js index 8a5b64fb2d..4c5a12a099 100644 --- a/Client-Side Components/Catalog Client Script/Clear all fields/script.js +++ b/Client-Side Components/Catalog Client Script/Clear all fields/script.js @@ -1,26 +1,47 @@ -/**SNDOC - @name clearFields - @description Clear/reset all fields on a form - @param {Array} [dontClearFieldsArray] - Fields to not clear - @example - clearFields(['field1', 'field2']); -*/ +/** + * Clears or resets all editable fields on a form, except those explicitly excluded. + * Compatible with Classic UI and Service Portal/Mobile. + * Intended for use in onChange client scripts. + * + * @function clearFields + * @param {Array} dontClearFieldsArray - Array of field names to exclude from clearing. + * @returns {Array} - Array of field names that were cleared. + * + * @example + * // Clears all fields except 'short_description' and 'priority' + * clearFields(['short_description', 'priority']); + */ +function clearFields(dontClearFieldsArray) { + // Ensure the exclusion list is defined and is an array + dontClearFieldsArray = Array.isArray(dontClearFieldsArray) ? dontClearFieldsArray : []; -function clearFields(dontClearFieldsArray){ + // Helper function to check if a field should be cleared + function shouldClear(fieldName) { + return dontClearFieldsArray.indexOf(fieldName) === -1; + } - try{ // Classic UI - var pFields = g_form.nameMap; - pFields.forEach(function(field){ - if(dontClearFieldsArray.indexOf(field.prettyName) == -1){ - g_form.clearValue(field.prettyName); - } - }); - }catch(e){ // Service Portal or Mobile - var fields = g_form.getEditableFields(); - fields.forEach(function(field){ - if(dontClearFieldsArray.indexOf(fields) == -1){ - g_form.clearValue(field); - } - }); - } -} \ No newline at end of file + var clearedFields = []; + + try { + // Classic UI: use g_form.nameMap to get all fields + var allFields = g_form.nameMap; + allFields.forEach(function(field) { + var fieldName = field.prettyName; + if (shouldClear(fieldName)) { + g_form.clearValue(fieldName); + clearedFields.push(fieldName); + } + }); + } catch (e) { + // Service Portal or Mobile: use getEditableFields() + var editableFields = g_form.getEditableFields(); + editableFields.forEach(function(fieldName) { + if (shouldClear(fieldName)) { + g_form.clearValue(fieldName); + clearedFields.push(fieldName); + } + }); + } + + return clearedFields; +} diff --git a/Client-Side Components/Catalog Client Script/Document validation/Client script.JS b/Client-Side Components/Catalog Client Script/Document validation/Client script.JS new file mode 100644 index 0000000000..c6e4447e7d --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Document validation/Client script.JS @@ -0,0 +1,12 @@ +function onSubmit() { + var ga = new GlideAjax('DocumentValidationHelper'); + ga.addParam('sysparm_name', 'validateAttachments'); + ga.addParam('sysparm_item_id', g_form.getUniqueValue()); + ga.getXMLAnswer(function(response) { + if (response !== 'valid') { + alert('Document validation failed: ' + response); + return false; + } + }); + return true; +} diff --git a/Client-Side Components/Catalog Client Script/Document validation/Readme.md b/Client-Side Components/Catalog Client Script/Document validation/Readme.md new file mode 100644 index 0000000000..66975c579e --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Document validation/Readme.md @@ -0,0 +1 @@ +This project enhances a Service Catalog item by allowing users to upload supporting documents (e.g., ID proof, approval letters) and validating them before the request is submitted. It ensures compliance, completeness, and proper documentation for sensitive or regulated requests. diff --git a/Client-Side Components/Catalog Client Script/Document validation/Script include.js b/Client-Side Components/Catalog Client Script/Document validation/Script include.js new file mode 100644 index 0000000000..71c25a3771 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Document validation/Script include.js @@ -0,0 +1,22 @@ +var DocumentValidationHelper = Class.create(); +DocumentValidationHelper.prototype = Object.extendsObject(AbstractAjaxProcessor, { + validateAttachments: function() { + var itemId = this.getParameter('sysparm_item_id'); + var attachmentGR = new GlideRecord('sys_attachment'); + attachmentGR.addQuery('table_name', 'sc_req_item'); + attachmentGR.addQuery('table_sys_id', itemId); + attachmentGR.query(); + + while (attachmentGR.next()) { + var fileName = attachmentGR.file_name.toLowerCase(); + if (!fileName.endsWith('.pdf') && !fileName.endsWith('.docx')) { + return 'Only PDF or DOCX files are allowed.'; + } + if (attachmentGR.size_bytes > 5 * 1024 * 1024) { + return 'File size exceeds 5MB limit.'; + } + } + + return 'valid'; + } +}); diff --git a/Client-Side Components/Catalog Client Script/Hide Attachment icon.js b/Client-Side Components/Catalog Client Script/Hide attachment icon/Hide Attachment icon.js similarity index 100% rename from Client-Side Components/Catalog Client Script/Hide Attachment icon.js rename to Client-Side Components/Catalog Client Script/Hide attachment icon/Hide Attachment icon.js diff --git a/Client-Side Components/Catalog Client Script/Hide attachment icon/README.md b/Client-Side Components/Catalog Client Script/Hide attachment icon/README.md new file mode 100644 index 0000000000..015af4c47a --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Hide attachment icon/README.md @@ -0,0 +1,17 @@ +# Hide Attachment Icon on Catalog Items + +## Use Case / Requirement +Hide the attachment icon on a specific catalog item when the end user should not submit supporting documents. This can reduce confusion and prevent oversized uploads. + +## Solution +Use an onLoad catalog client script to target the attachment button rendered on the Service Portal form and hide it with jQuery. The snippet works for both classic and Service Portal experiences. + +## Implementation +1. Create a new catalog client script with Type set to onLoad. +2. Copy the contents of Hide Attachment icon.js into the script field. +3. Adjust the selector if your catalog item uses a custom portal or markup. + +## Notes +- Requires jQuery, which is available on standard Service Portal forms. +- The DOM can change between releases; retest after theme or layout updates. +- Remove the script if the catalog item later requires attachments. diff --git a/Client-Side Components/Catalog Client Script/Incident Sentiment Detector (Using Simple Word Matching, No AI)/SentimentAnalyzer.js b/Client-Side Components/Catalog Client Script/Incident Sentiment Detector (Using Simple Word Matching, No AI)/SentimentAnalyzer.js new file mode 100644 index 0000000000..ba38447891 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Incident Sentiment Detector (Using Simple Word Matching, No AI)/SentimentAnalyzer.js @@ -0,0 +1,16 @@ +var SentimentAnalyzer = Class.create(); +SentimentAnalyzer.prototype = Object.extendsObject(AbstractAjaxProcessor, { + getSentiment: function() { + var text = (this.getParameter('sysparm_text') || '').toLowerCase(); + var positive = ['thanks', 'great', 'resolved', 'appreciate']; + var negative = ['issue', 'error', 'not working', 'fail', 'problem']; + + var score = 0; + positive.forEach(function(word) { if (text.includes(word)) score++; }); + negative.forEach(function(word) { if (text.includes(word)) score--; }); + + if (score > 0) return 'Positive'; + if (score < 0) return 'Negative'; + return 'Neutral'; + } +}); diff --git a/Client-Side Components/Catalog Client Script/Incident Sentiment Detector (Using Simple Word Matching, No AI)/onChangeClientscript.js b/Client-Side Components/Catalog Client Script/Incident Sentiment Detector (Using Simple Word Matching, No AI)/onChangeClientscript.js new file mode 100644 index 0000000000..214a08b28b --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Incident Sentiment Detector (Using Simple Word Matching, No AI)/onChangeClientscript.js @@ -0,0 +1,11 @@ +function onChange(control, oldValue, newValue, isLoading) { + if (isLoading || !newValue) return; + + var ga = new GlideAjax('SentimentAnalyzer'); + ga.addParam('sysparm_name', 'getSentiment'); + ga.addParam('sysparm_text', newValue); + ga.getXMLAnswer(function(sentiment) { + g_form.addInfoMessage('Sentiment: ' + sentiment); + g_form.setValue('u_sentiment', sentiment); + }); +} diff --git a/Client-Side Components/Catalog Client Script/Incident Sentiment Detector (Using Simple Word Matching, No AI)/readme.md b/Client-Side Components/Catalog Client Script/Incident Sentiment Detector (Using Simple Word Matching, No AI)/readme.md new file mode 100644 index 0000000000..14e16be3f7 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Incident Sentiment Detector (Using Simple Word Matching, No AI)/readme.md @@ -0,0 +1,23 @@ +Incident Sentiment Detector (No AI, Pure JavaScript) + +A lightweight ServiceNow utility that detects sentiment (Positive / Negative / Neutral) of an Incident’s short description or comments using simple keyword matching — no AI APIs or external libraries required. + +Useful for support teams to auto-tag sentiment and analyze user frustration or satisfaction trends without expensive integrations. + +🚀 Features + +✅ Detects sentiment directly inside ServiceNow ✅ Works without external APIs or ML models ✅ Instant classification on form update ✅ Adds detected sentiment to a custom field (u_sentiment) ✅ Simple to extend — just add more positive/negative keywords + +🧩 Architecture Overview + +The solution consists of two main scripts: + +Component Type Purpose SentimentAnalyzer Script Include Processes text and returns sentiment Client Script (onChange) Client Script Calls SentimentAnalyzer via GlideAjax on short description change 🧱 Setup Instructions 1️⃣ Create Custom Field + +Create a new field on the Incident table: + +Name: u_sentiment + +Type: Choice + +Choices: Positive, Neutral, Negative 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/Multi-User Collaboration/Readme.md b/Client-Side Components/Catalog Client Script/Multi-User Collaboration/Readme.md new file mode 100644 index 0000000000..e524d91907 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Multi-User Collaboration/Readme.md @@ -0,0 +1 @@ +This project introduces a collaboration feature for Service Catalog requests, allowing multiple users to contribute to a single request. It’s ideal for scenarios like team onboarding, shared resource provisioning, or cross-functional workflows. diff --git a/Client-Side Components/Catalog Client Script/Multi-User Collaboration/Script include.JS b/Client-Side Components/Catalog Client Script/Multi-User Collaboration/Script include.JS new file mode 100644 index 0000000000..637bce5fdf --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Multi-User Collaboration/Script include.JS @@ -0,0 +1,18 @@ +var CollaboratorHandler = Class.create(); +CollaboratorHandler.prototype = Object.extendsObject(AbstractAjaxProcessor, { + addCollaborators: function() { + var requestId = this.getParameter('sysparm_request_id'); + var users = this.getParameter('sysparm_users').split(','); + + users.forEach(function(userId) { + var gr = new GlideRecord('x_your_scope_collaborators'); + gr.initialize(); + gr.request = requestId; + gr.collaborator = userId; + gr.status = 'Pending'; + gr.insert(); + }); + + return 'Collaborators added successfully'; + } +}); diff --git a/Client-Side Components/Catalog Client Script/Multi-User Collaboration/client script.JS b/Client-Side Components/Catalog Client Script/Multi-User Collaboration/client script.JS new file mode 100644 index 0000000000..6365d4e9dc --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Multi-User Collaboration/client script.JS @@ -0,0 +1,13 @@ +function onSubmit() { + var collaborators = g_form.getValue('collaborators'); // Multi-user reference field + if (collaborators) { + var ga = new GlideAjax('CollaboratorHandler'); + ga.addParam('sysparm_name', 'addCollaborators'); + ga.addParam('sysparm_request_id', g_form.getUniqueValue()); + ga.addParam('sysparm_users', collaborators); + ga.getXMLAnswer(function(response) { + alert('Collaborators added: ' + response); + }); + } + return true; +} diff --git a/Client-Side Components/Catalog Client Script/Multi-User Collaboration/widget client controller.JS b/Client-Side Components/Catalog Client Script/Multi-User Collaboration/widget client controller.JS new file mode 100644 index 0000000000..0273b282bd --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Multi-User Collaboration/widget client controller.JS @@ -0,0 +1,19 @@ +function($scope, $http) { + $scope.approve = function(sysId) { + $http.post('/api/x_your_scope/collab_action', { + action: 'approve', + sys_id: sysId + }).then(function(response) { + $scope.server.update(); + }); + }; + + $scope.reject = function(sysId) { + $http.post('/api/x_your_scope/collab_action', { + action: 'reject', + sys_id: sysId + }).then(function(response) { + $scope.server.update(); + }); + }; +} diff --git a/Client-Side Components/Catalog Client Script/Multi-User Collaboration/widget server script.JS b/Client-Side Components/Catalog Client Script/Multi-User Collaboration/widget server script.JS new file mode 100644 index 0000000000..7ca090d60e --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Multi-User Collaboration/widget server script.JS @@ -0,0 +1,15 @@ +(function() { + var collabs = []; + var gr = new GlideRecord('x_your_scope_collaborators'); + gr.addQuery('request', $sp.getParameter('request_id')); + gr.query(); + while (gr.next()) { + collabs.push({ + sys_id: gr.getUniqueValue(), + name: gr.collaborator.name.toString(), + status: gr.status.toString(), + comments: gr.comments.toString() + }); + } + data.collaborators = collabs; +})(); diff --git a/Client-Side Components/Catalog Client Script/Normalise and Reset a MRVS based on Variable Changes/README.md b/Client-Side Components/Catalog Client Script/Normalise and Reset a MRVS based on Variable Changes/README.md new file mode 100644 index 0000000000..bff708fbe8 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Normalise and Reset a MRVS based on Variable Changes/README.md @@ -0,0 +1,31 @@ +# MRVS - Normalise and Reset Rows on Change + +## What this solves +When a controlling variable changes (for example, Environment), existing MRVS rows may no longer be valid. This client script: +- Clears or normalises specific MRVS columns +- Deduplicates rows +- Optionally sorts rows for a cleaner UX +- Works entirely client-side using MRVS JSON + +## Where to use +Catalog Item → OnChange client script on your controlling variable. + +## How it works +- Reads the MRVS value as JSON via `g_form.getValue('my_mrvs')` +- Applies transforms (clear columns, unique by key, sort) +- Writes back the JSON with `g_form.setValue('my_mrvs', JSON.stringify(rows))` + +## Setup +1. Replace `CONTROLLING_VARIABLE` with your variable name. +2. Replace `MY_MRVS` with your MRVS variable name. +3. Adjust `COLUMNS_TO_CLEAR`, `UNIQUE_KEY`, and `SORT_BY` as needed. + +## Notes +- To clear the MRVS entirely, set `rows = []` before `setValue`. +- Works with Catalog Client Scripts; no server call required. + +## References +- GlideForm API (client): `getValue`, `setValue`, `clearValue` + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideForm/concept/c_GlideFormAPI.html +- Working with MRVS values on the client (community examples) + https://www.servicenow.com/community/developer-articles/accessing-multi-row-variable-set-value-outside-the-multi-row/ta-p/2308876 diff --git a/Client-Side Components/Catalog Client Script/Normalise and Reset a MRVS based on Variable Changes/mrvs_normalise_onchange.js b/Client-Side Components/Catalog Client Script/Normalise and Reset a MRVS based on Variable Changes/mrvs_normalise_onchange.js new file mode 100644 index 0000000000..1bbf3695ee --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Normalise and Reset a MRVS based on Variable Changes/mrvs_normalise_onchange.js @@ -0,0 +1,45 @@ +function onChange(control, oldValue, newValue, isLoading) { + if (isLoading) return; + + var MRVS_NAME = 'MY_MRVS'; // your MRVS variable name + var COLUMNS_TO_CLEAR = ['env', 'owner']; // MRVS column names to clear + var UNIQUE_KEY = 'hostname'; // MRVS column that should be unique + var SORT_BY = 'hostname'; // MRVS column to sort by + + try { + var raw = g_form.getValue(MRVS_NAME); + var rows = raw ? JSON.parse(raw) : []; + if (!Array.isArray(rows)) rows = []; + + // Clear specified columns + rows.forEach(function(row) { + COLUMNS_TO_CLEAR.forEach(function(col) { if (row.hasOwnProperty(col)) row[col] = ''; }); + }); + + // Deduplicate by UNIQUE_KEY + if (UNIQUE_KEY) { + var seen = {}; + rows = rows.filter(function(row) { + var key = String(row[UNIQUE_KEY] || '').toLowerCase(); + if (!key || seen[key]) return false; + seen[key] = true; + return true; + }); + } + + // Sort (case-insensitive) + if (SORT_BY) { + rows.sort(function(a, b) { + var A = String(a[SORT_BY] || '').toLowerCase(); + var B = String(b[SORT_BY] || '').toLowerCase(); + if (A < B) return -1; + if (A > B) return 1; + return 0; + }); + } + + g_form.setValue(MRVS_NAME, JSON.stringify(rows)); + } catch (e) { + console.error('MRVS normalise failed', e); + } +} diff --git a/Client-Side Components/Catalog Client Script/Normalise and Reset a MRVS based on Variable Changes/variant_reset_specific_columns.js b/Client-Side Components/Catalog Client Script/Normalise and Reset a MRVS based on Variable Changes/variant_reset_specific_columns.js new file mode 100644 index 0000000000..5430d817ae --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Normalise and Reset a MRVS based on Variable Changes/variant_reset_specific_columns.js @@ -0,0 +1,17 @@ +function onChange(control, oldValue, newValue, isLoading) { + if (isLoading) return; + + var MRVS_NAME = 'MY_MRVS'; + var COLUMNS_TO_CLEAR = ['env', 'region']; + + var rows = []; + try { rows = JSON.parse(g_form.getValue(MRVS_NAME) || '[]'); } catch (e) {} + if (!Array.isArray(rows)) rows = []; + + rows.forEach(function(row) { + COLUMNS_TO_CLEAR.forEach(function(col) { if (row.hasOwnProperty(col)) row[col] = ''; }); + }); + + g_form.setValue(MRVS_NAME, JSON.stringify(rows)); +} + diff --git a/Client-Side Components/Catalog Client Script/Onsubmit validation/Readme.md b/Client-Side Components/Catalog Client Script/Onsubmit validation/Readme.md new file mode 100644 index 0000000000..6de0662aab --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Onsubmit validation/Readme.md @@ -0,0 +1 @@ +This project adds pre-validation for hardware availability in ServiceNow Catalog Items. Before submitting a request, the system checks if the requested hardware is available in inventory and blocks submission if stock is insufficient. we can easy to extend other validations (budget, licenses, etc.).Improves user experience by validating before approval.Prevents unnecessary approvals and fulfillment. diff --git a/Client-Side Components/Catalog Client Script/Onsubmit validation/on submit scriptinclude.JS b/Client-Side Components/Catalog Client Script/Onsubmit validation/on submit scriptinclude.JS new file mode 100644 index 0000000000..bcb4631341 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Onsubmit validation/on submit scriptinclude.JS @@ -0,0 +1,25 @@ +var HardwareValidationUtils = Class.create(); +HardwareValidationUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, { + + validateHardware: function() { + var hardware = this.getParameter('sysparm_hardware'); + var qty = parseInt(this.getParameter('sysparm_quantity'), 10); + + if (!hardware || isNaN(qty)) { + return 'Invalid input!'; + } + + var gr = new GlideRecord('u_hardware_inventory'); + if (gr.get(hardware)) { + var availableQty = parseInt(gr.getValue('available_quantity'), 10); + if (availableQty >= qty) { + return 'OK'; + } else { + return 'Not enough stock available!'; + } + } + return 'Hardware not found!'; + }, + + type: 'HardwareValidationUtils' +}); diff --git a/Client-Side Components/Catalog Client Script/Onsubmit validation/submit validation client script.js b/Client-Side Components/Catalog Client Script/Onsubmit validation/submit validation client script.js new file mode 100644 index 0000000000..a521c0acfe --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Onsubmit validation/submit validation client script.js @@ -0,0 +1,29 @@ +function onSubmit() { + + + var hardware = g_form.getValue('hardware_name'); + var qty = g_form.getValue('quantity'); + + + var ga = new GlideAjax('HardwareValidationUtils'); + ga.addParam('sysparm_name', 'validateHardware'); + ga.addParam('sysparm_hardware', hardware); + ga.addParam('sysparm_quantity', qty); + + + ga.getXMLAnswer(function(response) { + + + if (response !== 'OK') { + alert(response); + + g_form.addErrorMessage(response); // Optional inline error + g_form.setSubmit(false); // Prevent submission in Service Portal + } else { + + g_form.setSubmit(true); // Allow submission + } + }); + + return false; +} diff --git a/Client-Side Components/Catalog Client Script/Percentage Symbol/readme.md b/Client-Side Components/Catalog Client Script/Percentage Symbol/readme.md new file mode 100644 index 0000000000..61268653b8 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Percentage Symbol/readme.md @@ -0,0 +1,7 @@ +Sets the field value to the formatted percentage string (e.g., 45 becomes 45.00%). + +Retrieves the current value of the field. +Removes any existing % symbol to avoid duplication. +Validates the input to ensure it's a number. +Converts the value to a float. +Formats it to two decimal places and appends a % symbol. diff --git a/Client-Side Components/Catalog Client Script/Percentage Symbol/script.js b/Client-Side Components/Catalog Client Script/Percentage Symbol/script.js new file mode 100644 index 0000000000..1685dad322 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Percentage Symbol/script.js @@ -0,0 +1,28 @@ +/*Sets the field value to the formatted percentage string (e.g., 45 becomes 45.00%). + +Retrieves the current value of the field. +Removes any existing % symbol to avoid duplication. +Validates the input to ensure it's a number. +Converts the value to a float. +Formats it to two decimal places and appends a % symbol. */ + + +function onChange(control, oldValue, newValue, isLoading) { + if (isLoading || newValue == '') { + return; + } + + function formatPercent(value) { + if (value === null || value === undefined || value === '' || isNaN(value)) { + return ""; + } + var num = parseFloat(value); + if (isNaN(num)) + return ""; + return num.toFixed(2) + "%"; +} +var des_amount = g_form.getValue("").replace("%",""); + +g_form.setValue("", formatPercent(des_amount)); + +} diff --git a/Client-Side Components/Catalog Client Script/Real time count of letters/Count letters.js b/Client-Side Components/Catalog Client Script/Real time count of letters/Count letters.js new file mode 100644 index 0000000000..71a6a70f0e --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Real time count of letters/Count letters.js @@ -0,0 +1,26 @@ +function onChange(control, oldValue, newValue, isLoading) { + if (isLoading) { + return; + } + + var maxChars = 100;//count of charaters + var currentLength = newValue.length; + + // Clear previous messages + g_form.clearMessages(); + + // Show info message + g_form.addInfoMessage('Character count: ' + currentLength + ' / ' + maxChars); + + if (currentLength > maxChars) { + // Show error message + g_form.addErrorMessage('Character limit exceeded! Please shorten your text.'); + g_form.showFieldMsg('short_description', 'Too many characters!', 'error'); + + // Make field mandatory to block submission + g_form.setMandatory('short_description', true); + } else { + // Remove mandatory if valid + g_form.setMandatory('short_description', false); + } +} diff --git a/Client-Side Components/Catalog Client Script/Real time count of letters/readme.md b/Client-Side Components/Catalog Client Script/Real time count of letters/readme.md new file mode 100644 index 0000000000..668903071b --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Real time count of letters/readme.md @@ -0,0 +1 @@ +This onChange Catalog Client Script displays the current character count for a text field and enforces a maximum limit by showing error messages and making the field mandatory to prevent form submission when exceeded. diff --git a/Client-Side Components/Catalog Client Script/Regex Validation/README.md b/Client-Side Components/Catalog Client Script/Regex Validation/README.md deleted file mode 100644 index f4bcb7aff4..0000000000 --- a/Client-Side Components/Catalog Client Script/Regex Validation/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Regular Expression on Catalog Client script - -With the help of this code you can easily validate the input value from the user and if it's not a email format you can clear and throw a error message below the variable. Of course you can use Email type variable as well but you cannot have a formatted error message. - -* [Click here for script](script.js) - - - - diff --git a/Client-Side Components/Catalog Client Script/Regex Validation/script.js b/Client-Side Components/Catalog Client Script/Regex Validation/script.js deleted file mode 100644 index e8eb396a26..0000000000 --- a/Client-Side Components/Catalog Client Script/Regex Validation/script.js +++ /dev/null @@ -1,12 +0,0 @@ -function onChange(control, oldValue, newValue, isLoading) { - if (isLoading || newValue == '') { - return; - } - //Defining the regular expression to validate if the given value is valid email address or not - var emailValidation = /^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$/; - if (!emailValidation.test(newValue)) { - g_form.clearValue('VARIABLE_NAME'); // Clear's the variable - g_form.showFieldMsg('VARIABLE_NAME', 'Please enter a valid email address', 'error'); // Display a message below variable - return false; // Stop submission - } - return true; 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/Reusable GlideAjax Client Script/DynamicTableQueryUtil.js b/Client-Side Components/Catalog Client Script/Reusable GlideAjax Client Script/DynamicTableQueryUtil.js new file mode 100644 index 0000000000..689c1812af --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Reusable GlideAjax Client Script/DynamicTableQueryUtil.js @@ -0,0 +1,73 @@ +var DynamicTableQueryUtil = Class.create(); +DynamicTableQueryUtil.prototype = Object.extendsObject(AbstractAjaxProcessor, { + + getTableRow: function() { + var tableName = this.getParameter('sysparm_table_name'); + var keyField = this.getParameter('sysparm_key_field'); + var keyValue = this.getParameter('sysparm_key_value'); + var fieldsParam = this.getParameter('sysparm_fields'); + var limitFields = !JSUtil.nil(fieldsParam); + var desiredFields = limitFields ? fieldsParam.split(',') : []; + + var result = {}; + var tableObj = {}; + var gr = new GlideRecord(tableName); + + // Use addQuery for non-sys_id fields + if (keyField === 'sys_id') { + if (!gr.get(keyValue)) { + return null; + } + } else { + gr.addQuery(keyField, keyValue); + gr.query(); + if (!gr.next()) { + return null; + } + } + + // Handle variables (if present) + if (gr.variables) { + for (var key in gr.variables) { + if (!JSUtil.nil(gr.variables[key])) { + var variableObj = gr.variables[key]; + tableObj['variables.' + key] = { + fieldDisplayVal: variableObj.getDisplayValue() || String(variableObj), + fieldVal: String(variableObj) + }; + } + } + } + + // Handle standard fields + var fields = gr.getFields(); + for (var i = 0; i < fields.size(); i++) { + var field = fields.get(i); + var fieldName = field.getName(); + tableObj[fieldName] = { + fieldDisplayVal: field.getDisplayValue() || String(field), + fieldVal: String(field) + }; + } + + // Add sys_id explicitly + tableObj['sys_id'] = { + fieldDisplayVal: 'Sys ID', + fieldVal: gr.getUniqueValue() + }; + + // Filter fields if requested + if (limitFields) { + desiredFields.forEach(function(field) { + field = field.trim(); + if (tableObj[field]) { + result[field] = tableObj[field]; + } + }); + } else { + result = tableObj; + } + + return new JSON().encode(result); + } +}); diff --git a/Client-Side Components/Catalog Client Script/Reusable GlideAjax Client Script/Readme.md b/Client-Side Components/Catalog Client Script/Reusable GlideAjax Client Script/Readme.md new file mode 100644 index 0000000000..9815575f4b --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Reusable GlideAjax Client Script/Readme.md @@ -0,0 +1,25 @@ +This solution provides a generic and reusable GlideAjax-based client-server interaction in ServiceNow that allows querying any table by passing: + +Table name +Key field and value +Desired fields to retrieve + +It dynamically returns field values from the server and populates them on the form, making it ideal for use cases like CMDB enrichment, entitlement lookups, or dynamic form population. + +1. Client Script (onChange) +Triggers on field change. +Sends parameters to the Script Include via GlideAjax. +Receives JSON response and sets target field value. + +Parameters: +sysparm_table_name: Table to query (e.g., sys_user) +sysparm_key_field: Field to match (e.g., sys_id) +sysparm_key_value: Value to match +sysparm_fields: Comma-separated list of fields to retrieve + +2. Script Include: DynamicTableQueryUtil + +Processes incoming parameters. +Queries the specified table and retrieves requested fields. +Supports both standard fields and catalog item variables. +Returns a JSON object with field values and display values. diff --git a/Client-Side Components/Catalog Client Script/Reusable GlideAjax Client Script/clientscript.js b/Client-Side Components/Catalog Client Script/Reusable GlideAjax Client Script/clientscript.js new file mode 100644 index 0000000000..a4e2bc8a1a --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Reusable GlideAjax Client Script/clientscript.js @@ -0,0 +1,32 @@ +function onChange(control, oldValue, newValue, isLoading) { + if (isLoading || newValue === '') { + return; + } + + // Define parameters dynamically + var tableName = 'sys_user'; // Change as needed + var keyField = 'sys_id'; // Change as needed + var fieldsToFetch = 'email'; // Comma-separated list + var targetField = 'user'; // Field to populate + + var ga = new GlideAjax('DynamicTableQueryUtil'); + ga.addParam('sysparm_name', 'getTableRow'); + ga.addParam('sysparm_table_name', tableName); + ga.addParam('sysparm_key_field', keyField); + ga.addParam('sysparm_key_value', newValue); + ga.addParam('sysparm_fields', fieldsToFetch); + ga.getXML(function(response) { + var answer = response.responseXML.documentElement.getAttribute("answer"); + if (!answer) { + alert('No response from Script Include'); + return; + } + + var parsedAnswer = JSON.parse(answer); + if (parsedAnswer[fieldsToFetch]) { + g_form.setValue(targetField, parsedAnswer[fieldsToFetch]['fieldVal']); + } else { + alert('error'); + } + }); +} diff --git a/Client-Side Components/Catalog Client Script/Schedule Request/Readme.JS b/Client-Side Components/Catalog Client Script/Schedule Request/Readme.JS new file mode 100644 index 0000000000..6d5fe42ac6 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Schedule Request/Readme.JS @@ -0,0 +1 @@ +This project allows users to schedule a Service Catalog request for a future date and time. Instead of submitting immediately, the request is stored and automatically submitted later using a Scheduled Job diff --git a/Client-Side Components/Catalog Client Script/Schedule Request/scheduled client script.js b/Client-Side Components/Catalog Client Script/Schedule Request/scheduled client script.js new file mode 100644 index 0000000000..eaa13fa462 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Schedule Request/scheduled client script.js @@ -0,0 +1,17 @@ +function onSubmit() { + var scheduledTime = g_form.getValue('scheduled_time'); + var currentTime = new Date().toISOString(); + + if (scheduledTime > currentTime) { + var ga = new GlideAjax('ScheduledRequestHelper'); + ga.addParam('sysparm_name', 'storeScheduledRequest'); + ga.addParam('sysparm_item', g_form.getUniqueValue()); + ga.addParam('sysparm_time', scheduledTime); + ga.getXMLAnswer(function(response) { + alert('Your request has been scheduled for: ' + scheduledTime); + }); + return false; // Prevent immediate submission + } + + return true; // Submit immediately if time is now or past +} diff --git a/Client-Side Components/Catalog Client Script/Schedule Request/scheduled scriptinclude.JS b/Client-Side Components/Catalog Client Script/Schedule Request/scheduled scriptinclude.JS new file mode 100644 index 0000000000..15411e4561 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Schedule Request/scheduled scriptinclude.JS @@ -0,0 +1,15 @@ +var ScheduledRequestHelper = Class.create(); +ScheduledRequestHelper.prototype = Object.extendsObject(AbstractAjaxProcessor, { + storeScheduledRequest: function() { + var itemID = this.getParameter('sysparm_item'); + var scheduledTime = this.getParameter('sysparm_time'); + + var record = new GlideRecord('x_snc_scheduled_requests'); // Custom table + record.initialize(); + record.catalog_item = itemID; + record.scheduled_time = scheduledTime; + record.insert(); + + return 'Scheduled successfully'; + } +}); diff --git a/Client-Side Components/Catalog Client Script/Schedule Request/scheduled_job.JS b/Client-Side Components/Catalog Client Script/Schedule Request/scheduled_job.JS new file mode 100644 index 0000000000..102de236d2 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Schedule Request/scheduled_job.JS @@ -0,0 +1,14 @@ +var now = new GlideDateTime(); +var gr = new GlideRecord('x_snc_scheduled_requests'); +gr.addQuery('scheduled_time', '<=', now); +gr.query(); + +while (gr.next()) { + var request = new GlideRecord('sc_request'); + request.initialize(); + request.requested_for = gr.requested_for; + request.cat_item = gr.catalog_item; + request.insert(); + + gr.deleteRecord(); // Remove after submission +} diff --git a/Client-Side Components/Catalog Client Script/Set and Lock Variable by Group/README.md b/Client-Side Components/Catalog Client Script/Set and Lock Variable by Group/README.md new file mode 100644 index 0000000000..e7d932757d --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Set and Lock Variable by Group/README.md @@ -0,0 +1,5 @@ +**Set and Lock Variable by Group** + +This solution provides a secure and dynamic way to control data entry on a Service Catalog form based on the user's group membership. It is typically used to pre-fill and lock certain justification or approval bypass fields for authorized users (like managers or executive staff), improving their efficiency while maintaining an accurate audit trail. + +This functionality requires a combined Client-side (Catalog Client Script) and Server-side (Script Include) approach to ensure the group check is done securely. diff --git a/Client-Side Components/Catalog Client Script/Set and Lock Variable by Group/set_lock_variable_by_grp.js b/Client-Side Components/Catalog Client Script/Set and Lock Variable by Group/set_lock_variable_by_grp.js new file mode 100644 index 0000000000..b1c04555c7 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/Set and Lock Variable by Group/set_lock_variable_by_grp.js @@ -0,0 +1,33 @@ +// onload Catalog Client Script with Catalog Name +function onLoad() { + var variableName = 'bypass_approval_reason'; + var targetGroupName = 'ServiceNow Support'; // The group authorized to skip this step + var ga = new GlideAjax('UserUtils'); + ga.addParam('sysparm_name', 'isMemberOf'); + ga.addParam('sysparm_group_name', targetGroupName); + ga.getXMLAnswer(checkAndLockVariable); + function checkAndLockVariable(response) { + var isMember = response; + if (isMember == 'true') { + var message = 'Value set and locked due to your ' + targetGroupName + ' membership.'; + var setValue = 'Bypassed by authorized ' + targetGroupName + ' member.'; + g_form.setValue(variableName, setValue); + g_form.setReadOnly(variableName, true); + g_form.showFieldMsg(variableName, message, 'info'); + } else { + g_form.setReadOnly(variableName, false); + } + } +} + +//Script Include +var UserUtils = Class.create(); +UserUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, { + isMemberOf: function() { + var groupName = this.getParameter('sysparm_group_name'); + var isMember = gs.getUser().isMemberOf(groupName); + return isMember.toString(); + }, + + type: 'UserUtils' +}); diff --git a/Client-Side Components/Catalog Client Script/Special Characters/README.md b/Client-Side Components/Catalog Client Script/Special Characters/README.md deleted file mode 100644 index 90fd463a18..0000000000 --- a/Client-Side Components/Catalog Client Script/Special Characters/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Validate Special Characters for a catalog variable - -With this onChange catalog client script you can validate if there are any special characters present in the input given by user in a particular field and show an error message below the field and clear the field value. Although we have other methods to do this, it is much easier and you can customize your error message. - diff --git a/Client-Side Components/Catalog Client Script/Special Characters/script.js b/Client-Side Components/Catalog Client Script/Special Characters/script.js deleted file mode 100644 index edd1262e11..0000000000 --- a/Client-Side Components/Catalog Client Script/Special Characters/script.js +++ /dev/null @@ -1,15 +0,0 @@ -function onChange(control, oldValue, newValue, isLoading, isTemplate) { - - if (isLoading || newValue === '') { - return; - } - - //In the below regex, you can add or remove any special characters as per your requirement - var special_chars = /[~@|$^<>\*+=;?`')[\]]/; - - if (special_chars.test(newValue)) { - g_form.clearValue(''); - g_form.showErrorBox('','Special Characters are not allowed'); //you can change the error message as required. - } - -} diff --git a/Client-Side Components/Catalog Client Script/Strong Username Validation Script/README.md b/Client-Side Components/Catalog Client Script/Strong Username Validation Script/README.md deleted file mode 100644 index 3ae51d1735..0000000000 --- a/Client-Side Components/Catalog Client Script/Strong Username Validation Script/README.md +++ /dev/null @@ -1,9 +0,0 @@ -Description of the Strong Username Validation Script -Purpose -The script is designed to validate a username entered by the user in a ServiceNow catalog item form. It ensures that the username adheres to specific criteria before the form can be successfully submitted. - -Validation Criteria -The username must: -Start with a letter: The first character of the username must be an alphabetic character (a-z or A-Z). -Be at least 6 characters long: The username must contain a minimum of six characters. -Contain only letters and numbers: The username can only include alphabetic characters and digits (0-9). diff --git a/Client-Side Components/Catalog Client Script/catalog Draft/Readm.md b/Client-Side Components/Catalog Client Script/catalog Draft/Readm.md new file mode 100644 index 0000000000..a4ef78f42e --- /dev/null +++ b/Client-Side Components/Catalog Client Script/catalog Draft/Readm.md @@ -0,0 +1,9 @@ +This project implements an Auto Save Draft feature for ServiceNow Catalog Items. It automatically saves the user’s progress (form variables) every few minutes to prevent data loss if the session times out or the browser closes. it Prevents data loss during long form filling. + + + +features +Auto-save catalog form data every 2 minutes. + Stores draft data in a custom table. +Restores saved data when the user reopens the catalog item. + Works in Service Portal diff --git a/Client-Side Components/Catalog Client Script/catalog Draft/Script include.JS b/Client-Side Components/Catalog Client Script/catalog Draft/Script include.JS new file mode 100644 index 0000000000..f8687b4754 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/catalog Draft/Script include.JS @@ -0,0 +1,27 @@ +var CatalogDraftUtils = Class.create(); +CatalogDraftUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, { + saveDraft: function() { + var userId = gs.getUserID(); + var catalogItem = this.getParameter('sysparm_catalog_item'); + var draftData = this.getParameter('sysparm_draft_data'); + + var gr = new GlideRecord('u_catalog_draft'); + gr.addQuery('user', userId); + gr.addQuery('catalog_item', catalogItem); + gr.query(); + if (gr.next()) { + gr.variables_json = draftData; + gr.last_saved = new GlideDateTime(); + gr.update(); + } else { + gr.initialize(); + gr.user = userId; + gr.catalog_item = catalogItem; + gr.variables_json = draftData; + gr.last_saved = new GlideDateTime(); + gr.insert(); + } + return 'Draft saved successfully'; + }, + type: 'CatalogDraftUtils' +}); diff --git a/Client-Side Components/Catalog Client Script/catalog Draft/client script.js b/Client-Side Components/Catalog Client Script/catalog Draft/client script.js new file mode 100644 index 0000000000..7b908008cf --- /dev/null +++ b/Client-Side Components/Catalog Client Script/catalog Draft/client script.js @@ -0,0 +1,19 @@ +function onLoad() { + console.log('Auto Save Draft initialized'); + + setInterval(function() { + var draftData = { + hardware_name: g_form.getValue('hardware_name'), + quantity: g_form.getValue('quantity') + }; + + var ga = new GlideAjax('CatalogDraftUtils'); + ga.addParam('sysparm_name', 'saveDraft'); + ga.addParam('sysparm_catalog_item', g_form.getValue('sys_id')); // Catalog item sys_id + ga.addParam('sysparm_draft_data', JSON.stringify(draftData)); + + ga.getXMLAnswer(function(response) { + console.log('Draft saved: ' + response); + }); + }, 120000); // Every 2 minutes +} diff --git a/Client-Side Components/Catalog Client Script/previous Request/Readme.MD b/Client-Side Components/Catalog Client Script/previous Request/Readme.MD new file mode 100644 index 0000000000..8719bdbf32 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/previous Request/Readme.MD @@ -0,0 +1,3 @@ +Show previous request ON requested for selection + +This feature enhances the Service Catalog experience by displaying previous requests for the selected Requested For user. When a user selects the Requested For variable in a catalog item form, a confirmation message appears showing the last few requests created for that user. diff --git a/Client-Side Components/Catalog Client Script/previous Request/previous request client script.js b/Client-Side Components/Catalog Client Script/previous Request/previous request client script.js new file mode 100644 index 0000000000..b0757fedaa --- /dev/null +++ b/Client-Side Components/Catalog Client Script/previous Request/previous request client script.js @@ -0,0 +1,24 @@ +function onChange(control, oldValue, newValue, isLoading) { + if (isLoading || newValue == '') return; + + var ga = new GlideAjax('PreviousRequestsUtils'); + ga.addParam('sysparm_name', 'getPreviousRequests'); + ga.addParam('sysparm_requested_for', newValue); + ga.getXMLAnswer(function(response) { + var requests = JSON.parse(response); + if (requests.length === 0) { + alert('No previous requests found for this user.'); + } else { + var message = 'Previous Requests:\n\n'; + requests.forEach(function(req) { + message += 'Number: ' + req.number + ' | Item: ' + req.item + ' | Date: ' + req.date + '\n'; + }); + if (confirm(message + '\nDo you want to continue?')) { + // User clicked OK + } else { + // User clicked Cancel + g_form.setValue('requested_for', oldValue); + } + } + }); +} diff --git a/Client-Side Components/Catalog Client Script/previous Request/previous request script include.js b/Client-Side Components/Catalog Client Script/previous Request/previous request script include.js new file mode 100644 index 0000000000..ef8ffd6292 --- /dev/null +++ b/Client-Side Components/Catalog Client Script/previous Request/previous request script include.js @@ -0,0 +1,20 @@ +var PreviousRequestsUtils = Class.create(); +PreviousRequestsUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, { + getPreviousRequests: function() { + var requestedFor = this.getParameter('sysparm_requested_for'); + var result = []; + var gr = new GlideRecord('sc_req_item'); + gr.addQuery('requested_for', requestedFor); + gr.orderByDesc('sys_created_on'); + gr.setLimit(5); // Show last 5 requests + gr.query(); + while (gr.next()) { + result.push({ + number: gr.number.toString(), + item: gr.cat_item.getDisplayValue(), + date: gr.sys_created_on.getDisplayValue() + }); + } + return JSON.stringify(result); + } +}); 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/Abort action when description is empty/Code.js b/Client-Side Components/Client Scripts/Abort action when description is empty/Code.js new file mode 100644 index 0000000000..763e34d973 --- /dev/null +++ b/Client-Side Components/Client Scripts/Abort action when description is empty/Code.js @@ -0,0 +1,12 @@ +function onSubmit() { + //Type appropriate comment here, and begin script below + var description = g_form.getValue('description'); + var state = g_form.getValue('state'); + + if ((!description) && (state == 'completed')) { + g_form.addErrorMessage('Please provide Description Value, Description Cannot be empty'); + + return false; + } + +} diff --git a/Client-Side Components/Client Scripts/Abort action when description is empty/ReadMe.md b/Client-Side Components/Client Scripts/Abort action when description is empty/ReadMe.md new file mode 100644 index 0000000000..62906c16fe --- /dev/null +++ b/Client-Side Components/Client Scripts/Abort action when description is empty/ReadMe.md @@ -0,0 +1,2 @@ +When an Incident record is being closed, the system should validate that the Description field is not empty and state is completed. +If the Description field is blank and state is completed, the record submission (update) should be aborted, and the user should be prompted to provide a description before closing the incident. diff --git a/Client-Side Components/Client Scripts/Abort direct incident closure without Resolve State/readme.md b/Client-Side Components/Client Scripts/Abort direct incident closure without Resolve State/readme.md new file mode 100644 index 0000000000..3f6cb95b33 --- /dev/null +++ b/Client-Side Components/Client Scripts/Abort direct incident closure without Resolve State/readme.md @@ -0,0 +1,31 @@ +📘 README — Incident State Validation (Client Script) +Overview + +This Client Script enforces the correct ITIL incident lifecycle by preventing users from directly closing an incident without first transitioning it through the Resolved state. +If a user attempts to move the state directly to Closed Complete, the system reverts the change and displays a notification. + +What This Code Does +Monitors changes to the state field on Incident form +Checks if new selection is trying to skip the Resolved step +Reverts state to its previous value when the rule is violated +Alerts the user with a clear and guided message +Refreshes the form to maintain data consistency +Usage Instructions + +Create a Client Script on the Incident table +Type: onChange +Field Name: state +Paste the script under the script section +Test by trying to directly move an Incident to Closed Complete +S +cript Requirements +State values must be configured as: +6 → Resolved +7 → Closed Complete + +Script runs only on Incident records +Must be active in applicable ITIL views +Notes for Developers +This code supports clean transition handling for ITSM workflows +Helps enforce process compliance without server-side overhead +Recommended for environments requiring strict closure governance diff --git a/Client-Side Components/Client Scripts/Abort direct incident closure without Resolve State/script.js b/Client-Side Components/Client Scripts/Abort direct incident closure without Resolve State/script.js new file mode 100644 index 0000000000..f423c6109c --- /dev/null +++ b/Client-Side Components/Client Scripts/Abort direct incident closure without Resolve State/script.js @@ -0,0 +1,30 @@ +function onChange(control, oldValue, newValue, isLoading, isTemplate) { + // Prevent execution during form load or when value is empty + if (isLoading || newValue === '') { + return; + } + + // Dummy values for state comparison + // Assuming: + // 6 = Resolved + // 7 = Closed Complete + var RESOLVED = 6; + var CLOSED = 7; + + // Prevent direct move to Closed without passing through Resolved + if (newValue == CLOSED && oldValue != RESOLVED) { + + // Reset to previous state + g_form.setValue('state', oldValue); + + // Show validation warning + g_form.addErrorMessage( + 'Direct closure is not allowed. Please first move the incident to Resolved state.' + ); + + // Reload page after showing error + setTimeout(function() { + location.reload(); + }, 3000); + } +} diff --git a/Client-Side Components/Client Scripts/Auto Update Priority based on Impact and Urgency/readme.md b/Client-Side Components/Client Scripts/Auto Update Priority based on Impact and Urgency/readme.md new file mode 100644 index 0000000000..8d1628deb7 --- /dev/null +++ b/Client-Side Components/Client Scripts/Auto Update Priority based on Impact and Urgency/readme.md @@ -0,0 +1,81 @@ +🧩 Readme : Client Script: Auto Priority Update Based on Impact and Urgency +📘 Overview + +This client script automatically updates the Priority field on the Incident form whenever the Impact or Urgency value changes. +It follows the ITIL standard mapping to ensure the correct priority is always set automatically, improving data accuracy and efficiency for service desk agents. + +⚙️ Script Details +Field Value +Name Auto Priority Update based on Impact and Urgency +Type onChange +Applies to Table Incident +Applies on Fields impact, urgency +UI Type All (Classic, Mobile, Workspace) +Active ✅ Yes +Condition Leave blank +💻 Script Code +// ========================================================================== +// Script Name: Auto Priority Update based on Impact and Urgency +// Table: Incident +// Type: onChange | Fields: impact, urgency +// UI Type: All +// Version: 2025 Production Ready +// ========================================================================== + +function onChange(control, oldValue, newValue, isLoading, isTemplate) { + // Skip execution if form is loading or field is empty + if (isLoading || newValue == '') { + return; + } + + // Get Impact and Urgency values + var impact = g_form.getValue('impact'); + var urgency = g_form.getValue('urgency'); + + // Define Priority Matrix (ITIL standard) + var priorityMatrix = { + '1': { '1': '1', '2': '2', '3': '3' }, + '2': { '1': '2', '2': '3', '3': '4' }, + '3': { '1': '3', '2': '4', '3': '5' } + }; + + // Find the new Priority + var newPriority = priorityMatrix[impact]?.[urgency]; + + // Update the Priority field if valid + if (newPriority) { + if (g_form.getValue('priority') != newPriority) { + g_form.setValue('priority', newPriority); + g_form.showFieldMsg('priority', 'Priority auto-updated based on Impact and Urgency', 'info'); + } + } else { + // Optional: Clear Priority if invalid combination is selected + g_form.clearValue('priority'); + g_form.showFieldMsg('priority', 'Invalid Impact/Urgency combination — priority cleared', 'error'); + } +} + +🧠 How It Works + +The script runs automatically when Impact or Urgency changes. +It checks the ITIL-based matrix to determine the correct Priority. +If a valid combination is found, the Priority field updates automatically. +A small info message appears to confirm the update. + +🔢 ITIL Mapping Table +Impact Urgency Resulting Priority +1 (High) 1 (High) 1 (Critical) +1 2 2 +1 3 3 +2 1 2 +2 2 3 +2 3 4 +3 1 3 +3 2 4 +3 3 5 +✅ Benefits + +Automatically enforces ITIL priority standards +Reduces manual effort and user errors +Ensures consistency in priority calculation +Compatible with Classic UI, Next Experience, and Agent Workspace diff --git a/Client-Side Components/Client Scripts/Auto Update Priority based on Impact and Urgency/script.js b/Client-Side Components/Client Scripts/Auto Update Priority based on Impact and Urgency/script.js new file mode 100644 index 0000000000..b718b3185a --- /dev/null +++ b/Client-Side Components/Client Scripts/Auto Update Priority based on Impact and Urgency/script.js @@ -0,0 +1,58 @@ +//Auto Priority Update based on Impact and Urgency + +// ========================================================================== +// Script Name: Auto Priority Update based on Impact and Urgency +// Table: Incident (or any Task-based table) +// Type: onChange | Fields: impact, urgency +// UI Type: All +// ========================================================================== + +function onChange(control, oldValue, newValue, isLoading, isTemplate) { + // Prevent the script from running when the form loads or when the field is empty + if (isLoading || newValue == '') { + return; + } + + // ---------------------------------------------------------------------- + // Step 1: Fetch field values from the form + // ---------------------------------------------------------------------- + var impact = g_form.getValue('impact'); // e.g., 1 - High, 2 - Medium, 3 - Low + var urgency = g_form.getValue('urgency'); // e.g., 1 - High, 2 - Medium, 3 - Low + + // ---------------------------------------------------------------------- + // Step 2: Define the ITIL-based Priority Matrix + // ---------------------------------------------------------------------- + // Each row represents "Impact", and each column represents "Urgency" + // The resulting value sets the "Priority" + var priorityMatrix = { + '1': { '1': '1', '2': '2', '3': '3' }, // Impact = High + '2': { '1': '2', '2': '3', '3': '4' }, // Impact = Medium + '3': { '1': '3', '2': '4', '3': '5' } // Impact = Low + }; + + // ---------------------------------------------------------------------- + // Step 3: Determine the new priority based on selected Impact/Urgency + // ---------------------------------------------------------------------- + var newPriority = priorityMatrix[impact]?.[urgency]; // optional chaining prevents errors + + // ---------------------------------------------------------------------- + // Step 4: Update the Priority field and inform the user + // ---------------------------------------------------------------------- + if (newPriority) { + // Only update if priority is different from current value + if (g_form.getValue('priority') != newPriority) { + g_form.setValue('priority', newPriority); + + // Show message (works in both Classic UI and Next Experience) + g_form.showFieldMsg('priority', 'Priority auto-updated based on Impact and Urgency', 'info'); + } + } else { + // Optional: clear priority if invalid combination is selected + g_form.clearValue('priority'); + g_form.showFieldMsg('priority', 'Invalid Impact/Urgency combination — priority cleared', 'error'); + } + + // ---------------------------------------------------------------------- + // End of Script + // ---------------------------------------------------------------------- +} diff --git a/Client-Side Components/Client Scripts/Auto-Populate Planned End Date/README.md b/Client-Side Components/Client Scripts/Auto-Populate Planned End Date/README.md new file mode 100644 index 0000000000..5b6175153d --- /dev/null +++ b/Client-Side Components/Client Scripts/Auto-Populate Planned End Date/README.md @@ -0,0 +1,2 @@ +An onChange client script that calls the script includes one that adds the specified number of business days to the planned start date and autopopulates the planned end date based on the type. +For the client callable script include refer to the README file in the Add Business Days Script include folder. Link: [Add Business Days Script Include](/Server-Side%20Components/Script%20Includes/Add%20Business%20Days/README.md) diff --git a/Client-Side Components/Client Scripts/Auto-Populate Planned End Date/autoPopulatePlannedEndDate.js b/Client-Side Components/Client Scripts/Auto-Populate Planned End Date/autoPopulatePlannedEndDate.js new file mode 100644 index 0000000000..70b4930070 --- /dev/null +++ b/Client-Side Components/Client Scripts/Auto-Populate Planned End Date/autoPopulatePlannedEndDate.js @@ -0,0 +1,25 @@ +//Client script +//Table: Change Request +//UI Type: All +//Type: onChange +//Field: Planned Start Date +function onChange(control, oldValue, newValue, isLoading, isTemplate) { + if (isLoading || newValue === '') { + return; + } + var daysToAdd; + if(g_form.getValue('type') =='standard' || g_form.getValue('type') =='normal') + daysToAdd = 3; + else if(g_form.getValue('type') =='emergency') + daysToAdd = 1; + var ga = new GlideAjax("addBusinessDays"); //Calling the add business days script include, which is in the Server-Side Components/Script Includes/Add Business Days/addBusinessDays.js + ga.addParam('sysparm_name', 'addDays'); + ga.addParam('sysparm_days', daysToAdd); + ga.addParam('sysparm_date', newValue); + ga.getXML(processResponse); + + function processResponse(response) { + var answer = response.responseXML.documentElement.getAttribute("answer").toString(); + g_form.setValue('end_date', answer); + } +} diff --git a/Client-Side Components/Client Scripts/Auto-Populate Short Discription/Auto populate short description.js b/Client-Side Components/Client Scripts/Auto-Populate Short Discription/Auto populate short description.js new file mode 100644 index 0000000000..ab05f00a7c --- /dev/null +++ b/Client-Side Components/Client Scripts/Auto-Populate Short Discription/Auto populate short description.js @@ -0,0 +1,29 @@ +function onChange(control, oldValue, newValue, isLoading, isTemplate) { + if (isLoading || newValue === '') { + return; + } + + // Define category-to-short description mapping + var categoryToShortDescription = { + 'hardware': 'Hardware Issue - ', + 'software': 'Software Issue - ', + 'network': 'Network Issue - ', + 'inquiry': 'Inquiry/Help - ', + 'database': 'Database - ' + }; + + // Convert the selected value to lowercase for matching + var selectedCategory = newValue.toLowerCase(); + + // If category exists in mapping, update the short description + if (categoryToShortDescription.hasOwnProperty(selectedCategory)) { + var existingDesc = g_form.getValue('short_description') || ''; + var prefix = categoryToShortDescription[selectedCategory]; + + // Only add prefix if it's not already there + if (!existingDesc.startsWith(prefix)) { + g_form.setValue('short_description', prefix + existingDesc); + g_form.showFieldMsg('short_description', 'Short Description auto-updated based on category.', 'info'); + } + } +} diff --git a/Client-Side Components/Client Scripts/Auto-Populate Short Discription/Readme.md b/Client-Side Components/Client Scripts/Auto-Populate Short Discription/Readme.md new file mode 100644 index 0000000000..4dab2bdbd8 --- /dev/null +++ b/Client-Side Components/Client Scripts/Auto-Populate Short Discription/Readme.md @@ -0,0 +1,36 @@ +# Auto-Populate Short Description (Client Script) +## Overview + +This client script automatically updates the Short Description field in ServiceNow whenever a user selects a category on the form. It improves data consistency, saves time, and ensures that short descriptions remain meaningful and standardized. + +## How It Works + +When a user selects a category such as Hardware, Software, or Network, the script automatically prefixes the existing short description with a corresponding label (for example, “Hardware Issue –”). +This makes incident records easier to identify and improves the quality of data entry. + +## Configuration Steps + +1. Log in to your ServiceNow instance with admin or developer access. +2. Navigate to System Definition → Client Scripts. +3. Create a new Client Script with the following details: + +``` +Table: Incident +Type: onChange +Field name: category +Copy and paste the provided script into the Script field. +Save the record and make sure the script is active. +``` + +## Testing + +1. Open any existing or new Incident record. +2. Select a category such as Hardware or Software. +3. The Short Description field will automatically update with the corresponding prefix. + +## Benefits + +Speeds up data entry for users. +Maintains consistency in short descriptions. +Reduces manual effort and human errors. +Enhances clarity in incident listings and reports. diff --git a/Client-Side Components/Client Scripts/Call SI to recover User data/README.md b/Client-Side Components/Client Scripts/Call SI to recover User data/README.md new file mode 100644 index 0000000000..d159135c0d --- /dev/null +++ b/Client-Side Components/Client Scripts/Call SI to recover User data/README.md @@ -0,0 +1,5 @@ +# Call Script Include to recover User data + +In this onChange Client Script, the scenario is this: +- In a form, we have a reference field to the User table to allow our user to select any user within the platform. +- Once a user is selected, we are passing that Sys ID to recover more information regarding the selected record. diff --git a/Client-Side Components/Client Scripts/Call SI to recover User data/script.js b/Client-Side Components/Client Scripts/Call SI to recover User data/script.js new file mode 100644 index 0000000000..be7141d1e1 --- /dev/null +++ b/Client-Side Components/Client Scripts/Call SI to recover User data/script.js @@ -0,0 +1,33 @@ +/* +Type: onChange +Field: a reference to the User table +*/ +function onChange(control, oldValue, newValue, isLoading, isTemplate) { + if (isLoading || newValue === '') { + return; + } + + var test = newValue; + + //The Script Include called here can be found in: + //Server-Side Components / Script Includes / Get User Data by Id + //It is in Global scope + var ga = new GlideAjax('GetUserData');//script include name + ga.addParam('sysparm_name', 'GetUserBy_id'); //method to be called + ga.addParam('sysparm_userid', test); //send a parameter to the method. + ga.getXMLAnswer(userInfoParse); + + function userInfoParse(response) { + if (response == '') { + g_form.addErrorMessage('User not found with the informed sys_id.'); + } else { + //alert(response); + var obj = JSON.parse(response); + g_form.setValue('u_first_name', obj.first_name.toString()); + g_form.setValue('u_last_name', obj.last_name.toString()); + g_form.setValue('u_email', obj.email.toString()); + } + + } + +} diff --git a/Client-Side Components/Client Scripts/Change incident Number label color based on priority/Script.js b/Client-Side Components/Client Scripts/Change incident Number label color based on priority/Script.js new file mode 100644 index 0000000000..d28d45d491 --- /dev/null +++ b/Client-Side Components/Client Scripts/Change incident Number label color based on priority/Script.js @@ -0,0 +1,19 @@ +function onLoad () +{ + var aPriority = g_form.getValue('priority'); + var label = g_form.getLable('number'); + label.style.backgroundColor = "lightblue"; + if(aPriority==1) + { + label.style.color = "red"; + } + else if (aPriority==2) + { + label.style.color = "yellow"; + } + else + { + label.style.color = "blue"; + } +} + diff --git a/Client-Side Components/Client Scripts/Change incident Number label color based on priority/readme.md b/Client-Side Components/Client Scripts/Change incident Number label color based on priority/readme.md new file mode 100644 index 0000000000..240deede54 --- /dev/null +++ b/Client-Side Components/Client Scripts/Change incident Number label color based on priority/readme.md @@ -0,0 +1,2 @@ +Write Client Script +Change Incident Number Lable color based on priority diff --git a/Client-Side Components/Client Scripts/Check all mandatory fields using mandatoryCheck()/README.md b/Client-Side Components/Client Scripts/Check all mandatory fields using mandatoryCheck()/README.md index ea9a494c64..da8569c194 100644 --- a/Client-Side Components/Client Scripts/Check all mandatory fields using mandatoryCheck()/README.md +++ b/Client-Side Components/Client Scripts/Check all mandatory fields using mandatoryCheck()/README.md @@ -1,11 +1,17 @@ -**Client Script** +# Mandatory Field Check on Form Change -Client script which is showing how to use g_form.mandatoryCheck() function, which allows to easily detect if any of mandatory field is not filled on record. +This client script demonstrates how to use `g_form.mandatoryCheck()` to validate whether all mandatory fields on a form are filled. -**Example configuration** +It is typically used in an **onChange** catalog client script to trigger validation when a specific field changes. -![Coniguration](ScreenShot_1.PNG) +If mandatory fields are missing, the script: +- Displays an info message listing the missing fields. Note: the red message is the existing functionality that will give you an error message. +- Visually highlights each missing field using a flashing effect to guide the user. -**Example execution** +This approach improves user experience by clearly indicating which fields require attention. -![Execution](ScreenShot_2.PNG) +**Example configuration** +image + +**Example execution** +image diff --git a/Client-Side Components/Client Scripts/Check all mandatory fields using mandatoryCheck()/script.js b/Client-Side Components/Client Scripts/Check all mandatory fields using mandatoryCheck()/script.js index 197edcfb5e..d83106348e 100644 --- a/Client-Side Components/Client Scripts/Check all mandatory fields using mandatoryCheck()/script.js +++ b/Client-Side Components/Client Scripts/Check all mandatory fields using mandatoryCheck()/script.js @@ -1,19 +1,17 @@ function onChange(control, oldValue, newValue, isLoading, isTemplate) { - if (isLoading || newValue === '') { - return; - } - - //Client script, which shows how to use mandatoryCheck() function - //mandatoryCheck() allows validating if all mandatory fields are filled - //If all mandatory fields are filled it return true, otherwise it returns false + if (isLoading || newValue === '') { + return; + } - //Check if all mandatory fields are filled on record + // Check if all mandatory fields are filled if (!g_form.mandatoryCheck()) { - - //Example action when not all mandatory fields are filled - display message and remove state field + //if not get all missing fields var missing = g_form.getMissingFields(); - g_form.addInfoMessage("State field removed, because not all mandatory fields are filled: " + missing); - g_form.setDisplay('state', false); + //Info message displaying the fields that are missing + g_form.addInfoMessage("Please complete the following mandatory fields: " + missing.join(", ")); + //go through missing fields and flash them + missing.forEach(function (fieldName) { + g_form.flash(fieldName, "#FFFACD",0); // Flash to draw attention + }); } - } diff --git a/Client-Side Components/Client Scripts/Client Validation of Attachments by File Type and Count/README.md b/Client-Side Components/Client Scripts/Client Validation of Attachments by File Type and Count/README.md new file mode 100644 index 0000000000..864a87ed09 --- /dev/null +++ b/Client-Side Components/Client Scripts/Client Validation of Attachments by File Type and Count/README.md @@ -0,0 +1,8 @@ +This ServiceNow client script is designed to validate file attachments on a form before submission. It's most likely used as a "Client Script" or "Client Script in a Catalog Item" that runs in the browser when a user tries to submit a form. + +This client script runs when a form is submitted in ServiceNow. It checks if the user has: + +Attached at least one file (shows an error if none). +Attached no more than three files (shows an error if more). +Only uploaded files of type PDF or PNG (shows an error for other types). +If any of these checks fail, the form submission is blocked and an appropriate error message is displayed. diff --git a/Client-Side Components/Client Scripts/Client Validation of Attachments by File Type and Count/code.js b/Client-Side Components/Client Scripts/Client Validation of Attachments by File Type and Count/code.js new file mode 100644 index 0000000000..1d18ffb059 --- /dev/null +++ b/Client-Side Components/Client Scripts/Client Validation of Attachments by File Type and Count/code.js @@ -0,0 +1,24 @@ +(function executeRule(gForm, gUser, gSNC) { + var attachments = gForm.getAttachments(); + if (!attachments || attachments.length === 0) { + gForm.addErrorMessage("You must attach at least one file."); + return false; + } + + if (attachments.length > 3) { + gForm.addErrorMessage("You can only upload up to 3 files."); + return false; + } + + var allowedTypes = ['pdf', 'png']; + for (var i = 0; i < attachments.length; i++) { + var fileName = attachments[i].file_name.toLowerCase(); + var ext = fileName.split('.').pop(); + if (!allowedTypes.includes(ext)) { + gForm.addErrorMessage("Only PDF and PNG files are allowed."); + return false; + } + } + + return true; +})(gForm, gUser, gSNC); 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/Conditional Auto-Routing and Dynamic Mandatory Fields/Conditional_AutoRouting_Dynamic_Mandatory_Fields.js b/Client-Side Components/Client Scripts/Conditional Auto-Routing and Dynamic Mandatory Fields/Conditional_AutoRouting_Dynamic_Mandatory_Fields.js new file mode 100644 index 0000000000..b93bdf08d0 --- /dev/null +++ b/Client-Side Components/Client Scripts/Conditional Auto-Routing and Dynamic Mandatory Fields/Conditional_AutoRouting_Dynamic_Mandatory_Fields.js @@ -0,0 +1,16 @@ +function onChange(control, oldValue, newValue, isLoading) { + if (isLoading) return; + + if (newValue === 'hardware') { + g_form.setMandatory('asset_tag', true); + g_form.setDisplay('asset_tag', true); + g_form.setValue('assignment_group', 'Hardware Support Group'); + } else if (newValue === 'software') { + g_form.setMandatory('asset_tag', false); + g_form.setDisplay('asset_tag', false); + g_form.setValue('assignment_group', 'Software Support Group'); + } else { + g_form.setMandatory('asset_tag', false); + g_form.setDisplay('asset_tag', true); + } +} diff --git a/Client-Side Components/Client Scripts/Conditional Auto-Routing and Dynamic Mandatory Fields/Readme.md b/Client-Side Components/Client Scripts/Conditional Auto-Routing and Dynamic Mandatory Fields/Readme.md new file mode 100644 index 0000000000..d073b7d0e8 --- /dev/null +++ b/Client-Side Components/Client Scripts/Conditional Auto-Routing and Dynamic Mandatory Fields/Readme.md @@ -0,0 +1,2 @@ +If an Incident Category = Hardware, make Asset Tag mandatory and automatically assign to Hardware Support Group. +If Software, assign to Software Support Group and hide Asset Tag. diff --git a/Client-Side Components/Client Scripts/Count Assigned To Field/README.md b/Client-Side Components/Client Scripts/Count Assigned To Field/README.md new file mode 100644 index 0000000000..24bc571aa9 --- /dev/null +++ b/Client-Side Components/Client Scripts/Count Assigned To Field/README.md @@ -0,0 +1,14 @@ +Count Assigned To Field + +1. Write a Client Script name as getAssignedToCount +2. Glide the Incident Table +3. Use onChange Client Script +4. Use the Field name as "assigned_to" field +5. Glide the Script Include using "GlideAjax". +6. Call the function "getCount" from Script Include +7. Add the parameter for the newValue. +8. Use the getXML for asynchronous response. +9. Get the answer using the callback function +10. Use the logic for the more than how many tickets that error needs to populate +11. Use the addErrorMessage for marking the error message +12. Use the setValue for the "assigned_to" field. diff --git a/Client-Side Components/Client Scripts/Count Assigned To Field/getAssignedToCount.js b/Client-Side Components/Client Scripts/Count Assigned To Field/getAssignedToCount.js new file mode 100644 index 0000000000..31105ebe56 --- /dev/null +++ b/Client-Side Components/Client Scripts/Count Assigned To Field/getAssignedToCount.js @@ -0,0 +1,18 @@ +function onChange(control,oldValue,newValue,isLoading,isTemplate) { + if(isLoading || newValue === '') { + return; + } + + var ga = new GlideAjax('countAssignedUtil'); + ga.addParam('sysparm','getCount'); + ga.addParam('sysparm_assignedto', newValue); + ga.getXML(callback); + + function callback(response){ + var answer = response.responseXML.documentElement.getAttribute("answer"); + if(answer >=5){ + g_form.addErrorMessage("Please select another person to work on this Incident, selected user is already having 5 tickets in his/her Queue"); + g_form.setValue("assigned_to", ""); + } + } +} diff --git a/Client-Side Components/Client Scripts/Date Range Validation (Within 30 Days)/README.md b/Client-Side Components/Client Scripts/Date Range Validation (Within 30 Days)/README.md new file mode 100644 index 0000000000..b020f6f58d --- /dev/null +++ b/Client-Side Components/Client Scripts/Date Range Validation (Within 30 Days)/README.md @@ -0,0 +1,10 @@ +Date Range Validation (Within 30 Days) in Client Side + +This ServiceNow client script provides real-time date validation for form fields, ensuring users can only select dates within a specific 30-day window from today's date. The script runs automatically when a user changes a date field value, providing immediate feedback and preventing invalid date submissions. + +The script validates that any date entered in a form field meets these criteria: +Minimum Date: Today's date (no past dates allowed) +Maximum Date: 30 days from today's date +Real-time Validation: Instant feedback as users type or select dates +User-friendly Errors: Clear error messages explaining the valid date range +Automatic Field Clearing: Invalid dates are automatically cleared to prevent submission diff --git a/Client-Side Components/Client Scripts/Date Range Validation (Within 30 Days)/dateRangeValidation.js b/Client-Side Components/Client Scripts/Date Range Validation (Within 30 Days)/dateRangeValidation.js new file mode 100644 index 0000000000..5c9f15fd1c --- /dev/null +++ b/Client-Side Components/Client Scripts/Date Range Validation (Within 30 Days)/dateRangeValidation.js @@ -0,0 +1,23 @@ +function onChange(control, oldValue, newValue, isLoading, isTemplate) { + if (isLoading || newValue === '') { + return; + } + + var todayDate = new Date(); + var futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 30); + + var todayDateStr = formatDate(todayDate, g_user_date_format); + var futureDateStr = formatDate(futureDate, g_user_date_format); + + var selectedDateNum = getDateFromFormat(newValue, g_user_date_format); + var todayDateNum = getDateFromFormat(todayDateStr, g_user_date_format); + var futureDateNum = getDateFromFormat(futureDateStr, g_user_date_format); + + if (selectedDateNum < todayDateNum || selectedDateNum > futureDateNum) { + g_form.showFieldMsg(control, 'Date must be between today and 30 days from today', 'error'); + g_form.clearValue(control); + } else { + g_form.hideFieldMsg(control); + } +} 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/Display Custom Field Based on Incident Channel Field and populate with Caller Information/Display_CustomField_Autopopulate_Caller_1.png b/Client-Side Components/Client Scripts/Display Custom Field Based on Incident Channel Field and populate with Caller Information/Display_CustomField_Autopopulate_Caller_1.png new file mode 100644 index 0000000000..9bdf814c7c Binary files /dev/null and b/Client-Side Components/Client Scripts/Display Custom Field Based on Incident Channel Field and populate with Caller Information/Display_CustomField_Autopopulate_Caller_1.png differ diff --git a/Client-Side Components/Client Scripts/Display Custom Field Based on Incident Channel Field and populate with Caller Information/Display_CustomField_Autopopulate_Caller_2.png b/Client-Side Components/Client Scripts/Display Custom Field Based on Incident Channel Field and populate with Caller Information/Display_CustomField_Autopopulate_Caller_2.png new file mode 100644 index 0000000000..5bbafd0f19 Binary files /dev/null and b/Client-Side Components/Client Scripts/Display Custom Field Based on Incident Channel Field and populate with Caller Information/Display_CustomField_Autopopulate_Caller_2.png differ diff --git a/Client-Side Components/Client Scripts/Display Custom Field Based on Incident Channel Field and populate with Caller Information/Display_CustomField_Autopopulate_Caller_3.png b/Client-Side Components/Client Scripts/Display Custom Field Based on Incident Channel Field and populate with Caller Information/Display_CustomField_Autopopulate_Caller_3.png new file mode 100644 index 0000000000..34f020bb30 Binary files /dev/null and b/Client-Side Components/Client Scripts/Display Custom Field Based on Incident Channel Field and populate with Caller Information/Display_CustomField_Autopopulate_Caller_3.png differ diff --git a/Client-Side Components/Client Scripts/Display Custom Field Based on Incident Channel Field and populate with Caller Information/GlideAjaxCallerInfo.js b/Client-Side Components/Client Scripts/Display Custom Field Based on Incident Channel Field and populate with Caller Information/GlideAjaxCallerInfo.js new file mode 100644 index 0000000000..d5653ff5f5 --- /dev/null +++ b/Client-Side Components/Client Scripts/Display Custom Field Based on Incident Channel Field and populate with Caller Information/GlideAjaxCallerInfo.js @@ -0,0 +1,22 @@ +var CallerInfoHelper = Class.create(); +CallerInfoHelper.prototype = Object.extendsObject(AbstractAjaxProcessor, { + + getCallerInfo: function() { + var callerSysId = this.getParameter('sysparm_caller'); + if (!callerSysId) + return JSON.stringify({ email: '', mobile: '' }); + + var userGR = new GlideRecord('sys_user'); + if (!userGR.get(callerSysId)) + return JSON.stringify({ email: '', mobile: '' }); + + var userObj = { + email: userGR.email.toString(), + mobile: userGR.mobile_phone.toString() + }; + + return JSON.stringify(userObj); + }, + + type: 'CallerInfoHelper' +}); diff --git a/Client-Side Components/Client Scripts/Display Custom Field Based on Incident Channel Field and populate with Caller Information/README.md b/Client-Side Components/Client Scripts/Display Custom Field Based on Incident Channel Field and populate with Caller Information/README.md new file mode 100644 index 0000000000..d91289cec2 --- /dev/null +++ b/Client-Side Components/Client Scripts/Display Custom Field Based on Incident Channel Field and populate with Caller Information/README.md @@ -0,0 +1,43 @@ +# Display Custom Email/Phone Field Based on Incident Channel Field and Populate those Field with Caller Information + +Displays either the **Email** or **Phone** field on the **Incident** form based on the selected **Channel** value (Email or Phone) and populate the fields with the caller’s details. + +### Use Case +- When **Channel = Email**, the **Email** field becomes visible and is auto-populated with the caller’s email address +- When **Channel = Phone**, the **Phone** field becomes visible and is auto-populated with the caller’s mobile number +- Both details fetched from the caller’s record from **sys_user** table. +- The custom Email and Phone fields may also serve as placeholder to update if details differ from the caller record + +### Prerequisites +- Create Two custom fields on Incident Table + - **u_email** which captures store the caller’s email address + - **u_phone** which capture caller’s mobile number +- Create **Two UI Policies** which hides the u_email and u_phone field unless channel choice is phone or email +- Create an onChange Client Script that calls a GlideAjax Script to fetch the caller’s contact details and populate the custom Email or Phone field on the Incident form +- To further enhance usecase Regex used on Phone field. Refer (https://github.com/ServiceNowDevProgram/code-snippets/pull/2375) + +--- + +### Incident Record when channel choice is other than Email or Phone + +![Display_CustomField_Autopopulate_Caller_3](Display_CustomField_Autopopulate_Caller_3.png) + +--- + +### Incident Record when Channel choice is email and populate Email Field by caller's Email + +![Display_CustomField_Autopopulate_Caller_1](Display_CustomField_Autopopulate_Caller_1.png) + +--- + +### Incident Record when channel choice is phone and populate Phone Field by caller's Phone Number + +![Display_CustomField_Autopopulate_Caller_2](Display_CustomField_Autopopulate_Caller_2.png) + +--- + + + + + + diff --git a/Client-Side Components/Client Scripts/Display Custom Field Based on Incident Channel Field and populate with Caller Information/clientScriptOnChangeCaller.js b/Client-Side Components/Client Scripts/Display Custom Field Based on Incident Channel Field and populate with Caller Information/clientScriptOnChangeCaller.js new file mode 100644 index 0000000000..b3001513b2 --- /dev/null +++ b/Client-Side Components/Client Scripts/Display Custom Field Based on Incident Channel Field and populate with Caller Information/clientScriptOnChangeCaller.js @@ -0,0 +1,26 @@ +function onChange(control, oldValue, newValue, isLoading) { + if (isLoading || newValue === '') return; + + var ga = new GlideAjax('CallerInfoHelper'); + ga.addParam('sysparm_name', 'getCallerInfo'); + ga.addParam('sysparm_caller', newValue); + + ga.getXMLAnswer(function(answer) { + // Confirm what you’re actually receiving + console.log("GlideAjax raw answer:", answer); + + if (!answer) return; + + var info; + try { + info = JSON.parse(answer); + } catch (e) { + console.log("Error parsing JSON:", e); + return; + } + + g_form.setValue('u_email', info.email || ''); + g_form.setValue('u_phone', info.mobile || ''); + + }); +} diff --git a/Client-Side Components/Client Scripts/Display Incident Count of Assigned-To User When Field Changes/Incident_Count_message_1.png b/Client-Side Components/Client Scripts/Display Incident Count of Assigned-To User When Field Changes/Incident_Count_message_1.png new file mode 100644 index 0000000000..197c4f9c77 Binary files /dev/null and b/Client-Side Components/Client Scripts/Display Incident Count of Assigned-To User When Field Changes/Incident_Count_message_1.png differ diff --git a/Client-Side Components/Client Scripts/Display Incident Count of Assigned-To User When Field Changes/Incident_Count_message_2.png b/Client-Side Components/Client Scripts/Display Incident Count of Assigned-To User When Field Changes/Incident_Count_message_2.png new file mode 100644 index 0000000000..719060d503 Binary files /dev/null and b/Client-Side Components/Client Scripts/Display Incident Count of Assigned-To User When Field Changes/Incident_Count_message_2.png differ diff --git a/Client-Side Components/Client Scripts/Display Incident Count of Assigned-To User When Field Changes/README.md b/Client-Side Components/Client Scripts/Display Incident Count of Assigned-To User When Field Changes/README.md new file mode 100644 index 0000000000..dc4ce7ff51 --- /dev/null +++ b/Client-Side Components/Client Scripts/Display Incident Count of Assigned-To User When Field Changes/README.md @@ -0,0 +1,17 @@ +## Display Info Message of Incident Count of Assigned-To User When Field Assigned-To Changes + +Displays a message showing the count of **open incidents** assigned to a user whenever the **Assigned To** field changes on the Incident form. + +- Helps assess the assignee’s **current workload** by fetching and displaying active incident counts (excluding *Resolved*, *Closed*, and *Canceled* states) +- Shows an **info message** with the count of the assignee's assigned incidents +- Uses an **onChange Client Script** on the **Assigned To** field and a **GlideAjax Script Include** called from the client script to fetch the count dynamically + +--- + +### Info Message Example 1 +![Incident_Count_message_1](Incident_Count_message_1.png) + +### Info Message Example 2 +![Incident_Count_message_2](Incident_Count_message_2.png) + +--- diff --git a/Client-Side Components/Client Scripts/Display Incident Count of Assigned-To User When Field Changes/clientScript.js b/Client-Side Components/Client Scripts/Display Incident Count of Assigned-To User When Field Changes/clientScript.js new file mode 100644 index 0000000000..b241347f81 --- /dev/null +++ b/Client-Side Components/Client Scripts/Display Incident Count of Assigned-To User When Field Changes/clientScript.js @@ -0,0 +1,31 @@ +function onChange(control, oldValue, newValue, isLoading, isTemplate) { + if (isLoading || newValue === '') return; + + // Clear any previous messages + g_form.clearMessages(); + + // Create GlideAjax object to call the Script Include + var ga = new GlideAjax('IncidentAssignmentCheck'); + ga.addParam('sysparm_name', 'getIncidentCount'); + ga.addParam('sysparm_user', newValue); + + + ga.getXMLAnswer(function(response) { + var count = parseInt(response, 10); + + + if (isNaN(count)) { + g_form.addErrorMessage("Could not retrieve open incident count."); + return; + } + + var userName = g_form.getDisplayValue('assigned_to'); + var msg = userName + " currently has " + count + " incidents assigned "; + + if (count >= 5) { + g_form.addInfoMessage(msg + " Please review workload before assigning more incidents"); + } else { + g_form.addInfoMessage(msg); + } + }); +} diff --git a/Client-Side Components/Client Scripts/Display Incident Count of Assigned-To User When Field Changes/glideAjaxIncidentCount.js b/Client-Side Components/Client Scripts/Display Incident Count of Assigned-To User When Field Changes/glideAjaxIncidentCount.js new file mode 100644 index 0000000000..93d62b9af3 --- /dev/null +++ b/Client-Side Components/Client Scripts/Display Incident Count of Assigned-To User When Field Changes/glideAjaxIncidentCount.js @@ -0,0 +1,23 @@ +var IncidentAssignmentCheck = Class.create(); +IncidentAssignmentCheck.prototype = Object.extendsObject(AbstractAjaxProcessor, { + + getIncidentCount: function() { + var user = this.getParameter('sysparm_user'); + var count = 0; + + if (user) { + var gr = new GlideAggregate('incident'); + gr.addQuery('assigned_to', user); + gr.addQuery('state', 'NOT IN', '6,7,8'); + gr.addAggregate('COUNT');a + gr.query(); + + if (gr.next()) { + count = gr.getAggregate('COUNT'); + } + } + return count; + }, + + type: 'IncidentAssignmentCheck' +}); diff --git a/Client-Side Components/Client Scripts/Display a Live Word Count for Description Field/README.md b/Client-Side Components/Client Scripts/Display a Live Word Count for Description Field/README.md new file mode 100644 index 0000000000..95c5f586f7 --- /dev/null +++ b/Client-Side Components/Client Scripts/Display a Live Word Count for Description Field/README.md @@ -0,0 +1,5 @@ +The script enhances a form field (specifically the description field) by: +-Adding a live word counter below the field. +-Visually warning the user if the word count exceeds 150 words. + +This client-side script, intended for use in a ServiceNow form (e.g., catalog item or incident form), dynamically appends a custom `
` element below the `description` field to display a real-time word count. It leverages the `g_form.getControl()` API to access the field's DOM element and attaches an `input` event listener to monitor user input. The script calculates the word count by splitting the input text using a regular expression (`\s+`) and updates the counter accordingly. It applies conditional styling to the counter (`green` if ≤150 words, `red` if >150), providing immediate visual feedback to the user to enforce input constraints. diff --git a/Client-Side Components/Client Scripts/Display a Live Word Count for Description Field/code.js b/Client-Side Components/Client Scripts/Display a Live Word Count for Description Field/code.js new file mode 100644 index 0000000000..127a1fcb51 --- /dev/null +++ b/Client-Side Components/Client Scripts/Display a Live Word Count for Description Field/code.js @@ -0,0 +1,11 @@ +function onChange(control, oldValue, newValue, isLoading, isTemplate) { + if (isLoading || newValue === oldValue) { + return; + } + + var wordCount = newValue.trim().split(/\s+/).length; + var message = 'Word Count: ' + (newValue ? wordCount : 0); + var messageType = (wordCount > 150) ? 'error' : 'info'; + + g_form.showFieldMsg('description', message, messageType); +} diff --git a/Client-Side Components/Client Scripts/Dynamic Field Dependencies with GlideAjax/README.md b/Client-Side Components/Client Scripts/Dynamic Field Dependencies with GlideAjax/README.md new file mode 100644 index 0000000000..a5806e34f6 --- /dev/null +++ b/Client-Side Components/Client Scripts/Dynamic Field Dependencies with GlideAjax/README.md @@ -0,0 +1,191 @@ +# Dynamic Field Dependencies with GlideAjax + +This folder contains advanced Client Script examples demonstrating real-time field dependencies using GlideAjax for server-side data retrieval, cascading dropdowns, and dynamic form behavior. + +## Overview + +Modern ServiceNow forms often require dynamic behavior based on user selections. This example demonstrates: +- **Real-time field updates** using GlideAjax for server-side queries +- **Cascading dropdown menus** that filter based on parent selections +- **Conditional field visibility** based on complex business logic +- **Debouncing** to prevent excessive server calls +- **Loading indicators** for better user experience +- **Error handling** for failed AJAX calls +- **Performance optimization** with caching and batching + +## Script Descriptions + +- **dynamic_category_subcategory.js**: Client Script implementing cascading category/subcategory dropdowns with real-time filtering. +- **conditional_field_loader.js**: Advanced example showing conditional field loading based on multiple dependencies. +- **ajax_script_include.js**: Server-side Script Include (Client Callable) that provides data for the client scripts. +- **debounced_field_validator.js**: Real-time field validation with debouncing to reduce server load. + +## Key Features + +### 1. GlideAjax Communication +Efficient client-server communication: +```javascript +var ga = new GlideAjax('MyScriptInclude'); +ga.addParam('sysparm_name', 'getSubcategories'); +ga.addParam('sysparm_category', categoryValue); +ga.getXMLAnswer(callback); +``` + +### 2. Cascading Dropdowns +Dynamic filtering of child fields: +- Parent field change triggers child field update +- Child options filtered based on parent selection +- Multiple levels of cascading supported +- Maintains previously selected values when possible + +### 3. Debouncing +Prevents excessive server calls: +- Waits for user to stop typing before making request +- Configurable delay (typically 300-500ms) +- Cancels pending requests when new input received +- Improves performance and user experience + +### 4. Loading Indicators +Visual feedback during AJAX calls: +- Shows loading spinner or message +- Disables fields during data fetch +- Clears loading state on completion or error +- Prevents duplicate submissions + +## Use Cases + +- **Category/Subcategory Selection**: Filter subcategories based on selected category +- **Location-Based Fields**: Update city/state/country fields dynamically +- **Product Configuration**: Show/hide fields based on product type +- **Assignment Rules**: Dynamically populate assignment groups based on category +- **Cost Estimation**: Calculate and display costs based on selections +- **Availability Checking**: Real-time validation of resource availability +- **Dynamic Pricing**: Update pricing fields based on quantity/options + +## Implementation Requirements + +### Client Script Configuration +- **Type**: onChange (for field changes) or onLoad (for initial setup) +- **Table**: Target table (e.g., incident, sc_req_item) +- **Field**: Trigger field (e.g., category) +- **Active**: true +- **Global**: false (table-specific for better performance) + +### Script Include Configuration +- **Name**: Descriptive name (e.g., CategoryAjaxUtils) +- **Client callable**: true (REQUIRED for GlideAjax) +- **Active**: true +- **Access**: public or specific roles + +### Required Fields +Ensure dependent fields exist on the form: +- Add fields to form layout +- Configure field properties (mandatory, read-only, etc.) +- Set up choice lists for dropdown fields + +## Performance Considerations + +### Optimization Techniques +1. **Cache responses**: Store frequently accessed data client-side +2. **Batch requests**: Combine multiple queries into single AJAX call +3. **Minimize payload**: Return only required fields +4. **Use indexed queries**: Ensure server-side queries use indexed fields +5. **Debounce input**: Wait for user to finish typing +6. **Lazy loading**: Load data only when needed + +### Best Practices +- Keep Script Includes focused and single-purpose +- Validate input parameters server-side +- Handle errors gracefully with user-friendly messages +- Test with large datasets to ensure performance +- Use browser developer tools to monitor network calls +- Implement timeout handling for slow connections + +## Security Considerations + +### Input Validation +Always validate parameters server-side: +```javascript +// BAD: No validation +var category = this.getParameter('sysparm_category'); +var gr = new GlideRecord('cmdb_ci_category'); +gr.addQuery('parent', category); // SQL injection risk + +// GOOD: Validate and sanitize +var category = this.getParameter('sysparm_category'); +if (!category || !this._isValidSysId(category)) { + return '[]'; +} +``` + +### Access Control +- Respect ACLs in Script Includes +- Use GlideRecordSecure when appropriate +- Don't expose sensitive data to client +- Implement role-based filtering + +### XSS Prevention +- Sanitize data before displaying +- Use g_form.setValue() instead of innerHTML +- Validate choice list values +- Escape special characters + +## Error Handling + +### Client-Side +```javascript +ga.getXMLAnswer(function(response) { + if (!response || response === 'error') { + g_form.addErrorMessage('Failed to load data. Please try again.'); + return; + } + // Process response +}); +``` + +### Server-Side +```javascript +try { + // Query logic +} catch (ex) { + gs.error('Error in getSubcategories: ' + ex.message); + return 'error'; +} +``` + +## Testing Checklist + +- [ ] Test with empty/null values +- [ ] Test with invalid input +- [ ] Test with large datasets (1000+ records) +- [ ] Test on slow network connections +- [ ] Test concurrent user interactions +- [ ] Test browser compatibility (Chrome, Firefox, Safari, Edge) +- [ ] Test mobile responsiveness +- [ ] Verify ACL enforcement +- [ ] Check for console errors +- [ ] Monitor network tab for performance + +## Browser Compatibility + +Tested and compatible with: +- Chrome 90+ +- Firefox 88+ +- Safari 14+ +- Edge 90+ +- ServiceNow Mobile App + +## Related APIs + +- **GlideAjax**: Client-server communication +- **GlideForm (g_form)**: Form manipulation +- **GlideUser (g_user)**: User context +- **GlideRecord**: Server-side queries +- **JSON**: Data serialization + +## Additional Resources + +- ServiceNow Client Script Best Practices +- GlideAjax Documentation +- Client-Side Scripting API Reference +- Performance Optimization Guide diff --git a/Client-Side Components/Client Scripts/Dynamic Field Dependencies with GlideAjax/ajax_script_include.js b/Client-Side Components/Client Scripts/Dynamic Field Dependencies with GlideAjax/ajax_script_include.js new file mode 100644 index 0000000000..9fdafce23d --- /dev/null +++ b/Client-Side Components/Client Scripts/Dynamic Field Dependencies with GlideAjax/ajax_script_include.js @@ -0,0 +1,323 @@ +/** + * CategoryAjaxUtils Script Include + * + * Name: CategoryAjaxUtils + * Client callable: true (REQUIRED) + * Active: true + * + * Description: Server-side Script Include providing data for dynamic field dependencies + * Supports multiple AJAX methods for category/subcategory and related field operations + */ + +var CategoryAjaxUtils = Class.create(); +CategoryAjaxUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, { + + /** + * Get subcategories for a given category + * Parameters: sysparm_category, sysparm_table + * Returns: JSON array of {value, label} objects + */ + getSubcategories: function() { + var category = this.getParameter('sysparm_category'); + var table = this.getParameter('sysparm_table') || 'incident'; + + // Validate input + if (!category) { + return JSON.stringify([]); + } + + var subcategories = []; + + try { + // Query subcategory choices + var gr = new GlideRecord('sys_choice'); + gr.addQuery('name', table); + gr.addQuery('element', 'subcategory'); + gr.addQuery('dependent_value', category); + gr.addQuery('inactive', false); + gr.orderBy('sequence'); + gr.orderBy('label'); + gr.query(); + + while (gr.next()) { + subcategories.push({ + value: gr.getValue('value'), + label: gr.getValue('label') + }); + } + + // If no dependent choices found, get all subcategories + if (subcategories.length === 0) { + gr = new GlideRecord('sys_choice'); + gr.addQuery('name', table); + gr.addQuery('element', 'subcategory'); + gr.addQuery('inactive', false); + gr.orderBy('sequence'); + gr.orderBy('label'); + gr.setLimit(100); // Limit to prevent performance issues + gr.query(); + + while (gr.next()) { + subcategories.push({ + value: gr.getValue('value'), + label: gr.getValue('label') + }); + } + } + + } catch (ex) { + gs.error('[CategoryAjaxUtils] Error in getSubcategories: ' + ex.message); + return 'error'; + } + + return JSON.stringify(subcategories); + }, + + /** + * Get all dependent field data in a single call (performance optimization) + * Parameters: sysparm_category, sysparm_priority, sysparm_assignment_group, sysparm_table + * Returns: JSON object with multiple field data + */ + getDependentFields: function() { + var category = this.getParameter('sysparm_category'); + var priority = this.getParameter('sysparm_priority'); + var assignmentGroup = this.getParameter('sysparm_assignment_group'); + var table = this.getParameter('sysparm_table') || 'incident'; + + var result = { + subcategories: [], + suggested_assignment_group: null, + sla_info: null, + estimated_cost: null, + recommendations: [] + }; + + try { + // Get subcategories + result.subcategories = this._getSubcategoriesArray(category, table); + + // Get suggested assignment group based on category + result.suggested_assignment_group = this._getSuggestedAssignmentGroup(category); + + // Get SLA information + result.sla_info = this._getSLAInfo(category, priority); + + // Get estimated cost (if applicable) + result.estimated_cost = this._getEstimatedCost(category, priority); + + // Get recommendations + result.recommendations = this._getRecommendations(category, priority); + + } catch (ex) { + gs.error('[CategoryAjaxUtils] Error in getDependentFields: ' + ex.message); + return 'error'; + } + + return JSON.stringify(result); + }, + + /** + * Validate field dependencies + * Parameters: sysparm_category, sysparm_subcategory + * Returns: JSON object with validation result + */ + validateDependencies: function() { + var category = this.getParameter('sysparm_category'); + var subcategory = this.getParameter('sysparm_subcategory'); + + var result = { + valid: false, + message: '' + }; + + try { + // Check if subcategory is valid for the category + var gr = new GlideRecord('sys_choice'); + gr.addQuery('name', 'incident'); + gr.addQuery('element', 'subcategory'); + gr.addQuery('value', subcategory); + gr.addQuery('dependent_value', category); + gr.query(); + + if (gr.hasNext()) { + result.valid = true; + result.message = 'Valid combination'; + } else { + result.valid = false; + result.message = 'Invalid subcategory for selected category'; + } + + } catch (ex) { + gs.error('[CategoryAjaxUtils] Error in validateDependencies: ' + ex.message); + result.valid = false; + result.message = 'Validation error: ' + ex.message; + } + + return JSON.stringify(result); + }, + + // ======================================== + // Private Helper Methods + // ======================================== + + /** + * Get subcategories as array (internal method) + * @private + */ + _getSubcategoriesArray: function(category, table) { + var subcategories = []; + + var gr = new GlideRecord('sys_choice'); + gr.addQuery('name', table); + gr.addQuery('element', 'subcategory'); + gr.addQuery('dependent_value', category); + gr.addQuery('inactive', false); + gr.orderBy('sequence'); + gr.orderBy('label'); + gr.query(); + + while (gr.next()) { + subcategories.push({ + value: gr.getValue('value'), + label: gr.getValue('label') + }); + } + + return subcategories; + }, + + /** + * Get suggested assignment group based on category + * @private + */ + _getSuggestedAssignmentGroup: function(category) { + // This could be a lookup table or business rule + // For demonstration, using a simple mapping + var categoryGroupMap = { + 'hardware': 'Hardware Support', + 'software': 'Application Support', + 'network': 'Network Operations', + 'database': 'Database Team', + 'inquiry': 'Service Desk' + }; + + var groupName = categoryGroupMap[category]; + if (!groupName) { + return null; + } + + // Look up the actual group sys_id + var gr = new GlideRecord('sys_user_group'); + gr.addQuery('name', groupName); + gr.addQuery('active', true); + gr.query(); + + if (gr.next()) { + return gr.getUniqueValue(); + } + + return null; + }, + + /** + * Get SLA information based on category and priority + * @private + */ + _getSLAInfo: function(category, priority) { + // Simplified SLA calculation + // In production, this would query actual SLA definitions + var resolutionHours = 24; // Default + + if (priority == '1') { + resolutionHours = 4; + } else if (priority == '2') { + resolutionHours = 8; + } else if (priority == '3') { + resolutionHours = 24; + } else if (priority == '4') { + resolutionHours = 72; + } + + // Adjust based on category + if (category === 'hardware') { + resolutionHours *= 1.5; // Hardware takes longer + } + + return { + resolution_time: resolutionHours, + response_time: resolutionHours / 4 + }; + }, + + /** + * Get estimated cost based on category and priority + * @private + */ + _getEstimatedCost: function(category, priority) { + // Simplified cost estimation + var baseCost = 100; + + var categoryMultiplier = { + 'hardware': 2.0, + 'software': 1.5, + 'network': 1.8, + 'database': 1.7, + 'inquiry': 0.5 + }; + + var priorityMultiplier = { + '1': 3.0, + '2': 2.0, + '3': 1.0, + '4': 0.5, + '5': 0.3 + }; + + var catMult = categoryMultiplier[category] || 1.0; + var priMult = priorityMultiplier[priority] || 1.0; + + return Math.round(baseCost * catMult * priMult); + }, + + /** + * Get recommendations based on category and priority + * @private + */ + _getRecommendations: function(category, priority) { + var recommendations = []; + + // Add category-specific recommendations + if (category === 'hardware') { + recommendations.push('Attach hardware diagnostic logs'); + recommendations.push('Include serial number and model'); + } else if (category === 'software') { + recommendations.push('Include application version'); + recommendations.push('Attach error screenshots'); + } else if (category === 'network') { + recommendations.push('Run network diagnostics'); + recommendations.push('Include IP address and subnet'); + } + + // Add priority-specific recommendations + if (priority == '1' || priority == '2') { + recommendations.push('Consider escalating to manager'); + recommendations.push('Notify stakeholders immediately'); + } + + return recommendations; + }, + + /** + * Validate if a string is a valid sys_id + * @private + */ + _isValidSysId: function(value) { + if (!value || typeof value !== 'string') { + return false; + } + // sys_id is 32 character hex string + return /^[0-9a-f]{32}$/.test(value); + }, + + type: 'CategoryAjaxUtils' +}); diff --git a/Client-Side Components/Client Scripts/Dynamic Field Dependencies with GlideAjax/conditional_field_loader.js b/Client-Side Components/Client Scripts/Dynamic Field Dependencies with GlideAjax/conditional_field_loader.js new file mode 100644 index 0000000000..f96c977659 --- /dev/null +++ b/Client-Side Components/Client Scripts/Dynamic Field Dependencies with GlideAjax/conditional_field_loader.js @@ -0,0 +1,245 @@ +/** + * Advanced Conditional Field Loader with Multiple Dependencies + * + * Table: incident + * Type: onChange + * Field: category, priority, assignment_group (multiple scripts or combined) + * + * Description: Shows/hides and populates fields based on multiple conditions + * Demonstrates debouncing, caching, and complex business logic + */ + +// Global cache object (persists across onChange calls) +if (typeof window.fieldDependencyCache === 'undefined') { + window.fieldDependencyCache = {}; + window.fieldDependencyTimers = {}; +} + +/** + * Main onChange handler for category field + */ +function onChangeCategoryWithDependencies(control, oldValue, newValue, isLoading, isTemplate) { + if (isLoading) { + return; + } + + var priority = g_form.getValue('priority'); + var assignmentGroup = g_form.getValue('assignment_group'); + + // Update field visibility based on category + updateFieldVisibility(newValue); + + // Load dependent data with debouncing + loadDependentFields(newValue, priority, assignmentGroup); +} + +/** + * Update field visibility based on category selection + */ +function updateFieldVisibility(category) { + // Example: Show additional fields for specific categories + var showAdvancedFields = false; + var showHardwareFields = false; + + // Check cache first + var cacheKey = 'visibility_' + category; + if (window.fieldDependencyCache[cacheKey]) { + var cached = window.fieldDependencyCache[cacheKey]; + showAdvancedFields = cached.advanced; + showHardwareFields = cached.hardware; + } else { + // Determine visibility (this could also be an AJAX call) + if (category === 'hardware' || category === 'network') { + showHardwareFields = true; + } + + if (category === 'software' || category === 'database') { + showAdvancedFields = true; + } + + // Cache the result + window.fieldDependencyCache[cacheKey] = { + advanced: showAdvancedFields, + hardware: showHardwareFields + }; + } + + // Show/hide fields with animation + if (showHardwareFields) { + g_form.setSectionDisplay('hardware_details', true); + g_form.setDisplay('cmdb_ci', true); + g_form.setDisplay('hardware_model', true); + g_form.setMandatory('cmdb_ci', true); + } else { + g_form.setSectionDisplay('hardware_details', false); + g_form.setDisplay('cmdb_ci', false); + g_form.setDisplay('hardware_model', false); + g_form.setMandatory('cmdb_ci', false); + g_form.clearValue('cmdb_ci'); + g_form.clearValue('hardware_model'); + } + + if (showAdvancedFields) { + g_form.setDisplay('application', true); + g_form.setDisplay('version', true); + g_form.setMandatory('application', true); + } else { + g_form.setDisplay('application', false); + g_form.setDisplay('version', false); + g_form.setMandatory('application', false); + g_form.clearValue('application'); + g_form.clearValue('version'); + } +} + +/** + * Load dependent fields with debouncing to prevent excessive AJAX calls + */ +function loadDependentFields(category, priority, assignmentGroup) { + // Clear existing timer + if (window.fieldDependencyTimers.loadFields) { + clearTimeout(window.fieldDependencyTimers.loadFields); + } + + // Set new timer (debounce for 300ms) + window.fieldDependencyTimers.loadFields = setTimeout(function() { + executeDependentFieldLoad(category, priority, assignmentGroup); + }, 300); +} + +/** + * Execute the actual AJAX call to load dependent data + */ +function executeDependentFieldLoad(category, priority, assignmentGroup) { + // Check cache first + var cacheKey = 'fields_' + category + '_' + priority + '_' + assignmentGroup; + if (window.fieldDependencyCache[cacheKey]) { + applyDependentFieldData(window.fieldDependencyCache[cacheKey]); + return; + } + + // Show loading indicators + showLoadingIndicators(['subcategory', 'assignment_group', 'assigned_to']); + + // Make AJAX call + var ga = new GlideAjax('CategoryAjaxUtils'); + ga.addParam('sysparm_name', 'getDependentFields'); + ga.addParam('sysparm_category', category); + ga.addParam('sysparm_priority', priority); + ga.addParam('sysparm_assignment_group', assignmentGroup); + ga.addParam('sysparm_table', g_form.getTableName()); + + ga.getXMLAnswer(function(response) { + // Hide loading indicators + hideLoadingIndicators(['subcategory', 'assignment_group', 'assigned_to']); + + if (!response || response === 'error') { + g_form.addErrorMessage('Failed to load dependent fields. Please refresh and try again.'); + return; + } + + try { + var data = JSON.parse(response); + + // Cache the response + window.fieldDependencyCache[cacheKey] = data; + + // Apply the data + applyDependentFieldData(data); + + } catch (ex) { + g_form.addErrorMessage('Error processing field dependencies: ' + ex.message); + console.error('Field dependency error:', ex); + } + }); +} + +/** + * Apply dependent field data to the form + */ +function applyDependentFieldData(data) { + // Update subcategories + if (data.subcategories) { + g_form.clearOptions('subcategory'); + for (var i = 0; i < data.subcategories.length; i++) { + var sub = data.subcategories[i]; + g_form.addOption('subcategory', sub.value, sub.label); + } + } + + // Update suggested assignment group + if (data.suggested_assignment_group) { + g_form.setValue('assignment_group', data.suggested_assignment_group); + g_form.showFieldMsg('assignment_group', 'Auto-populated based on category', 'info', 3000); + } + + // Update SLA information + if (data.sla_info) { + var slaMsg = 'Expected resolution time: ' + data.sla_info.resolution_time + ' hours'; + g_form.showFieldMsg('priority', slaMsg, 'info'); + } + + // Update estimated cost (if applicable) + if (data.estimated_cost) { + g_form.setValue('estimated_cost', data.estimated_cost); + } + + // Show recommendations + if (data.recommendations && data.recommendations.length > 0) { + var recMsg = 'Recommendations: ' + data.recommendations.join(', '); + g_form.addInfoMessage(recMsg); + } +} + +/** + * Show loading indicators on multiple fields + */ +function showLoadingIndicators(fields) { + for (var i = 0; i < fields.length; i++) { + var field = fields[i]; + g_form.showFieldMsg(field, 'Loading...', 'info'); + g_form.setReadOnly(field, true); + } +} + +/** + * Hide loading indicators on multiple fields + */ +function hideLoadingIndicators(fields) { + for (var i = 0; i < fields.length; i++) { + var field = fields[i]; + g_form.hideFieldMsg(field); + g_form.setReadOnly(field, false); + } +} + +/** + * Clear cache (useful for testing or when data changes) + */ +function clearFieldDependencyCache() { + window.fieldDependencyCache = {}; + console.log('Field dependency cache cleared'); +} + +/** + * onLoad script to initialize form + * Type: onLoad + */ +function onLoadInitializeDependencies() { + // Initialize cache + if (typeof window.fieldDependencyCache === 'undefined') { + window.fieldDependencyCache = {}; + window.fieldDependencyTimers = {}; + } + + // Load initial dependencies if category is already set + var category = g_form.getValue('category'); + if (category) { + updateFieldVisibility(category); + } + + // Add cache clear button for admins (optional) + if (g_user.hasRole('admin')) { + console.log('Field dependency cache available. Use clearFieldDependencyCache() to clear.'); + } +} diff --git a/Client-Side Components/Client Scripts/Dynamic Field Dependencies with GlideAjax/dynamic_category_subcategory.js b/Client-Side Components/Client Scripts/Dynamic Field Dependencies with GlideAjax/dynamic_category_subcategory.js new file mode 100644 index 0000000000..b45b46aa0c --- /dev/null +++ b/Client-Side Components/Client Scripts/Dynamic Field Dependencies with GlideAjax/dynamic_category_subcategory.js @@ -0,0 +1,74 @@ +/** + * Dynamic Category/Subcategory Client Script + * + * Table: incident (or any table with category/subcategory fields) + * Type: onChange + * Field: category + * + * Description: Dynamically populates subcategory field based on selected category + * using GlideAjax to fetch filtered options from server + */ + +function onChange(control, oldValue, newValue, isLoading, isTemplate) { + // Don't run during form load or if value hasn't changed + if (isLoading || newValue === '') { + return; + } + + // Clear subcategory when category changes + g_form.clearValue('subcategory'); + + // Show loading indicator + g_form.showFieldMsg('subcategory', 'Loading subcategories...', 'info'); + g_form.setReadOnly('subcategory', true); + + // Make AJAX call to get filtered subcategories + var ga = new GlideAjax('CategoryAjaxUtils'); + ga.addParam('sysparm_name', 'getSubcategories'); + ga.addParam('sysparm_category', newValue); + ga.addParam('sysparm_table', g_form.getTableName()); + + ga.getXMLAnswer(function(response) { + // Clear loading indicator + g_form.hideFieldMsg('subcategory'); + g_form.setReadOnly('subcategory', false); + + // Handle error response + if (!response || response === 'error') { + g_form.addErrorMessage('Failed to load subcategories. Please try again.'); + return; + } + + // Parse response + try { + var subcategories = JSON.parse(response); + + // Clear existing options (except empty option) + g_form.clearOptions('subcategory'); + + // Add new options + if (subcategories.length === 0) { + g_form.addInfoMessage('No subcategories available for this category.'); + g_form.setReadOnly('subcategory', true); + } else { + // Add each subcategory as an option + for (var i = 0; i < subcategories.length; i++) { + var sub = subcategories[i]; + g_form.addOption('subcategory', sub.value, sub.label); + } + + // Auto-select if only one option + if (subcategories.length === 1) { + g_form.setValue('subcategory', subcategories[0].value); + } + + // Show success message + g_form.showFieldMsg('subcategory', subcategories.length + ' subcategories loaded', 'info', 2000); + } + + } catch (ex) { + g_form.addErrorMessage('Error parsing subcategory data: ' + ex.message); + console.error('Subcategory parsing error:', ex); + } + }); +} diff --git a/Client-Side Components/Client Scripts/Dynamic Location Validation Approach/Readme.md b/Client-Side Components/Client Scripts/Dynamic Location Validation Approach/Readme.md new file mode 100644 index 0000000000..68602d0bb4 --- /dev/null +++ b/Client-Side Components/Client Scripts/Dynamic Location Validation Approach/Readme.md @@ -0,0 +1,19 @@ +**User Location Validator** +This script restricts form submissions based on the physical location of the user. The current location is obtained using the browser’s geolocation API (latitude and longitude), and is then compared against the user's assigned business location stored in ServiceNow. + +**How It Works** +- The **server-side Script Include**(UserLocationUtils.js) fetches the assigned business location’s latitude, longitude, and name for the logged-in user. +- The **client-side script**(User Location Validator.js) uses the browser API to obtain the current latitude and longitude of the user at form submission. +- It calculates the distance between these two points using the **Haversine formula**, which accounts for the spherical shape of the Earth. +- The key constant `earthRadiusKm = 6371` defines the Earth's radius in kilometers and is essential for accurate distance calculation. +- If the user’s current location is outside the predefined radius (default 10 km), the form submission is blocked with an error message showing the distance and allowed location. +- If the user is within range, a confirmation info message is displayed and the submission proceeds. + +**Sample Output** +- **Success:** "Location validated successfully within range of Headquarters." +- **Failure:** "You are 15.23 km away from your registered location: Headquarters." + +**Usage Notes** +- Requires user consent for geolocation access in the browser. +- The script uses descriptive variable names for clarity and maintainability. +- Suitable for scenarios requiring geo-fencing compliance or location-based workflow restrictions. diff --git a/Client-Side Components/Client Scripts/Dynamic Location Validation Approach/User Location Validator.js b/Client-Side Components/Client Scripts/Dynamic Location Validation Approach/User Location Validator.js new file mode 100644 index 0000000000..44095b7110 --- /dev/null +++ b/Client-Side Components/Client Scripts/Dynamic Location Validation Approach/User Location Validator.js @@ -0,0 +1,52 @@ +function onSubmit() { + // Check if the browser supports geolocation + if ("geolocation" in navigator) { + // Request current user position + navigator.geolocation.getCurrentPosition(function(position) { + var currentLatitude = position.coords.latitude; // Current user latitude + var currentLongitude = position.coords.longitude; // Current user longitude + + // Allowed business location coordinates fetched from server + var allowedLatitude = locData.latitude; + var allowedLongitude = locData.longitude; + var locationName = locData.name; + + // Earth's radius in kilometers - constant used in distance calculation formula + var earthRadiusKm = 6371; + + // Convert degree differences to radians + var deltaLatitude = (currentLatitude - allowedLatitude) * Math.PI / 180; + var deltaLongitude = (currentLongitude - allowedLongitude) * Math.PI / 180; + + // Haversine formula components + var a = Math.sin(deltaLatitude / 2) * Math.sin(deltaLatitude / 2) + + Math.cos(allowedLatitude * Math.PI / 180) * + Math.cos(currentLatitude * Math.PI / 180) * + Math.sin(deltaLongitude / 2) * Math.sin(deltaLongitude / 2); + + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + // Calculate distance in kilometers between current and allowed locations + var distanceKm = earthRadiusKm * c; + + // Check if user's current distance exceeds tolerance (e.g., 10 km) + if (distanceKm > 10) { + alert("You are " + distanceKm.toFixed(2) + " km away from your registered location: " + locationName); + g_form.addErrorMessage("Location validation failed: Submission outside the allowed radius."); + return false; // Cancel form submission + } else { + g_form.addInfoMessage("Location validated successfully within range of " + locationName); + return true; // Allow form submission + } + }, function(error) { + alert("Geolocation error: " + error.message); + return false; // Stop submission if geolocation fails + }); + + // Prevent form submission while waiting for async geolocation result + return false; + } else { + g_form.addErrorMessage("Geolocation is not supported by your browser."); + return false; // Block if geolocation API unsupported + } +} diff --git a/Client-Side Components/Client Scripts/Dynamic Location Validation Approach/UserLocationUtils.js b/Client-Side Components/Client Scripts/Dynamic Location Validation Approach/UserLocationUtils.js new file mode 100644 index 0000000000..5435dbd06b --- /dev/null +++ b/Client-Side Components/Client Scripts/Dynamic Location Validation Approach/UserLocationUtils.js @@ -0,0 +1,22 @@ +var UserLocationUtils = Class.create(); +UserLocationUtils.prototype = { + initialize: function() { + + }, + getUserLocationCoords: function() { + var user = gs.getUser(); + var loc = user.getRecord().location; + if (loc) { + var locGR = new GlideRecord('cmn_location'); + if (locGR.get(loc)) + return { + latitude: parseFloat(locGR.latitude), + longitude: parseFloat(locGR.longitude), + name: locGR.name.toString() + }; + } + return null; + }, + + type: 'UserLocationUtils' +}; diff --git a/Client-Side Components/Client Scripts/Dynamic Reference Qualifier with Filtering/README.md b/Client-Side Components/Client Scripts/Dynamic Reference Qualifier with Filtering/README.md new file mode 100644 index 0000000000..351b9a5e92 --- /dev/null +++ b/Client-Side Components/Client Scripts/Dynamic Reference Qualifier with Filtering/README.md @@ -0,0 +1,6 @@ +**Dynamic Reference Qualifier with Filtering** +This Client Script provides a solution for dynamically updating the available options in a Reference Field based on the value selected in another field on the same form. + +This technique is essential for ensuring data quality and improving the user experience (UX). + +A typical use case is filtering the Assignment Group field to show only groups relevant to the selected Service, Category, or Location. diff --git a/Client-Side Components/Client Scripts/Dynamic Reference Qualifier with Filtering/reference_qual_dynamic.js b/Client-Side Components/Client Scripts/Dynamic Reference Qualifier with Filtering/reference_qual_dynamic.js new file mode 100644 index 0000000000..cc972cc534 --- /dev/null +++ b/Client-Side Components/Client Scripts/Dynamic Reference Qualifier with Filtering/reference_qual_dynamic.js @@ -0,0 +1,14 @@ + function onChange(control, oldValue, newValue, isLoading, isTemplate) { + if (isLoading || newValue === '') { + return; + } + var controlledField = 'assignment_group'; + var controllingField = 'u_service'; + var serviceSysId = g_form.getValue(controllingField); + var encodedQuery = 'typeLIKEITIL^u_related_service=' + serviceSysId; + g_form.setQuery(controlledField, encodedQuery); + var currentGroupSysId = g_form.getValue(controlledField); + if (currentGroupSysId && oldValue !== '' && currentGroupSysId !== '') { + g_form.setValue(controlledField, ''); + } +} diff --git a/Client-Side Components/Client Scripts/Dynamic script to make fields read only/reame.md b/Client-Side Components/Client Scripts/Dynamic script to make fields read only/reame.md new file mode 100644 index 0000000000..ada42d5c30 --- /dev/null +++ b/Client-Side Components/Client Scripts/Dynamic script to make fields read only/reame.md @@ -0,0 +1,5 @@ +Dynamic script to make fields read only + +It runs when the field value changes.On change client script +If the new value equals '7', it retrieves all editable fields using g_form.getEditableFields(). +Then it loops through each field and sets it to read-only using g_form.setReadOnly(). diff --git a/Client-Side Components/Client Scripts/Dynamic script to make fields read only/script.js b/Client-Side Components/Client Scripts/Dynamic script to make fields read only/script.js new file mode 100644 index 0000000000..a685a9f35c --- /dev/null +++ b/Client-Side Components/Client Scripts/Dynamic script to make fields read only/script.js @@ -0,0 +1,20 @@ +/* +It runs when the field value changes. +If the new value equals '7', it retrieves all editable fields using g_form.getEditableFields(). +Then it loops through each field and sets it to read-only using g_form.setReadOnly(). + +*/ + + +function onChange(control, oldValue, newValue, isLoading, isTemplate) { + if (isLoading || newValue === '') { + return; + } + + + if (newValue == '7') { // update condition as required + var fields = g_form.getEditableFields(); + for (var i = 0; i < fields.length; i++) { + g_form.setReadOnly(fields[i].getName(), true); + } + diff --git a/Client-Side Components/Client Scripts/Dynamically Switch Form View Based on Field Value/README.md b/Client-Side Components/Client Scripts/Dynamically Switch Form View Based on Field Value/README.md new file mode 100644 index 0000000000..3b22ef62bf --- /dev/null +++ b/Client-Side Components/Client Scripts/Dynamically Switch Form View Based on Field Value/README.md @@ -0,0 +1,20 @@ +## Dynamically Switch Form View Based on Field Value + +This client script demonstrates how to **automatically switch form views** based on the value of a field. + +**Use case:** +For example, if the **Category** field is set to *Hardware*, the form view switches to **ess**. +You can extend this by updating the mapping object to support additional fields and values (e.g., *Software → itil*, *Network → support*). + +**Benefit:** +Improves user experience by guiding users to the **most relevant form view**, ensuring the right fields are shown for the right scenario. + +**Test:** +- Change the **Category** field to *Hardware* → Form view should switch to **ess**. +- Update mapping to add new conditions (e.g., *Software → itil*) and verify the view switches accordingly. + +**How to Use:** +1. **Modify the table name** in the `switchView` function to match your target table: + ```javascript + switchView("section", "", targetView); +2. **Modify the view mapping** diff --git a/Client-Side Components/Client Scripts/Dynamically Switch Form View Based on Field Value/dynamic-form-view-onchange.js b/Client-Side Components/Client Scripts/Dynamically Switch Form View Based on Field Value/dynamic-form-view-onchange.js new file mode 100644 index 0000000000..07e32f6441 --- /dev/null +++ b/Client-Side Components/Client Scripts/Dynamically Switch Form View Based on Field Value/dynamic-form-view-onchange.js @@ -0,0 +1,33 @@ +/** + * dynamic-form-view-onchange.js + * + * Dynamically switches the form view automatically depending on the value of a specific field. + * Example: If Category = Hardware → switch to ess view. + * Extendable by modifying the mapping object for different fields/values. + * + */ + +function onChange(control, oldValue, newValue, isLoading) { + if (isLoading || !newValue) { + return; + } + + // Field value → view name mapping + var viewMapping = { + "hardware": "ess", + "software": "itil", + "network": "support" + }; + + var fieldValue = newValue.toLowerCase(); + var targetView = viewMapping[fieldValue]; + + if (targetView) { + try { + // Here for example the table name is incident + switchView("section", "incident", targetView); + } catch (e) { + console.error("View switch failed: ", e); + } + } +} diff --git a/Client-Side Components/Client Scripts/Field Completion Counter/README.md b/Client-Side Components/Client Scripts/Field Completion Counter/README.md new file mode 100644 index 0000000000..f9b3848388 --- /dev/null +++ b/Client-Side Components/Client Scripts/Field Completion Counter/README.md @@ -0,0 +1,18 @@ +# Field Completion Counter + +## Use Case / Requirement +Display a simple message showing how many fields are completed vs total fields on a form. This helps users track their progress while filling out forms. + +## Solution +A simple onLoad client script that: +- Counts filled vs empty fields +- Shows completion status in an info message +- Updates when fields are modified + +## Implementation +Add this as an **onLoad** client script on any form. + +## Notes +- Excludes system fields and read-only fields +- Updates in real-time as users fill fields +- Simple and lightweight solution \ No newline at end of file diff --git a/Client-Side Components/Client Scripts/Field Completion Counter/script.js b/Client-Side Components/Client Scripts/Field Completion Counter/script.js new file mode 100644 index 0000000000..65e727b63e --- /dev/null +++ b/Client-Side Components/Client Scripts/Field Completion Counter/script.js @@ -0,0 +1,54 @@ +function onLoad() { + // Display field completion counter + showFieldProgress(); + + // Set up listener for field changes + setupProgressUpdater(); + + function showFieldProgress() { + var allFields = g_form.getFieldNames(); + var visibleFields = []; + var filledFields = 0; + + // Count visible, editable fields + for (var i = 0; i < allFields.length; i++) { + var fieldName = allFields[i]; + + // Skip system fields and hidden/readonly fields + if (fieldName.indexOf('sys_') === 0 || + !g_form.isVisible(fieldName) || + g_form.isReadOnly(fieldName)) { + continue; + } + visibleFields.push(fieldName); + + // Check if field has value + if (g_form.getValue(fieldName)) { + filledFields++; + } + } + var totalFields = visibleFields.length; + var percentage = totalFields > 0 ? Math.round((filledFields / totalFields) * 100) : 0; + + g_form.addInfoMessage('Form Progress: ' + filledFields + '/' + totalFields + ' fields completed (' + percentage + '%)'); + } + + function setupProgressUpdater() { + // Simple debounced update + var updateTimer; + + function updateProgress() { + clearTimeout(updateTimer); + updateTimer = setTimeout(function() { + g_form.clearMessages(); + showFieldProgress(); + }, 500); + } + + // Listen for any field change + var allFields = g_form.getFieldNames(); + for (var i = 0; i < allFields.length; i++) { + g_form.addElementChangeListener(allFields[i], updateProgress); + } + } +} \ No newline at end of file diff --git a/Client-Side Components/Client Scripts/Field Validation/README.md b/Client-Side Components/Client Scripts/Field Validation/README.md deleted file mode 100644 index 9e5f32c701..0000000000 --- a/Client-Side Components/Client Scripts/Field Validation/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Field valiation for Special Characters on any table - -With this onChange Client script you can validate if there are any special characters present in the input given by user in a particular field and show an error message below the field and clear the field value. -Although we have various methods to do this, it is much easier and you can customize your error message. diff --git a/Client-Side Components/Client Scripts/Field Validation/specialcharacter.js b/Client-Side Components/Client Scripts/Field Validation/specialcharacter.js deleted file mode 100644 index 8488adbee2..0000000000 --- a/Client-Side Components/Client Scripts/Field Validation/specialcharacter.js +++ /dev/null @@ -1,15 +0,0 @@ -function onChange(control, oldValue, newValue, isLoading, isTemplate) { - - if (isLoading || newValue === '') { - return; - } - - //In the below regex, you can add or remove any special characters as per your requirement - var special_chars = /[~@|$^<>\*+=;?`')[\]]/; - - if (special_chars.test(newValue)) { - g_form.clearValue(''); - g_form.showErrorBox('','Special Characters are not allowed'); - } - -} diff --git a/Client-Side Components/Client Scripts/Field Validations/README.md b/Client-Side Components/Client Scripts/Field Validations/README.md new file mode 100644 index 0000000000..08cd26b5c3 --- /dev/null +++ b/Client-Side Components/Client Scripts/Field Validations/README.md @@ -0,0 +1,12 @@ +An `onLoad` client script that validates required fields in specific ServiceNow form views. + +This ServiceNow client script provides automatic validation of required form fields when users access specific form views. The script runs immediately when a form loads and checks that critical fields are populated, displaying user-friendly error messages for any missing required information. This ensures data completeness and improves form submission success rates by catching validation issues early in the user workflow. + +What This Script Does: +The onLoad client script performs comprehensive field validation with these key capabilities: +View-Specific Validation: Only triggers validation when accessing a designated form view +Multiple Field Support: Validates multiple required fields simultaneously in a single operation +Smart Field Detection: Uses field labels (not technical names) in error messages for better user experience +Consolidated Error Display: Shows all missing required fields in a single, clear error message +Immediate Feedback: Provides instant validation results as soon as the form loads +Non-Intrusive Design: Displays informational errors without blocking form interaction diff --git a/Client-Side Components/Client Scripts/Field Validations/fieldValidation.js b/Client-Side Components/Client Scripts/Field Validations/fieldValidation.js new file mode 100644 index 0000000000..bf6f297795 --- /dev/null +++ b/Client-Side Components/Client Scripts/Field Validations/fieldValidation.js @@ -0,0 +1,23 @@ +function onLoad(){ + var targetViewName = 'your_target_view_name'; + var requiredFields = ['field1', 'field2', 'field3']; + + var currentViewName = g_form.getViewName(); + + if (currentViewName === targetViewName) { + var emptyFields = []; + + for (var i = 0; i < requiredFields.length; i++) { + var fieldValue = g_form.getValue(requiredFields[i]); + if (!fieldValue || fieldValue.trim() === '') { + emptyFields.push(g_form.getLabelOf(requiredFields[i])); + } + } + + if (emptyFields.length > 0) { + var errorMessage = "The following required fields cannot be empty: " + + emptyFields.join(', '); + g_form.addErrorMessage(errorMessage); + } + } +} diff --git a/Client-Side Components/Client Scripts/Form Dirty State Prevention/README.md b/Client-Side Components/Client Scripts/Form Dirty State Prevention/README.md new file mode 100644 index 0000000000..c8e63f5df5 --- /dev/null +++ b/Client-Side Components/Client Scripts/Form Dirty State Prevention/README.md @@ -0,0 +1,109 @@ +# Form Dirty State Prevention + +## Overview +Detects form changes and warns users before navigating away or closing the form, preventing accidental data loss. + +## What It Does +- Tracks form field changes (dirty state) +- Warns user before leaving unsaved form +- Allows user to cancel navigation +- Works with all form fields +- Prevents accidental data loss +- Clean, reusable pattern + +## Use Cases +- Complex multi-field forms +- Long data entry forms +- Forms with expensive operations +- Critical data entry (financial, medical) +- Any form where accidental exit would cause issues + +## Files +- `form_dirty_state_manager.js` - Client Script to manage form state + +## How to Use + +### Step 1: Create Client Script +1. Go to **System Definition > Scripts** (any table) +2. Create new Client Script +3. Set **Type** to "onChange" +4. Copy code from `form_dirty_state_manager.js` +5. Set to run on any field + +### Step 2: Add Navigation Handler +1. Add to form's onLoad script: +```javascript +// Initialize dirty state tracking +var formStateManager = new FormDirtyStateManager(); +``` + +### Step 3: Test +1. Open form and make changes +2. Try to navigate away without saving +3. User sees warning dialog +4. User can choose to stay or leave + +## Example Usage +```javascript +// Automatically tracks all field changes +// When user tries to close/navigate: +// "You have unsaved changes. Do you want to leave?" +// - Leave (discard changes) +// - Stay (return to form) +``` + +## Key Features +- ✅ Detects any field change +- ✅ Persistent across form interactions +- ✅ Works with new records and updates +- ✅ Ignores read-only fields +- ✅ Resets after save +- ✅ No performance impact + +## Output Examples +``` +User opens form and changes a field +→ Form marked as "dirty" + +User clicks close/back button +→ Warning dialog appears: "You have unsaved changes" + +User clicks Leave +→ Form closes, changes discarded + +User clicks Save then navigates +→ No warning (form is clean) +``` + +## Customization +```javascript +// Customize warning message +var warningMessage = "Warning: You have unsaved changes!"; + +// Add specific field tracking +g_form.addOnFieldChange('priority', myCustomHandler); + +// Reset dirty flag after save +g_form.save(); // Automatically triggers cleanup +``` + +## Requirements +- ServiceNow instance +- Client Script access +- Any table form + +## Browser Support +- Chrome, Firefox, Safari, Edge (all modern browsers) +- Works with ServiceNow classic and modern UI + +## Related APIs +- [g_form API](https://docs.servicenow.com/bundle/sandiego-application-development/page/app-store/dev_apps/concept/c_FormAPI.html) +- [Client Script Events](https://docs.servicenow.com/bundle/sandiego-application-development/page/app-store/dev_apps/concept/c_ClientScriptEvents.html) +- [Form Field Changes](https://docs.servicenow.com/bundle/sandiego-application-development/page/app-store/dev_apps/concept/c_FieldChanges.html) + +## Best Practices +- Apply to important data entry forms +- Test with real users +- Consider accessibility for screen readers +- Use with save shortcuts (Ctrl+S) +- Combine with auto-save patterns diff --git a/Client-Side Components/Client Scripts/Form Dirty State Prevention/form_dirty_state_manager.js b/Client-Side Components/Client Scripts/Form Dirty State Prevention/form_dirty_state_manager.js new file mode 100644 index 0000000000..f7749edaac --- /dev/null +++ b/Client-Side Components/Client Scripts/Form Dirty State Prevention/form_dirty_state_manager.js @@ -0,0 +1,60 @@ +// Client Script: Form Dirty State Manager +// Purpose: Track form changes and warn user before leaving with unsaved changes + +var FormDirtyStateManager = Class.create(); +FormDirtyStateManager.prototype = { + initialize: function() { + this.isDirty = false; + this.setupFieldChangeListeners(); + this.setupNavigationWarning(); + }, + + // Mark form as changed when any field is modified + setupFieldChangeListeners: function() { + var self = this; + g_form.addOnFieldChange('*', function(control, oldValue, newValue, isLoading) { + // Ignore system updates and form loads + if (isLoading || oldValue === newValue) { + return; + } + self.setDirty(true); + }); + }, + + // Warn user before navigating away with unsaved changes + setupNavigationWarning: function() { + var self = this; + + // Warn on form close attempt + window.addEventListener('beforeunload', function(e) { + if (self.isDirty && !g_form.isNewRecord()) { + e.preventDefault(); + e.returnValue = 'You have unsaved changes. Do you want to leave?'; + return e.returnValue; + } + }); + + // Warn on GlideForm navigation + g_form.addOnSave(function() { + // Reset dirty flag after successful save + self.setDirty(false); + return true; + }); + }, + + setDirty: function(isDirty) { + this.isDirty = isDirty; + if (isDirty) { + // Optional: Show visual indicator + document.title = '* ' + document.title.replace(/^\* /, ''); + gs.info('[Form State] Unsaved changes detected'); + } + }, + + isDirtyState: function() { + return this.isDirty; + } +}; + +// Initialize on form load +var formDirtyState = new FormDirtyStateManager(); diff --git a/Client-Side Components/Client Scripts/Get Form Elements/README.MD b/Client-Side Components/Client Scripts/Get Form Elements/README.MD new file mode 100644 index 0000000000..82da81e2e0 --- /dev/null +++ b/Client-Side Components/Client Scripts/Get Form Elements/README.MD @@ -0,0 +1,3 @@ +This script is a Client Script (onLoad) for ServiceNow which retrieves all field names present on the current form and displays them in an alert message. + +Example: Hi Sai, please find the elements of the form short_description, description, number, etc. diff --git a/Client-Side Components/Client Scripts/Get Form Elements/getFormElements.js b/Client-Side Components/Client Scripts/Get Form Elements/getFormElements.js new file mode 100644 index 0000000000..1b8f7c7ea1 --- /dev/null +++ b/Client-Side Components/Client Scripts/Get Form Elements/getFormElements.js @@ -0,0 +1,8 @@ +function onLoad() { + //Type appropriate comment here, and begin script below + var arr = []; + for (var i = 0; i < g_form.elements.length; i++) { + arr.push(g_form.elements[i].fieldName); + } + alert("Hi Sai, please find the form elements: " + arr.join(",")); +} 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/Health Scan Prevent Insert Update in Before BRs/README.md b/Client-Side Components/Client Scripts/Health Scan Prevent Insert Update in Before BRs/README.md new file mode 100644 index 0000000000..0eab8c5369 --- /dev/null +++ b/Client-Side Components/Client Scripts/Health Scan Prevent Insert Update in Before BRs/README.md @@ -0,0 +1,11 @@ +**Client script Deatils** +1. Table: sys_script +2. Type: onSubmit +3. UI Type: Desktop + +**Benefits** +1. This client script will prevent admin users to do insert/update operation in onBefore business rules. +2. It will help to avoid HealthScan findings thus increasing healthscan score. +3. Will prevent recursive calls. + +Link to ServiceNow Business Rules best Practise : https://developer.servicenow.com/dev.do#!/guide/orlando/now-platform/tpb-guide/business_rules_technical_best_practices diff --git a/Client-Side Components/Client Scripts/Health Scan Prevent Insert Update in Before BRs/script.js b/Client-Side Components/Client Scripts/Health Scan Prevent Insert Update in Before BRs/script.js new file mode 100644 index 0000000000..78fa387872 --- /dev/null +++ b/Client-Side Components/Client Scripts/Health Scan Prevent Insert Update in Before BRs/script.js @@ -0,0 +1,15 @@ +function onSubmit() { + /* + This client script will prevent insert and update operation in onBefore business rules. + Table: sys_script + Type: onSubmit + Ui Type: Desktop + */ + var whenCond = g_form.getValue('when'); // when condition of business rule + var scriptVal = g_form.getValue('script'); // script value of business rule. + + if (whenCond == 'before' && (scriptVal.indexOf('insert()') > -1 || scriptVal.indexOf('update()')) > -1) { + alert("As per ServiceNow best Practise insert and update should be avoided in onBefore BRs. If you still want tp proceed try deactivating client script : " + g_form.getUniqueValue()); + return false; + } +} diff --git a/Client-Side Components/Client Scripts/Hide Dependent Choice field if there no dependent choices/HideDependentChoiceClientScript.png b/Client-Side Components/Client Scripts/Hide Dependent Choice field if there no dependent choices/HideDependentChoiceClientScript.png new file mode 100644 index 0000000000..2e57a81203 Binary files /dev/null and b/Client-Side Components/Client Scripts/Hide Dependent Choice field if there no dependent choices/HideDependentChoiceClientScript.png differ 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/Incident Count of Selected CI with Clickable Link to Related Incidents/CI_Incident_Message_Count_1.png b/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/CI_Incident_Message_Count_1.png new file mode 100644 index 0000000000..1d25997eaf Binary files /dev/null and b/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/CI_Incident_Message_Count_1.png differ diff --git a/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/CI_Incident_Message_Count_2.png b/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/CI_Incident_Message_Count_2.png new file mode 100644 index 0000000000..b60337b8e7 Binary files /dev/null and b/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/CI_Incident_Message_Count_2.png differ diff --git a/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/CI_Incident_Message_Count_3.png b/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/CI_Incident_Message_Count_3.png new file mode 100644 index 0000000000..28befe4b42 Binary files /dev/null and b/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/CI_Incident_Message_Count_3.png differ diff --git a/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/CI_Incident_Message_Count_4.png b/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/CI_Incident_Message_Count_4.png new file mode 100644 index 0000000000..e48f9d99fb Binary files /dev/null and b/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/CI_Incident_Message_Count_4.png differ diff --git a/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/README.md b/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/README.md new file mode 100644 index 0000000000..627ac7255e --- /dev/null +++ b/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/README.md @@ -0,0 +1,34 @@ +# Incident Count of Selected Configuration Item with Info Message and Link to its Related Incident + +Displays a message showing the count of open incidents associated with a selected **Configuration Item (CI)** whenever the **Configuration Item** field changes on the Incident form. + +- Helps quickly identify whether the selected CI has existing incident by fetching and displaying active incident counts (excluding *Resolved*, *Closed*, and *Canceled* states). +- Shows an **info message** with a **clickable link** that opens a filtered list of related incidents for that CI +- If more than five incidents are linked, a **warning message** appears suggesting Problem investigation for frequent or repeated CI issues. +- Uses an **onChange Client Script** on the *Configuration Item* field and a **GlideAjax Script Include** called from the client script to fetch the incident count + +--- + +## Warning Message displayed on form when CI has 5 or more incidents + +![CI_Incident_Message_Count_1](CI_Incident_Message_Count_1.png) + +--- + +## Info Message displayed on form when CI has no incidents + +![CI_Incident_Message_Count_2](CI_Incident_Message_Count_2.png) + +--- + +## Info Message displayed on form when CI has incidents less than 5 + +![CI_Incident_Message_Count_3](CI_Incident_Message_Count_3.png) + +--- + +## Upon clicking the url link filter list opens with incidents associated with CI + +![CI_Incident_Message_Count_4](CI_Incident_Message_Count_4.png) + +--- diff --git a/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/clientScriptCiIncident.js b/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/clientScriptCiIncident.js new file mode 100644 index 0000000000..2338f69250 --- /dev/null +++ b/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/clientScriptCiIncident.js @@ -0,0 +1,36 @@ +function onChange(control, oldValue, newValue, isLoading, isTemplate) { + if (isLoading || newValue === '') return; + + g_form.clearMessages(); + + // Call Script Include to count incidents by CI + var ga = new GlideAjax('ConfigurationIncidentCheck'); + ga.addParam('sysparm_name', 'getIncidentCount'); + ga.addParam('sysparm_ci', newValue); + + ga.getXMLAnswer(function(response) { + var count = parseInt(response, 10); + if (isNaN(count)) { + g_form.addErrorMessage("Could not retrieve incident count for this CI."); + return; + } + + var ciName = g_form.getDisplayValue('cmdb_ci'); + var url = '/incident_list.do?sysparm_query=cmdb_ci=' + newValue + '^stateNOT IN6,7,8'; + var msg = 'Configuration Item ' + ciName + ' has ' + count + ' related incident(s).'; + + if (count === 0) { + g_form.addInfoMessage( + 'Configuration Item ' + ciName + ' has no incidents associated with it.' + ); +} else { + if (count >= 5) { + g_form.addWarningMessage( + msg + ' consider Problem investigation.' + ); + } else { + g_form.addInfoMessage(msg); + } +} + }); +} diff --git a/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/glideAjaxIncidentCiCount.js b/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/glideAjaxIncidentCiCount.js new file mode 100644 index 0000000000..6eed4248e8 --- /dev/null +++ b/Client-Side Components/Client Scripts/Incident Count of Selected CI with Clickable Link to Related Incidents/glideAjaxIncidentCiCount.js @@ -0,0 +1,17 @@ +var ConfigurationIncidentCheck = Class.create(); +ConfigurationIncidentCheck.prototype = Object.extendsObject(AbstractAjaxProcessor, { +getIncidentCount: function() { + var ci = this.getParameter('sysparm_ci'); + if (!ci) return 0; + + var gr = new GlideAggregate('incident'); + gr.addQuery('cmdb_ci', ci); + gr.addQuery('state', 'NOT IN', '6,7,8'); + gr.addAggregate('COUNT'); + gr.query(); + + return gr.next() ? gr.getAggregate('COUNT') : 0; + }, + + type: 'ConfigurationIncidentCheck' +}); diff --git a/Client-Side Components/Client Scripts/Live Character Counter and Validator/README.md b/Client-Side Components/Client Scripts/Live Character Counter and Validator/README.md new file mode 100644 index 0000000000..24c529a6cc --- /dev/null +++ b/Client-Side Components/Client Scripts/Live Character Counter and Validator/README.md @@ -0,0 +1,11 @@ +This solution dynamically provides users with real-time feedback on the length of a text input field (like short_description or a single-line text variable). +It immediately displays a character count beneath the field and uses visual cues to indicate when a pre-defined character limit has been reached or exceeded. + +This is a vital User Experience (UX) enhancement that helps agents and users write concise, actionable information, leading to improved data quality and better integration reliability. + +Name Live_Character_Counter_ShortDesc_OnLoad +Table : Custom Table or Incident +Type onChange +Field : Description +UI Type All +Isolate Script false diff --git a/Client-Side Components/Client Scripts/Live Character Counter and Validator/character_counter.js b/Client-Side Components/Client Scripts/Live Character Counter and Validator/character_counter.js new file mode 100644 index 0000000000..1c4295b740 --- /dev/null +++ b/Client-Side Components/Client Scripts/Live Character Counter and Validator/character_counter.js @@ -0,0 +1,31 @@ +function onChange(control, oldValue, newValue, isLoading, isTemplate) { + + var FIELD_NAME = 'short_description'; + var MAX_CHARS = 100; + var currentLength = newValue.length; + var counterId = FIELD_NAME + '_counter_label'; + if (typeof g_form.getControl(FIELD_NAME) !== 'undefined' && !document.getElementById(counterId)) { + var controlElement = g_form.getControl(FIELD_NAME); + var counterLabel = document.createElement('div'); + counterLabel.setAttribute('id', counterId); + counterLabel.style.fontSize = '85%'; + counterLabel.style.marginTop = '2px'; + controlElement.parentNode.insertBefore(counterLabel, controlElement.nextSibling); + } + var counterElement = document.getElementById(counterId); + + if (counterElement) { + var remaining = MAX_CHARS - currentLength; + + + counterElement.innerHTML = 'Characters remaining: ' + remaining + ' (Max: ' + MAX_CHARS + ')'; + + // Apply red color if the limit is exceeded + if (remaining < 0) { + counterElement.style.color = 'red'; + } else { + // Revert color if back within limits + counterElement.style.color = 'inherit'; + } + } +} diff --git a/Client-Side Components/Client Scripts/Mandatory Field Highlighter/README.md b/Client-Side Components/Client Scripts/Mandatory Field Highlighter/README.md new file mode 100644 index 0000000000..0d875f0d55 --- /dev/null +++ b/Client-Side Components/Client Scripts/Mandatory Field Highlighter/README.md @@ -0,0 +1,79 @@ +# Mandatory Field Highlighter + +## Use Case +Provides visual feedback for empty mandatory fields on ServiceNow forms by showing error messages when the form loads. Helps users quickly identify which required fields need to be completed. + +## Requirements +- ServiceNow instance +- Client Script execution rights +- Forms with mandatory fields + +## Implementation +1. Create a new Client Script with Type "onLoad" +2. Copy the script code from script.js +3. **Customize the fieldsToCheck string** with your form's mandatory field names +4. Apply to desired table/form +5. Save and test on a form with mandatory fields + +## Configuration +Edit the `fieldsToCheck` variable to include your form's mandatory fields as a comma-separated string: + +```javascript +// Example configurations for different forms: + +// For Incident forms: +var fieldsToCheck = 'short_description,priority,category,caller_id,assignment_group'; + +// For Request forms: +var fieldsToCheck = 'short_description,requested_for,category,priority'; + +// For Change Request forms: +var fieldsToCheck = 'short_description,category,priority,assignment_group,start_date,end_date'; + +// For Problem forms: +var fieldsToCheck = 'short_description,category,priority,assignment_group'; + +// Custom fields (add as needed): +var fieldsToCheck = 'short_description,priority,u_business_justification,u_cost_center'; +``` + +## Features +- Shows error messages under empty mandatory fields on form load +- Easy configuration with comma-separated field names +- Automatically skips fields that don't exist on the form +- Only processes fields that are actually mandatory and visible +- Uses proper ServiceNow client scripting APIs +- No DOM manipulation or unsupported methods + +## Common Field Names +- `short_description` - Short Description +- `priority` - Priority +- `category` - Category +- `caller_id` - Caller +- `requested_for` - Requested For +- `assignment_group` - Assignment Group +- `assigned_to` - Assigned To +- `state` - State +- `urgency` - Urgency +- `impact` - Impact +- `start_date` - Start Date +- `end_date` - End Date +- `due_date` - Due Date +- `location` - Location +- `company` - Company +- `department` - Department + +## Notes +- Uses g_form.showFieldMsg() method to display error messages +- Uses g_form.hasField() to safely check field existence (built into the safety checks) +- Only runs on form load - provides initial validation feedback +- Easy to customize for different forms by modifying the field list +- Compatible with all standard ServiceNow forms +- Lightweight and focused on essential functionality + +## Example Usage +For a typical incident form, simply change the configuration line to: +```javascript +var fieldsToCheck = 'short_description,priority,category,caller_id,assignment_group'; +``` +Save the Client Script and test on an incident form to see error messages appear under empty mandatory fields. \ No newline at end of file diff --git a/Client-Side Components/Client Scripts/Mandatory Field Highlighter/script.js b/Client-Side Components/Client Scripts/Mandatory Field Highlighter/script.js new file mode 100644 index 0000000000..457a1ca5e0 --- /dev/null +++ b/Client-Side Components/Client Scripts/Mandatory Field Highlighter/script.js @@ -0,0 +1,26 @@ +function onLoad() { + + // USER CONFIGURATION: Add field names you want to check (comma-separated) + var fieldsToCheck = 'short_description,priority,category,caller_id'; + + // Convert to array and process + var fieldArray = fieldsToCheck.split(','); + + // Check each field + for (var i = 0; i < fieldArray.length; i++) { + var fieldName = fieldArray[i]; + + // Skip if field is not mandatory or not visible + if (!g_form.isMandatory(fieldName) || !g_form.isVisible(fieldName)) { + continue; + } + + // Get current field value + var value = g_form.getValue(fieldName); + + // Show error message if field is empty + if (!value || value === '') { + g_form.showFieldMsg(fieldName, 'This field is required', 'error'); + } + } +} \ No newline at end of file diff --git a/Client-Side Components/Client Scripts/MultiSelect in Portal/README.md b/Client-Side Components/Client Scripts/MultiSelect in Portal/README.md new file mode 100644 index 0000000000..55aa9aa3de --- /dev/null +++ b/Client-Side Components/Client Scripts/MultiSelect in Portal/README.md @@ -0,0 +1 @@ +The custom widget that enables you to select multiple incidents in the portal page. diff --git a/Client-Side Components/Client Scripts/MultiSelect in Portal/script.js b/Client-Side Components/Client Scripts/MultiSelect in Portal/script.js new file mode 100644 index 0000000000..141c7c0bca --- /dev/null +++ b/Client-Side Components/Client Scripts/MultiSelect in Portal/script.js @@ -0,0 +1,90 @@ +//HTML code that displays the incidents to select multiple at once. +
+
+

Complaints

+ +
+ +
+
+ + + + +
+
+
+
+ +
+ +
+ + {{c.getSelectedCount()}} complaints selected + (across pages) + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Run Date + + + + + Case ID + + + + Policy ID + + +
+ + {{item.batchno}}{{item.caseid}}{{item.policyid}}
No incidents found.
+ + +
+ + Page {{c.pageNumber}} of {{c.totalPages}} + +
+
+
diff --git a/Client-Side Components/Client Scripts/Prevent Rejection Without Comments/readme.md b/Client-Side Components/Client Scripts/Prevent Rejection Without Comments/readme.md new file mode 100644 index 0000000000..8476b55a5f --- /dev/null +++ b/Client-Side Components/Client Scripts/Prevent Rejection Without Comments/readme.md @@ -0,0 +1,72 @@ +🧩 Readme: Prevent Rejection Without Comments – Client Script +📘 Overview + +This Client Script enforces that approvers must enter comments before rejecting a record in the Approval [sysapproval_approver] table. +It ensures accountability, audit readiness, and clear justification for rejection decisions. + +🧠 Use Case +Field Details +Table sysapproval_approver +Type Client Script – onSubmit +Purpose Prevent users from rejecting approvals without comments +Business Value Ensures transparency and proper audit trail in approval workflows +⚙️ Configuration Steps + +Navigate to System Definition → Client Scripts. + +Click New. + +Fill the form as follows: + +Field Value +Name Prevent Rejection Without Comments +Table sysapproval_approver +UI Type All +Type onSubmit +Active ✅ +Applies on Update + +Paste the following script in the Script field. + +💻 Script +function onSubmit() { + // Get the current state value of the approval record + var state = g_form.getValue('state'); + + // Get the comments entered by the approver + var comments = g_form.getValue('comments'); + + // Check if the approver is trying to REJECT the record + // The out-of-box (OOB) value for rejection in sysapproval_approver is "rejected" + // If state is 'rejected' and comments are empty, stop the submission + if (state == 'rejected' && !comments) { + + // Display an error message to the user + g_form.addErrorMessage('Please provide comments before rejecting the approval.'); + + // Prevent the form from being submitted (block save/update) + return false; + } + + // Allow the form submission if validation passes + return true; +} + +🧪 Example Scenario +Field Value +Approver John Doe +State Rejected +Comments (empty) + +User Action: Clicks Update +System Response: Shows error message — + +“Please provide comments before rejecting the approval.” +Record submission is blocked until comments are provided. + +✅ Expected Outcome +🚫 Prevents rejection without comments +⚠️ Displays user-friendly validation message +📝 Ensures that every rejection has a reason logged for compliance + + diff --git a/Client-Side Components/Client Scripts/Prevent Rejection Without Comments/script.js b/Client-Side Components/Client Scripts/Prevent Rejection Without Comments/script.js new file mode 100644 index 0000000000..8661d77e41 --- /dev/null +++ b/Client-Side Components/Client Scripts/Prevent Rejection Without Comments/script.js @@ -0,0 +1,23 @@ +//Prevent Rejection Without Comments +function onSubmit() { + // Get the current state value of the approval record + var state = g_form.getValue('state'); + + // Get the comments entered by the approver + var comments = g_form.getValue('comments'); + + // Check if the approver is trying to REJECT the record + // The out-of-box (OOB) value for rejection in sysapproval_approver is "rejected" + // If state is 'rejected' and comments are empty, stop the submission + if (state == 'rejected' && !comments) { + + // Display an error message to the user + g_form.addErrorMessage('Please provide comments before rejecting the approval.'); + + // Prevent the form from being submitted (block save/update) + return false; + } + + // Allow the form submission if validation passes + return true; +} diff --git a/Client-Side Components/Client Scripts/Price field restriction to one currency/README.md b/Client-Side Components/Client Scripts/Price field restriction to one currency/README.md new file mode 100644 index 0000000000..725a1c8801 --- /dev/null +++ b/Client-Side Components/Client Scripts/Price field restriction to one currency/README.md @@ -0,0 +1,13 @@ + +# Client Script - Set Price type field to only one currency + +In a multi currecny enabled servicenow environment, if you have a requirement to enable only one currency choice for a particular table and field of type Price. + +## Usage + +- Create a new client script +- Set the type to OnLoad. +- Copy the script to your client script. +- Update the in the client script to 'Your field name' +- Add your currency code and symbol in place of USD & $ +- Save \ No newline at end of file diff --git a/Client-Side Components/Client Scripts/Price field restriction to one currency/price_field_restriction_to_one_currency.js b/Client-Side Components/Client Scripts/Price field restriction to one currency/price_field_restriction_to_one_currency.js new file mode 100644 index 0000000000..c2fed5261f --- /dev/null +++ b/Client-Side Components/Client Scripts/Price field restriction to one currency/price_field_restriction_to_one_currency.js @@ -0,0 +1,10 @@ +function onLoad(){ + // Remove all currency options + g_form.clearOptions('.currency_type'); + + // Add only one currency option (e.g., USD) + g_form.addOption('.currency_type', 'USD', '$'); + + // Set the currency field to the only available option + g_form.setValue('.currency_type', 'USD'); +} \ No newline at end of file diff --git a/Client-Side Components/Client Scripts/Reinstate Error status/README.md b/Client-Side Components/Client Scripts/Reinstate Error status/README.md new file mode 100644 index 0000000000..36493cb2f5 --- /dev/null +++ b/Client-Side Components/Client Scripts/Reinstate Error status/README.md @@ -0,0 +1,14 @@ +Table: Time Worked [task_time_worked] +Type: onsubmit + +#Objective : +Ensure that time entries (represented by the work_date field) are not submitted after 8:00 PM CST on two key dates: +The 16th of the month and The last day of the month +If a user tries to submit time for a current or past date after the cut-off time, the submission is blocked and a clear error message is displayed. + +#Business Scenario +Imagine a consulting firm where employees log billable hours against customer cases. There are internal controls in place that lock the timekeeping system after a certain cut-off time to ensure accurate payroll and billing. + +The finance department requires that: +On the 16th and last day of each month, submissions must be in before 8:00 PM CST. +If employees miss the deadline, they can only log time for future dates (not today or the past). diff --git a/Client-Side Components/Client Scripts/Reinstate Error status/script.js b/Client-Side Components/Client Scripts/Reinstate Error status/script.js new file mode 100644 index 0000000000..a328df1543 --- /dev/null +++ b/Client-Side Components/Client Scripts/Reinstate Error status/script.js @@ -0,0 +1,39 @@ +function onSubmit() { + // Cutoff time for submission in CST. + var cutoffTime = "20:00:00"; + + // Get the current date and time in CST + var currentDate = new Date(); + var currentCSTDate = new Date( + currentDate.toLocaleString("en-US", { + timeZone: "America/Chicago" + }) + ); + + // Get time from current CST date + var currentCSTTime = currentCSTDate.toTimeString().substring(0, 8); + + // Get last day of the month + var dayOfMonth = currentCSTDate.getDate(); + var lastDayOfMonth = new Date( + currentCSTDate.getFullYear(), + currentCSTDate.getMonth() + 1, + 0 + ).getDate(); + + if ((dayOfMonth === 16 || dayOfMonth === lastDayOfMonth) && currentCSTTime > cutoffTime) { + var workDate = g_form.getValue("work_date"); + + if (workDate) { + var formattedWorkDate = new Date(workDate + "T00:00:00"); + // If work_date is on or before current date, block submission + if (formattedWorkDate <= currentCSTDate) { + g_form.addErrorMessage( + "The time period closed for time submission at 8:00 PM CST. Time must be billed in the next time period." + ": " + lastDayOfMonth + ); + return false; + } + } + } + return true; +} diff --git a/Client-Side Components/Client Scripts/Require comment onPriority change/README.md b/Client-Side Components/Client Scripts/Require comment onPriority change/README.md new file mode 100644 index 0000000000..bcc58a3054 --- /dev/null +++ b/Client-Side Components/Client Scripts/Require comment onPriority change/README.md @@ -0,0 +1,6 @@ +Table: sn_customerservice_case +Type: OnChange +Field: Priority + +Use Case: +Make additional comments mandatory on priority change for the case table. diff --git a/Client-Side Components/Client Scripts/Require comment onPriority change/script.js b/Client-Side Components/Client Scripts/Require comment onPriority change/script.js new file mode 100644 index 0000000000..29aece0c53 --- /dev/null +++ b/Client-Side Components/Client Scripts/Require comment onPriority change/script.js @@ -0,0 +1,10 @@ +function onChange(control, oldValue, newValue, isLoading, isTemplate) { + if (isLoading || newValue === '') { + return; + } + if ((!g_form.isNewRecord()) && (newValue != oldValue)) { + g_form.setMandatory('comments', true); + g_form.addErrorMessage('Additional comment required when changing Priority.'); + } else { + g_form.setMandatory('comments', false); + } diff --git a/Client-Side Components/Client Scripts/Restrict Fields on Template/README.md b/Client-Side Components/Client Scripts/Restrict Fields on Template/README.md new file mode 100644 index 0000000000..6482017798 --- /dev/null +++ b/Client-Side Components/Client Scripts/Restrict Fields on Template/README.md @@ -0,0 +1,12 @@ +**Details** + +This is a on change client script on sys_template table. This script will restrict users to select defined fields while template creation. +Type: OnChange +Field: Template +Table: sys_template + +**Use Case** + +There is an OOB functionality to restrict fields using "**save as template**" ACL, but it has below limitations: +1. If the requirement is to restrcit more number of fields (example: 20), 20 ACLs will have to be created. +2. The ACls will have instance wide effect, this script will just restrict on client side. diff --git a/Client-Side Components/Client Scripts/Restrict Fields on Template/script.js b/Client-Side Components/Client Scripts/Restrict Fields on Template/script.js new file mode 100644 index 0000000000..625f432899 --- /dev/null +++ b/Client-Side Components/Client Scripts/Restrict Fields on Template/script.js @@ -0,0 +1,20 @@ +/* +Type: onChnage +Table: sys_template +Field: Template +*/ +function onChange(control, oldValue, newValue, isLoading, isTemplate) { + if (isLoading || newValue === '') { + return; + } + if (g_form.getValue('table') == 'incident') { // table on which sys_template is being created. + var fields = ['active', 'comments']; // array of fields to be restricted while template creation. + for (var i = 0; i < fields.length; i++) { + if (newValue.indexOf(fields[i]) > -1) { + alert("You Cannot Add " + fields[i]); // alert if user selects the restricted field. + var qry = newValue.split(fields[i]); + g_form.setValue('template', qry[0] + 'EQ'); // set the template value to previous values (oldValue does not work in this case). + } + } + } +} diff --git a/Client-Side Components/Client Scripts/Show Current Domain/DomainCheckUtil.js b/Client-Side Components/Client Scripts/Show Current Domain/DomainCheckUtil.js new file mode 100644 index 0000000000..9b5ef69bb2 --- /dev/null +++ b/Client-Side Components/Client Scripts/Show Current Domain/DomainCheckUtil.js @@ -0,0 +1,15 @@ +var DomainCheckUtil = Class.create(); +DomainCheckUtil.prototype = Object.extendsObject(global.AbstractAjaxProcessor, { + //get current domain of user session + getCurrentDomainName: function() { + var sessionDomainId = gs.getSession().getCurrentDomainID(); + var gr = new GlideRecord('domain'); + if (gr.get(sessionDomainId)){ + return gr.name; + } + //Return global domain name + return 'Global'; + }, + + type: 'DomainCheckUtil' +}); diff --git a/Client-Side Components/Client Scripts/Show Current Domain/Readme.md b/Client-Side Components/Client Scripts/Show Current Domain/Readme.md new file mode 100644 index 0000000000..6c5bc08b5d --- /dev/null +++ b/Client-Side Components/Client Scripts/Show Current Domain/Readme.md @@ -0,0 +1,20 @@ +Domain Separation Current Domain Display +Overview +This functionality provides real-time awareness to users about the current selected domain within ServiceNow's Domain Separation framework. It displays an informational message on form load indicating the active domain context, helping prevent accidental configuration or data entry in the wrong domain. + +Components +Script Include: DomainCheckUtil +Global, client-callable Script Include allowing client scripts to query the current domain name via GlideAjax. + +Methods: +isCurrentDomain(domainSysId) — Checks if a given domain sys_id matches the current session domain. + +Client Script +An onLoad client script configured globally on the Global table, set to true to load on all forms. +Calls the Script Include via GlideAjax to retrieve current domain name asynchronously. + +Displays the domain name as an informational message (g_form.addInfoMessage) on the form header on every page load. + +Usage +Upon loading any record form, users see a message stating: +"You are currently working in Domain Separation domain: [domain_name]." diff --git a/Client-Side Components/Client Scripts/Show Current Domain/Show Current Domain.js b/Client-Side Components/Client Scripts/Show Current Domain/Show Current Domain.js new file mode 100644 index 0000000000..f59f5df1e0 --- /dev/null +++ b/Client-Side Components/Client Scripts/Show Current Domain/Show Current Domain.js @@ -0,0 +1,9 @@ +function onLoad() { + var ga = new GlideAjax('DomainCheckUtil'); + ga.addParam('sysparm_name', 'getCurrentDomainName'); + ga.getXMLAnswer(showDomainMessage); + function showDomainMessage(response) { + var message = 'You are currently working in Domain Separation domain: ' + response + '.'; + g_form.addInfoMessage(message); + } +} diff --git a/Client-Side Components/Client Scripts/Smart-Field-Suggestions/README.md b/Client-Side Components/Client Scripts/Smart-Field-Suggestions/README.md new file mode 100644 index 0000000000..fe418b6e0d --- /dev/null +++ b/Client-Side Components/Client Scripts/Smart-Field-Suggestions/README.md @@ -0,0 +1,33 @@ +# Smart Field Suggestions Based on Keywords + +## Category +Client-Side Components / Client Scripts + +## Description +This is an onChange Client Script designed for the Incident table that dynamically suggests and populates the Category, Subcategory, and Priority fields based on keywords detected in the Short Description field. By matching keywords, it prompts users to confirm applying suggestions aligned with backend choice values for seamless integration. + +## Use Case +During incident creation or update, manually categorizing tickets correctly is critical for IT operations efficiency. This snippet automates early triage by analyzing user-entered short descriptions, providing actionable suggestions to improve categorization accuracy, accelerate routing, and enhance resolution speed. + +## How to Use +- Add this script as an "onChange" client script on the Incident table's `short_description` field. +- Ensure the Category, Subcategory, and Priority fields have choice lists aligned with backend values specified in the snippet. +- Modify the keyword list to align with your organizational terminologies if needed. +- The user will be prompted with suggestions and may confirm or dismiss them, allowing balanced automation and human control. + +## Why This Use Case is Unique and Valuable + +- Dynamically assists in categorizing incidents early, improving routing and resolution time. +- Uses only platform APIs (`g_form`) without custom backend code or external integrations, making it lightweight and maintainable. +- Uses real backend choice values ensuring seamless compatibility with existing configurations, reducing errors. +- Provides prompt suggestions with user confirmation, balancing automation and user control. +- Easily adaptable for other fields, keywords, or use cases beyond Incident management. +- Designed without fragile DOM manipulations, following ServiceNow best practices, tailored for real environments. + +## Compatibility +This client script is compatible with all standard ServiceNow instances without requiring ES2021 features. + +## Files +- `Smart Field Suggestions Based on Keyword.js` — the client script implementing the logic. + + diff --git a/Client-Side Components/Client Scripts/Smart-Field-Suggestions/Smart Field Suggestions Based on Keyword.js b/Client-Side Components/Client Scripts/Smart-Field-Suggestions/Smart Field Suggestions Based on Keyword.js new file mode 100644 index 0000000000..b3af251bbc --- /dev/null +++ b/Client-Side Components/Client Scripts/Smart-Field-Suggestions/Smart Field Suggestions Based on Keyword.js @@ -0,0 +1,73 @@ +function onChange(control, oldValue, newValue, isLoading, isTemplate) { + if (isLoading || !newValue || newValue.length < 10) { + return; + } + + var keywords = [ + { + pattern: /password|login|access/i, + category: 'inquiry | Help ', + subcategory: 'antivirus', + priority: '3', + suggestion: 'This appears to be a Inquiry issue.' + }, + { + pattern: /slow|performance|hanging/i, + category: 'software', + subcategory: 'email', + priority: '2', + suggestion: 'This appears to be a Software issue.' + }, + { + pattern: /printer|print|printing/i, + category: 'hardware', + subcategory: 'monitor', + priority: '3', + suggestion: 'This appears to be a Hardware issue.' + }, + { + pattern: /database|data/i, + category: 'database', + subcategory: 'db2', + priority: '3', + suggestion: 'This appears to be an Database issue.' + }, + { + pattern: /network|internet|wifi|connection/i, + category: 'network', + subcategory: 'vpn', + priority: '2', + suggestion: 'This appears to be a network issue.' + } + + ]; + + var lowerDesc = newValue.toLowerCase(); + var matched = null; + + for (var i = 0; i < keywords.length; i++) { + if (keywords[i].pattern.test(lowerDesc)) { + matched = keywords[i]; + break; + } + } + + g_form.hideFieldMsg('short_description', true); + g_form.clearMessages(); + + if (matched) { + g_form.showFieldMsg('short_description', matched.suggestion, 'info', false); + + if (confirm(matched.suggestion + "\n\nApply these suggestions?")) { + g_form.setValue('category', matched.category); + g_form.setValue('subcategory', matched.subcategory); // Make sure you use backend value for subcategory! + g_form.setValue('priority', matched.priority); + g_form.addInfoMessage('Suggestions applied automatically!'); + } else { + g_form.addInfoMessage('Suggestions dismissed.'); + g_form.hideFieldMsg('short_description', true); + } + } else { + g_form.addInfoMessage('No keywords matched in description.'); + } +} diff --git a/Client-Side Components/Client Scripts/Validate Email Format/README.md b/Client-Side Components/Client Scripts/Validate Email Format/README.md deleted file mode 100644 index b3ecfc1f99..0000000000 --- a/Client-Side Components/Client Scripts/Validate Email Format/README.md +++ /dev/null @@ -1,3 +0,0 @@ -onSubmit Function: This client script validates the email format when the form is submitted. -Regular Expression: It uses a regex pattern to check if the entered email matches a standard email format. -Error Message: If the email is invalid, an error message is displayed, and form submission is prevented. diff --git a/Client-Side Components/Client Scripts/Validate Email Format/ValidateEmailFormat.js b/Client-Side Components/Client Scripts/Validate Email Format/ValidateEmailFormat.js deleted file mode 100644 index ce21af0e39..0000000000 --- a/Client-Side Components/Client Scripts/Validate Email Format/ValidateEmailFormat.js +++ /dev/null @@ -1,12 +0,0 @@ -// Client Script: Validate Email Format on User Record - -function onSubmit() { - var emailField = g_form.getValue('email'); - var emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - - if (!emailPattern.test(emailField)) { - g_form.addErrorMessage('Please enter a valid email address.'); - return false; // Prevent form submission - } - return true; // Allow form submission -} diff --git a/Client-Side Components/Client Scripts/Validate Interaction record for FCR(First Call Resolution)/readme.md b/Client-Side Components/Client Scripts/Validate Interaction record for FCR(First Call Resolution)/readme.md new file mode 100644 index 0000000000..b56913d420 --- /dev/null +++ b/Client-Side Components/Client Scripts/Validate Interaction record for FCR(First Call Resolution)/readme.md @@ -0,0 +1,71 @@ +README — Client Script: Validate Interaction Resolution +📌 Purpose +This Client Script ensures proper validation when resolving an Interaction record in ServiceNow. +It prevents a user from marking an Interaction as Closed Complete without proper justification. + +🎯 What It Does + +When a user attempts to submit the form: +✔ Allows submission only if: +Interaction Type is "walkup" +And Related Task Boolean is true + +OR + +✔ If work notes are provided for First Contact Resolution (FCR) +❌ Prevents submission if: +State = Closed Complete +Work Notes are empty +And no related task condition is met + +🧠 Validations Performed +Field Condition Action +state closed_complete Trigger validation +type walkup AND u_boolean_no_related_task = true Submission allowed ✅ +work_notes Must not be empty Show error & stop submission ❌ +🔔 User Feedback + +If work notes are missing: +Displays inline field message + +Shows popup alert: +"Provide Worknotes for FCR Interaction" + +📍 Script Location + +Client Script → Type: onSubmit() +Applicable to Interaction table (interaction) + +📌 Script Code +//Client Script to validate an Interaction record is resolved with out any related record created. +function onSubmit() { + var relatedTask = g_form.getValue('u_boolean_no_related_task'); + var state = g_form.getValue('state'); + var type = g_form.getValue('type'); + var workNotes = g_form.getValue('work_notes'); // Get the value of work notes + + // Clear previous field messages + g_form.clearMessages(); + + // Check if state is changing to 'Closed Complete' + if (state == 'closed_complete') { + // Check additional conditions + if (type == 'walkup' && relatedTask == 'true') { + return true; // Allow form submission + } else if (!workNotes) { // Check if work notes is empty + g_form.showFieldMsg('work_notes', 'Provide Worknotes for FCR Interaction', 'error'); + alert('Provide Worknotes for FCR Interaction'); + return false; // Prevent form submission + } + } + return true; // Allow form submission for other states +} + +✅ Benefits + +Maintains consistent resolution standards +Ensures justification/documentation for FCR interactions +Reduces incorrect closure of requests without related actions + + + diff --git a/Client-Side Components/Client Scripts/Validate Interaction record for FCR(First Call Resolution)/script.js b/Client-Side Components/Client Scripts/Validate Interaction record for FCR(First Call Resolution)/script.js new file mode 100644 index 0000000000..1b43c255d2 --- /dev/null +++ b/Client-Side Components/Client Scripts/Validate Interaction record for FCR(First Call Resolution)/script.js @@ -0,0 +1,23 @@ +//Client Script to validate an Interaction record is resolved with out any related record created. +function onSubmit() { + var relatedTask = g_form.getValue('u_boolean_no_related_task'); + var state = g_form.getValue('state'); + var type = g_form.getValue('type'); + var workNotes = g_form.getValue('work_notes'); // Get the value of work notes + + // Clear previous field messages + g_form.clearMessages(); + + // Check if state is changing to 'Closed Complete' + if (state == 'closed_complete') { + // Check additional conditions + if (type == 'walkup' && relatedTask == 'true') { + return true; // Allow form submission + } else if (!workNotes) { // Check if work notes is empty + g_form.showFieldMsg('work_notes', 'Provide Worknotes for FCR Interaction', 'error'); + alert('Provide Worknotes for FCR Interaction'); + return false; // Prevent form submission + } + } + return true; // Allow form submission for other states +} diff --git a/Client-Side Components/Client Scripts/Zurich - Upgraded info messages/README.md b/Client-Side Components/Client Scripts/Zurich - Upgraded info messages/README.md new file mode 100644 index 0000000000..8de65d319a --- /dev/null +++ b/Client-Side Components/Client Scripts/Zurich - Upgraded info messages/README.md @@ -0,0 +1,5 @@ +**This feature will be used in the instance of Zurich++ release** + +Demonstrate different messages that has been introduced as part of Zurich release. + +Use Case: Display different information messages based on priority of the incident that will be showed on load and state is not Closed, Resolved or Cancelled. diff --git a/Client-Side Components/Client Scripts/Zurich - Upgraded info messages/infoMessages.js b/Client-Side Components/Client Scripts/Zurich - Upgraded info messages/infoMessages.js new file mode 100644 index 0000000000..2d7c5e55ff --- /dev/null +++ b/Client-Side Components/Client Scripts/Zurich - Upgraded info messages/infoMessages.js @@ -0,0 +1,23 @@ +function onLoad() { + var state = g_form.getValue('state'); //Get value of 'state' field + + if (state != '6' && state != '7' && state != '8') { + var priority = g_form.getValue('priority'); // Get value of 'priority' field + switch (priority) { + case '1': + g_form.addErrorMessage('Critical Incident'); + break; + case '2': + g_form.addHighMessage('High Priority Incident'); // addHighMessage() method will display message in orange color + break; + case '3': + g_form.addModerateMessage('Medium Priority Incident'); // addModerateMessage() method will display message in purple color + break; + case '4': + g_form.addLowMessage('Low Priority Incident'); // addLowMessage() method will display message in grey color + break; + } + } else if (state == '6' || state == '7') { + g_form.addSuccessMessage('Incident closed'); // addSuccessMessage() method will display message in green color + } +} 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/Client Scripts/field-character-counter/script.js b/Client-Side Components/Client Scripts/field-character-counter/script.js new file mode 100644 index 0000000000..42bd9ae34f --- /dev/null +++ b/Client-Side Components/Client Scripts/field-character-counter/script.js @@ -0,0 +1,22 @@ +function onChange(control, oldValue, newValue, isLoading) { + if (isLoading) return; + + // USER CONFIGURATION: Set field name and character limit + var fieldName = 'Description'; // Change to your field name + var maxLength = 80; // Change to your character limit + + var currentLength = newValue ? newValue.length : 0; + var remaining = maxLength - currentLength; + + // Clear any existing messages + g_form.hideFieldMsg(fieldName); + + // Show appropriate message based on remaining characters + if (remaining < 0) { + g_form.showFieldMsg(fieldName, 'Exceeds limit by ' + Math.abs(remaining) + ' characters', 'error'); + } else if (remaining <= 20) { + g_form.showFieldMsg(fieldName, remaining + ' characters remaining', 'warning'); + } else if (remaining <= 50) { + g_form.showFieldMsg(fieldName, remaining + ' characters remaining', 'info'); + } +} \ No newline at end of file diff --git a/Client-Side Components/Client Scripts/validate phone number/Readme.md b/Client-Side Components/Client Scripts/validate phone number/Readme.md new file mode 100644 index 0000000000..5f47923199 --- /dev/null +++ b/Client-Side Components/Client Scripts/validate phone number/Readme.md @@ -0,0 +1,39 @@ +Phone Number Validation — Client Script +Overview + +This Client Script validates that users enter their phone numbers in the strict format: (123) 456-7890. + +It is triggered whenever the Phone field changes on a sys_user record. If the input does not match the required format, the script: + +Displays an inline error message directly below the field. + +Clears the invalid input so the user can re-enter the correct value. + +This script is designed to be dynamic, simple, and user-friendly. + +Features + +Ensures phone numbers follow the exact format (123) 456-7890. + +Provides immediate feedback via field-level error messages. + +Clears invalid entries automatically to prevent submission errors. + +Works on Classic UI forms and provides clear messaging to the user. + +Usage Instructions +1. Create the Client Script + +Navigate to System Definition → Client Scripts. + +Click New to create a client script. + +2. Configure the Script + +Name: Phone Number Validation + +Table: sys_user + +Type: onChange + +Field: phone diff --git a/Client-Side Components/Client Scripts/validate phone number/validate_phone_format_(123)_456-7890_no_regex.js.js b/Client-Side Components/Client Scripts/validate phone number/validate_phone_format_(123)_456-7890_no_regex.js.js new file mode 100644 index 0000000000..9a293deb03 --- /dev/null +++ b/Client-Side Components/Client Scripts/validate phone number/validate_phone_format_(123)_456-7890_no_regex.js.js @@ -0,0 +1,23 @@ +function onChange(control, oldValue, newValue, isLoading) { + if (isLoading || !newValue) return; + + var fieldName = control.name; + + // Split the string + var area = newValue.substring(1, 4); + var firstThree = newValue.substring(6, 9); + var lastFour = newValue.substring(10, 14); + + if ( + newValue[0] !== '(' || newValue[4] !== ')' || newValue[5] !== ' ' || newValue[9] !== '-' || + isNaN(parseInt(area)) || isNaN(parseInt(firstThree)) || isNaN(parseInt(lastFour)) + ) { + g_form.showFieldMsg( + fieldName, + 'Phone Number must be in the format (123) 456-7890', + 'error', + false + ); + g_form.setValue(fieldName, ''); + } +} diff --git a/Client-Side Components/UI Actions/Add Loggedin user as Incident assigned to/ReadMe.md b/Client-Side Components/UI Actions/Add Loggedin user as Incident assigned to/ReadMe.md new file mode 100644 index 0000000000..d017057fbe --- /dev/null +++ b/Client-Side Components/UI Actions/Add Loggedin user as Incident assigned to/ReadMe.md @@ -0,0 +1,2 @@ +>**UI Action** +When a new Incident record is created, user can come to incident ticket and assigned to themself. Once they click on UI Action. diff --git a/Client-Side Components/UI Actions/Add Loggedin user as Incident assigned to/code.js b/Client-Side Components/UI Actions/Add Loggedin user as Incident assigned to/code.js new file mode 100644 index 0000000000..06b301c362 --- /dev/null +++ b/Client-Side Components/UI Actions/Add Loggedin user as Incident assigned to/code.js @@ -0,0 +1,12 @@ +var currentUser = gs.getUserID(); //Getting loggedIn User Id + +//Checing wheather user is available or not in Assignee field +if(current.assigned_to == ""){ //checking assigned to is there or not + current.assigned_to = currentUser; //Setting the current loggedIn user + current.update(); //updating the record. + gs.addInfoMessage("Incident has been assigned to You."); + action.setRedirectURL(current); +} else { + gs.addErrorMessage("Incident is already assigned"); + action.setRedirectURL(current); +} diff --git a/Client-Side Components/UI Actions/Cancel Flow Executions/README.md b/Client-Side Components/UI Actions/Cancel Flow Executions/README.md new file mode 100644 index 0000000000..c02c24b681 --- /dev/null +++ b/Client-Side Components/UI Actions/Cancel Flow Executions/README.md @@ -0,0 +1,49 @@ +# CancelFlow UI Action + +A ServiceNow utility that dynamically cancels flows associated with the current record, ensuring seamless process management. + +## Challenge + +Managing running flows in ServiceNow can be challenging, particularly when multiple flows are tied to a single record. This utility streamlines the process by offering a dynamic solution to identify and cancel running flows, minimizing manual intervention and ensuring seamless operations. + +This tool is especially useful in scenarios where you need to halt the current execution and initiate a new flow or process. Additionally, it can be leveraged to forcefully terminate the automation lifecycle when necessary, providing greater control over flow management. + +## Description + +This UI Action is designed to identify and cancel all running flows associated with the current record in a ServiceNow instance. It provides a user-friendly interface for administrators and developers to manage flow cancellations efficiently. This utility is particularly useful in scenarios where flows need to be terminated to prevent conflicts or errors during record updates. + +## Functionality + +The CancelFlow UI Action provides the following capabilities: +- Dynamically identifies running flows for the current record. +- Cancels the identified flows programmatically. +- Displays success or error messages to the user for better visibility. +- Ensures smooth handling of flow cancellations without manual intervention. + +## Usage Instructions + +### UI Action Script + +Add the given script to your UI Action: + + +### Example Usage + +1. Open the record where you want to cancel the associated flows. +2. Click on the **Cancel Flow** UI Action button. +3. The system will identify and cancel all running flows for the current record. +4. The same can be used in Business rules as well based on trigger conditions + + +### Visibility for UI Action + +In certain scenarios, it may be necessary to restrict the visibility of this operation to specific user groups. For example, only HR administrators or members of a designated group (e.g., "X Group") should have access to this functionality. These requirements can be addressed by configuring the **Condition** field in the UI Action. You can tailor the conditions to align with your specific use case, ensuring that only authorized users can execute this operation. One edge case about not having an active flow execution can also be handled in the condition which will restrict the visibility if no active flow execution is present. + + +## Dependencies + +- `sn_fd.FlowAPI` + +## Category + +Client-Side Components / UI Actions diff --git a/Client-Side Components/UI Actions/Cancel Flow Executions/cancelFlow.js b/Client-Side Components/UI Actions/Cancel Flow Executions/cancelFlow.js new file mode 100644 index 0000000000..7ae3f7d0d8 --- /dev/null +++ b/Client-Side Components/UI Actions/Cancel Flow Executions/cancelFlow.js @@ -0,0 +1,16 @@ +function cancelRunningFlows() { + + try { + var grFlowExecution = new GlideRecord("sys_flow_context"); + grFlowExecution.addQuery("source_record", current.sys_id); + grFlowExecution.query(); + + while (grFlowExecution.next()) { + sn_fd.FlowAPI.cancel(grFlowExecution.getUniqueValue(), "Canceling Flows"); + } + } catch (error) { + gs.error("Error cancelling flows: " + error.message); + } +} + + diff --git a/Client-Side Components/UI Actions/Cancel Incident/README.md b/Client-Side Components/UI Actions/Cancel Incident/README.md index 62710388e3..d20ab341a9 100644 --- a/Client-Side Components/UI Actions/Cancel Incident/README.md +++ b/Client-Side Components/UI Actions/Cancel Incident/README.md @@ -1 +1,57 @@ -This is a ui action run on incident table which is client callable, run on condition current.state == '1' which means when state is new.It cancel the incident through the form. +# Cancel Incident UI Action + +A UI Action in ServiceNow is a script that defines an action or button within the platform's user interface. It enables users to perform specific operations on forms and lists, such as creating, updating, or deleting records, or executing custom scripts. UI Actions enhance the user experience by providing functional buttons, links, or context menus. + +## Overview + +This UI Action allows users to cancel incidents directly from the incident form. It provides a confirmation dialog to prevent accidental cancellations and updates the incident state to "Cancelled" (state value 8) when confirmed. + +## Features + +- **Confirmation Dialog**: Uses GlideModal to display a confirmation prompt before cancelling +- **State Management**: Updates incident state to "Cancelled" (value 8) +- **Client-Side Validation**: Runs client-side for better user experience +- **Conditional Display**: Only shows when incident state is "New" (state value 1) + +## Configuration + +Create a UI Action with the following field values: + +**Name**: Cancel Incident + +**Action Name**: cancel_incident + +**Table**: Incident [incident] + +**Client**: checked (true) + +**Onclick**: cancelIncident(); + +**Condition**: current.state == '1' + +**Script**: Use the provided script.js file + +## Usage + +1. Navigate to an incident record in "New" state +2. Click the "Cancel Incident" button +3. Confirm the action in the modal dialog +4. The incident state will be updated to "Cancelled" + +## Technical Details + +- **Client-Side Function**: `cancelIncident()` - Displays confirmation modal +- **Server-Side Function**: `serverCancel()` - Updates the incident state +- **Modal Configuration**: Uses `glide_ask_standard` modal with custom title +- **State Value**: Sets incident state to '8' (Cancelled) + +## Prerequisites + +- User must have write access to the incident table +- Incident must be in "New" state (state = 1) for the UI Action to be visible + +## Notes + +- This UI Action only appears on incident forms when the state is "New" +- The confirmation dialog helps prevent accidental cancellations +- The server-side script executes only after user confirmation diff --git a/Client-Side Components/UI Actions/Cancel Incident/SETUP.md b/Client-Side Components/UI Actions/Cancel Incident/SETUP.md new file mode 100644 index 0000000000..6d5458a07c --- /dev/null +++ b/Client-Side Components/UI Actions/Cancel Incident/SETUP.md @@ -0,0 +1,147 @@ +# Setup Instructions for Cancel Incident UI Action + +This document provides detailed step-by-step instructions for implementing the Cancel Incident UI Action in your ServiceNow instance. + +## Prerequisites + +- Administrative access to ServiceNow instance +- Access to System Definition > UI Actions module +- Understanding of ServiceNow UI Actions and client-server scripting + +## Step-by-Step Setup + +### 1. Navigate to UI Actions + +1. In ServiceNow, go to **System Definition > UI Actions** +2. Click **New** to create a new UI Action + +### 2. Configure Basic Settings + +Fill in the following fields: + +| Field | Value | Description | +|-------|-------|-------------| +| **Name** | Cancel Incident | Display name for the UI Action | +| **Table** | Incident [incident] | Target table for the UI Action | +| **Action name** | cancel_incident | Unique identifier for the action | +| **Active** | ✓ (checked) | Enables the UI Action | + +### 3. Configure Display Settings + +| Field | Value | Description | +|-------|-------|-------------| +| **Form button** | ✓ (checked) | Shows button on form view | +| **Form link** | ☐ (unchecked) | Optional: Show as link instead | +| **List banner button** | ☐ (unchecked) | Not needed for this action | +| **List choice** | ☐ (unchecked) | Not needed for this action | + +### 4. Configure Client Settings + +| Field | Value | Description | +|-------|-------|-------------| +| **Client** | ✓ (checked) | Enables client-side execution | +| **Onclick** | `cancelIncident();` | Client-side function to call | + +### 5. Configure Conditions + +| Field | Value | Description | +|-------|-------|-------------| +| **Condition** | `current.state == '1'` | Only show for "New" incidents | + +### 6. Add the Script + +Copy the entire content from `script.js` and paste it into the **Script** field of the UI Action. + +### 7. Configure Advanced Settings (Optional) + +| Field | Value | Description | +|-------|-------|-------------| +| **Order** | 100 | Display order (adjust as needed) | +| **Hint** | Cancel this incident | Tooltip text | +| **Comments** | UI Action to cancel incidents in New state | Internal documentation | + +## Verification Steps + +### 1. Test the UI Action + +1. Navigate to an incident in "New" state +2. Verify the "Cancel Incident" button appears +3. Click the button and confirm the modal appears +4. Test both "OK" and "Cancel" in the confirmation dialog + +### 2. Verify State Changes + +1. After confirming cancellation, check that: + - Incident state changes to "Cancelled" + - Work notes are added with cancellation details + - Success message appears + +### 3. Test Edge Cases + +1. Try accessing the UI Action on incidents in other states (should not appear) +2. Test with different user roles to ensure proper permissions +3. Verify error handling works correctly + +## Troubleshooting + +### Common Issues + +**UI Action doesn't appear:** +- Check that the incident is in "New" state (state = 1) +- Verify the condition field: `current.state == '1'` +- Ensure the UI Action is marked as Active + +**Script errors:** +- Check browser console for JavaScript errors +- Verify the script is properly copied from `script.js` +- Ensure proper syntax and formatting + +**Permission issues:** +- Verify user has write access to incident table +- Check ACL rules for incident cancellation +- Ensure proper role assignments + +### Debug Mode + +To enable debug logging, add this line at the beginning of the `serverCancel()` function: + +```javascript +gs.info('Debug: Starting incident cancellation for ' + current.number); +``` + +## Security Considerations + +- The UI Action respects existing ACL rules +- Only users with incident write permissions can cancel incidents +- All cancellations are logged for audit purposes +- Work notes provide cancellation history + +## Customization Options + +### Modify Confirmation Message + +Edit line 33 in the script to customize the confirmation dialog: + +```javascript +gm.setPreference("question", "Your custom message here"); +``` + +### Change Cancellation Reason + +Modify the work note in the `serverCancel()` function (line 79): + +```javascript +var workNote = 'Custom cancellation reason: ' + gs.getUserDisplayName() + ' on ' + gs.nowDateTime(); +``` + +### Add Additional Validations + +Add custom validation logic in the `cancelIncident()` function before showing the modal. + +## Support + +For issues or questions: +1. Check ServiceNow system logs +2. Review browser console for client-side errors +3. Test in a development instance first +4. Consult ServiceNow documentation for UI Actions diff --git a/Client-Side Components/UI Actions/Cancel Incident/script.js b/Client-Side Components/UI Actions/Cancel Incident/script.js index fd7a2f0c96..ed7439c7c5 100644 --- a/Client-Side Components/UI Actions/Cancel Incident/script.js +++ b/Client-Side Components/UI Actions/Cancel Incident/script.js @@ -1,17 +1,101 @@ -function cancelIncident(){ - var gm = new GlideModal("glide_ask_standard", false, 600); // glide modal to get the confirmation - gm.setPreference("title", "Are you sure you wanna cancel incident!!!"); - gm.setPreference("onPromptComplete", function() { - gsftSubmit(null,g_form.getFormElement(),'cancel_incident');}); //calling same ui action - gm.render(); - +/** + * Client-side function to initiate incident cancellation + * Displays a confirmation modal before proceeding with the cancellation + */ +function cancelIncident() { + try { + // Validate that we have a valid form and record + if (!g_form || !g_form.getUniqueValue()) { + alert('Error: Unable to access incident record. Please refresh the page and try again.'); + return; + } + + // Check if incident is in the correct state for cancellation + var currentState = g_form.getValue('state'); + if (currentState !== '1') { + alert('Error: This incident cannot be cancelled. Only incidents in "New" state can be cancelled.'); + return; + } + + // Create confirmation modal with improved messaging + var gm = new GlideModal("glide_ask_standard", false, 600); + gm.setPreference("title", "Cancel Incident Confirmation"); + gm.setPreference("warning", true); + gm.setPreference("onPromptComplete", function() { + // Show loading message + g_form.addInfoMessage('Cancelling incident...'); + + // Submit the form to trigger server-side processing + gsftSubmit(null, g_form.getFormElement(), 'cancel_incident'); + }); + + // Set the confirmation message + gm.setPreference("question", "Are you sure you want to cancel this incident?\n\nThis action will change the incident state to 'Cancelled' and cannot be easily undone."); + + // Render the modal + gm.render(); + + } catch (error) { + // Handle any unexpected errors + console.error('Error in cancelIncident function:', error); + alert('An unexpected error occurred. Please contact your system administrator.'); + } } -if(typeof window == 'undefined'){ - serverCancel(); +/** + * Server-side execution block + * This code runs on the server when the UI Action is submitted + */ +if (typeof window == 'undefined') { + serverCancel(); } -function serverCancel(){ - current.state = '8'; //setting the state to canceled - current.update(); +/** + * Server-side function to cancel the incident + * Updates the incident state to 'Cancelled' and adds a work note + */ +function serverCancel() { + try { + // Validate that we have a current record + if (!current || !current.isValidRecord()) { + gs.addErrorMessage('Error: Invalid incident record.'); + return; + } + + // Double-check the current state before cancelling + if (current.state.toString() !== '1') { + gs.addErrorMessage('Error: This incident cannot be cancelled. Only incidents in "New" state can be cancelled.'); + return; + } + + // Store original values for logging + var incidentNumber = current.number.toString(); + var originalState = current.state.getDisplayValue(); + + // Update the incident state to 'Cancelled' (state value 8) + current.state = '8'; + + // Add a work note to document the cancellation + var workNote = 'Incident cancelled by ' + gs.getUserDisplayName() + ' on ' + gs.nowDateTime(); + if (current.work_notes.nil()) { + current.work_notes = workNote; + } else { + current.work_notes = current.work_notes + '\n\n' + workNote; + } + + // Update the record + current.update(); + + // Log the action for audit purposes + gs.info('Incident ' + incidentNumber + ' cancelled by user ' + gs.getUserName() + + '. State changed from "' + originalState + '" to "Cancelled"'); + + // Provide user feedback + gs.addInfoMessage('Incident ' + incidentNumber + ' has been successfully cancelled.'); + + } catch (error) { + // Handle server-side errors + gs.error('Error cancelling incident: ' + error.message); + gs.addErrorMessage('An error occurred while cancelling the incident. Please contact your system administrator.'); + } } diff --git a/Client-Side Components/UI Actions/Close Related HR cases & HR tasks/README.md b/Client-Side Components/UI Actions/Close Related HR cases & HR tasks/README.md new file mode 100644 index 0000000000..0e2ccec060 --- /dev/null +++ b/Client-Side Components/UI Actions/Close Related HR cases & HR tasks/README.md @@ -0,0 +1,14 @@ +Scenario:- + +Table: HR Case + +Create a form button named "Check related item and Close Complete" feature and list down the related child HR cases and HR tasks +in the pop-up message. +Upon confirmation, it will close the current case and other listed items. + +This will help in reducing the manual effort of closing items manually. + +Scripts: +Client UI script to handle the confirmation popup and state of current case. + +GlideAJAX enabled script include to fetch the data and close the related items. diff --git a/Client-Side Components/UI Actions/Close Related HR cases & HR tasks/Script Include.js b/Client-Side Components/UI Actions/Close Related HR cases & HR tasks/Script Include.js new file mode 100644 index 0000000000..c61e9c1ea0 --- /dev/null +++ b/Client-Side Components/UI Actions/Close Related HR cases & HR tasks/Script Include.js @@ -0,0 +1,50 @@ +var close_item = Class.create(); +close_item.prototype = Object.extendsObject(global.AbstractAjaxProcessor, { + getRelatedItems: function() { + var caseId = this.getParameter('sysparm_case_id'); + var results = []; + + // Get child HR cases + var childCases = new GlideRecord('sn_hr_core_case'); + childCases.addQuery('parent', caseId); + childCases.query(); + while (childCases.next()) { + results.push({ type: 'HR Case', number: childCases.getValue('number') }); + } + + // Get tasks + var tasks = new GlideRecord('sn_hr_core_task'); + tasks.addQuery('hr_case', caseId); + tasks.query(); + while (tasks.next()) { + results.push({ type: 'HR Task', number: tasks.getValue('number') }); + } + + return JSON.stringify(results); + }, + closeRelatedItems: function() { + var caseId = this.getParameter('sysparm_case_id'); + + // Close child cases + var childCases = new GlideRecord('sn_hr_core_case'); + childCases.addQuery('parent', caseId); + childCases.query(); + while (childCases.next()) { + childCases.setValue('state', '3'); + childCases.update(); + } + + // Close tasks + var tasks = new GlideRecord('sn_hr_core_task'); + tasks.addQuery('hr_case', caseId); + tasks.query(); + while (tasks.next()) { + tasks.setValue('state', '3'); + tasks.update(); + } + + return "done"; + + }, + type: 'close_task' +}); diff --git a/Client-Side Components/UI Actions/Close Related HR cases & HR tasks/UI Action script.js b/Client-Side Components/UI Actions/Close Related HR cases & HR tasks/UI Action script.js new file mode 100644 index 0000000000..3d7b06fc37 --- /dev/null +++ b/Client-Side Components/UI Actions/Close Related HR cases & HR tasks/UI Action script.js @@ -0,0 +1,28 @@ +// Demo- OnClick function to execute +function demo() { + var ga = new GlideAjax('sn_hr_core.close_items'); + ga.addParam('sysparm_name', 'getRelatedItems'); + ga.addParam('sysparm_case_id', g_form.getUniqueValue()); + ga.getXMLAnswer(function(response) { + // If there exist related items + var items = JSON.parse(response); + if (items.length > 0) { + var msg = "This case has related items:\n"; + items.forEach(function(item) { + msg += "- " + item.type + ": " + item.number + "\n"; + }); + msg += "\nDo you want to close them as well?"; + if (confirm(msg)) { + // close current HR case + g_form.setValue('state', '3'); + g_form.save(); + } + } else { + // If no related item is associated + if (confirm("No related items found. Close this case?")) { + g_form.setValue('state', '3'); + g_form.save(); + } + } + }); +} diff --git a/Client-Side Components/UI Actions/CloseChildCases/CloseChildCases.js b/Client-Side Components/UI Actions/CloseChildCases/CloseChildCases.js new file mode 100644 index 0000000000..458278ddbd --- /dev/null +++ b/Client-Side Components/UI Actions/CloseChildCases/CloseChildCases.js @@ -0,0 +1,23 @@ +(function executeAction() { + var grCase = new GlideRecord('sn_customerservice_case'); + grCase.addQuery('parent', current.sys_id); + grCase.query(); + + var counter = 0; + while (grCase.next()) { + if (grCase.state != 3) { // 3 = Closed + grCase.resolution_code = '16'; + grCase.close_notes = 'This case was auto closed from the parent case.'; + grCase.state = 3; + grCase.update(); + counter++; + } + } + + // Show info message only if any cases were closed + if (counter > 0) { + gs.addInfoMessage(counter + ' child case(s) have been closed.'); + } + + action.setRedirectURL(current); +})(); diff --git a/Client-Side Components/UI Actions/CloseChildCases/README.md b/Client-Side Components/UI Actions/CloseChildCases/README.md new file mode 100644 index 0000000000..61f910aae0 --- /dev/null +++ b/Client-Side Components/UI Actions/CloseChildCases/README.md @@ -0,0 +1,6 @@ +Name: Close all Child Case +Table:sn_customerservice_case +Condition: (gs.hasRole('sn_customerservice_agent') || gs.hasRole('admin') ) && (new GlideRecord('sn_customerservice_case').addQuery('parent', current.sys_id).query().hasNext()) + +Use Case: +Provide UI action button to close all the associated child cases from the parent Case. diff --git a/Client-Side Components/UI Actions/Copy Bulk SysIDs/Copy Bulk Sysids.js b/Client-Side Components/UI Actions/Copy Bulk SysIDs/Copy Bulk Sysids.js new file mode 100644 index 0000000000..be315f21e3 --- /dev/null +++ b/Client-Side Components/UI Actions/Copy Bulk SysIDs/Copy Bulk Sysids.js @@ -0,0 +1,2 @@ +var sysIds = g_list.getChecked(); +copyToClipboard(sysIds); diff --git a/Client-Side Components/UI Actions/Copy Bulk SysIDs/README.md b/Client-Side Components/UI Actions/Copy Bulk SysIDs/README.md new file mode 100644 index 0000000000..41ad602959 --- /dev/null +++ b/Client-Side Components/UI Actions/Copy Bulk SysIDs/README.md @@ -0,0 +1,33 @@ +# Copy SysIDs in Bulk — ServiceNow Utility + +> Simplify copying checked sys_ids from a list view with a one-click UI Action. + +--- + +## Purpose / Use Case + +Often, you may need to extract sys_ids from records listed in a ServiceNow list view (for scripting, validations, data workflows, etc.). Instead of exporting CSVs or manually gathering IDs, this utility enables direct copying of the selected records’ sys_ids (comma-separated) from the list itself. + +--- + +## How It Works + +It adds a global UI Action (on lists) that, when clicked, collects the sys_ids of checked records and copies them to the clipboard using a small client-side script. + +--- + +## Installation Steps + +1. Navigate to **System Definition > UI Actions**. +2. Create a **new UI Action** with these settings: + - **Name**: e.g. `Copy Bulk SysIDs` + - **Table**: `Global` (so it works on every list) + - **Check** the **Client** and **List** checkboxes (so it appears in list context on client side) +3. In the **Onclick / Client script** field, paste: + + ```javascript + var sysIds = g_list.getChecked(); + copyToClipboard(sysIds); + +## Result +image diff --git a/Client-Side Components/UI Actions/Email Watermark Utility/GenericEmailUtility.js b/Client-Side Components/UI Actions/Email Watermark Utility/GenericEmailUtility.js new file mode 100644 index 0000000000..1626209823 --- /dev/null +++ b/Client-Side Components/UI Actions/Email Watermark Utility/GenericEmailUtility.js @@ -0,0 +1,80 @@ +var GenericEmailUtility = Class.create(); +GenericEmailUtility.prototype = { + initialize: function() {}, + + // Generate an Outlook (mailto) link with watermark tracking + get_Outlook_link: function() { + try { + const email_payload = JSON.stringify({ + "REQUESTOR_ID": "", + "TITLE": "", + "BODY": "", + "REQUEST_ID": "", + "TABLE_ID": "" + }); + + var mailtoLink = false; + const raw_data = this.getParameter("sysparm_email_body") || email_payload; + + if (global.JSUtil.notNil(raw_data)) { + var email_data = JSON.parse(raw_data); + + const to = this.getEmail(email_data.REQUESTOR_ID); + const cc = gs.getProperty("instanceEmailAddress"); // instance default CC + const subject = email_data.TITLE || ''; + const body = email_data.BODY || ''; + + const watermark = this.getWatermark(email_data.REQUEST_ID, email_data.TABLE_ID); + + // Construct mailto link + mailtoLink = 'mailto:' + to + '?cc=' + cc; + + if (subject) + mailtoLink += '&subject=' + encodeURIComponent(subject); + + if (body) + mailtoLink += '&body=' + encodeURIComponent(body); + + if (watermark) + mailtoLink += encodeURIComponent("\n\nRef: " + watermark); + } + + return mailtoLink; + + } catch (ex) { + gs.error("Error in get_Outlook_link(): " + ex.message); + return false; + } + }, + + // Fetch watermark ID (creates one if missing) + getWatermark: function(record_id, table_name) { + var wm = new GlideRecord('sys_watermark'); + wm.addQuery('source_id', record_id); + wm.orderByDesc('sys_created_on'); + wm.query(); + + if (wm.next()) { + return wm.getValue('number'); + } + + wm.initialize(); + wm.source_id = record_id; + wm.source_table = table_name; + wm.insert(); + + return wm.getValue('number'); + }, + + // Retrieve user’s email address + getEmail: function(user_id) { + if (global.JSUtil.notNil(user_id)) { + var user = new GlideRecordSecure('sys_user'); + if (user.get(user_id)) + return user.email.toString(); + } + return ''; + }, + + type: 'GenericEmailUtility' +}; diff --git a/Client-Side Components/UI Actions/Email Watermark Utility/README.md b/Client-Side Components/UI Actions/Email Watermark Utility/README.md new file mode 100644 index 0000000000..29a7da7425 --- /dev/null +++ b/Client-Side Components/UI Actions/Email Watermark Utility/README.md @@ -0,0 +1,56 @@ +# Outlook Email Watermark Utility for ServiceNow + +# Overview +This reusable utility allows users to send emails **outside ServiceNow** (e.g., using Outlook or any default mail client) while still maintaining the conversation within ServiceNow. +By embedding a unique watermark reference, any replies to the email will automatically append to the original record's activity feed. + +This helps teams collaborate externally without losing internal record visibility — ideal for customers or vendors who communicate via Outlook. + +--- + +# Objective +- Enable ServiceNow users to send Outlook emails directly from a record. +- Maintain conversation history in ServiceNow using watermark tracking. +- Make the solution **generic**, reusable across tables (Incident, Change, Request, etc.). +- Prevent dependency on outbound mail scripts or custom integrations. + +# Components + +## 1. Script Include: GenericEmailUtility +Handles the logic for: +- Constructing the mailto: link. +- Fetching recipient and instance email addresses. +- Generating or retrieving the watermark ID. +- Returning a formatted Outlook link to the client script. + +## Key Methods +1. get_Outlook_link() - Builds the full Outlook mail link with subject, body, and watermark. +2. getWatermark(record_id, table_name) - Ensures a watermark exists for the record. +3. getEmail(user_id) - Fetches the email address for the target user. + +## 2. UI Action (Client Script) +Executes on the record form when the button/link is clicked. +It gathers record data, constructs a payload, calls the Script Include using GlideAjax, and opens Outlook. + +## Key Steps +1. Collect field data like requestor, short description, and description. +2. Pass record details to the Script Include (GenericEmailUtility). +3. Receive a ready-to-use Outlook link. +4. Open the mail client with prefilled details and watermark reference. + +## How It Works +1. User clicks "Send Outlook Email" UI Action on a record. +2. Script gathers record data and passes it to GenericEmailUtility. +3. The utility builds a 'mailto:' link including the watermark. +4. Outlook (or default mail client) opens with pre-filled To, CC, Subject, and Body fields. +5. When the recipient replies, ServiceNow uses the watermark to append comments to the correct record. + +## Example Usage +**User clicks “Send Outlook Email”** on a Request record: +Outlook opens prefilled like this: + +image + + +image + diff --git a/Client-Side Components/UI Actions/Email Watermark Utility/Send Email UI Action.js b/Client-Side Components/UI Actions/Email Watermark Utility/Send Email UI Action.js new file mode 100644 index 0000000000..000424a283 --- /dev/null +++ b/Client-Side Components/UI Actions/Email Watermark Utility/Send Email UI Action.js @@ -0,0 +1,25 @@ +function onClick(g_form) { + var separator = "\n--------------------------------\n"; + var email_body = "Record URL:\n" + g_form.getDisplayValue('number') + separator; + email_body += "Short Description:\n" + g_form.getValue('short_description') + separator; + email_body += "Description:\n" + g_form.getValue('description') + separator; + + var email_data = {}; + email_data.REQUESTOR_ID = g_form.getValue('caller_id') || g_form.getValue('opened_by') || g_form.getValue('requested_for'); + email_data.TITLE = g_form.getValue('short_description') || 'ServiceNow Communication'; + email_data.BODY = email_body; + email_data.REQUEST_ID = g_form.getUniqueValue(); + email_data.TABLE_ID = g_form.getTableName(); + + var ga = new GlideAjax('GenericEmailUtility'); + ga.addParam('sysparm_name', 'get_Outlook_link'); + ga.addParam('sysparm_email_body', JSON.stringify(email_data)); + ga.getXMLAnswer(function(response) { + var mailto_link = response; + if (mailto_link && mailto_link != 'false') { + window.open(mailto_link); + } else { + g_form.addErrorMessage('Unable to generate Outlook link.'); + } + }); +} diff --git a/Client-Side Components/UI Actions/Expire Timer in Flows/README.md b/Client-Side Components/UI Actions/Expire Timer in Flows/README.md new file mode 100644 index 0000000000..eeacddf70f --- /dev/null +++ b/Client-Side Components/UI Actions/Expire Timer in Flows/README.md @@ -0,0 +1,7 @@ +This UI Action adds an admin-only button to Flow Context records in ServiceNow. It is designed to expire active timers within a flow, allowing developers and testers to bypass waiting stages during sub-production testing. This helps accelerate flow validation and debugging during development cycles, especially useful during events like Hacktoberfest. + +Flows in ServiceNow often include Wait for Condition or Wait for Duration steps that can delay testing. This UI Action provides a quick way to expire those timers, enabling the flow to proceed immediately without waiting for the configured duration or condition. Features + +Adds a button labeled "Expire Timers" to Flow Context records. Visible only to users with the admin role. Executes a script to expire all active timers associated with the selected Flow Context. Ideal for sub-production environments (e.g., development, test, or staging). Speeds up flow development and validation. + +image diff --git a/Client-Side Components/UI Actions/Expire Timer in Flows/script.js b/Client-Side Components/UI Actions/Expire Timer in Flows/script.js new file mode 100644 index 0000000000..8d8d67704c --- /dev/null +++ b/Client-Side Components/UI Actions/Expire Timer in Flows/script.js @@ -0,0 +1,46 @@ +/* +This script should be placed in the UI action on the table sys_flow_context form view. +This UI action should be marked as client. +Use runClientCode() function in the Onclick field. +*/ +function runClientCode() { + + if (confirm('Are you sure you want to Expire the Timer activity ?\n\nThis Action Cannot Be Undone!')) { + //Call the UI Action and skip the 'onclick' function + gsftSubmit(null, g_form.getFormElement(), 'ExpireTimer'); //MUST call the 'Action name' set in this UI Action + } else { + return false; + } +} + +if (typeof window == 'undefined') { + ExpireTimer(); +} + +function ExpireTimer() { + var grTrigger = new GlideRecord('sys_trigger'); + grTrigger.addQuery('name', 'flow.fire'); + grTrigger.addQuery('script', 'CONTAINS', current.sys_id); + grTrigger.addQuery('state', 0); + grTrigger.setLimit(1); + grTrigger.query(); + if (grTrigger.next()) { + var grEvent = new GlideRecord('sysevent'); + grEvent.initialize(); + grEvent.setNewGuid(); + grEvent.setValue('name', 'flow.fire'); + grEvent.setValue('queue', 'flow_engine'); + grEvent.setValue('parm1', grTrigger.getValue('job_context').toString().slice(6)); + grEvent.setValue('parm2', ''); + grEvent.setValue('instance', current.sys_id); + grEvent.setValue('table', 'sys_flow_context'); + grEvent.setValue('state', 'ready'); + grEvent.setValue('process_on', new GlideDateTime().getValue()); //aka run immediately + grEvent.insert(); + grTrigger.deleteRecord(); + gs.addInfoMessage("You have chosen to end any timers related to this flow."); + } + + + action.setRedirectURL(current); +} diff --git a/Client-Side Components/UI Actions/Field Review of User Record when on form using action button/Field_Review_userTable_1.png b/Client-Side Components/UI Actions/Field Review of User Record when on form using action button/Field_Review_userTable_1.png new file mode 100644 index 0000000000..68dab0991a Binary files /dev/null and b/Client-Side Components/UI Actions/Field Review of User Record when on form using action button/Field_Review_userTable_1.png differ diff --git a/Client-Side Components/UI Actions/Field Review of User Record when on form using action button/Field_Review_userTable_2.png b/Client-Side Components/UI Actions/Field Review of User Record when on form using action button/Field_Review_userTable_2.png new file mode 100644 index 0000000000..ce7437609e Binary files /dev/null and b/Client-Side Components/UI Actions/Field Review of User Record when on form using action button/Field_Review_userTable_2.png differ diff --git a/Client-Side Components/UI Actions/Field Review of User Record when on form using action button/Field_Review_userTable_3.png b/Client-Side Components/UI Actions/Field Review of User Record when on form using action button/Field_Review_userTable_3.png new file mode 100644 index 0000000000..b05fd71b36 Binary files /dev/null and b/Client-Side Components/UI Actions/Field Review of User Record when on form using action button/Field_Review_userTable_3.png differ diff --git a/Client-Side Components/UI Actions/Field Review of User Record when on form using action button/README.md b/Client-Side Components/UI Actions/Field Review of User Record when on form using action button/README.md new file mode 100644 index 0000000000..60dae0e3cf --- /dev/null +++ b/Client-Side Components/UI Actions/Field Review of User Record when on form using action button/README.md @@ -0,0 +1,34 @@ +## Field Review of User Record when on form using action button + +Displays informational messages suggesting improvements to field formatting on the User Table (**sys_user**) form when the **Fields Check** button is clicked. + +- Helps maintain consistency in user data by checking capitalization of names and titles, validating email format, ensuring phone numbers contain only digits, and preventing duplicate phone entries. +- Also suggests users not to leave the **user_name** field empty. +- Shows Info messages below each field highlighting fields that may need attention. +- Simple Prerequisite is that: when form loads give Info message to check **Field Check** button to bring user's attention +- Uses a Client-side UI Action (**Fields Check**) that to review entered data and display friendly suggestions + - Name: Fields Check + - Table: User (sys_user) + - Client: true + - Form button: true + - Onclick: onClickCheckDetails() + +--- + +### Grab user's attention on Field Check Button using Info message at top + +![Field Review on User Table_1](Field_Review_userTable_1.png) + +--- + +### After clicking Field Check Button where suggestions are displayed below fields + +![Field Review on User Table_2](Field_Review_userTable_2.png) + +--- + +### When user fixes the suggested issues and click the **Fields Check** button again, a message confirms that all fields are correctly formatted + +![Field Review on User Table_3](Field_Review_userTable_3.png) + +--- diff --git a/Client-Side Components/UI Actions/Field Review of User Record when on form using action button/actionButtonScript.js b/Client-Side Components/UI Actions/Field Review of User Record when on form using action button/actionButtonScript.js new file mode 100644 index 0000000000..638849f390 --- /dev/null +++ b/Client-Side Components/UI Actions/Field Review of User Record when on form using action button/actionButtonScript.js @@ -0,0 +1,68 @@ +function onClickCheckDetails() { + // Friendly helper for field normalization guidance + g_form.hideAllFieldMsgs(); + g_form.clearMessages(); + + // --- Get Field values --- + var firstName = g_form.getValue('first_name'); + var lastName = g_form.getValue('last_name'); + var title = g_form.getValue('title'); + var userId = g_form.getValue('user_name'); + var email = g_form.getValue('email'); + var businessPhone = g_form.getValue('phone'); + var mobilePhone = g_form.getValue('mobile_phone'); + + // --- Regex patterns --- + var capitalRegex = /^[A-Z][a-zA-Z\s]*$/; // Names & titles start with a capital + var emailRegex = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/; + var phoneRegex = /^\d+$/; + + var suggestions = []; + + if (firstName && !capitalRegex.test(firstName)) { + g_form.showFieldMsg('first_name', 'Suggestion: Start the name with a capital letter.', 'info'); + suggestions.push('First Name'); + } + + if (lastName && !capitalRegex.test(lastName)) { + g_form.showFieldMsg('last_name', 'Suggestion: Start the name with a capital letter.', 'info'); + suggestions.push('Last Name'); + } + + if (title && !capitalRegex.test(title)) { + g_form.showFieldMsg('title', 'Suggestion: Titles usually start with a capital letter.', 'info'); + suggestions.push('Title'); + } + + if (!userId) { + g_form.showFieldMsg('user_name', 'Suggestion: Do not keep the User ID empty.', 'info'); + suggestions.push('User ID'); + } + + if (email && !emailRegex.test(email)) { + g_form.showFieldMsg('email', 'Suggestion: Please use a valid email format like name@example.com.', 'info'); + suggestions.push('Email'); + } + + if (businessPhone && !phoneRegex.test(businessPhone)) { + g_form.showFieldMsg('phone', 'Suggestion: Use digits only avoid letters.', 'info'); + suggestions.push('Business Phone'); + } + + if (mobilePhone && !phoneRegex.test(mobilePhone)) { + g_form.showFieldMsg('mobile_phone', 'Suggestion: Use digits only avoid letters.', 'info'); + suggestions.push('Mobile Phone'); + } + + / + if (businessPhone && mobilePhone && businessPhone === mobilePhone) { + g_form.showFieldMsg('phone', 'Work and mobile numbers appear identical, use different Numbers!', 'info'); + suggestions.push('Phone Numbers'); + } + + if (suggestions.length > 0) { + g_form.addInfoMessage('Quick review complete! Please check: ' + suggestions.join(', ') + '.'); + } else { + g_form.addInfoMessage('looks good! Nicely formatted data.'); + } +} diff --git a/Client-Side Components/UI Actions/Generate QR for Assets/ReadMe.md b/Client-Side Components/UI Actions/Generate QR for Assets/ReadMe.md new file mode 100644 index 0000000000..d184498218 --- /dev/null +++ b/Client-Side Components/UI Actions/Generate QR for Assets/ReadMe.md @@ -0,0 +1,50 @@ +# 🧩 ServiceNow Asset QR Code Generator (UI Action) + +This repository contains a **ServiceNow UI Action** script that generates and displays a QR Code for an Asset record from list view. +When the user selects a record and clicks the UI Action, a modal window pops up showing a dynamically generated QR Code that links to asset details. + + +A supporting **Script Include** (server-side) is required in your ServiceNow instance but **is not included** in this repository. +At the bottom of file , a sample Script Include Code is given , check for the reference. + +--- + +## 🚀 Features + +- Generates a QR Code for the selected Asset record. +- Displays the QR Code inside a ServiceNow modal (`GlideModal`). +- Uses **QrIckit API** for quick and free QR code generation. +- Clean, modular client-side code that integrates seamlessly with UI Actions. +- Includes a `qr-code-image` file showing example QR Code generated. + +--- + +## 🧠 How It Works + +1. The `onClickQR()` function is triggered when the user clicks a UI Action button. +2. It calls `generateQRCodeForAsset(sys_id)` and passes the record’s `sys_id`. +3. A `GlideAjax` request fetches asset data from a **Script Include** on the server. +4. That data is encoded and sent to the **QrIckit** API to generate a QR Code image. +5. A ServiceNow modal (`GlideModal`) displays the generated QR Code to the user. + +--- + + +**Note :** +1) As the UI action calls a Script Include , in this folder no script include is present +2) You can modify script include part as required(i.e Which fields are to be shown when QR is scanned) +3) A sample Client Callable Script-Include is given here. + +``` Script Include Code + var GenerateAssetQR = Class.create(); +GenerateAssetQR.prototype = Object.extendsObject(AbstractAjaxProcessor, { + getAssetQRData: function() { + var sys_id = this.getParameter('sysparm_sys_id'); + var asset = new GlideRecord('alm_asset'); + if (asset.get(sys_id)) { + return 'Asset: ' + asset.name + ', Serial: ' + asset.serial_number; + } + return 'Invalid asset record.'; + } +}); +``` diff --git a/Client-Side Components/UI Actions/Generate QR for Assets/qr-code-image.png b/Client-Side Components/UI Actions/Generate QR for Assets/qr-code-image.png new file mode 100644 index 0000000000..d3488d45a1 Binary files /dev/null and b/Client-Side Components/UI Actions/Generate QR for Assets/qr-code-image.png differ diff --git a/Client-Side Components/UI Actions/Generate QR for Assets/ui-action-script.js b/Client-Side Components/UI Actions/Generate QR for Assets/ui-action-script.js new file mode 100644 index 0000000000..4f8609d88e --- /dev/null +++ b/Client-Side Components/UI Actions/Generate QR for Assets/ui-action-script.js @@ -0,0 +1,26 @@ +function onClickQR() { + generateQRCodeForAsset(g_sysId);//get the sysid of selected record +} + +function generateQRCodeForAsset(sys_id) { + var ga = new GlideAjax('GenerateAssetQR');//Script Include which stores data to be presented when QR-Code is Scanned + ga.addParam('sysparm_name', 'getAssetQRData'); + ga.addParam('sysparm_sys_id', sys_id); + + ga.getXMLAnswer(function(response) { + var qrData = response; + var qrURL = 'https://qrickit.com/api/qr.php?d=' + encodeURIComponent(qrData) + '&addtext=Get Asset Data'; + //QrIckit is a tool using which Customized QR-Codes can be generated + var modalHTML = ` +
+ QR Code +

Scan to view asset details

+
+ `; + + var gModal = new GlideModal("QR Code"); + gModal.setTitle('Asset QR Code'); + gModal.setWidth(500); + gModal.renderWithContent(modalHTML); + }); +} diff --git a/Client-Side Components/UI Actions/Generate a PDF/README.md b/Client-Side Components/UI Actions/Generate a PDF/README.md new file mode 100644 index 0000000000..2a61438b92 --- /dev/null +++ b/Client-Side Components/UI Actions/Generate a PDF/README.md @@ -0,0 +1,8 @@ +# Introduction +Generating a PDF using PDFGenerationAPI +Calling an OOTB convertToPDFWithHeaderFooter() method to generate a PDF with your header and footer. +PDFGenerationAPI – convertToPDFWithHeaderFooter(String html, String targetTable, String targetTableSysId, String pdfName, Object headerFooterInfo, String fontFamilySysId, Object documentConfiguration) + +# Example: +image + diff --git a/Client-Side Components/UI Actions/Generate a PDF/serverscript.js b/Client-Side Components/UI Actions/Generate a PDF/serverscript.js new file mode 100644 index 0000000000..66c8378e1c --- /dev/null +++ b/Client-Side Components/UI Actions/Generate a PDF/serverscript.js @@ -0,0 +1,50 @@ +//Table: Change Request +// UI action button on the change form that exports all the related incidents into a PDF format. +//ServiceNows PDFGenerationAPI allows you to customize the page size, header, footer, header image, page orientation, and more. + +var inc = new GlideRecord('incident'), + incidentsList = [], + v = new sn_pdfgeneratorutils.PDFGenerationAPI, + html = '', + hfInfo = new Object(), + result; +inc.addQuery('rfc', current.sys_id); +inc.query(); +while (inc.next()) { + incidentsList.push(inc.number); + incidentsList.push(inc.getDisplayValue('caller_id')); + incidentsList.push(inc.getDisplayValue('category')); + incidentsList.push(inc.getDisplayValue('subcategory')); + incidentsList.push(inc.getValue('priority')); + incidentsList.push(inc.getValue('short_description')); + incidentsList.push(inc.getValue('description')); + incidentsList.push(inc.getDisplayValue('assignment_group')); +} +var json = { + incidents: incidentsList.toString() +}; + + JSON.stringify(json); +html = '

Incidents Related to the Change: ' + current.number + '


'; + + +html += '

Incidents List  

'; +html += '' + getIncidentsTable(json.incidents) + '
NumberCallerCategorySub CategoryPriorityShort DescriptionDescriptionAssignment Group
'; +hfInfo["FooterTextAlignment"] = "BOTTOM_CENTER"; +hfInfo["FooterText"] = "Incidents List"; +hfInfo["PageOrientation"] = "LANDSCAPE"; +hfInfo["GeneratePageNumber"] = "true"; + +result = v.convertToPDFWithHeaderFooter(html, 'change_request', current.sys_id, "Incidents Related to the Change:_" + current.number, hfInfo); +action.setRedirectURL(current); + +function getIncidentsTable(incidents) { + if (incidents == '') + return ''; + var table = '', + i; + incidents = incidents.split(','); + for (i = 0; i < incidents.length; i += 8) + table += '' + incidents[i] + '' + incidents[i + 1] + '' + incidents[i +2] + '' + incidents[i + 3] + '' + incidents[i + 4] + '' + incidents[i + 5] + '' + incidents[i + 6] + '' + incidents[i + 7] + ''; + return table; +} diff --git a/Client-Side Components/UI Actions/Group Membership Admin Util/README.md b/Client-Side Components/UI Actions/Group Membership Admin Util/README.md new file mode 100644 index 0000000000..68ab60055f --- /dev/null +++ b/Client-Side Components/UI Actions/Group Membership Admin Util/README.md @@ -0,0 +1,40 @@ +# Group Membership Utility + +The **Group Membership Utility** is a ServiceNow server-side tool designed to streamline the management of user membership in assignment groups. It provides administrators with two UI actions on the Assignment Group table to add or remove themselves from a group, ensuring efficient group membership management. Super helpful when doing group membership testing. + +## Challenge + +Managing assignment group memberships manually can be time-consuming and frustrating when doing group change related testings. + +## Features + +- **Add Me**: Adds the current user to the selected assignment group, ensuring quick inclusion. +- **Remove Me**: Removes the current user from the selected assignment group, simplifying group updates. +- **Admin-Only Visibility**: Both actions are restricted to users with administrative privileges i.e admin user role, ensuring controlled access. + +## Functionality + +The Group Membership Utility provides the following capabilities: +- Detects the current user's membership status in the selected group. +- Dynamically enables or disables the **Add Me** and **Remove Me** actions based on the user's membership. +- Ensures visibility of these actions only for users with administrative privileges. + +## Visibility + +Add below condition script for the "Add Me" UI action +```javascript +gs.hasRole('admin') && !gs.getUser().isMemberOf(current.sys_id); +``` +Add below condition script for the "Remove Me" UI action +```javascript +gs.hasRole('admin') && gs.getUser().isMemberOf(current.sys_id); +``` + +## Usage Instructions + +1. Navigate to the Assignment Group table. +2. Select a group. +3. Use the **Add Me** button to add yourself to the group if you're not already a member. +4. Use the **Remove Me** button to remove yourself from the group if you're already a member. + + diff --git a/Client-Side Components/UI Actions/Group Membership Admin Util/addMeUIActionScript.js b/Client-Side Components/UI Actions/Group Membership Admin Util/addMeUIActionScript.js new file mode 100644 index 0000000000..348c46fbc0 --- /dev/null +++ b/Client-Side Components/UI Actions/Group Membership Admin Util/addMeUIActionScript.js @@ -0,0 +1,26 @@ +try { + var groupSysId = current.sys_id; + var userSysId = gs.getUserID(); + + // Validate input + if (!groupSysId || !userSysId) { + throw new Error("Group Sys ID and User Sys ID are required."); + } + + // Create a new record in the sys_user_grmember table + var gr = new GlideRecord("sys_user_grmember"); + gr.initialize(); + gr.group = groupSysId; + gr.user = userSysId; + var sysId = gr.insert(); + + if (sysId) { + gs.addInfoMessage( + "User successfully added to the group. Record Sys ID: " + sysId + ); + } else { + throw new Error("Failed to add user to the group."); + } +} catch (error) { + gs.addErrorMessage("Error adding user to group: " + error.message); +} diff --git a/Client-Side Components/UI Actions/Group Membership Admin Util/removeMeUIActionScript.js b/Client-Side Components/UI Actions/Group Membership Admin Util/removeMeUIActionScript.js new file mode 100644 index 0000000000..378b8b2d40 --- /dev/null +++ b/Client-Side Components/UI Actions/Group Membership Admin Util/removeMeUIActionScript.js @@ -0,0 +1,29 @@ +try { + var groupSysId = current.sys_id; + var userSysId = gs.getUserID(); + + // Validate input + if (!groupSysId || !userSysId) { + throw new Error("Group Sys ID and User Sys ID are required."); + } + + // Query the sys_user_grmember table to find the record + var gr = new GlideRecord("sys_user_grmember"); + gr.addQuery("group", groupSysId); + gr.addQuery("user", userSysId); + gr.query(); + + if (gr.next()) { + // Delete the record + var deleted = gr.deleteRecord(); + if (deleted) { + gs.addInfoMessage("User successfully removed from the group."); + } else { + throw new Error("Failed to remove user from the group."); + } + } else { + throw new Error("No matching record found for the user in the group."); + } +} catch (error) { + gs.addErrorMessage("Error removing user from group: " + error.message); +} \ No newline at end of file diff --git a/Client-Side Components/UI Actions/Group dependency/README.md b/Client-Side Components/UI Actions/Group dependency/README.md new file mode 100644 index 0000000000..94126acb3c --- /dev/null +++ b/Client-Side Components/UI Actions/Group dependency/README.md @@ -0,0 +1,24 @@ +Easily assess where a user group is used across your ServiceNow instance — before you retire, modify, or repurpose it. + +This solution adds a UI Action to the sys_user_group form that opens a clean, dynamic UI Page showing all the dependencies across modules like Tasks, Script Includes, Business Rules, Workflows, Catalog Items, Reports, and more. + +Key Features: + +• One-click access to group dependency insights + +• Displays usage across 10+ key modules + +• Modular architecture using UI Page, Script Include, and client-side UI Action + +• Easily extensible to include custom tables or rules + + +Use Cases: + +• Pre-deactivation impact checks for groups + +• Governance and cleanup tasks + +• Platform documentation and audit support + +• Extensible framework for users, catalog items, or roles diff --git a/Client-Side Components/UI Actions/Group dependency/uiaction.js b/Client-Side Components/UI Actions/Group dependency/uiaction.js new file mode 100644 index 0000000000..9ab0bec05f --- /dev/null +++ b/Client-Side Components/UI Actions/Group dependency/uiaction.js @@ -0,0 +1,14 @@ +/* +This script should be placed in the UI action on the table sys_user_group form view. +This UI action should be marked as client. +Use popupDependency() function in the Onclick field. +condition - gs.hasRole('admin') +*/ + +function popupDependency() { + var groupSysId = gel('sys_uniqueValue').value; + var gdw = new GlideDialogWindow('display_group_dependency_list'); + gdw.setTitle('Group Dependency'); + gdw.setPreference('sysparm_group', groupSysId); + gdw.render(); +} diff --git a/Client-Side Components/UI Actions/Group dependency/uipage.js b/Client-Side Components/UI Actions/Group dependency/uipage.js new file mode 100644 index 0000000000..ca76ebdb6a --- /dev/null +++ b/Client-Side Components/UI Actions/Group dependency/uipage.js @@ -0,0 +1,215 @@ + + + + + + var groupSysId = trim(jelly.sysparm_group); + var groupName = ''; + var taskRecords = 0; + var reportRecords = 0; + var workflowRecords = 0; + var workflowVersions = ''; + var scriptIncludeRecords = 0; + var businessRulesRecords = 0; + var clientScriptRecords = 0; + var maintainItemRecords = 0; + var businessServiceRecords=0; + var systemPropertiesRecords=0; + var avaiforGroupsRecords = 0; + var grACLRecords = 0; + + var grGroupName = new GlideRecord('sys_user_group'); + if (grGroupName.get('sys_id', groupSysId)) { + groupName = grGroupName.name; + } + + // TASK + var grTask = new GlideRecord("task"); + grTask.addEncodedQuery('sys_created_onRELATIVEGE@month@ago@9^assignment_group.sys_id='+groupSysId+'^active=true^GROUPBYsys_class_name^ORDERBYsys_created_on'); + grTask.query(); + taskRecords = grTask.getRowCount(); + + // REPORTS + var grReport = new GlideRecord("sys_report"); + grReport.addEncodedQuery('filterLIKE'+groupSysId+'^ORfilterLIKEE'+groupName); + grReport.query(); + reportRecords = grReport.getRowCount(); + + + // WORKFLOWS + var grWFActivity = new GlideRecord('wf_activity'); + grWFActivity.addQuery('sys_id', 'IN', getWfActivities(groupSysId, groupName)); + grWFActivity.addQuery('workflow_version.published', 'true'); + grWFActivity.query(); + while (grWFActivity.next()) { + + + var grh = new GlideRecord("wf_workflow_version"); + grh.addEncodedQuery("sys_id=" + grWFActivity.workflow_version.sys_id); + grh.query(); + while (grh.next()) { + + workflowVersions += grWFActivity.workflow_version.sys_id + ','; +} +} + workflowRecords = getCount(workflowVersions); + if(workflowRecords == 1){ + var sci = sciWorkflow(grh.workflow.sys_id.toString()); + if(sci){ + workflowRecords =0; + workflowVersions =''; + } + } + + function sciWorkflow(workflow){ +var gr12 = new GlideRecord("sc_cat_item"); +gr12.addEncodedQuery("active=false^workflow="+workflow); +gr12.query(); +if (gr12.next()) { + return true; +} + } + + function getCount(sysid){ + var gr = new GlideRecord('wf_workflow_version'); + gr.addQuery('sys_id','IN',sysid); + gr.query(); + return gr.getRowCount(); + } + + function getWfActivities(group_id, group_name) { + var grVariables = new GlideRecord('sys_variable_value'); + grVariables.addEncodedQuery('valueLIKE'+group_name+'^ORvalueLIKE'+group_id+'^document=wf_activity'); + grVariables.query(); + var results = []; + while (grVariables.next()) { + results.push(grVariables.document_key + ''); + } + return results; + } + + // SCRIPT INCLUDES + var grScriptInclude = new GlideRecord("sys_script_include"); + grScriptInclude.addEncodedQuery('scriptLIKE'+groupSysId+'^ORscriptLIKE'+groupName+'^active=true'); + grScriptInclude.query(); + scriptIncludeRecords = grScriptInclude.getRowCount(); + + // BUSINESS RULES + var grBusinessRules = new GlideRecord("sys_script"); + grBusinessRules.addEncodedQuery('active=true^scriptLIKE'+groupName+'^ORscriptLIKE'+groupSysId); + grBusinessRules.query(); + businessRulesRecords = grBusinessRules.getRowCount(); + + // CLIENT SCRIPT + var grClientScript = new GlideRecord("sys_script_client"); + grClientScript.addEncodedQuery('sys_class_name=sys_script_client^active=true^scriptLIKE'+groupName+'^ORscriptLIKE'+groupSysId); + grClientScript.query(); + clientScriptRecords = grClientScript.getRowCount(); + + + // MAINTAIN ITEMS (CATALOG ITEMS) + var grMaintainItems = new GlideRecord("sc_cat_item"); +grMaintainItems.addEncodedQuery('u_approval_group_1='+groupSysId+'^ORu_approval_group_2='+groupSysId+'^ORgroup='+groupSysId+'^ORu_fulfillment_group_2='+groupSysId+'^active=true'); + grMaintainItems.query(); + maintainItemRecords = grMaintainItems.getRowCount(); + + //CMDB CI's + + var grBusinessServices = new GlideRecord('cmdb_ci'); +grBusinessServices.addEncodedQuery('install_status!=7^change_control='+groupSysId+'^ORsupport_group='+groupSysId) + grBusinessServices.query(); + businessServiceRecords= grBusinessServices.getRowCount(); + + //System Properties + var grsysProperties = new GlideRecord('sys_properties'); + grsysProperties.addEncodedQuery('valueLIKE'+groupSysId); + grsysProperties.query(); + systemPropertiesRecords = grsysProperties.getRowCount(); + + //Available for Groups + var grAvaiForGroups = new GlideRecord("sc_cat_item_group_mtom"); + grAvaiForGroups.addEncodedQuery('sc_cat_item.active=true^sc_avail_group='+ groupSysId); + + grAvaiForGroups.query(); + avaiforGroupsRecords = grAvaiForGroups.getRowCount(); + + //Available for Notifications + var grAvaiForNotifications = new GlideRecord("sysevent_email_action"); + grAvaiForNotifications.addEncodedQuery('active=true^conditionLIKE'+ groupSysId +'^ORrecipient_groupsLIKE'+ groupSysId + '^ORadvanced_conditionLIKE'+ groupSysId); + + grAvaiForNotifications.query(); + NotificationRecords = grAvaiForNotifications.getRowCount(); + + //ACL for Groups + + var grACL = new GlideRecord("sys_security_acl"); + grACL.addEncodedQuery('scriptLIKE' + groupName + '^ORscriptLIKE' + groupSysId + '^ORconditionLIKE' + groupName + '^ORconditionLIKE' + groupSysId + '^active=true'); +grACL.query(); +grACLRecords = grACL.getRowCount(); + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
ModuleRecordsDetails
Workflows${workflowRecords}View records
+ + + + + + + + + + + + + + + + +
timer sys_idnext actiontime leftkill?
+ ${jvar_timer.sys_id} + + ${jvar_timer.waitUntil} + + ${jvar_timer.timeLeft} + + + +
+ +
+ + + +//Client script: +// handler for clicking ok on modal. gathers the sys_ids for the timers that have checked checkbox +function okDialog() { + var c = gel('sysids'); + var sysids = []; + $j('input[type="checkbox"]:checked').each(function () { + var checkboxId = $j(this).attr('id').replace("ni.", ""); + sysids.push(checkboxId); + }); + c.value = sysids.toString(); + return true; +} + +//Processing script: +// queries for timer jobs and sets the job and new flow.fire event to process instantly -> timer on flow completes +var waitJob = new GlideRecord("sys_trigger"); +waitJob.addQuery("sys_id", "IN", sysids); +waitJob.query(); +while (waitJob.next()) { + var currentScript = waitJob.getValue("script"); + var now = new GlideDateTime().getValue(); + var replaceScript = currentScript.replace(/gr\.setValue\('process_on',\s*'[^']*'\)/, "gr.setValue('process_on','" + now + "')"); + waitJob.setValue("script", replaceScript); + waitJob.setValue("next_action", now); + waitJob.update(); +} +//redirect back to bottom of nav stack +var urlOnStack = GlideSession.get().getStack().bottom(); +response.sendRedirect(urlOnStack); diff --git a/Client-Side Components/UI Actions/Kill flow timers/timer.png b/Client-Side Components/UI Actions/Kill flow timers/timer.png new file mode 100644 index 0000000000..b9e099b559 Binary files /dev/null and b/Client-Side Components/UI Actions/Kill flow timers/timer.png differ diff --git a/Client-Side Components/UI Actions/Knowledge Link Validator/Readme.md b/Client-Side Components/UI Actions/Knowledge Link Validator/Readme.md new file mode 100644 index 0000000000..c71181de73 --- /dev/null +++ b/Client-Side Components/UI Actions/Knowledge Link Validator/Readme.md @@ -0,0 +1,8 @@ +This utility script helps ServiceNow administrators and content managers ensure the integrity and usability of hyperlinks embedded within knowledge articles. It scans article content to identify and classify links pointing to catalog items and other knowledge articles, providing detailed insights into: + +Catalog Item Links: Detects and categorizes links as active, inactive, or not found. +Knowledge Article Links: Flags outdated articles based on workflow state and expiration (valid_to). +Non-Permalink KB Links: Identifies knowledge article links that do not follow the recommended permalink format (i.e., missing sysparm_article=KBxxxxxxx), even if they use kb_view.do. +The solution includes a Jelly-based UI that displays categorized results with direct links to the affected records, enabling quick remediation. It's ideal for improving content quality, ensuring consistent user experience, and maintaining best practices in knowledge management. + +image diff --git a/Client-Side Components/UI Actions/Knowledge Link Validator/uiaction.js b/Client-Side Components/UI Actions/Knowledge Link Validator/uiaction.js new file mode 100644 index 0000000000..cd6aa46b55 --- /dev/null +++ b/Client-Side Components/UI Actions/Knowledge Link Validator/uiaction.js @@ -0,0 +1,13 @@ +/* +This script should be placed in the UI action on the table kb_knowledge form view. +This UI action should be marked as client. +Use validateLinksInArticle() function in the Onclick field. +*/ + +function validateLinksInArticle() { + var articleSysId = g_form.getUniqueValue(); + var gdw = new GlideDialogWindow('validate_links_dialog'); + gdw.setTitle('Validate Article Links'); + gdw.setPreference('sysparm_article_id', articleSysId); + gdw.render(); +} diff --git a/Client-Side Components/UI Actions/Knowledge Link Validator/uipage.js b/Client-Side Components/UI Actions/Knowledge Link Validator/uipage.js new file mode 100644 index 0000000000..2a8cd9e8c2 --- /dev/null +++ b/Client-Side Components/UI Actions/Knowledge Link Validator/uipage.js @@ -0,0 +1,193 @@ + + + + tags + var regex = /]+href=["']([^"']+)["']/gi; + var urls = []; + var match; + while ((match = regex.exec(content)) !== null) { + + urls.push(match[1]); + } + for (var i = 0; i < urls.length; i++) { + var url = urls[i]; + + // --- 1. Check if link is a Catalog Item --- + var sysId = extractSysId(url, 'sysparm_id') || extractSysId(url, 'sys_id'); + if (sysId) { + var grItem = new GlideRecord('sc_cat_item'); + if (grItem.get(sysId)) { + if (grItem.active){ + activeIds.push(sysId); + activeCount++; + } + else if(grItem.active == false){ + inactiveIds.push(sysId); + inActiveCount++; + } + } else { + notFoundIds.push(sysId); + notFoundCount++; + } + } + // --- 2. Check if link is a Knowledge Article --- + // --- 1. Check for outdated knowledge articles via permalink --- + +// --- 1. Check for outdated knowledge articles via permalink --- +var decodedUrl = decodeURIComponent(url + ''); +decodedUrl = decodedUrl.replace(/&amp;amp;amp;/g, '&'); + +// Extract KB number or sys_id +var kbNumber = extractSysId(decodedUrl, 'sysparm_article'); +var kbSysId = extractSysId(decodedUrl, 'sys_kb_id') || extractSysId(decodedUrl, 'sys_id'); + +var grKb = new GlideRecord('kb_knowledge'); + +if (kbNumber && grKb.get('number', kbNumber)) { + var isOutdated = false; + if (grKb.workflow_state != 'published') { + isOutdated = true; + } else if (grKb.valid_to && grKb.valid_to.getGlideObject()) { + var now = new GlideDateTime(); + if (grKb.valid_to.getGlideObject().compareTo(now) <= 0) { + isOutdated = true; + } + } + + if (isOutdated) { + outdatedArticles.push(grKb.sys_id.toString()); + outdatedCount++; + } +} else if (kbSysId && grKb.get(kbSysId)) { + var isOutdated = false; + if (grKb.workflow_state != 'published') { + isOutdated = true; + } else if (grKb.valid_to && grKb.valid_to.getGlideObject()) { + var now = new GlideDateTime(); + if (grKb.valid_to.getGlideObject().compareTo(now) <= 0) { + isOutdated = true; + } + } + + if (isOutdated) { + outdatedArticles.push(grKb.sys_id.toString()); + outdatedCount++; + } +} + +// --- 2. Check for non-permalink knowledge links --- +if ( + decodedUrl.indexOf('kb_knowledge.do?sys_id=') !== -1 || // form view + ( + decodedUrl.indexOf('/kb_view.do') !== -1 && + decodedUrl.indexOf('sysparm_article=KB') === -1 // missing KB number + ) +) { + var kbSysId = extractSysId(decodedUrl, 'sys_kb_id') || extractSysId(decodedUrl, 'sys_id'); + if (kbSysId) { + var grBadKB = new GlideRecord('kb_knowledge'); + if (grBadKB.get(kbSysId)) { + badPermalinks.push(kbSysId); + badPermalinkCount++; + } + } +} + } + } + } + function extractSysId(url, param) { + try { + var decoded = decodeURIComponent(url + ''); + decoded = decoded + .replace(/&amp;amp;/g, '&') + .replace(/&amp;/g, '&') + .replace(/&/g, '&') + .replace(/=/g, '=') + .replace(/&#61;/g, '='); + + var parts = decoded.split(param + '='); + if (parts.length > 1) { + var id = parts[1].split('&')[0]; + return id && id.length === 32 ? id : null; + } + } catch (e) { + var parts = url.split(param + '='); + if (parts.length > 1) { + var id = parts[1].split('&')[0]; + return id && id.length === 32 ? id : null; + } + } + return null; +} + // Expose variables to Jelly +inactiveQuery = "sys_idIN"+inactiveIds.join(','); +activeQuery = "sys_idIN"+activeIds.join(','); +notFoundQuery = "sys_idIN"+notFoundIds.join(','); +outdatedQuery = "sys_idIN"+outdatedArticles.join(','); +badPermalinkQuery = "sys_idIN"+badPermalinks.join(','); + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ModuleRecordsDetails
+
diff --git a/Client-Side Components/UI Actions/Populate Due Date based on Priority/Readme.md b/Client-Side Components/UI Actions/Populate Due Date based on Priority/Readme.md new file mode 100644 index 0000000000..46a18f1fb3 --- /dev/null +++ b/Client-Side Components/UI Actions/Populate Due Date based on Priority/Readme.md @@ -0,0 +1,47 @@ +**Calculate the due date based on the Priority** + +Script Type: UI Action, Table: incident, Form button: True, Show update: True, Condition: (current.due_date == '' && current.priority != '5'), OnClick: functionName() + +Script Type: Script Include, Glide AJAX enabled: False + +Schedule- Name: Holidays, Time Zone: GMT + +Schedule Entry - Name: New Year's Day, Type: Exclude, Show as: Busy, When: 31-12-2024, To: 01-01-2025 +Schedule Entry - Name: Christmas Day, Type: Exclude, Show as: Busy, When: 24-12-2025, To: 25-12-2025 +Schedule Entry - Name: Thanksgiving Day, Type: Exclude, Show as: Busy, When: 26-11-2025, To: 27-11-2025 +Schedule Entry - Name: Diwali, Type: Exclude, Show as: Busy, When: 19-10-2025, To: 20-10-2025 + +**Goal:** To Calculate Due-Date based on Priority with some conditions. + +**Walk through of code:** So in this use case the UI Action is been used and then Script Include for server calculate is used.So the main to calculate the due-date by the user trigger. + +UI Action- So this button will check the priority and check the due date field is empty or not if not then will fetch the value of "Priority" and "Created date" and pass the data to the Script Include for calculation once it gets the response will populate the value to the due_date field in the incident table and then update it. + +Script Include- The role of this is to get the "Priority" and "Created date" based on prioriy this will calculate the time and date by using th GlidDateTime API and the will do some additional changes based on each priorit which is mentioned below and then return the response back to the UI Action, + +Schedule & Schedule Entry- It is used for the P3 and P4 Priority which is mentioned below for the use case.To exclude the Holidays. + +These are the use case which the above functionality works, + +**1-> P1** - add 4hrs to the Created date +**2-> P2 **- add 4hrs to the Created date but if it's exceed the working hrs of of 5 PM the add to the next day or if the is before the working hours of 8 AM set 5 PM to the same Created date. +**3-> P3 or P4** - Kind of low priority so add the due date to the next day but it should exclude the holidays and the weekend's and the populate the next business working day. +**4-> P5 **- User manually will populate the due date based on the process. + +The UI Action on the Incident Form +Button + +UI Action which will call the Script Include +UI Action + +Script Include +SI + +Schedules and Schedule Entry +Schedules +Schedule Entry + +Output +Priority 1 +Priority 2 +Priority 4 diff --git a/Client-Side Components/UI Actions/Populate Due Date based on Priority/ScriptInclude.js b/Client-Side Components/UI Actions/Populate Due Date based on Priority/ScriptInclude.js new file mode 100644 index 0000000000..4390ed5633 --- /dev/null +++ b/Client-Side Components/UI Actions/Populate Due Date based on Priority/ScriptInclude.js @@ -0,0 +1,94 @@ +/* + +Input +1. Created Date +2. Priority + +Output +1. Due Date + +Based on Priority equivalent due dates + +P1 - add 4hrs to the Created date +P2 - add 4hrs to the Created date but if it's exceed the working hrs of of 5 PM the add to the next day or if the is before the working hours of 8 AM set 5 PM to the same Created date. +P3 or P4 - Kind of low priority so add the due date to the next day but it should exclude the holidays and the weekend's and the populate the next business working day. + +*/ + + +// This SI findDueDate() function will help to calculate the duration based on the each priority. + +var CalculateDueDates = Class.create(); +CalculateDueDates.prototype = { + initialize: function() {}, + + findDueDate: function(priority, created) { + var dueDateVal; + + + // For the Priority 1 and adding 4 hours in reagrd less of 8-5 working hours and then holidays + if (priority == 1) { + var now = new GlideDateTime(created); + now.addSeconds(60 * 60 * 4); // Add 4 hours + dueDateVal = now; + return dueDateVal; + + } + + // For the Priority 2 and adding the 4 hours if exceed the workin hours then add the next day before 5'o Clock + else if (priority == 2) { + var dueDate = new GlideDateTime(created); + dueDate.addSeconds(60 * 60 * 4); // Add 4 hours + dueDate = dueDate+''; + var hours = Number((dueDate + '').slice(11, 13)); + + if (hours >= 0 && hours < 12) { + gs.addInfoMessage('if Inside 8-5/7'); + dueDateVal = dueDate.slice(0, 10) + " 17:00:00"; + return dueDateVal; + + } else if (hours >= 17 && hours <= 23) { + var nextDate = new GlideDateTime(created); + nextDate.addDaysUTC(1); + var newDue = new GlideDateTime(nextDate.getDate().getValue() + " 17:00:00"); + dueDateVal = newDue; + return dueDateVal; + } else { + dueDateVal = dueDate; + return dueDateVal; + } + + } + + // For the Priority 3 or 4 add the next day and then if the due date is holiday or weekend populate the next working day in a respective field + else if (priority == 3 || priority == 4) { + var schedule = new GlideSchedule(); + // cmn_schedule for the Holidays + var scheduleId = 'bd6d74b2c3fc72104f7371edd40131b7'; + schedule.load(scheduleId); + + var nextDay = new GlideDateTime(created); + nextDay.addDaysUTC(1); + + + //Checking for weekends + var dayOfWeek = nextDay.getDayOfWeekUTC(); + + var isWeekend = (dayOfWeek == 6 || dayOfWeek == 7); + + + // Loop until next working day (weekdays excluding holidays) + while (schedule.isInSchedule(nextDay) || isWeekend) { + nextDay.addDaysUTC(1); + dayOfWeek = nextDay.getDayOfWeekUTC(); + isWeekend = (dayOfWeek == 6 || dayOfWeek == 7); + } + + // Set to 12:00 PM on that valid day + var validDate = new GlideDateTime(nextDay.getDate().getValue() + " 17:00:00"); + return validDate; + } + }, + + type: 'CalculateDueDates' +}; \ No newline at end of file diff --git a/Client-Side Components/UI Actions/Populate Due Date based on Priority/UI Action.js b/Client-Side Components/UI Actions/Populate Due Date based on Priority/UI Action.js new file mode 100644 index 0000000000..2597aea189 --- /dev/null +++ b/Client-Side Components/UI Actions/Populate Due Date based on Priority/UI Action.js @@ -0,0 +1,34 @@ +/* +Table - incident +Show Update - True +Form Button - True +Condition - (current.due_date == '' && current.priority != '5') + +Input +1. Created Date +2. Priority + +Validation +Will not appeare if the value is already there and the priority is 5 + +Output +1. Due Date + +*/ + + +// The function duedate is used to pass the priority and then created display value to the script include where the calculate of Due date is done will get the response and the set the value to the due_date field of incident. +function duedate() { + + var priority = current.getValue('priority'); + var created = current.getDisplayValue('sys_created_on'); + var si = new CalculateDueDates(); + var response = si.findDueDate(priority, created); + var gdt = new GlideDateTime(); + gdt.setDisplayValue(response); + current.setValue('due_date', gdt); + current.update(); + action.setRedirectURL(current); + +} +duedate(); \ No newline at end of file diff --git a/Client-Side Components/UI Actions/Printer Friendly Version/README.md b/Client-Side Components/UI Actions/Printer Friendly Version/README.md new file mode 100644 index 0000000000..b59ad213f4 --- /dev/null +++ b/Client-Side Components/UI Actions/Printer Friendly Version/README.md @@ -0,0 +1,5 @@ +## Overview + + +This code snippet UI Action will allow you to have a printer friendly version of whatever record you might are trying to print. +This UI action uses the GlideNavigation API which you can find here [GlideNavigation API](https://developer.servicenow.com/dev.do#!/reference/api/zurich/client/c_GlideNavigationV3API#r_GNV3-openPopup_S_S_S_B) diff --git a/Client-Side Components/UI Actions/Printer Friendly Version/printer_friendly_verison.js b/Client-Side Components/UI Actions/Printer Friendly Version/printer_friendly_verison.js new file mode 100644 index 0000000000..f1049cb24f --- /dev/null +++ b/Client-Side Components/UI Actions/Printer Friendly Version/printer_friendly_verison.js @@ -0,0 +1,15 @@ +printView() + +function printView() { + + var table = g_form.getTableName(); + var recordID = g_form.getUniqueValue(); + var view = {{Insert the view you want to print here}}; //You can pass in an empty string and it will still work + var windowName = {{Insert the name you want your window to display}}; //You can pass in an empty string and it will still work + var features = 'resizeable,scrollbar'; //You can pass in an empty string and it will still work + var urlString = '/' + table + ".do?sys_id=" + recordID + "&sysparm_view=" + view + "&sysparm_media=print"; + var noStack = true; //Flag that indicates whether to append sysparm_stack=no to the URL + + g_navigation.openPopup(urlString, windowName, features, noStack); + +} diff --git a/Client-Side Components/UI Actions/Publish a Retired Knowledge Article again/README.md b/Client-Side Components/UI Actions/Publish a Retired Knowledge Article again/README.md new file mode 100644 index 0000000000..c53505944e --- /dev/null +++ b/Client-Side Components/UI Actions/Publish a Retired Knowledge Article again/README.md @@ -0,0 +1,9 @@ +**UI Action**: +Publish a Retired Knowledge Article again after it is already retired. + +**How it works:** +1. The code finds existing articles in the Knowledge base that share the same Article ID but are not the current article and are 'retired'. +2. It updates the workflow state of these found articles to 'outdated'. +3. For the current article, it sets the workflow_state to 'pubblished', records the current date and updates the 'published' field with the date. +4. It also removes the pre-existing 'retired' date from the 'retired' field. +5. It redirects the user to the updated current record. diff --git a/Client-Side Components/UI Actions/Publish a Retired Knowledge Article again/publishretiredkb.js b/Client-Side Components/UI Actions/Publish a Retired Knowledge Article again/publishretiredkb.js new file mode 100644 index 0000000000..81d31b78b7 --- /dev/null +++ b/Client-Side Components/UI Actions/Publish a Retired Knowledge Article again/publishretiredkb.js @@ -0,0 +1,15 @@ +var kbArticle = new GlideRecord('kb_knowledge'); +kbArticle.setWorkflow(false); +kbArticle.addQuery('article_id', current.article_id); +kbArticle.addQuery('sys_id', "!=", current.sys_id); //articles that are not the current one +kbArticle.addQuery('workflow_state', 'retired'); +kbArticle.query(); +while (kbArticle.next()) { + kbArticle.workflow_state = 'outdated'; //setting the articles as outdated + kbArticle.update(); +} +current.workflow_state = 'published'; //publishing retired kb article again +current.published = new GlideDate(); +current.retired = ""; //clearing retired field value +current.update(); +action.setRedirectURL(current); diff --git a/Client-Side Components/UI Actions/Smart Assign to available member/README.md b/Client-Side Components/UI Actions/Smart Assign to available member/README.md new file mode 100644 index 0000000000..664161b011 --- /dev/null +++ b/Client-Side Components/UI Actions/Smart Assign to available member/README.md @@ -0,0 +1,18 @@ +**Use-case:** +The primary goal of this UI Action is load-balancing. +It assigns tasks based on the fewest currently Active tasks assigned to a member in a group. + +**Example Scenario**: +An assignment group has 10 members. Instead of assigning a new task to the whole group or any random member, the user/agent clicks on +"Smart Assign" to find the member with the fewest currently Active tasks in the same group and assign the task to them. + +**UI Action Name:** +Smart Assign + +**Condition**: !current.assignment_group.nil() && current.assigned_to.nil() + +**How it works:** +1. The code queries the Group members table to find every single user associated with the currently selected assignment group. + If someone removes a previous assignement group and then clicks on Smart Assign button, they are shown an error message to choose an Assignment group. +2. There is a loop on the task table. This loop uses GlideAggregate to count how many active records are assigned to a specific user. +3. It tracks the user that has the lowest count of tasks assigned to them and assigns the current task to them. diff --git a/Client-Side Components/UI Actions/Smart Assign to available member/smartAssigntoAvailablemember.js b/Client-Side Components/UI Actions/Smart Assign to available member/smartAssigntoAvailablemember.js new file mode 100644 index 0000000000..0317deb5c9 --- /dev/null +++ b/Client-Side Components/UI Actions/Smart Assign to available member/smartAssigntoAvailablemember.js @@ -0,0 +1,47 @@ +var assignedToId = ''; +var minOpenTasks = 77777; +var targetGroup = current.assignment_group; + +if (!targetGroup) { + gs.addErrorMessage('Please select an Assignment Group first.'); + action.setRedirectURL(current); +} + +//Finding all active members in the target group +var member = new GlideRecord('sys_user_grmember'); +member.addQuery('group', targetGroup); +member.query(); + +while (member.next()) { + var userId = member.user.toString(); + + //CountIng the number of active tasks currently assigned to the member + var taskCountGR = new GlideAggregate('task'); + taskCountGR.addQuery('assigned_to', userId); + taskCountGR.addQuery('active', true); + taskCountGR.addAggregate('COUNT'); + taskCountGR.query(); + + var openTasks = 0; + if (taskCountGR.next()) { + openTasks = taskCountGR.getAggregate('COUNT'); + } + + //Checking if this member has fewer tasks than the current minimum + if (openTasks < minOpenTasks) { + minOpenTasks = openTasks; + assignedToId = userId; + } +} + +//Assigning the current record to the chosen user +if (assignedToId) { + current.assigned_to = assignedToId; + current.work_notes = 'Assigned via Smart Assign to the user with the fewest active tasks (' + minOpenTasks + ' open tasks).'; + current.update(); + gs.addInfoMessage('Incident assigned to ' + current.assigned_to.getDisplayValue() + '.'); +} else { + gs.addErrorMessage('Could not find an active member in the group to assign the task.'); +} + +action.setRedirectURL(current); diff --git a/Client-Side Components/UI Actions/Variable Ownership/Readme.md b/Client-Side Components/UI Actions/Variable Ownership/Readme.md new file mode 100644 index 0000000000..4621d0668b --- /dev/null +++ b/Client-Side Components/UI Actions/Variable Ownership/Readme.md @@ -0,0 +1,13 @@ +The Variable Ownerships UI Action is a lightweight admin utility designed to help manage request item (RITM) variables more effectively. + +With just one click, it provides direct access to the variable values table, allowing administrators to quickly review, update, or remove sensitive data entered by mistake. This eliminates the need to navigate multiple related tables manually, saving time and reducing risk. + +Key Benefits: + +• Direct access to RITM variable values + +• Faster cleanup of sensitive information + +• Improves data hygiene and admin efficiency + +• Simple to implement and lightweight diff --git a/Client-Side Components/UI Actions/Variable Ownership/script.js b/Client-Side Components/UI Actions/Variable Ownership/script.js new file mode 100644 index 0000000000..4778df6d5d --- /dev/null +++ b/Client-Side Components/UI Actions/Variable Ownership/script.js @@ -0,0 +1,12 @@ +/* +This script should be placed in the UI action on the table sc_req_item form view. +This UI action should be marked as client. +Use viewMtom() function in the Onclick field. +*/ + +function viewMtom() { + + var url = 'sc_item_option_mtom_list.do?sysparm_query=request_item=' + g_form.getUniqueValue(); + g_navigation.openPopup(url); + +} diff --git a/Client-Side Components/UI Macros/FormBackground/form_background.xml b/Client-Side Components/UI Macros/FormBackground/form_background.xml new file mode 100644 index 0000000000..c8cbd798c5 --- /dev/null +++ b/Client-Side Components/UI Macros/FormBackground/form_background.xml @@ -0,0 +1,25 @@ + + + + diff --git a/Client-Side Components/UI Macros/FormBackground/readme.md b/Client-Side Components/UI Macros/FormBackground/readme.md new file mode 100644 index 0000000000..61ce061aa5 --- /dev/null +++ b/Client-Side Components/UI Macros/FormBackground/readme.md @@ -0,0 +1,55 @@ +# ServiceNow Form Background Macro + +> A lightweight UI Macro to style ServiceNow forms with a custom background and simple element theming (labels, buttons, sections). +--- + +## Features + +* Adds a full-cover background image to a form (supports cover, center positioning). +* Makes table/form/section backgrounds transparent so the background shows through. +* Easy to customize (image path, label styles, button styles, additional CSS selectors). + +## Requirements + +* ServiceNow instance with admin access. +* An image to set as background + +> ⚠️ Note: This macro uses Jelly/CSS that may not work as expected in some Next Experience workspaces or future UI updates. Test in a non-production instance first. + +## Installation + +1. **Upload the background image** + + * Navigate to **System UI > Images** and upload your background image (e.g., `formbg.png`). + +2. **Create the UI Macro** + + * Go to **System UI > UI Macros** and create a new macro (e.g., `ui_form_background`). + * Copy the example macro content below into the UI Macro. + +3. **Create a UI Formatter** + + * Go to **System UI > Formatters**. Create a new formatter for the target table (for example, `incident` table). + * In the *Formatter* field, reference the macro name you created (e.g., `ui_form_background.xml`). + +4. **Add the Formatter to the Form Layout** + + * Open the form layout for the target table (Form Layout / Form Designer) and place the formatter region on the form. + * Save and open a record to see the background applied. + +## Compatibility + +* Tested on ServiceNow classic forms (UI16). May require tweaks for Next Experience, Service Portal, or Workspace. +* If your instance uses strict Content Security Policy (CSP) or image hosting constraints, host the image in a supported location or adapt the implementation. + +## Troubleshooting + +* If no background appears: + + * Confirm the image is uploaded and the filename matches. + * Ensure the formatter is placed on the form layout and published. + * Inspect (browser devtools) to confirm CSS selectors are applied. + +## Result +image + diff --git a/Client-Side Components/UI Macros/JSON Formatter and Viewer/README.md b/Client-Side Components/UI Macros/JSON Formatter and Viewer/README.md new file mode 100644 index 0000000000..a331ad0cd1 --- /dev/null +++ b/Client-Side Components/UI Macros/JSON Formatter and Viewer/README.md @@ -0,0 +1,9 @@ +This solution provides a significant User Experience (UX) enhancement for fields that store complex data in JSON format (e.g., integration payloads, configuration properties, or setup data). + +Instead of forcing users (developers, administrators) to read or edit raw, unformatted JSON in a plain text area, this macro adds a "JSON Viewer" button next to the field + +Name json_formatter_macro +Active true +Type XML + +Navigate to **System UI > UI** Macros and create a new record named json_formatter_macro , Use XML Attached File as Script diff --git a/Client-Side Components/UI Macros/JSON Formatter and Viewer/json_formatter.xml b/Client-Side Components/UI Macros/JSON Formatter and Viewer/json_formatter.xml new file mode 100644 index 0000000000..124a507e27 --- /dev/null +++ b/Client-Side Components/UI Macros/JSON Formatter and Viewer/json_formatter.xml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/Client-Side Components/UI Macros/Show Open Incident of Caller/Readme.md b/Client-Side Components/UI Macros/Show Open Incident of Caller/Readme.md new file mode 100644 index 0000000000..9f5883d95a --- /dev/null +++ b/Client-Side Components/UI Macros/Show Open Incident of Caller/Readme.md @@ -0,0 +1,21 @@ +Show Open Incident of caller + +Script Type: UI Macro + +Goal: In Form view caller can see what are the open incident of that particular caller. + +Walk through of code: So for this use case a new macro will be added to the caller field, when it is triggered it will open a new popup window where it will show the list of particular caller which are all open incident.So for this a new UImacro have been used in that a new list icon have been rendered from the db_image table and inside that a showopentckts() function this will get the current caller and then add the query to filter out the list of open incident and then open a popup to show the list of that particular caller which are all open(other than Closed and Cancelled). + +Note: To inherite the UI Macro in that particular field (Caller) we need to add the attribute in the Dictionary Entry = ref_contributions=caller_inc_lists [ref_contributions="name of the macro"] + +UI Macro +UIMacro + +Dictonary Entry in Attribute section +UIMacroDictionary + +UI Macro in Incident Form near the Caller Field +From UIMacro + +Result: +UIMacro Result diff --git a/Client-Side Components/UI Macros/Show Open Incident of Caller/macro.js b/Client-Side Components/UI Macros/Show Open Incident of Caller/macro.js new file mode 100644 index 0000000000..440bb8cfe0 --- /dev/null +++ b/Client-Side Components/UI Macros/Show Open Incident of Caller/macro.js @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/Client-Side Components/UI Pages/Add Multiple Items to Order Guide/README.md b/Client-Side Components/UI Pages/Add Multiple Items to Order Guide/README.md new file mode 100644 index 0000000000..abae7b1e1c --- /dev/null +++ b/Client-Side Components/UI Pages/Add Multiple Items to Order Guide/README.md @@ -0,0 +1,16 @@ +**Details** +1. This code will add multiple items in an order guide in single click +2. Order guide rule base creation will be automatic +3. This code will also add variable set to selected catalog items automatically. + +**How to use** +1. Go to "sc_cat_item" table and select the items to be added in list view. +2. Look for "Add to order guide" in list actions. +3. The list action will give you an option to select order guide and variable set to be added to catalog items + +**Components** +1. UI Action +2. UI Page +3. Script Include + +image diff --git a/Client-Side Components/UI Pages/Add Multiple Items to Order Guide/Script Include.js b/Client-Side Components/UI Pages/Add Multiple Items to Order Guide/Script Include.js new file mode 100644 index 0000000000..e4b73f56cf --- /dev/null +++ b/Client-Side Components/UI Pages/Add Multiple Items to Order Guide/Script Include.js @@ -0,0 +1,58 @@ + +var AddtoOG = Class.create(); +AddtoOG.prototype = Object.extendsObject(AbstractAjaxProcessor, { + addToOrderGuide: function() { + var msgArrNotAdded = []; // array to store not added catalog items. + var msgArrAdded = []; // array to store added catalog items. + var msg = ''; + var item = this.getParameter('sysparm_itemList').toString().split(','); + var order_guide = this.getParameter('sysparm_og'); + var var_set = this.getParameter('sysparm_set'); + for (var i = 0; i < item.length; i++) { + var itemName = new GlideRecord('sc_cat_item'); + itemName.get(item[i]); // get item name + var itemBckName = itemName.name.toString().replace(/[^a-zA-Z0-9]/g, "_"); + // check if item is present in order guide + var checkStatus = new GlideRecord('sc_cat_item_guide_items'); + checkStatus.addQuery('guide', order_guide); + checkStatus.addQuery('item', item[i]); + checkStatus.query(); + if (checkStatus.next()) { + msgArrNotAdded.push(itemName.name); + } else { + // Add variable set to all catalog items selected + var set = new GlideRecord('io_set_item'); + var orderVar = new GlideRecord('item_option_new'); + set.initialize(); + set.variable_set = var_set; + set.sc_cat_item = item[i]; + set.order = '200'; // set order as per your requirement + set.insert(); + + // Add checkbox variable in order guide for each catalog item + orderVar.initialize(); + orderVar.setValue('type', 7); + orderVar.setValue('cat_item', order_guide); + orderVar.setValue('question_text', itemName.name); + orderVar.setValue('name', itemBckName); + orderVar.setValue('order', 1200); // set order as per your requirement + orderVar.insert(); + } + + // Add rule base to order guide + var ruleBase = new GlideRecord('sc_cat_item_guide_items'); + ruleBase.initialize(); + ruleBase.setValue('item', item[i]); + ruleBase.setValue('guide', order_guide); + ruleBase.setValue('condition', 'IO:' + orderVar.sys_id + '=true^EQ'); + ruleBase.insert(); + msgArrAdded.push(itemName.name); + } + if (msgArrNotAdded.length > 0) { + msg = "Not added item are " + msgArrNotAdded + ' Added Items are ' + msgArrAdded; // array of items which are not added + } else + msg = 'Added Items are ' + msgArrAdded; // array of added items + return msg; + }, + type: 'AddtoOG' +}); diff --git a/Client-Side Components/UI Pages/Add Multiple Items to Order Guide/UI Action.js b/Client-Side Components/UI Pages/Add Multiple Items to Order Guide/UI Action.js new file mode 100644 index 0000000000..199133250c --- /dev/null +++ b/Client-Side Components/UI Pages/Add Multiple Items to Order Guide/UI Action.js @@ -0,0 +1,12 @@ + +/* +onClick function name : addToOrderGuide +The UI action will prompt UI page to select order guide and variable set. +*/ +function addToOrderGuide() { + var items = g_list.getChecked(); + var dialog = new GlideDialogWindow("add_to_og"); // UI page name + dialog.setTitle("Select Order Guide and Variable Set"); // Prompt title. + dialog.setPreference("items", items); + dialog.render(); +} diff --git a/Client-Side Components/UI Pages/Add Multiple Items to Order Guide/UI Page.js b/Client-Side Components/UI Pages/Add Multiple Items to Order Guide/UI Page.js new file mode 100644 index 0000000000..dd9cf1fe67 --- /dev/null +++ b/Client-Side Components/UI Pages/Add Multiple Items to Order Guide/UI Page.js @@ -0,0 +1,39 @@ + + + +
+ + + + +
+ +
+ + + + +
+ + +
+ +//Client Script of UI page + +function addItems(catItems) { + var og = document.getElementById("order_guide").value; + var varSet = document.getElementById("var_set").value; + + var orderG = new GlideAjax('AddtoOG'); + orderG.addParam('sysparm_name', 'addToOrderGuide'); + orderG.addParam('sysparm_itemList', catItems); + orderG.addParam('sysparm_og', og); + orderG.addParam('sysparm_set', varSet); + orderG.getXML(addOrderGuide); +} + +function addOrderGuide(response) { + var answer = response.responseXML.documentElement.getAttribute("answer"); + alert(answer); + GlideDialogWindow.get().destroy(); +} diff --git a/Client-Side Components/UI Pages/BulkUpdate Worknotes/List Button.png b/Client-Side Components/UI Pages/BulkUpdate Worknotes/List Button.png new file mode 100644 index 0000000000..8daf4a35be Binary files /dev/null and b/Client-Side Components/UI Pages/BulkUpdate Worknotes/List Button.png differ diff --git a/Client-Side Components/UI Pages/BulkUpdate Worknotes/Readme.md b/Client-Side Components/UI Pages/BulkUpdate Worknotes/Readme.md new file mode 100644 index 0000000000..09ff84e7b4 --- /dev/null +++ b/Client-Side Components/UI Pages/BulkUpdate Worknotes/Readme.md @@ -0,0 +1,11 @@ + +Bulk Update Worknotes + +Script Type: UI Action, Table: incident, List banner button: True, Client: True, Show update: True, OnClick: functionName() + +Script Type: UI Page Category: General + +Goal: To update the worknotes for multiple tickets in a single view + +Walk through of code: So in the incident List view we do have a list banner button called "Bulk Updates" so that was the UI Action configured with the script which has used the GlideModel API to call the UI page which is responsible to get the multiple tickets and worknotes value and then update to the respective ticket and store in the worknotes in journal entry. For this the HTML part in the UI page is configured with two fields, one for the multiple list of tickets, and then the worknotes field, and one submit button to save that into the table. +And the Processing Script is used to get each ticket number and then check the valid tickets and query the respective table, and then update the worknotes of each respective ticket if it is valid. Otherwise, it won't update. diff --git a/Client-Side Components/UI Pages/BulkUpdate Worknotes/UI Action.js b/Client-Side Components/UI Pages/BulkUpdate Worknotes/UI Action.js new file mode 100644 index 0000000000..43c936646e --- /dev/null +++ b/Client-Side Components/UI Pages/BulkUpdate Worknotes/UI Action.js @@ -0,0 +1,7 @@ +// Table: incident, List banner button: True, Client: True, Show update: True, OnClick: bulkupdate() + +function bulkupdate() { + var modalT = new GlideModal("BulkUpdate", false, 1200); + modalT.setTitle("Bulk Update Worknotes"); + modalT.render(); +} \ No newline at end of file diff --git a/Client-Side Components/UI Pages/BulkUpdate Worknotes/UI Page_ClientScript.js b/Client-Side Components/UI Pages/BulkUpdate Worknotes/UI Page_ClientScript.js new file mode 100644 index 0000000000..aa8e3ae275 --- /dev/null +++ b/Client-Side Components/UI Pages/BulkUpdate Worknotes/UI Page_ClientScript.js @@ -0,0 +1,5 @@ +function bulkupdate() { + document.getElementById("spinner-btn").style.display = "block"; + document.getElementById("submit-btn").style.display = "none"; + +} \ No newline at end of file diff --git a/Client-Side Components/UI Pages/BulkUpdate Worknotes/UI Page_HTML.html b/Client-Side Components/UI Pages/BulkUpdate Worknotes/UI Page_HTML.html new file mode 100644 index 0000000000..a904187fb7 --- /dev/null +++ b/Client-Side Components/UI Pages/BulkUpdate Worknotes/UI Page_HTML.html @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + +
+ diff --git a/Client-Side Components/UI Pages/Edit Last WorkNotes/uipage_clientcode.js b/Client-Side Components/UI Pages/Edit Last WorkNotes/uipage_clientcode.js new file mode 100644 index 0000000000..f97145f823 --- /dev/null +++ b/Client-Side Components/UI Pages/Edit Last WorkNotes/uipage_clientcode.js @@ -0,0 +1,40 @@ +fetchLastComment(); + +function closeDialog() { + GlideDialogWindow.get().destroy(); + return false; +} + +function fetchLastComment() { + var dialogWindow = GlideDialogWindow.get(); + var incidentSysId = dialogWindow.getPreference('incid'); + var glideAjax = new GlideAjax('UpdateINCworkNotes'); + glideAjax.addParam('sysparm_name', 'getIncLastWorknotes'); + glideAjax.addParam('sysparm_id', incidentSysId); + glideAjax.getXMLAnswer(setCommentFieldValue); +} + +function setCommentFieldValue(answer) { + var commentField = document.getElementById('commenttext'); + if (commentField) { + commentField.value = answer || ''; + } +} + +function submitComment() { + var dialogWindow = GlideDialogWindow.get(); + var incidentSysId = dialogWindow.getPreference('incid'); + var newCommentText = document.getElementById('commenttext').value; + + var glideAjax = new GlideAjax('UpdateINCworkNotes'); + glideAjax.addParam('sysparm_name', 'updateCommentsLatest'); + glideAjax.addParam('sysparm_id', incidentSysId); + glideAjax.addParam('sysparm_newcomment', newCommentText); + + glideAjax.getXMLAnswer(handleSuccessfulSubmit); + closeDialog(); +} + +function handleSuccessfulSubmit(answer) { + window.location.reload(); +} diff --git a/Client-Side Components/UI Pages/Progress Loader/REAMDE.md b/Client-Side Components/UI Pages/Progress Loader/REAMDE.md new file mode 100644 index 0000000000..d40f830872 --- /dev/null +++ b/Client-Side Components/UI Pages/Progress Loader/REAMDE.md @@ -0,0 +1,17 @@ +# Loaders UI Page + +This UI Page showcases a collection of loaders and spinners designed to visually represent ongoing processes in web applications. These components enhance user experience by providing clear feedback during data loading or processing. + +## Features +- A diverse set of loader and spinner designs. +- Fully responsive and easily customizable. +- Lightweight and seamlessly integrable into glideModal or other UI components. + + +## Usage +1. Select the desired loader's HTML and CSS code from the examples. +2. Go to the Application Navigator and search for UI Pages under **System UI > UI Pages**. +3. Click on New and create new record by adding given HTML in the HTML section. +4. Adjust the styles as needed to align with your design requirements. + + diff --git a/Client-Side Components/UI Pages/Progress Loader/example_use_GlideModal.js b/Client-Side Components/UI Pages/Progress Loader/example_use_GlideModal.js new file mode 100644 index 0000000000..8cae06d761 --- /dev/null +++ b/Client-Side Components/UI Pages/Progress Loader/example_use_GlideModal.js @@ -0,0 +1,14 @@ + + +//Example code to show the UI Page in GlideModal + +var dialog = new GlideModal("YOUR_UI_PAGE_NAME"); + +//Set the dialog title +dialog.setTitle('Demo title'); + +//Set the dialog width +dialog.setWidth(550); + +//Display the modal +dialog.render(); diff --git a/Client-Side Components/UI Pages/Progress Loader/loaderHTML.html b/Client-Side Components/UI Pages/Progress Loader/loaderHTML.html new file mode 100644 index 0000000000..490af48e92 --- /dev/null +++ b/Client-Side Components/UI Pages/Progress Loader/loaderHTML.html @@ -0,0 +1,278 @@ + + + +
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
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/scriptinclude.js b/Client-Side Components/UI Pages/Resolve Incident UI Page/scriptinclude.js new file mode 100644 index 0000000000..963202cdfd --- /dev/null +++ b/Client-Side Components/UI Pages/Resolve Incident UI Page/scriptinclude.js @@ -0,0 +1,19 @@ +var ResolutionProcessor = Class.create(); +ResolutionProcessor.prototype = Object.extendsObject(global.AbstractAjaxProcessor, { + updateRecord: function() { + var recordId = this.getParameter('sysparm_record_id'); + var reason = this.getParameter('sysparm_reason'); + var resolution = this.getParameter('sysparm_resolution'); + gs.info("Updating record " + recordId + " with reason: " + reason + " and resolution: " + resolution); + var grinc = new GlideRecord('incident'); + if (grinc.get(recordId)) { + grinc.close_code = resolution; + grinc.close_notes = reason; + grinc.state = '6'; //set to resolved + grinc.update(); + } else { + gs.error('No Record found for ' + recordId); + } + }, + type: 'ResolutionProcessor' +}); 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 @@ + + + +
+

+ + +

+

+ + +

+ +
+
diff --git a/Client-Side Components/UI Pages/Send Email On Form Incident/EmailScript.js b/Client-Side Components/UI Pages/Send Email On Form Incident/EmailScript.js new file mode 100644 index 0000000000..7890cfcd32 --- /dev/null +++ b/Client-Side Components/UI Pages/Send Email On Form Incident/EmailScript.js @@ -0,0 +1,19 @@ +// Email is been used to set the cc and subjed where we got from the UI Page and this will be attached to the Notification to run dymanically + + +(function runMailScript( /* GlideRecord */ current, /* TemplatePrinter */ template, + /* Optional EmailOutbound */ + email, /* Optional GlideRecord */ email_action, + /* Optional GlideRecord */ + event) { + + + // In this we get the data in the 4th parameter so that we use the parm2 to get the object of data and the used in the certain purpose + + var obj = JSON.parse(event.parm2); + email.addAddress("cc", obj.cc); + email.setSubject(obj.subject); + + template.print("
Hi Team,

Details are :

" + obj.body); + +})(current, template, email, email_action, event); \ No newline at end of file diff --git a/Client-Side Components/UI Pages/Send Email On Form Incident/Notification.js b/Client-Side Components/UI Pages/Send Email On Form Incident/Notification.js new file mode 100644 index 0000000000..ac5b15c10b --- /dev/null +++ b/Client-Side Components/UI Pages/Send Email On Form Incident/Notification.js @@ -0,0 +1,22 @@ +/* +Notificaiton for this proble + +Table : Incident (incident) + +When to Send +Send when - Event is fried +Event name - SendEamilInForm + +Who will receive +Event parm 1 contains recipient - True + +What it will contain +// This will hold the email script which we use to populate the subjec and the body. +Message HTML - ${mail_script:IncidentFormScript} +Email template - Unsubscribe and Preferences +Content type - HTML only + +*/ + +// Name of the Email Script will be populated when the notification has been triggered. +${mail_script:IncidentFormScript} \ No newline at end of file diff --git a/Client-Side Components/UI Pages/Send Email On Form Incident/Readme.md b/Client-Side Components/UI Pages/Send Email On Form Incident/Readme.md new file mode 100644 index 0000000000..78dfaffa32 --- /dev/null +++ b/Client-Side Components/UI Pages/Send Email On Form Incident/Readme.md @@ -0,0 +1,39 @@ +Send Email On Form for every Record + +Script Type: UI Action, Table: incident, Form button: True, Client: True, Show update: True, OnClick: functionName() + +Script Type: UI Page Category: General + +Script Type: Email Script + +Event Registry : Table: incident, Fired by: UI Page, Event Name: SendEmailInForm + +Notification : Table: incident, Type: EMAIL, Category: Uncategorized, + +Goal: To Send Email on Form Directly by population some field and then customize the body and trigger it. + +Walk through of code: So to send the Email directly on each and every record there will be a UI Action which will help to populate the UI Page which we use some field to be populate in the UI Page directly to the particulat HTML content and these are the fields will be populate (Caller Email as the To and then Short Description as the Subjet of the Email) and othe field will be CC and Body which the user want to decide what data can be filled out and then send. + +UIAction_INC_fomr + + +UI Page - This will have 5 components +1. To Caller +2. CC +3. Subject +4. Body +5. Send button + +UI Page Email template + + + +Once the Send button has been triggered this will call the Processing Script where the event will trigger once this will call the Event Registry event("SendEmailInForm") which we use for this problem statment.Where the Notification will trigger when the Event is fried and then for the email content we uset the Email Script which dynamic content will be populated which we got from the UI page as the event parm2 and then will send the email to the respective caller. + +Notification_Contains +Notification_Receive +Notification1 + + + +Email Preview diff --git a/Client-Side Components/UI Pages/Send Email On Form Incident/UIAction.js b/Client-Side Components/UI Pages/Send Email On Form Incident/UIAction.js new file mode 100644 index 0000000000..709e766e23 --- /dev/null +++ b/Client-Side Components/UI Pages/Send Email On Form Incident/UIAction.js @@ -0,0 +1,18 @@ +// This function will creat the model object and then route to the UI page which we configured to show the field details to fill by the user + +/* +Table- Incident (incident) +Form button - True +Client -True +Show Update - True +Onclick - Function name (sendEmail) + + + +*/ + function sendEmail(){ + var modalT = new GlideModal("SendEmailEvent", false, 1200); + modalT.setTitle("Send Email"); + modalT.setPreference("sysparm_sys_id", g_form.getUniqueValue()); + modalT.render(); + } \ No newline at end of file diff --git a/Client-Side Components/UI Pages/Send Email On Form Incident/UIPage_Html.html b/Client-Side Components/UI Pages/Send Email On Form Incident/UIPage_Html.html new file mode 100644 index 0000000000..2d47ac6adc --- /dev/null +++ b/Client-Side Components/UI Pages/Send Email On Form Incident/UIPage_Html.html @@ -0,0 +1,109 @@ + + + + + + + + + + + + + var gr_inc = new GlideRecord('incident'); + gr_inc.addQuery('sys_id', '${jvar_sysId}'); + gr_inc.query(); + gr_inc; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Client-Side Components/UI Pages/Send Email On Form Incident/UIPage_ProcessingScript.js b/Client-Side Components/UI Pages/Send Email On Form Incident/UIPage_ProcessingScript.js new file mode 100644 index 0000000000..31d02504bc --- /dev/null +++ b/Client-Side Components/UI Pages/Send Email On Form Incident/UIPage_ProcessingScript.js @@ -0,0 +1,13 @@ +// Getting the value from the HTML and the make the body and trigger the event for further process. + +var obj={}; +obj.body = body.toString(); +obj.cc = cc.toString(); +obj.subject = subject.toString(); + +// Event Trigger and passing the paramenters +/* +Parm 1 : will have the to receipient +Parm 2 : will have the cc, subject and body +*/ +gs.eventQueue("SendEmailInForm",current,to,JSON.stringify(obj)); \ No newline at end of file diff --git a/Client-Side Components/UI Pages/Share reports with users and groups/Image 1.png b/Client-Side Components/UI Pages/Share reports with users and groups/Image 1.png new file mode 100644 index 0000000000..15888fba6e Binary files /dev/null and b/Client-Side Components/UI Pages/Share reports with users and groups/Image 1.png differ diff --git a/Client-Side Components/UI Pages/Share reports with users and groups/Image 2.png b/Client-Side Components/UI Pages/Share reports with users and groups/Image 2.png new file mode 100644 index 0000000000..a6b6cd8cc7 Binary files /dev/null and b/Client-Side Components/UI Pages/Share reports with users and groups/Image 2.png differ diff --git a/Client-Side Components/UI Pages/Share reports with users and groups/Image 3.png b/Client-Side Components/UI Pages/Share reports with users and groups/Image 3.png new file mode 100644 index 0000000000..51fdf9fe87 Binary files /dev/null and b/Client-Side Components/UI Pages/Share reports with users and groups/Image 3.png differ diff --git a/Client-Side Components/UI Pages/Share reports with users and groups/README.md b/Client-Side Components/UI Pages/Share reports with users and groups/README.md new file mode 100644 index 0000000000..292446c626 --- /dev/null +++ b/Client-Side Components/UI Pages/Share reports with users and groups/README.md @@ -0,0 +1,17 @@ +**Usecase:** +Currenlty there's no OOB feature to share the all the reports from the particular dashboard with the user or group at a time. Also, sharing the dashboard with the user/group doesnot share the corresponding reports with them automatically. +In order to do that, admin or report owner should open each report and share them individually. +If the dashboard has more reports i.e 20+, then it'll take a considerable amount of time to complete this task. +To reduce the manual effort, we can use this custom logic to share all the reports from the particular dashboard at a time. + +**Pre-requisite:** +A database view which shows the reports shared with atleast one dashboard need to be created. +ServiceNow community article link which explains how to build one..(Thanks to Adam Stout for this) +https://www.servicenow.com/community/performance-analytics-blog/view-reports-on-a-dashboard-and-dashboards-using-a-report/ba-p/2271548 + +**Components:** +1. UI Page: It contains Jelly script (HTML), Client script and Processing script. Used to capture the user/group info and share the rports with them. +2. UI Action(Client): Created on the Dashboards (pa_dashboards) table. Used to open the UI page as apopup/modal window + +This UI action is visible on the dashboard properties page (image attached) + diff --git a/Client-Side Components/UI Pages/Share reports with users and groups/UI Page/ui_page.html b/Client-Side Components/UI Pages/Share reports with users and groups/UI Page/ui_page.html new file mode 100644 index 0000000000..76a79dd3e6 --- /dev/null +++ b/Client-Side Components/UI Pages/Share reports with users and groups/UI Page/ui_page.html @@ -0,0 +1,34 @@ + + + + + + + + + + +
+ + +
+
+ +
diff --git a/Client-Side Components/UI Pages/Share reports with users and groups/UI Page/ui_page_client_script.js b/Client-Side Components/UI Pages/Share reports with users and groups/UI Page/ui_page_client_script.js new file mode 100644 index 0000000000..b31e2608c0 --- /dev/null +++ b/Client-Side Components/UI Pages/Share reports with users and groups/UI Page/ui_page_client_script.js @@ -0,0 +1,10 @@ +function onCancel() { + var c = gel('cancelled'); + c.value = "true"; + GlideModal.get().destroy(); + return false; +} + +function onSubmit() { + return true; +} diff --git a/Client-Side Components/UI Pages/Share reports with users and groups/UI Page/ui_page_processing_script.js b/Client-Side Components/UI Pages/Share reports with users and groups/UI Page/ui_page_processing_script.js new file mode 100644 index 0000000000..aaae977e80 --- /dev/null +++ b/Client-Side Components/UI Pages/Share reports with users and groups/UI Page/ui_page_processing_script.js @@ -0,0 +1,71 @@ +if (cancelled == "false") { + var dashboard_id = key; // sys id of the dashboard from the Jelly script + var group_id = group; // sys id of group from the Jelly script + var user_id = user; //sys id of user from the Jelly script + + if (!gs.nil(group_id) || !gs.nil(user_id)) { + groupShare(dashboard_id, group_id); + userShare(dashboard_id, user_id); + response.sendRedirect("pa_dashboards.do?sys_id=" + key); + } else { + response.sendRedirect("pa_dashboards.do?sys_id=" + key); + gs.addErrorMessage("Please select group/user"); + } +} else { + response.sendRedirect("pa_dashboards.do?sys_id=" + key); +} + +function groupShare(dashboard_id, group_id) { + var db_view = new GlideRecord('u_reports_shared_with_dashboard'); // Database view name + db_view.addEncodedQuery('repstat_report_sys_id!=^dt_dashboard=' + dashboard_id); + db_view.query(); + while (db_view.next()) { + var report_id = db_view.rep_sys_id; + + var rec = new GlideRecord("sys_report"); + rec.get(report_id); + rec.user = "group"; + rec.update(); + + var report_sharing = new GlideRecord('sys_report_users_groups'); + report_sharing.addQuery('group_id', group_id); + + report_sharing.addQuery('report_id', report_id); + report_sharing.query(); + if (!report_sharing.next()) { + var new_record = new GlideRecord('sys_report_users_groups'); + new_record.initialize(); + new_record.report_id = report_id; + new_record.group_id = group_id; + new_record.insert(); + } + + } +} + +function userShare(dashboard_id, user_id) { + var db_view = new GlideRecord('u_reports_shared_with_dashboard'); + db_view.addEncodedQuery('repstat_report_sys_id!=^dt_dashboard=' + dashboard_id); + db_view.query(); + while (db_view.next()) { + var report_id = db_view.rep_sys_id; + + var rec = new GlideRecord("sys_report"); + rec.get(report_id); + rec.user = "group"; + rec.update(); + + var report_sharing = new GlideRecord('sys_report_users_groups'); + report_sharing.addQuery('user_id', user_id); + report_sharing.addQuery('report_id', report_id); + report_sharing.query(); + if (!report_sharing.next()) { + var new_record = new GlideRecord('sys_report_users_groups'); + new_record.initialize(); + new_record.report_id = report_id; + new_record.user_id = user_id; + new_record.insert(); + } + + } +} diff --git a/Client-Side Components/UI Pages/Share reports with users and groups/ui_action_script.js b/Client-Side Components/UI Pages/Share reports with users and groups/ui_action_script.js new file mode 100644 index 0000000000..5e5c09aa85 --- /dev/null +++ b/Client-Side Components/UI Pages/Share reports with users and groups/ui_action_script.js @@ -0,0 +1,23 @@ +/* +UI Action details: + +Active: True +Name: Share reports +Table: Dashboard [pa_dashboards] +Order: 1000 +Action name: share_report +Show update: True +Client: True +List v2 Compatible: True +Form button: True +Form style: Primary +Onclick: shareReport(); + +*/ + +function shareReport() { + var modal = new GlideModal("sj_share_reports"); // UI Page id + modal.setTitle("Share Reports"); + modal.setPreference('sysparm_key', g_form.getUniqueValue()); + modal.render(); +} diff --git a/Client-Side Components/UI Scripts/Custom Change Schedule/README.md b/Client-Side Components/UI Scripts/Custom Change Schedule/README.md new file mode 100644 index 0000000000..dfecace0d5 --- /dev/null +++ b/Client-Side Components/UI Scripts/Custom Change Schedule/README.md @@ -0,0 +1,36 @@ +# 🧾 ServiceNow Change Schedule Enhancement +### _(UI Scripts: `sn_chg_soc.change_soc`, `sn.chg_soc.config`, `sn.chg_soc.data`)_ + +--- + +## 📘 Overview + +This customization extends the **ServiceNow Change Schedule (Change Calendar)** functionality. +The enhancement adds visibility and interactivity to the Change Calendar by including: + +- A **Short Description** column in the Change Schedule view. +- A **configurable UI** allowing users to toggle visibility of columns such as _Configuration Item_, _Short Description_, and _Duration_. +- Integration of additional data services for fetching and rendering change records with enhanced details. +- A **Change Schedule button** that refreshes and displays these changes dynamically. + +The result is a more informative and user-friendly Change Schedule interface for Change Managers, CAB members, and ITSM users. + +--- + +## 🧩 Architecture + +| Module | Description | +|--------|-------------| +| **`sn_chg_soc.change_soc`** | Main controller and directive for the Change Schedule Gantt Chart UI. Handles initialisation, rendering, zoom, and popovers. | +| **`sn.chg_soc.config`** | Manages configuration settings for displayed columns and schedules (blackout, maintenance). Allows toggling visibility. | +| **`sn.chg_soc.data`** | Provide the data on the gantt chat from the change records + + +Requirement: +As an ITIL user, you can click the Change Schedule button to navigate directly to the Change Schedule view. +This allows you to see all planned changes and plan your own changes accordingly, especially useful for customers who do not have a well-established CMDB integrated with Discovery. + +image + +image + diff --git a/Client-Side Components/UI Scripts/Custom Change Schedule/change_soc.js b/Client-Side Components/UI Scripts/Custom Change Schedule/change_soc.js new file mode 100644 index 0000000000..25c02cf500 --- /dev/null +++ b/Client-Side Components/UI Scripts/Custom Change Schedule/change_soc.js @@ -0,0 +1,1018 @@ +angular.module("sn.chg_soc.change_soc", [ + "ngAria", + "sn.common", + "sn.common.glide", + "sn.angularstrap", + "sn.chg_soc.accessibility", + "sn.chg_soc.tooltip_overflow", + "sn.chg_soc.notification", + "sn.chg_soc.mousedown", + "sn.chg_soc.gantt", + "sn.chg_soc.data", + "sn.chg_soc.style", + "sn.chg_soc.config", + "sn.chg_soc.share", + "sn.chg_soc.landing_wizard", + "sn.chg_soc.context_menu", + "sn.chg_soc.snCreateNewInvite", + "sn.chg_soc.keyboard", + "sn.chg_soc.popover", + "sn.chg_soc.duration", + "sn.app_itsm.now.filter", + "sn.chg_soc.filter_control", + "sn.chg_soc.loading", + "sn.itsm.change.overflow" + ]) + .constant("SOC", { + BLACKOUT: "blackout", + BLACKOUT_SPAN_COLOR: "#BDC0C4", + CHANGE_REQUEST: "change_request", + DATE_FORMAT: "%Y-%m-%d %H:%i:%s", + GET_CHANGE_SCHEDULE: "/api/sn_chg_soc/soc/changeschedule/", + GET_PARSE_QUERY: "/api/now/ui/query_parse/change_request?sysparm_query=", + ISO_WEEK: "isoWeek", + MAINT: "maint", + MAINT_SPAN_COLOR: "#BDDCFC", + STYLE_PREFIX: "soc_", + SYSPARM_ID: "sysparm_id", + ZOOM_LEVEL_PREF: "sn_chg_soc.change_soc_zoom_level", + COLUMN: { + SHORT_DESCRIPTION: "short_description", + CONFIG_ITEM: "config_item", + // DURATION: "duration", + NUMBER: "number" + }, + STYLE_CLASS_MAP: { + soc_event_bar: "soc-event-bar", + soc_row_child: "soc-row-child", + soc_row_child_end: "soc-row-child-end", + soc_row_child_start: "soc-row-child-start", + soc_row_child_single: "soc-row-child-single" + }, + KEYS: { + TABKEY: 9, + ENTER: 13, + ESCAPE: 27, + SPACE: 32, + LEFT_ARROW: 37, + UP_ARROW: 38, + RIGHT_ARROW: 39, + DOWN_ARROW: 40, + D: 68, + E: 69, + F: 70, + SLASH: 191 + } + }) + .config(["$httpProvider", "$locationProvider", function($httpProvider, $locationProvider) { + $locationProvider.html5Mode({ + enabled: true, + requireBase: false + }); + $httpProvider.interceptors.push("xhrInterceptor"); + }]) + .service("urlService", ["$location", "SOC", function($location, SOC) { + var urlService = this; + + urlService.socId = $location.search()[SOC.SYSPARM_ID]; + + urlService.setChangeScheduleId = function() { + var params = $location.search(); + urlService.socId = params[SOC.SYSPARM_ID]; + }; + }]) + .service("clientService", ["dataService", function(dataService) { + var clientService = this; + + clientService.filter = dataService.definition; + }]) + .directive("changeSoc", ["urlService", "ganttChart", "ganttScale", "dataService", "i18n", "getTemplateUrl", "$templateRequest", "$templateCache", "$filter", "$compile", "$window", "SOC", "TextSearchService", "socNotification", + function(urlService, ganttChart, ganttScale, dataService, i18n, getTemplateUrl, $templateRequest, $templateCache, $filter, $compile, $window, SOC, TextSearchService, socNotification) { + return { + restrict: "A", + scope: false, + transclude: true, + template: "
", + link: function($scope, $element, $attrs, changeSoCCtrl) { + var position = { + delta: { + top: 0, + left: 0 + }, + original: { + top: 0, + left: 0 + } + }; + $scope.ganttInstance = ganttChart.getInstance(urlService.socId); + $scope.gantt = $scope.ganttInstance.gantt; + + // destroy all popovers when resizing the window + angular.element(window).on("resize", function() { + angular.element(".popover.soc-task-popover").popover("destroy"); + _handleDestroyPopover(); + }); + + angular.element(window).on("keydown", function($event) { + if ($event.keyCode === SOC.KEYS.ESCAPE) + _handleDestroyPopover(); + }); + + angular.element(window).on("click", function($event) { + var target = getTargetElement($event); + if (target === null) + _handleDestroyPopover(); // Clicking outside a gantt task + }); + + //size of gantt + $scope.$watch(function() { + return $element[0].offsetWidth + "." + $element[0].offsetHeight; + }, function() { + $scope.gantt.setSizes(); + }); + + $scope.$watch("dataService.definition.condition.dryRun", function(newValue, oldValue) { + if (newValue) + angular.element(".control-left .filter-btn").addClass("dry-run"); + else + angular.element(".control-left .filter-btn").removeClass("dry-run"); + }); + /** + * Marker config + */ + $scope.gantt.config.show_markers = true; + + /** + * Column config + */ + var msgSelectRecord = i18n.getMessage("Show span start"); + $scope.gantt.config.columns = [{ + name: SOC.COLUMN.NUMBER, + label: i18n.getMessage("Number"), + align: "left", + tree: true, + width: 160, + min_width: 160, + resize: true, + template: function(content) { + return "" + content.number + "" + + ""; + } + }, + { + name: SOC.COLUMN.CONFIG_ITEM, + label: i18n.getMessage("Configuration Item"), + align: "left", + width: 220, + min_width: 220, + resize: true, + template: function (content) { + return "" + + content[SOC.COLUMN.CONFIG_ITEM] + + ""; + } + }, + { + + name: SOC.COLUMN.SHORT_DESCRIPTION, + label: "Short Description", + align: "left", + tree: true, + width: 160, + min_width: 160, + resize: true, + template: function(content) { + return "" + content.short_description + "" + + ""; + } + + }, + // { + // name: SOC.COLUMN.DURATION, + // label: i18n.getMessage("Duration"), + // align: "left", + // width: 130, + // min_width: 130, + // template: function(content) { + // return content.dur_display; + // }, + // resize: true + // } + ]; + + /** + * Core Config + */ + // internal date time format + $scope.gantt.config.xml_date = SOC.DATE_FORMAT; + // ARIA attributes + $scope.gantt.config.wai_aria_attributes = true; + // Keyboard navigation + $scope.gantt.config.keyboard_navigation = true; + + /** + * Scrolling + */ + // Prevents scrolling gantt on load of data + $scope.gantt.config.initial_scroll = false; + + $scope.gantt.showTask = function(id) { + var task = this.getTask(id); + var taskSize = this.getTaskPosition(task, task.start, task.end); + var left = Math.max(taskSize.left - this.config.task_scroll_offset, 0); + var ganttVerScrollWidth = angular.element(".gantt_ver_scroll").width(); + var ganttTaskWidth = angular.element(".gantt_task").width() - ganttVerScrollWidth; + + if (Math.abs(this.getScrollState().x - taskSize.left) < ganttTaskWidth && (taskSize.left + taskSize.width) > this.getScrollState().x) + left = null; + + var scrollStateTop = this.getScrollState().y; + var scrollStateBottom = scrollStateTop + this._scroll_sizes().y; + var visibleTaskTop = taskSize.top; + var visibleTaskBottom = taskSize.top + this.config.row_height; + var top = null; + + if (visibleTaskTop < scrollStateTop) + top = visibleTaskTop; + else if (visibleTaskTop > scrollStateTop && (visibleTaskTop < scrollStateBottom && visibleTaskBottom < scrollStateBottom)) + top = null; + else if (visibleTaskTop > scrollStateTop && visibleTaskBottom > scrollStateBottom) + top = visibleTaskBottom - scrollStateBottom + scrollStateTop; + + this.scrollTo(left, top); + }; + + function isPopoverInViewport(el) { + var visibleArea = { + minWidth: angular.element(".gantt_grid_data").width() - 15, // 15px considering the arrow can be shifted to the right (still visible) + maxWidth: angular.element("body").width(), + minTop: angular.element(".gantt_data_area").offset().top, + maxTop: angular.element("body").height() + }; + var currentArea = { + minWidth: el.offset().left, + maxWidth: el.offset().left + el.width(), + minTop: el.offset().top - 15, // 15px considering the arrow + maxTop: el.offset().top + el.height() + 15, // 15px considering the arrow + }; + if (currentArea.minWidth > visibleArea.minWidth && currentArea.maxWidth < visibleArea.maxWidth && + currentArea.minTop > visibleArea.minTop && currentArea.maxTop < visibleArea.maxTop) + return true; + return false; + } + + function adjustPopover() { + popoverElement = angular.element(".popover.soc-task-popover"); + if (position.delta.top - angular.element(".gantt_ver_scroll").scrollTop() === 0 && position.delta.left - angular.element(".gantt_hor_scroll").scrollLeft() === 0) + return; + if (popoverElement.hasClass("in")) { + var newPopoverPosition = { + top: position.original.top + position.delta.top - angular.element(".gantt_ver_scroll").scrollTop(), + left: position.original.left + position.delta.left - angular.element(".gantt_hor_scroll").scrollLeft() + }; + popoverElement.offset(newPopoverPosition); + if (!isPopoverInViewport(popoverElement)) + _handleDestroyPopover(); + } + } + + $scope.lastScrollTop = 0; + $scope.loadScrollTop = 0; + $scope.lazyLoading = false; + $scope.gantt.attachEvent("onGanttScroll", function(left, top) { + if ($scope.lazyLoading) + $scope.lastScrollTop = top; + + if (dataService.count >= $window.NOW.sn_chg_soc.limit) + return; + + var gridHeight = angular.element("div.gantt_ver_scroll").find("div").height(); + var shouldLoad = top > ($scope.loadScrollTop + ((gridHeight - $scope.loadScrollTop) / 4)); + adjustPopover(); + if (!shouldLoad || $scope.isLoading() || !dataService.more || $scope.lazyLoading || top <= $scope.loadScrollTop || top <= $scope.lastScrollTop) + return; + + $scope.loadScrollTop = top; + $scope.lazyLoading = true; + dataService.getChanges(urlService.socId).then(function(model) { + if (dataService.count >= $window.NOW.sn_chg_soc.limit) + socNotification.show("warning", i18n.format(i18n.getMessage("This schedule has exceeded the event limit. The first {0} events based on your order criteria will be displayed."), $window.NOW.sn_chg_soc.limit), 0); + + // Need to provide the tasks so it can calc min/max + ganttScale.setDateRange(dataService.tasks.data); + ganttScale.configureScale(); + $scope.gantt.clearAll(); + ganttChart.addNowMarker(urlService.socId); + // these are the created tasks that will be added to the gantt + $scope.gantt.parse(dataService.tasks, "json"); + $scope.lazyLoading = false; + }); + }); + + /** + * Scales + */ + // Only visible scale is rendered + $scope.gantt.config.smart_scales = true; + // Removes vertical borders on cells + $scope.gantt.config.show_task_cells = false; + $scope.gantt.config.scale_height = 60; + $scope.gantt.config.row_height = 40; + $scope.gantt.config.duration_unit = "hour"; + $scope.gantt.config.duration_step = 1; + $scope.gantt.config.scale_unit = "day"; + $scope.gantt.config.date_scale = "%j %M %Y"; + $scope.gantt.config.subscales = [{ + unit: "hour", + step: 1, + date: "%H:%i" + }]; + + /** + * UI Components + */ + $scope.gantt.config.show_progress = false; + $scope.gantt.config.drag_links = false; + $scope.gantt.config.drag_move = false; + $scope.gantt.config.drag_resize = false; + + /** + * Templates + */ + // Configure use of icons in the gantt rows + $scope.gantt.templates.grid_open = function(item) { + return "
"; + }; + $scope.gantt.templates.grid_folder = function(item) { + return ""; + }; + $scope.gantt.templates.grid_file = function(item) { + return ""; + }; + $scope.gantt.templates.grid_indent = function(item) { + return ""; + }; + $scope.gantt.templates.grid_row_class = function(start, end, task) { + return ""; + }; + $scope.gantt.templates.task_row_class = function(start, end, task) { + return ""; + }; + $scope.gantt.templates.task_class = function(start, end, task) { + return SOC.STYLE_CLASS_MAP.soc_event_bar; + }; + $scope.gantt.templates.task_text = function(start, end, task) { + return ""; + }; + + function getNode(node) { + if (node.hasClass("gantt_row")) + return angular.element(node.children(".gantt_cell")[0]); + if (!node.hasClass("gantt_cell") || node.hasClass("gantt_task_content") || node.hasClass("gantt_task_drag")) + node = node.parent(); + return node; + } + + function getTargetElement($event) { + var node = angular.element($event.target || $event.srcElement); + if ($event.type === "keydown") + return angular.element($event.target); + node = getNode(node); + if (node.hasClass("gantt_task_line")) + return node; + return null; + } + + function handleOpenRecord() { + var task = $filter("filter")(dataService.tasks.data, { + id: this.targetId + })[0]; + $window.location.href = task.table + ".do?&sys_id=" + task.sys_id + + "&sysparm_redirect=" + encodeURIComponent("sn_chg_soc_change_soc.do?sysparm_id=" + urlService.socId); + } + + function _handleDestroyPopover() { + if (angular.element("[soc-popover]").length === 0) + return ""; + angular.element("[soc-popover]").focus(); + angular.element("[soc-popover]").attr("aria-expanded", "false"); + angular.element("[soc-popover]").removeAttr("soc-popover"); + angular.element(".popover.soc-task-popover").popover("destroy"); + } + + function _handleDestroyFlyout() { + $scope.$broadcast("sn.aside.change_soc_side.close"); + } + + function getTargetSelector($event, taskObj) { + if ($event.type !== "keydown") { + var selector = ".gantt_grid_data ." + $event.target.className; + var result = angular.element(selector); + var targetClass = (result.length > 0) ? ".gantt_grid" : ".gantt_task"; + return (result.length > 0) ? targetClass + " [task_id='" + taskObj.id + "'] .gantt_cell:first" : targetClass + " .gantt_task_line[task_id='" + taskObj.id + "']"; + } else + return ".gantt_grid [task_id='" + taskObj.id + "'] .gantt_cell:first"; + } + + function getX(target) { + var result = { + "start": 0, + "end": 0 + }; + var targetElement = { + "start": angular.element(target).offset().left, + "end": angular.element(target).offset().left + angular.element(target).width() + }; + var visibleArea = angular.element(".gantt_task"); + var visibleAreaLimits = { + "start": visibleArea.offset().left, + "end": visibleArea.offset().left + visibleArea.width() + }; + result.start = (targetElement.start > visibleAreaLimits.start) ? targetElement.start : visibleAreaLimits.start; + result.end = (targetElement.end < visibleAreaLimits.end) ? targetElement.end : visibleAreaLimits.end; + return result.start + (result.end - result.start) / 2; + } + + // Callback function used for building the popover template + function buildPopoverTemplate(taskObj, $event, popoverContent, popoverTemplate) { + var $popoverScope = $scope.$new(true); + $popoverScope.openRecord = i18n.getMessage("Open Record"); + $popoverScope.handleOpenRecord = handleOpenRecord; + var targetSelector = getTargetSelector($event, taskObj); + var target = angular.element(targetSelector); + $popoverScope.targetId = taskObj.id; + popoverTemplate = $compile(popoverTemplate)($popoverScope); + target.attr("tabindex", "0"); + target.attr("aria-expanded", "true"); + target.attr("soc-popover", "opened"); + var options = { + "container": "body", + "viewport": { + "selector": "body", + "padding": 20 + }, + "html": true, + "trigger": "manual", + "placement": "auto", + "title": taskObj.number + " - " + (taskObj.record.short_description ? taskObj.record.short_description.display_value : ""), + "content": popoverContent, + "template": popoverTemplate + }; + target.popover(options); + if (targetSelector.indexOf("gantt_task") !== -1) { + target.data("bs.popover").options.atMouse = $event.pageX !== 0; + target.data("bs.popover").options.mousePos = { + "x": getX(target), + "y": $event.pageY + }; + } + var action = angular.element(".popover.soc-task-popover").hasClass("in") ? "hidden" : "shown"; + target.on(action + ".bs.popover", function($ev) { + if ($ev.type === "shown") { + _handleDestroyFlyout(); + angular.element(".soc-btn-open-record").focus(); + position.delta = { + top: angular.element(".gantt_ver_scroll").scrollTop(), + left: angular.element(".gantt_hor_scroll").scrollLeft() + }; + position.original = { + top: angular.element(".popover.soc-task-popover").offset().top, + left: angular.element(".popover.soc-task-popover").offset().left + }; + // Amend popover height if it is taller than remaining part of the window + var popoverElement = angular.element(".soc-task-popover"); + var windowHeight = angular.element(window).height(); + var maxHeight = windowHeight - popoverElement.offset().top; + if (popoverElement.height() > maxHeight) + popoverElement.height(maxHeight + "px"); + } else + _handleDestroyPopover(); + }); + target.popover("toggle"); + } + + function getTooltipTextToDisplay() { + + } + + // Callback function used for building the popover content + function buildPopoverContent(taskObj, $event, popoverContent) { + $templateCache.remove(getTemplateUrl("sn_chg_soc_change_soc_popover_template.xml")); + var $popoverContentScope = $scope.$new(true); + $popoverContentScope.leftFields = taskObj.left_fields; + $popoverContentScope.rightFields = taskObj.right_fields; + $popoverContentScope.emptyValue = "[" + i18n.getMessage("Empty") + "]"; + popoverContent = $compile(popoverContent)($popoverContentScope); + $templateRequest(getTemplateUrl("sn_chg_soc_change_soc_popover_template.xml")).then(buildPopoverTemplate.bind(this, taskObj, $event, popoverContent)); + } + + function openPopover(id, $event) { + var targetElement = getTargetElement($event); + var openedPopover = angular.element("[soc-popover]"); + if (targetElement === null || openedPopover.length > 0) { + _handleDestroyPopover(); + if (targetElement === null || openedPopover.attr("task_id") === id) + return; + } + _handleDestroyFlyout(); + $event.stopPropagation(); + var taskObj = $filter("filter")(dataService.tasks.data, { + "id": id + }, true)[0]; + $templateRequest(getTemplateUrl("sn_chg_soc_change_soc_task_popover.xml")).then(buildPopoverContent.bind(this, taskObj, $event)); + } + + /** + * Events + **/ + $scope.gantt.attachEvent("onTaskClick", function(id, $event) { + openPopover(id, $event); + return true; + }); + + $scope.gantt.attachEvent("onTaskDblClick", function(id, e) { + return false; + }); + + $scope.gantt.addShortcut("enter", function($event) { + openPopover(this.taskId, $event); + }, "taskRow"); + + $scope.gantt.addShortcut("tab", function($event) {}, "taskRow"); + + $scope.gantt.attachEvent("onTaskSelected", function(id, item) { + return true; + }); + + $scope.gantt.attachEvent("onBeforeTaskSelected", function(id, item) { + return true; + }); + + function getScheduleEvent(task, startDate, endDate, styleClass) { + startDate = $scope.gantt.date.parseDate(startDate, "xml_date"); + endDate = $scope.gantt.date.parseDate(endDate, "xml_date"); + var sizes = $scope.gantt.getTaskPosition(task, startDate, endDate); + var el = document.createElement("div"); + el.className = "schedule-bar " + styleClass; + el.style.left = sizes.left + "px"; + el.style.width = sizes.width + "px"; + el.style.top = sizes.top + "px"; + return el; + } + + // Add task layer for blackout windows + $scope.ganttInstance.addTaskLayer(function(task) { + if (task.blackout_spans.length === 0 && task.maint_spans.length === 0) + return; + var wrapper = document.createElement("div"); + if (dataService.definition.show_maintenance.value) + task.maint_spans.forEach(function(maintSpan) { + wrapper.appendChild(getScheduleEvent(task, maintSpan.start, maintSpan.end, "maint")); + }); + if (dataService.definition.show_blackout.value) + task.blackout_spans.forEach(function(blackoutSpan) { + wrapper.appendChild(getScheduleEvent(task, blackoutSpan.start, blackoutSpan.end, "blackout")); + }); + return wrapper; + }); + + $scope.gantt.attachEvent("onGanttRender", function() { + $element.find(".gantt_container").attr("role", "grid"); + angular.element('[data-toggle="tooltip"]').tooltip('destroy'); + angular.element(".tooltip[id^='tooltip']").remove(); + $element.find('[data-toggle="tooltip"]').tooltip(); + }); + + // Locale information must be associated with gantt object attached to window + $window.gantt.locale = { + date: { + month_full: [i18n.getMessage("January"), + i18n.getMessage("February"), + i18n.getMessage("March"), + i18n.getMessage("April"), + i18n.getMessage("May"), + i18n.getMessage("June"), + i18n.getMessage("July"), + i18n.getMessage("August"), + i18n.getMessage("September"), + i18n.getMessage("October"), + i18n.getMessage("November"), + i18n.getMessage("December") + ], + month_short: [i18n.getMessage("Jan"), + i18n.getMessage("Feb"), + i18n.getMessage("Mar"), + i18n.getMessage("Apr"), + i18n.getMessage("May"), + i18n.getMessage("Jun"), + i18n.getMessage("Jul"), + i18n.getMessage("Aug"), + i18n.getMessage("Sep"), + i18n.getMessage("Oct"), + i18n.getMessage("Nov"), + i18n.getMessage("Dec") + ], + day_full: [i18n.getMessage("Sunday"), + i18n.getMessage("Monday"), + i18n.getMessage("Tuesday"), + i18n.getMessage("Wednesday"), + i18n.getMessage("Thursday"), + i18n.getMessage("Friday"), + i18n.getMessage("Saturday") + ], + day_short: [i18n.getMessage("Sun"), + i18n.getMessage("Mon"), + i18n.getMessage("Tue"), + i18n.getMessage("Wed"), + i18n.getMessage("Thu"), + i18n.getMessage("Fri"), + i18n.getMessage("Sat") + ] + }, + labels: {} + }; + + $scope.zoomIn = function() { + _handleDestroyPopover(); + ganttScale.zoom(++$scope.ganttScale.level, urlService.socId); + }; + + $scope.zoomOut = function() { + _handleDestroyPopover(); + ganttScale.zoom(--$scope.ganttScale.level, urlService.socId); + }; + + $scope.gantt.init($element[0]); + } + }; + } + ]) + .controller("ChangeSoCCtrl", ["$scope", "$document", "$timeout", "$window", "$location", "ganttChart", "styleService", "configService", "shareService", "dataService", "urlService", "ganttScale", "getTemplateUrl", "i18n", "SOC", "TextSearchService", "socNotification", + function($scope, $document, $timeout, $window, $location, ganttChart, styleService, configService, shareService, dataService, urlService, ganttScale, getTemplateUrl, i18n, SOC, TextSearchService, socNotification) { + var changeSoCCtrl = this; + + changeSoCCtrl.share = { + canWrite: false + }; + + changeSoCCtrl.closeFlyout = function() { + $scope.$apply(function() { + $scope.$broadcast("sn.aside.change_soc_side.close"); + }); + }; + + $scope.loadingElements = {}; + $scope.dataService = dataService; + $scope.ganttScale = ganttScale; + $scope.urlService = urlService; + + $scope.pageLeft = function($event) { + if ($event && $event.keyCode !== SOC.KEYS.ENTER && $event.keyCode !== SOC.KEYS.SPACE) + return; + var gantt = ganttChart.getGantt(urlService.socId); + var left = gantt.getScrollState().x - angular.element("div.gantt_scale_cell").width(); + gantt.scrollTo(left < 0 ? 0 : left, null); + }; + + $scope.pageRight = function($event) { + if ($event && $event.keyCode !== SOC.KEYS.ENTER && $event.keyCode !== SOC.KEYS.SPACE) + return; + var gantt = ganttChart.getGantt(urlService.socId); + var left = gantt.getScrollState().x + angular.element("div.gantt_scale_cell").width(); + var scrollLength = angular.element("div.gantt_hor_scroll > div").width(); + gantt.scrollTo(left > scrollLength ? scrollLength : left, null); + }; + + $scope.scrollToday = function() { + var gantt = ganttChart.getGantt(urlService.socId); + gantt.showDate(new Date()); + }; + + $scope.openView = function(viewId, event) { + // We already have something open, need to deal with that first + if ($scope.activeAside === viewId) { + $scope.$broadcast("sn.aside.change_soc_side.close"); + if (event) + event.target.blur(); + } else { + var view; + switch (viewId) { + case "share": + view = getView(viewId, "sn_chg_soc_aside_share.xml"); + break; + case "style": + view = getView(viewId, "sn_chg_soc_aside_style.xml"); + break; + case "style_def": + view = getView(viewId, "sn_chg_soc_aside_style_page.xml", true); + break; + case "config": + view = getView(viewId, "sn_chg_soc_aside_config.xml"); + break; + case "keyboard": + view = getView(viewId, "sn_chg_soc_aside_keyboard.xml"); + break; + } + if (view !== undefined) { + angular.element(".sn-aside_right").show(); + $scope.activeAside = viewId; + $scope.$broadcast("sn.aside.change_soc_side.open", view, "320px"); + } + } + }; + + $scope.$on("sn.aside.change_soc_side.close", function() { + switch ($scope.activeAside) { + case "share": + angular.element("#share_side").focus(); + break; + case "style": + angular.element("#style_side").focus(); + break; + case "style_def": + angular.element("#style_side").focus(); + break; + case "config": + angular.element("#config_side").focus(); + break; + case "keyboard": + angular.element("#keyboard_side").focus(); + break; + } + $scope.activeAside = ""; + angular.element(".sn-aside_right").hide(); + }); + + $scope.$on("sn.aside.change_soc_side.open_style", function(event, style) { + styleService.selectedStyle = style; + $scope.openView("style_def"); + }); + + $scope.$on("sn.aside.change_soc_side.style_updated", function(event, result) { + if (result.style_sheet) { + var socStyleSheet = $document[0].getElementById("soc_span_style"); + socStyleSheet.innerHTML = result.style_sheet; + } + + if (result.records) { + var gantt = ganttChart.getGantt(urlService.socId); + for (var i = 0; i < dataService.tasks.data.length; i++) + if (result.records[dataService.tasks.data[i].id].style_rule) + dataService.tasks.data[i].style_class = SOC.STYLE_PREFIX + result.records[dataService.tasks.data[i].id].style_rule.sys_id; + gantt.parse(dataService.tasks, "json"); + } + }); + + $scope.$on("sn.aside.change_soc_side.open_share", function(event, model) { + shareService.model = model; + $scope.openView("share"); + }); + + $scope.$on("sn.aside.change_soc_side.open_share:closed", function(event, model) { + $scope.openView("share"); + }); + + $scope.$on("sn.aside.change_soc_side.historyBack.completed", function(e, view) { + $scope.activeAside = view.title; + }); + + function getView(name, template, isChild) { + return { + scope: $scope, + title: name, + templateUrl: getTemplateUrl(template), + isChild: isChild + }; + } + + // Global keyboard shortcuts + $document.on("keydown", function(event) { + // Open keyboard help side + if (event.shiftKey && event.which == SOC.KEYS.SLASH && event.originalEvent.target.tagName !== "INPUT") { + $scope.$apply(function() { + if ($scope.activeAside === "keyboard") { + $scope.$broadcast("sn.aside.change_soc_side.close"); + if (event) + event.target.blur(); + } else { + $scope.activeAside = "keyboard"; + $scope.$broadcast("sn.aside.change_soc_side.open", getView("keyboard", "sn_chg_soc_aside_keyboard.xml"), "320px"); + } + }); + } + }); + + var getChildTaskDividerClass = function(start, end, task) { + if (!task.parent) + return ""; + + var classStyle = " " + SOC.STYLE_CLASS_MAP.soc_row_child; + + var nextTask = this.ganttChart.getNext(task.id); + nextTask = nextTask ? this.ganttChart.getTask(nextTask) : null; + var previousTask = this.ganttChart.getPrev(task.id); + previousTask = previousTask ? this.ganttChart.getTask(previousTask) : null; + + // Only child task for a parent + if (previousTask && !previousTask.parent) + if (!nextTask || (nextTask && !nextTask.parent)) + return classStyle += " " + SOC.STYLE_CLASS_MAP.soc_row_child_single; + + // First child task for their parent + if (previousTask && !previousTask.parent && (nextTask && nextTask.parent)) + return classStyle += " " + SOC.STYLE_CLASS_MAP.soc_row_child_start; + + // Last child task for their parent + if (previousTask && previousTask.parent && ((nextTask && !nextTask.parent) || !nextTask)) + return classStyle += " " + SOC.STYLE_CLASS_MAP.soc_row_child_end; + + return classStyle; + }; + + var defineClassTemplate = function(start, end, task) { + var classStyle = ""; + + if (this.type) + classStyle += SOC.STYLE_CLASS_MAP[this.type]; + + classStyle += getChildTaskDividerClass.call({ + ganttChart: this.ganttChart + }, null, null, task); + + if (task.style_class) + classStyle += " " + task.style_class; + + return classStyle; + }; + + var dateToStr = gantt.date.date_to_str(gantt.config.task_date); + + function updateMarkerInterval(gantt, markerId) { + var today = gantt.getMarker(markerId); + today.start_date = new Date(); + today.title = dateToStr(today.start_date); + gantt.updateMarker(markerId); + } + + function addNowMarker(gantt) { + var markerId = gantt.addMarker({ + start_date: new Date(), + css: "today-marker", + title: dateToStr(new Date()), + text: " " + }); + setInterval(updateMarkerInterval(gantt, markerId), 1000 * 60); + } + + function addScheduleSpanStyle(definition) { + var socStyleSheet = $document[0].createElement("style"); + socStyleSheet.id = "soc_schedule_style"; + $document[0].head.appendChild(socStyleSheet); + + var maintColor = definition.maintenance_span_color.value ? definition.maintenance_span_color.value : SOC.MAINT_SPAN_COLOR; + var blackoutColor = definition.blackout_span_color.value ? definition.blackout_span_color.value : SOC.BLACKOUT_SPAN_COLOR; + + var spanStyleSheet; + for (var i = 0; i < $document[0].styleSheets.length; i++) + if ($document[0].styleSheets[i].ownerNode.id === socStyleSheet.id) { + spanStyleSheet = $document[0].styleSheets[i]; + break; + } + + if (!spanStyleSheet) + return; + + spanStyleSheet.insertRule("div.schedule-bar.maint {background-color: " + maintColor + ";}", 0); + spanStyleSheet.insertRule("div.schedule-bar.blackout {background-color: " + blackoutColor + ";}", 0); + } + + changeSoCCtrl.initPage = function() { + dataService.initPage(urlService.socId).then(function() { + styleService.initStyle(); + + // Setup for share panel + changeSoCCtrl.share.canWrite = dataService.canWrite(); + + // Setup configuration panel + configService.showBlackoutOption = configService.showBlackoutSchedules = dataService.definition.show_blackout.value; + configService.showMaintOption = configService.showMaintSchedules = dataService.definition.show_maintenance.value; + configService.setChildRecords(dataService.child_table); + + // Need to apply changes due to style info + var socStyleSheet = document.createElement("style"); + socStyleSheet.id = "soc_span_style"; + document.head.appendChild(socStyleSheet); + socStyleSheet.innerHTML = dataService.style.style_sheet; + + addScheduleSpanStyle(dataService.definition); + + var gantt = ganttChart.getGantt(urlService.socId); + gantt.templates.grid_row_class = defineClassTemplate.bind({ + ganttChart: gantt, + type: "", + }); + gantt.templates.task_row_class = getChildTaskDividerClass.bind({ + ganttChart: gantt + }); + gantt.templates.task_class = defineClassTemplate.bind({ + ganttChart: gantt, + type: "soc_event_bar", + }); + gantt.render(); + + // Need to provide the tasks so it can calc min/max + ganttScale.setDateRange(dataService.tasks.data); + ganttScale.configureScale(); + gantt.clearAll(); + addNowMarker(gantt); + // these are the created tasks that will be added to the gantt + gantt.parse(dataService.tasks, "json"); + if (dataService.tasks.data.length > 0) { + gantt.showTask(dataService.tasks.data[0].id); + $scope.noResults = false; + } else + $scope.noResults = true; + }).catch(function(response) { + socNotification.show("error", response.data.error.message); + }); + }; + + $scope.filter = { + filterConditions: ["number", "config_item", "Short Description", "children.number", "children.config_item"], + placeholderText: i18n.getMessage("Search Change Schedule") + }; + + function buildFilterData() { + var augmentedData = dataService.tasks.data; + dataService.tasks.data.forEach(function(obj, index) { + augmentedData[index].children = dataService.getChildren(obj.id); + }); + return augmentedData; + } + + $scope.$on("textSearch", function(event, textSearch) { + var filteredRecords = TextSearchService.getFilteredArray(buildFilterData(), textSearch); + ganttChart.updateGanttData(urlService.socId, filteredRecords); + $scope.noResults = filteredRecords.length === 0; + }); + + $scope.isLoading = function() { + return $scope.$parent.loading; + }; + + changeSoCCtrl.messages = { + "No records to display": i18n.getMessage("No records to display"), + "No records match the filter": i18n.getMessage("No records match the filter"), + "Change Schedule": i18n.getMessage("Change Schedule"), + "Close panel": i18n.getMessage("Close panel"), + "Configuration": i18n.getMessage("Configuration"), + "Share": i18n.getMessage("Share"), + "Open context menu": i18n.getMessage("Open context menu"), + "Filter": i18n.getMessage("Filter"), + "Keyboard Shortcuts": i18n.getMessage("Keyboard Shortcuts"), + "Search Change Schedule": i18n.getMessage("Search Change Schedule"), + "Span Styles": i18n.getMessage("Span Styles"), + "Today": i18n.getMessage("Today"), + "Zoom in": i18n.getMessage("Zoom in"), + "Zoom out": i18n.getMessage("Zoom out"), + "Page left": i18n.getMessage("Page left"), + "Page right": i18n.getMessage("Page right"), + "Show today": i18n.getMessage("Show today") + }; + + $scope.noResults = false; + var noResultsElem = "
" + changeSoCCtrl.messages["No records to display"] + "
"; + + function noResults(newValue, oldValue) { + if (newValue === oldValue) + return; + if (newValue) { + angular.element("div.gantt_marker.today-marker").hide(); + angular.element("div.gantt_task_scale").after(noResultsElem); + } else { + angular.element("div.gantt_marker.today-marker").show(); + angular.element("div.no-results").remove(); + } + } + $scope.$watch("noResults", noResults); + } + ]) + .filter("objectKeys", [function() { + return function(anObject) { + if (!anObject) + return null; + return Object.keys(anObject); + }; + }]) + .filter("objectKeysLength", [function() { + return function(anObject) { + if (!anObject) + return null; + return Object.keys(anObject).length; + }; + }]); diff --git a/Client-Side Components/UI Scripts/Custom Change Schedule/config.js b/Client-Side Components/UI Scripts/Custom Change Schedule/config.js new file mode 100644 index 0000000000..0a388521e6 --- /dev/null +++ b/Client-Side Components/UI Scripts/Custom Change Schedule/config.js @@ -0,0 +1,146 @@ +angular.module("sn.chg_soc.config", ["sn.common"]) + .service("configService", ["dataService", "ganttChart", "urlService", "SOC", function(dataService, ganttChart, urlService, SOC) { + var configService = this; + + configService.showConfigItem = true; + configService.showDuration = true; + configService.showShortDesc=true; + configService.showBlackoutOption = true; + configService.showBlackoutSchedules = true; + configService.showMaintOption = true; + configService.showMaintSchedules = true; + + configService.childRecords = {}; + + configService.setChildRecords = function(childTables) { + for (var tableName in childTables) + configService.childRecords[tableName] = { + inputId: tableName + "Option", + label: childTables[tableName].__label, + name: tableName + "Show", + show: true, + change: updateChildRecords + }; + }; + + function updateChildRecords(tableName) { + var gantt = ganttChart.getGantt(urlService.socId); + var ganttTasks = gantt.getTaskByTime(); + for (var i = 0; i < ganttTasks.length; i++) + if (ganttTasks[i].parent && ganttTasks[i].table === tableName) + ganttTasks[i].__visible = configService.childRecords[tableName].show; + gantt.attachEvent("onBeforeTaskDisplay", function(id, task) { + if (task.parent) + return task.__visible; + return true; + }); + gantt.templates.grid_open = gridOpen; + gantt.render(); + } + + function gridOpen(task) { + var gantt = ganttChart.getGantt(urlService.socId); + var children = gantt.getChildren(task.id); + + for (var i = 0; i < children.length; i++) { + var childTask = gantt.getTask(children[i]); + if (childTask.__visible) + return "
"; + } + + return "
"; + } + }]) + .directive("socAsideConfig", ["getTemplateUrl", "configService", "ganttChart", "dataService", "objectKeysLengthFilter", "SOC", "i18n", function(getTemplateUrl, configService, ganttChart, dataService, objectKeysLengthFilter, SOC, i18n) { + "use strict"; + return { + restrict: "A", + templateUrl: getTemplateUrl("sn_chg_soc_aside_config_body.xml"), + scope: { + socDefId: "=" + }, + controller: function($scope, objectKeysLengthFilter) { + // $scope.showConfigItem = configService.showConfigItem; + // $scope.showDuration = configService.showDuration; + $scope.showBlackoutOption = configService.showBlackoutOption; + $scope.showShortDesc = configService.showShortDesc; + $scope.showBlackoutSchedules = configService.showBlackoutSchedules; + $scope.showMaintOption = configService.showMaintOption; + $scope.showMaintSchedules = configService.showMaintSchedules; + $scope.childRecords = configService.childRecords; + $scope.objectKeysLengthFilter = objectKeysLengthFilter; + $scope.messages = { + "Child Records": i18n.getMessage("Child Records"), + "Columns": i18n.getMessage("Columns"), + "Configuration Item": i18n.getMessage("Configuration Item"), + "Short Description": i18n.getMessage("Short Description"), + "Duration": i18n.getMessage("Duration"), + "Related Records": i18n.getMessage("Related Records"), + "Schedules": i18n.getMessage("Schedules"), + "Blackout": i18n.getMessage("Blackout"), + "Maintenance": i18n.getMessage("Maintenance") + }; + + $scope.updateColumnLayout = function(columnId) { + var gantt = ganttChart.getGantt($scope.socDefId); + var column = gantt.getGridColumn(columnId); + if (SOC.COLUMN.CONFIG_ITEM === columnId) { + configService.showConfigItem = !configService.showConfigItem; + column.hide = !configService.showConfigItem; + } else if (SOC.COLUMN.DURATION === columnId) { + configService.showDuration = !configService.showDuration; + column.hide = !configService.showDuration; + } else if (SOC.COLUMN.SHORT_DESCRIPTION === columnId) { + configService.showShortDesc = !configService.showShortDesc; + column.hide = !configService.showShortDesc; + } else + return; + gantt.render(); + }; + + function getScheduleEvent(task, startDate, endDate, styleClass) { + var gantt = ganttChart.getGantt($scope.socDefId); + startDate = gantt.date.parseDate(startDate, "xml_date"); + endDate = gantt.date.parseDate(endDate, "xml_date"); + var sizes = gantt.getTaskPosition(task, startDate, endDate); + var el = document.createElement("div"); + el.className = "schedule-bar " + styleClass; + el.style.left = sizes.left + "px"; + el.style.width = sizes.width + "px"; + el.style.top = sizes.top + "px"; + return el; + } + + var scheduleTaskLayer = function(task) { + if ((!this.show_blackout && !this.show_maint) || (task.blackout_spans.length === 0 && task.maint_spans.length === 0)) + return; + var wrapper = document.createElement("div"); + if (this.show_blackout && dataService.definition.show_blackout.value) + task.blackout_spans.forEach(function(blackoutSpan) { + wrapper.appendChild(getScheduleEvent(task, blackoutSpan.start, blackoutSpan.end, "blackout")); + }); + if (this.show_maint && dataService.definition.show_maintenance.value) + task.maint_spans.forEach(function(maintSpan) { + wrapper.appendChild(getScheduleEvent(task, maintSpan.start, maintSpan.end, "maint")); + }); + return wrapper; + }; + + $scope.updateScheduleLayer = function() { + configService.showBlackoutSchedules = $scope.showBlackoutSchedules; + configService.showMaintSchedules = $scope.showMaintSchedules; + var ganttInstance = ganttChart.getInstance($scope.socDefId); + ganttInstance.removeTaskLayer(); + + if ($scope.showBlackoutSchedules || $scope.showMaintSchedules) { + ganttInstance.addTaskLayer(scheduleTaskLayer.bind({ + show_blackout: $scope.showBlackoutSchedules, + show_maint: $scope.showMaintSchedules + })); + ganttInstance.gantt.render(); + } + }; + } + }; + }]); diff --git a/Client-Side Components/UI Scripts/Custom Change Schedule/data.js b/Client-Side Components/UI Scripts/Custom Change Schedule/data.js new file mode 100644 index 0000000000..e521f41ba3 --- /dev/null +++ b/Client-Side Components/UI Scripts/Custom Change Schedule/data.js @@ -0,0 +1,272 @@ +angular.module("sn.chg_soc.data", []) + .service("dataService", ["$http", "$q", "$window", "i18n", "urlService", "ganttChart", "durationFormatter", "SOC", "$filter", function($http, $q, $window, i18n, urlService, ganttChart, durationFormatter, SOC, $filter) { + var dataService = this; + + dataService.more = false; + dataService.count = 0; + dataService.child_table = {}; + dataService.definition = {}; + dataService.style = { + chg_soc_style_rule: {}, + chg_soc_definition_style_rule: {}, + chg_soc_def_child_style_rule: {}, + style_sheet: "" + }; + dataService.tasks = { + data: [], + links: [] + }; + dataService.allRecords = {}; + + function isValidDate(date) { + if (Object.prototype.toString.call(date) === "[object Date]" && !isNaN(date.getTime())) + return true; + return false; + } + + function buildFields(record, selectedFieldsList, tableMeta) { + var result = []; + if (!selectedFieldsList) + return result; + var selectedFields = selectedFieldsList.split(","); + selectedFields.forEach(function(fieldName) { + if (fieldName && tableMeta[fieldName]) + result.push({ + column_name: fieldName, + label: tableMeta[fieldName].label, + display_value: record[fieldName].display_value, + value: record[fieldName].value, + }); + }); + return result; + } + + function buildRecord(record, chgSocDef, tableMeta, styleRule, scheduleWindow) { + var ganttUtil = ganttChart.getGantt(urlService.socId); + var startDate = ganttUtil.date.parseDate(record[chgSocDef.start_date_field.value].display_value_internal, "xml_date"); + var endDate = ganttUtil.date.parseDate(record[chgSocDef.end_date_field.value].display_value_internal, "xml_date"); + // Check start/end dates are valid before adding the task to gantt chart + if (!isValidDate(startDate) || !isValidDate(endDate)) + return; + + var recordEvent = { + id: record.sys_id ? record.sys_id.value : "", + text: record.number ? record.number.display_value : "", + number: record.number ? record.number.display_value : "", + chg_soc_def: chgSocDef.sys_id.value, + config_item: record.cmdb_ci ? record.cmdb_ci.display_value : "", + start_date: startDate, + end_date: endDate, + dur_display: durationFormatter.buildDurationDisplay(startDate, endDate), + order: 0, + progress: 0, + table: record.sys_class_name ? record.sys_class_name.value : chgSocDef.table_name.value, + left_fields: buildFields(record, chgSocDef.popover_left_col_fields.value, tableMeta), + right_fields: buildFields(record, chgSocDef.popover_right_col_fields.value, tableMeta), + record: record, + blackout_spans: [], + maint_spans: [], + sys_id: record.sys_id ? record.sys_id.value : "", + short_description: record.short_description ? record.short_description.display_value : "", + __visible: true + }; + + if (styleRule && styleRule.sys_id) + recordEvent.style_class = SOC.STYLE_PREFIX + styleRule.sys_id; + + if (scheduleWindow) { + if (chgSocDef.show_maintenance.value) + angular.forEach(scheduleWindow.maintenance, function (schedule) { + Array.prototype.push.apply(recordEvent.maint_spans, schedule.spans); + }); + if (chgSocDef.show_blackout.value) + angular.forEach(scheduleWindow.blackout, function (schedule) { + Array.prototype.push.apply(recordEvent.blackout_spans, schedule.spans); + }); + } else { + recordEvent.id = chgSocDef.sys_id.value + "_" + recordEvent.id; + recordEvent.parent = record[chgSocDef.reference_field.value].value; + } + + dataService.allRecords[recordEvent.id] = { + style_rule: styleRule, + sys_id: record.sys_id ? record.sys_id.value : "", + table_name: record.sys_class_name ? record.sys_class_name.value : chgSocDef.table_name.value, + chg_soc_def: chgSocDef.sys_id.value + }; + + return recordEvent; + } + + function buildItem(result, item) { + // Build change_request record + var record = buildRecord(result[item.table_name][item.sys_id], result.chg_soc_definition, result[item.table_name].__table_meta, item.style, item.schedule_window); + if (!record) + return; + + dataService.tasks.data.push(record); + + // Build related tasks + if (item.related) + for (var childSocDefId in item.related) { + var childRecords = item.related[childSocDefId]; + for (var i = 0; i < childRecords.length; i++) { + var childRecord = buildRecord(result[childRecords[i].table_name][childRecords[i].sys_id], result.chg_soc_definition.__child[childSocDefId], result[childRecords[i].table_name].__table_meta, childRecords[i].style); + if (childRecord) + dataService.tasks.data.push(childRecord); + } + } + } + + dataService.buildData = function(result) { + if (!result) + return; + + dataService.more = result.__more; + dataService.count = result.__change_count; + + // Start with the definition object + if (result.chg_soc_definition) + dataService.definition = result.chg_soc_definition; + + // Ordered change requests with style and related records + if (result.__struct) + for (var i = 0; i < result.__struct.length; i++) + buildItem(result, result.__struct[i]); + + // Find all child tables + for (var table in result) + if (result[table].__has_children) + dataService.child_table[table] = result[table].__table_meta; + + // Set style rules and style sheet to the model + dataService.style.chg_soc_style_rule = result.chg_soc_style_rule; + dataService.style.chg_soc_definition_style_rule = result.chg_soc_definition_style_rule; + dataService.style.chg_soc_def_child_style_rule = result.chg_soc_def_child_style_rule; + dataService.style.style_sheet = result._css; + }; + + dataService.addData = function(result) { + dataService.more = result.__more; + dataService.count = result.__change_count; + + if (result.__struct) + for (var i = 0; i < result.__struct.length; i++) + buildItem(result, result.__struct[i]); + + for (var table in result) + if (result[table].__has_children) + dataService.child_table[table] = result[table].__table_meta; + }; + + dataService.initPage = function(chgSocDefId, condition) { + var deferred = $q.defer(); + var url = SOC.GET_CHANGE_SCHEDULE + chgSocDefId; + var config = {}; + config.params = { + sysparm_ck: $window.g_ck + }; + if (condition) + config.params.condition = condition; + $http.get(url, config).then(function(response){ + deferred.resolve(dataService.buildData(response.data.result)); + }, function(response) { + deferred.reject(response); + }); + return deferred.promise; + }; + + dataService.getChanges = function() { + var deferred = $q.defer(); + var url = SOC.GET_CHANGE_SCHEDULE + dataService.definition.sys_id.value; + var config = {}; + config.params = { + sysparm_ck: $window.g_ck, + count: dataService.count + }; + if (dataService.definition.condition.dryRun) + config.params.condition = dataService.definition.condition.value; + + $http.get(url, config).then(function(response){ + deferred.resolve(dataService.addData(response.data.result)); + }, function(response) { + deferred.reject(response); + }); + return deferred.promise; + }; + + dataService.getChildren = function(parentId) { + var res = $filter("filter")(dataService.tasks.data, function(task) { + return task.parent === parentId; + }); + return res; + }; + + dataService.destroyData = function() { + dataService.more = false; + dataService.count = 0; + dataService.child_table = {}; + dataService.definition = {}; + dataService.style = { + chg_soc_style_rule: {}, + chg_soc_definition_style_rule: {}, + chg_soc_def_child_style_rule: {}, + style_sheet: "" + }; + dataService.tasks = { + data: [], + links: [] + }; + dataService.allRecords = {}; + }; + + dataService.parseQuery = function(condition) { + condition = condition + ""; + var deferred = $q.defer(); + var url = SOC.GET_PARSE_QUERY + condition; + var config = {}; + config.params = {}; + config.params.sysparm_ck = $window.g_ck; + + $http.get(url, config).then(function(response) { + deferred.resolve(response.data.result); + }, function(response) { + deferred.reject(response); + }); + + return deferred.promise; + }; + + function checkSecurityObject() { + return dataService.definition && dataService.definition.__security; + } + + dataService.canCreate = function() { + if (checkSecurityObject() && dataService.definition.__security.canCreate) + return dataService.definition.__security.canCreate; + return false; + }; + + dataService.canRead = function() { + if (checkSecurityObject() && dataService.definition.__security.canRead) + return dataService.definition.__security.canRead; + return false; + }; + + dataService.canWrite = function() { + if (checkSecurityObject() && dataService.definition.__security.canWrite) + return dataService.definition.__security.canWrite; + return false; + }; + + dataService.canDelete = function() { + if (checkSecurityObject() && dataService.definition.__security.canDelete) + return dataService.definition.__security.canDelete; + return false; + }; + + dataService.trackEvent = function(source) { + if ($window.GlideWebAnalytics && $window.GlideWebAnalytics.trackEvent) + $window.GlideWebAnalytics.trackEvent('com.snc.change_management.soc', 'Change Schedules', source, 0, 0); + }; + }]); diff --git a/Client-Side Components/UI Scripts/Disable Copy Paste For Portal/README.md b/Client-Side Components/UI Scripts/Disable Copy Paste For Portal/README.md new file mode 100644 index 0000000000..72b3a31cc2 --- /dev/null +++ b/Client-Side Components/UI Scripts/Disable Copy Paste For Portal/README.md @@ -0,0 +1,10 @@ +**Steps to Activate** +1. Open the portals you want to disable copy/paste operation in "sp_portal" table. +2. Open the theme attached to the portal. +In the theme under "JS Includes" relatd list, create new JS include and select the UI script you created. Go to your portal and try to copy/paste in any catalog item field or any text field on portal.The operation will be prevented with the alert message. + +**Use Case** +1. Many high security organizations like banks do not want the users to copy paste account number or passwords to ensure safety. +2. Many input form want the users to re-enter the password or username without copying from other fields. + +This UI script is applied through portal theme , so it will be specific to portals using that theme. It will not have instance wide affect. diff --git a/Client-Side Components/UI Scripts/Disable Copy Paste For Portal/script.js b/Client-Side Components/UI Scripts/Disable Copy Paste For Portal/script.js new file mode 100644 index 0000000000..42e57ed3b7 --- /dev/null +++ b/Client-Side Components/UI Scripts/Disable Copy Paste For Portal/script.js @@ -0,0 +1,13 @@ +/* +Disable Copy Paste on Portal Pages. +UI Type : Service Portal/Mobile. +*/ +document.addEventListener('copy', function(e) { //event listner for copy. + alert("Copy Operation is prevented on this page."); // alert for copy + e.preventDefault(); // prevent copy +}); + +document.addEventListener('paste', function(e) { //event listner for paste. + alert("Paste Operation is prevented on this page."); //alert for paste + e.preventDefault(); // prevent paste +}); diff --git a/Client-Side Components/UI Scripts/PersistentAnnouncementBanner/README.md b/Client-Side Components/UI Scripts/PersistentAnnouncementBanner/README.md new file mode 100644 index 0000000000..2562e719e5 --- /dev/null +++ b/Client-Side Components/UI Scripts/PersistentAnnouncementBanner/README.md @@ -0,0 +1,7 @@ +This UI Script is used to inject a custom, highly visible, and persistent notification banner across the top of the entire ServiceNow platform interface. + +It is ideal for communicating critical, non-transactional system messages such as scheduled maintenance, major outages, or company-wide policy announcements. + +The key feature of this banner is that it is dismissible, and it uses a User Preference to ensure that once a user closes a specific version of the announcement + +The Reason to use this is , Announcements Banner Module Sometimes Loads Slower or Doesnt trigger the notification banner , instead this scripts all the time,if user is logged in diff --git a/Client-Side Components/UI Scripts/PersistentAnnouncementBanner/annoucement_banner.js b/Client-Side Components/UI Scripts/PersistentAnnouncementBanner/annoucement_banner.js new file mode 100644 index 0000000000..ad1bb9c922 --- /dev/null +++ b/Client-Side Components/UI Scripts/PersistentAnnouncementBanner/annoucement_banner.js @@ -0,0 +1,51 @@ +(function() { + var BANNER_TEXT = ' **SYSTEM ALERT:** Scheduled maintenance for all services will occur this Saturday from 1 AM to 4 AM UTC. Expect minor downtime. 🚨'; + var BANNER_BG_COLOR = '#ffcc00'; // Warning yellow + var BANNER_TEXT_COLOR = '#333333'; + + // Check if the user has already dismissed this version of the banner + var isDismissed = (g_preference.get(PREF_KEY) === 'true'); + + if (isDismissed) { + return; + } + var banner = document.createElement('div'); + banner.setAttribute('id', 'global_announcement_banner'); + banner.innerHTML = BANNER_TEXT; + banner.style.cssText = [ + 'position: fixed;', + 'top: 0;', + 'left: 0;', + 'width: 100%;', + 'padding: 10px 40px 10px 15px;', // Added padding on the right for the close button + 'background-color: ' + BANNER_BG_COLOR + ';', + 'color: ' + BANNER_TEXT_COLOR + ';', + 'z-index: 10000;', // High z-index to ensure it sits on top of everything + 'text-align: center;', + 'font-weight: bold;', + 'box-shadow: 0 2px 5px rgba(0,0,0,0.2);' + ].join(''); + var closeButton = document.createElement('span'); + closeButton.innerHTML = '×'; // Times symbol + closeButton.style.cssText = [ + 'position: absolute;', + 'top: 50%;', + 'right: 15px;', + 'transform: translateY(-50%);', + 'font-size: 20px;', + 'cursor: pointer;', + 'font-weight: normal;', + 'line-height: 1;' + ].join(''); + + closeButton.onclick = function() { + // Remove the banner from the DOM + banner.remove(); + + // Set the User Preference so the banner stays dismissed across sessions + g_preference.set(PREF_KEY, 'true'); + }; + banner.appendChild(closeButton); + document.body.appendChild(banner); + +})(); diff --git a/Client-Side Components/UI Scripts/Prevent right click on portals/README.md b/Client-Side Components/UI Scripts/Prevent right click on portals/README.md new file mode 100644 index 0000000000..6bc76969dc --- /dev/null +++ b/Client-Side Components/UI Scripts/Prevent right click on portals/README.md @@ -0,0 +1,13 @@ +**Steps to Activate** +1. Open the portals you want to disable right-click in "sp_portal" table. +2. Open the theme attached to the portal. +3. In the theme under "JS Includes" relatd list, create new JS include and select the UI script you created. +Go to your portal and try to roght click, it will prevent and show the alert message. + +**Use Case** +1. Many high security organizations like banks do not want their images or links to be copied through "inspect" so right-click need to be disabled. +2. Many organizations want their source code to be hidden so they prevent right-click. + + + **Note** + 1. This UI script is applied through portal theme , so it will be specific to portals using that theme. It will not have instance wide affect. diff --git a/Client-Side Components/UI Scripts/Prevent right click on portals/script.js b/Client-Side Components/UI Scripts/Prevent right click on portals/script.js new file mode 100644 index 0000000000..ebcdbb7c06 --- /dev/null +++ b/Client-Side Components/UI Scripts/Prevent right click on portals/script.js @@ -0,0 +1,13 @@ +/* +Prevent right-click on portal for portal pages. +This will secure the site code, prevent users from saving images etc. +Ideal for high security organisations. + +UI Type : Mobile/service portal. +*/ +(function() { // self invoking function + document.addEventListener('contextmenu', function(event) { + event.preventDefault(); // Prevent right-click operation. + alert("Right-Click Prevented for Security Reasons."); // alert message shown on right click. + }); +})(); diff --git a/Client-Side Components/UI Scripts/User acknowledgement Using UI script and user preferences/UIpage.js b/Client-Side Components/UI Scripts/User acknowledgement Using UI script and user preferences/UIpage.js new file mode 100644 index 0000000000..2d7ce8d90e --- /dev/null +++ b/Client-Side Components/UI Scripts/User acknowledgement Using UI script and user preferences/UIpage.js @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/Client-Side Components/UI Scripts/User acknowledgement Using UI script and user preferences/UIscript.js b/Client-Side Components/UI Scripts/User acknowledgement Using UI script and user preferences/UIscript.js new file mode 100644 index 0000000000..78b032bbab --- /dev/null +++ b/Client-Side Components/UI Scripts/User acknowledgement Using UI script and user preferences/UIscript.js @@ -0,0 +1,20 @@ +addLoadEvent(function() { + try { + // Skip contexts where GlideDialogWindow isn't available (e.g., Service Portal) + if (typeof GlideDialogWindow === 'undefined' || (window.NOW && NOW.sp)) + return; + var prefName = 'login.consent1'; + // Only show the dialog when the pref is explicitly 'false' + var val = (typeof getPreference === 'function') ? getPreference(prefName) : null; + var shouldShow = String(val || '').toLowerCase() === 'false'; + //alert("val"+" "+val+" "+"shouldShow"+" "+shouldShow); + if (!shouldShow) + return; + var dialog = new GlideDialogWindow('acknowledgement_dialog'); // UI Page name + dialog.setTitle('Acknowledge Message'); + dialog.setSize(500, 300); + dialog.render(); + } catch (e) { + if (console && console.warn) console.warn('ack loader error', e); + } +}); diff --git a/Client-Side Components/UI Scripts/User acknowledgement Using UI script and user preferences/readme.md b/Client-Side Components/UI Scripts/User acknowledgement Using UI script and user preferences/readme.md new file mode 100644 index 0000000000..9f1a3d5597 --- /dev/null +++ b/Client-Side Components/UI Scripts/User acknowledgement Using UI script and user preferences/readme.md @@ -0,0 +1,14 @@ +**Create a user preference as follows:** +image +**Create a UI script:** +image +This script runs during login and checks the user preference. +If the preference is set to false, it displays the acknowledgement popup by calling UI page + +**UI Page details:** +image +Set the user preference to true so that the popup will not appear for every login. + +**Output**: +**On user login:** +image diff --git a/Client-Side Components/UX Client Script Include/README.md b/Client-Side Components/UX Client Script Include/Access global object from page scripts/README.md similarity index 100% rename from Client-Side Components/UX Client Script Include/README.md rename to Client-Side Components/UX Client Script Include/Access global object from page scripts/README.md diff --git a/Client-Side Components/UX Client Script Include/Access global object from page scripts/script.js b/Client-Side Components/UX Client Script Include/Access global object from page scripts/script.js new file mode 100644 index 0000000000..36a2760bb4 --- /dev/null +++ b/Client-Side Components/UX Client Script Include/Access global object from page scripts/script.js @@ -0,0 +1,5 @@ +function include({imports}) { + var FunctionConstructor = function() {}.constructor; + var global = FunctionConstructor("return this")(); + return global; +} diff --git a/Client-Side Components/UX Client Script Include/Reusable Debounce/DebounceUtil.js b/Client-Side Components/UX Client Script Include/Reusable Debounce/DebounceUtil.js new file mode 100644 index 0000000000..508f2fc7c5 --- /dev/null +++ b/Client-Side Components/UX Client Script Include/Reusable Debounce/DebounceUtil.js @@ -0,0 +1,18 @@ +function include() { + class DebounceUtil { + + /** + * @param callbackFunc - callback function following timeout + * @param timeout - debounce timeout + * @param helpers - the helpers object passed from a UX Client Script + */ + static debounce(callbackFunc, timeout = 750, helpers) { + let timer; + return (...args) => { + helpers.timing.clearTimeout(timer); // Clear anything currently in place + timer = helpers.timing.setTimeout(() => { callbackFunc.apply(this, args); }, timeout); + }; + } + } + return DebounceUtil; +} diff --git a/Client-Side Components/UX Client Script Include/Reusable Debounce/README.md b/Client-Side Components/UX Client Script Include/Reusable Debounce/README.md new file mode 100644 index 0000000000..1549034179 --- /dev/null +++ b/Client-Side Components/UX Client Script Include/Reusable Debounce/README.md @@ -0,0 +1,32 @@ +## Add a debounce to search fields or other inputs with a client script +Inputs, Typeaheads, and other components that can be used for searching the database, caches, or local storage. However, performing a search for every keypress or other change is often unnecessary and uses more resources than strictly necessary. This UX Client Script Include is a small utility for managing debounces, allowing a 'cool-off' from inputs before performing the activity. + +### Steps +1. Create a new UX Client Script Include (`sys_ux_client_script_include`), using the script from the associated snippet +2. Create a new Client Script in UI Builder, and add the include you created in 1 as a dependency +3. Within the Client Script, import the Script Include as follows, replacing `global.DebounceUtilName` with the scope and UX Client Script Include name: + ```js + const DebounceUtil = imports["global.DebounceUtilName"](); + ``` +4. Within the Client Script, declare a `function` to be called inside the debounce function + +### Example usage +```js +/** +* @param {params} params +* @param {api} params.api +* @param {any} params.event +* @param {any} params.imports +* @param {ApiHelpers} params.helpers +*/ +function handler({api, event, helpers, imports}) { + const DebounceUtil = imports["global.DebounceUtil"](); + var debounceSearch = DebounceUtil.debounce(takeAction, 500, helpers); + debounceSearch(); + + function takeAction(){ + const searchTerm = event.payload.value; + api.setState('fullRefQuery',`nameLIKE${searchTerm}`); + } +} +``` diff --git a/Core ServiceNow APIs/GlideAggregate/Count All Open Incidents Per Priority/readme.md b/Core ServiceNow APIs/GlideAggregate/Count All Open Incidents Per Priority/readme.md new file mode 100644 index 0000000000..e02e3968dd --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/Count All Open Incidents Per Priority/readme.md @@ -0,0 +1,21 @@ +# Count Open Incidents per Priority Using GlideAggregate + +## Overview +This script dynamically calculates the **number of open incidents** for each priority level using **server-side scripting** in ServiceNow. +Priority levels typically include: ++ 1 – Critical ++ 2 – High ++ 3 – Moderate ++ 4 – Low + +The solution leverages **GlideAggregate** to efficiently count records grouped by priority. This approach is useful for: ++ Dashboards ++ Automated scripts ++ Business rules ++ SLA monitoring and reporting + +--- + +## Table and Fields ++ **Table:** `incident` ++ **Fields:** `priority`, `state` diff --git a/Core ServiceNow APIs/GlideAggregate/Count All Open Incidents Per Priority/script.js b/Core ServiceNow APIs/GlideAggregate/Count All Open Incidents Per Priority/script.js new file mode 100644 index 0000000000..3823163d7f --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/Count All Open Incidents Per Priority/script.js @@ -0,0 +1,23 @@ +(function() { + // Create GlideAggregate object on 'incident' table + var ga = new GlideAggregate('incident'); + + // Filter only open incidents (state != Closed (7)) + ga.addQuery('state', '!=', 7); + + // Group results by priority + ga.groupBy('priority'); + + // Count number of incidents per priority + ga.addAggregate('COUNT'); + + ga.query(); + + gs.info('Open Incidents by Priority:'); + + while (ga.next()) { + var priority = ga.priority.getDisplayValue(); // e.g., Critical, High + var count = ga.getAggregate('COUNT'); + gs.info(priority + ': ' + count); + } +})(); diff --git a/Core ServiceNow APIs/GlideAggregate/Count Inactive Users with Active incidents/README.md b/Core ServiceNow APIs/GlideAggregate/Count Inactive Users with Active incidents/README.md new file mode 100644 index 0000000000..d9dd5244c0 --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/Count Inactive Users with Active incidents/README.md @@ -0,0 +1,38 @@ +Count Active Incidents Assigned to Inactive Users + +This script uses GlideAggregate to efficiently count the number of active incidents assigned to inactive users. + +This is a crucial task for maintaining data hygiene and preventing incidents from being stalled due to inactive assignees. + +Overview The script performs the following actions: Initializes GlideAggregate: Creates an aggregate query on the incident table. + +Filters Records: Uses addQuery() to restrict the search to incidents that are both active (true) and assigned to a user whose active status is false. + +This filter uses a "dot-walk" on the assigned_to field to check the user's active status directly within the query. + +Aggregates by Count: Uses addAggregate() to count the number of incidents, grouping the results by assigned_to user. + +Executes and Logs: Runs the query, then loops through the results. + +For each inactive user found, it logs their name and the number of active incidents assigned to them. Use case This script is essential for regular cleanup and maintenance. + +It can be used in: Scheduled Job: Automatically run the script daily or weekly to monitor for and report on incidents assigned to inactive users. + +Installation As a Scheduled Job Navigate to System Definition > Scheduled Jobs. + + +Click New and select Automatically run a script of your choosing. Name the job (e.g., Find Incidents Assigned to Inactive Users). + + +Set your desired frequency and time. Paste the script into the Run this script field. Save and activate the job. As a Fix Script Navigate to System Definition > Fix Scripts. + +Click New. Name it (e.g., Find Active Incidents with Inactive Assignee). + + +Paste the script into the Script field. Run the script to see the results in the System Log. + + +Customization Targeted tables: Change the table name from incident to task or any other table with an assigned_to field to check for active records assigned to inactive users. + + +Automated reassignment: Extend the script to automatically reassign the incidents to a group or another user. This is a common practice to ensure that tickets do not get stuck in the queue. Email notification: Instead of logging the information, modify the script to send an email notification to the group manager or another stakeholder with the list of incidents needing attention. diff --git a/Core ServiceNow APIs/GlideAggregate/Count Inactive Users with Active incidents/script.js b/Core ServiceNow APIs/GlideAggregate/Count Inactive Users with Active incidents/script.js new file mode 100644 index 0000000000..25409d7483 --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/Count Inactive Users with Active incidents/script.js @@ -0,0 +1,32 @@ +/* +This script uses GlideAggregate to find the number of active incidents +that are assigned to users who are currently marked as inactive. +GlideAggregate is used instead of a standard GlideRecord query +because it is more efficient for performing calculations like COUNT +directly in the database. +*/ +var ga = new GlideAggregate('incident'); + +// Query for active incidents. +ga.addQuery('active', true); + +// Use dot-walking to query for incidents assigned to inactive users. +ga.addQuery('assigned_to.active', false); + +// Add an aggregate function to count the incidents, grouped by the assigned user. +ga.addAggregate('COUNT', 'assigned_to'); + +// Execute the query. +ga.query(); + +// Process the results of the aggregate query. +while (ga.next()) { + // Get the display name of the inactive user from the current record. + var inactiveUser = ga.assigned_to.getDisplayValue(); + + // Get the count of active incidents for this specific user. + var incidentCount = ga.getAggregate('COUNT', 'assigned_to'); + + // Log the result to the system logs. + gs.info("Inactive user " + inactiveUser + " has " + incidentCount + " active incidents."); +} diff --git a/Core ServiceNow APIs/GlideAggregate/Count open Incidents per Priority and State using GlideAggregate/README.md b/Core ServiceNow APIs/GlideAggregate/Count open Incidents per Priority and State using GlideAggregate/README.md new file mode 100644 index 0000000000..608e9f25cc --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/Count open Incidents per Priority and State using GlideAggregate/README.md @@ -0,0 +1,29 @@ +# Count open Incidents per Priority and State using GlideAggregate + +## Overview +This script will dynamically calculate the **number of open incidents** for each priority level and also give you a total for what +current state the Incident is in using **server-side scripting** +Priority levels typically include: ++ 1 – Critical ++ 2 – High ++ 3 – Moderate ++ 4 – Low + +Incident State typically include: ++ New ++ In Progress ++ On Hold ++ Resolved ++ Closed ++ Canceled + +The scripting solution leverages **GlideAggregate** to efficiently count records grouped by priority and state. This scripts approach +is useful for: ++ Dashboards ++ Business Rules ++ SLA monitoring and reporting + +-- +## Table and Fields ++ **Table:** Task ++ **Fields:** Priority, State diff --git a/Core ServiceNow APIs/GlideAggregate/Count open Incidents per Priority and State using GlideAggregate/script.js b/Core ServiceNow APIs/GlideAggregate/Count open Incidents per Priority and State using GlideAggregate/script.js new file mode 100644 index 0000000000..662b5d33e1 --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/Count open Incidents per Priority and State using GlideAggregate/script.js @@ -0,0 +1,45 @@ +/* +*Going to define the Incident Closed and Canceled state since we dont want those records as part of our query. +*Also going to leverage the IncidentStateSNC script from ServiceNow +*/ + +/* +*Going to define the Incident Closed and Canceled state since we dont want those records as part of our query. +*/ +var incident_close = IncidentStateSNC.CLOSED; +var incident_canceled = IncidentStateSNC.CANCELED; +var incident_state_query = incident_close + "," + incident_canceled; + +/* +*Creating the Incident State value object that will house the correct incident state since we are working from the Task table. +*Leveraging the IncidentStateSNC script from ServiceNow to get the values that they should be +*/ +var incident_states = { + '1':'New', + '2':'In Progress', + '3':'On Hold', + '6':'Resolved', + '7':'Closed', + '8':'Canceled' +}; + +//Going to create the GlideAggregate object +var ga = new GlideAggregate('task'); +ga.addQuery('state', 'NOT IN', incident_state_query); //Going to exclude the canceled and closed incidents +ga.addQuery('sys_class_name', 'incident'); //Since working on the Task table need to grab only Incident records with task type +ga.groupBy('state'); +ga.groubBy('count'); +ga.addAggregate('COUNT'); +ga.query() + +gs.info('The following is a list of Open Incident records'); + +while (ga.next()) { + + var priorityValue = ga.getDisplayValue('priority'); + var state = ga.getValue('state'); + var count = ga.getValue('COUNT'); + + gs.info("There are a total of: " + count + " Incidents with a priority of " + priorityValue + " and in a state of " + incident_states[state]); + +} diff --git a/Core ServiceNow APIs/GlideAggregate/Create Problem based on incident volume/README.md b/Core ServiceNow APIs/GlideAggregate/Create Problem based on incident volume/README.md new file mode 100644 index 0000000000..6f7afc5eac --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/Create Problem based on incident volume/README.md @@ -0,0 +1,37 @@ +Key features +Automatic problem creation: The script uses a GlideAggregate query to count the number of incidents opened for a specific CI. +Time-based threshold: Problems are only created if more than five incidents are opened within a 60-minute window. +Targeted incidents: The script focuses on incidents related to the same CI, making it effective for identifying recurring infrastructure issues. +Customizable conditions: The number of incidents and the time frame are easily configurable within the script. +Efficient performance: The use of GlideAggregate ensures the database is queried efficiently, minimizing performance impact. + +How it works +The script is designed to be executed as a server-side script, typically within a Business Rule. When an incident is inserted or updated, the script performs the following actions: +Queries incidents: It executes a GlideAggregate query on the incident table. +Sets conditions: The query is filtered to count all incidents that meet the following conditions: +Same CI: The incident's cmdb_ci matches the cmdb_ci of the current record. +Within the last hour: The opened_at time is within the last 60 minutes. +Evaluates count: After the query is run, the script checks if the count of matching incidents exceeds the threshold (in this case, 5). +Creates problem: If the threshold is exceeded, a new problem record is initialized. +The short_description is automatically populated with a descriptive message. +The cmdb_ci is linked to the new problem record. +The new record is then inserted into the database. +Implementation steps +Create a Business Rule: +Navigate to System Definition > Business Rules. +Click New. +Configure the Business Rule: +Name: Auto Create Problem from Multiple Incidents +Table: Incident [incident] +Advanced: true +When to run: Select after and check the Insert and Update checkboxes. This ensures the script runs after an incident has been saved. +Filter conditions: Optionally, you can add conditions to limit when the script runs (e.g., cmdb_ci is not empty). +Add the script: +Navigate to the Advanced tab. +Copy and paste the script into the Script field. +Customize (optional): +Number of incidents: Change the > 5 value to adjust the threshold. +Time frame: Adjust the gs.minutesAgoStart(60) value to change the time window. +Other conditions: If you need to check for specific incident statuses or categories, add more addQuery lines to the GlideAggregate call. +Save the Business Rule. +Customization examples diff --git a/Core ServiceNow APIs/GlideAggregate/Create Problem based on incident volume/script.js b/Core ServiceNow APIs/GlideAggregate/Create Problem based on incident volume/script.js new file mode 100644 index 0000000000..dcec063918 --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/Create Problem based on incident volume/script.js @@ -0,0 +1,13 @@ +var incidentCheck = new GlideAggregate('incident'); + incidentCheck.addQuery('cmdb_ci', current.cmdb_ci); // Or any specific CI + incidentCheck.addQuery('opened_at', '>', 'javascript:gs.minutesAgoStart(60)'); + incidentCheck.addAggregate('COUNT'); + incidentCheck.query(); + + if (incidentCheck.next() && parseInt(incidentCheck.getAggregate('COUNT')) > 5) { + var problem = new GlideRecord('problem'); + problem.initialize(); + problem.short_description = 'Multiple incidents reported for CI: ' + current.cmdb_ci.getDisplayValue(); + problem.cmdb_ci = current.cmdb_ci; + problem.insert(); + } diff --git a/Core ServiceNow APIs/GlideAggregate/Find Oldest Open Incidents per Group/README.md b/Core ServiceNow APIs/GlideAggregate/Find Oldest Open Incidents per Group/README.md new file mode 100644 index 0000000000..46b62e333c --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/Find Oldest Open Incidents per Group/README.md @@ -0,0 +1,38 @@ +ServiceNow Script: Find Oldest Open Incidents per Group +This script leverages GlideAggregate to efficiently find the oldest active incident for each assignment group. This is a powerful tool for monitoring and reporting on potential service level agreement (SLA) risks and improving incident management processes. +Overview +The script performs the following actions: +Initializes GlideAggregate: Creates an aggregate query on the incident table. +Filters Active Incidents: Uses addActiveQuery() to restrict the search to only open (active) incidents. +Aggregates by Minimum Date: Finds the minimum (MIN) opened_at date, which represents the oldest record. +Groups by Assignment Group: Groups the results by the assignment_group to get a separate result for each team. +Iterates and Logs: Loops through the query results and logs the assignment group and the opening date of its oldest open incident. +How to use +This script is intended to be used in a server-side context within a ServiceNow instance. Common use cases include: +Scheduled Job: Run this script on a regular schedule (e.g., daily) to generate a report on aging incidents. +Script Include: Incorporate the logic into a reusable function within a Script Include, allowing other scripts to call it. + +Use code with caution. + +Installation +As a Scheduled Job +Navigate to System Definition > Scheduled Jobs. +Click New and select Automatically run a script of your choosing. +Name the job (e.g., Find Oldest Open Incidents). +Set your desired frequency and time. +Paste the script into the Run this script field. +Save and activate the job. +As a Script Include +Navigate to System Definition > Script Includes. +Click New. +Name it (e.g., IncidentHelper). +API Name: global.IncidentHelper + + +Customization +Change the output: Modify the gs.info() line to instead write to a custom log, send an email, or create a report. +Refine the query: Add more addQuery() statements to filter incidents by other criteria, such as priority or category. +Change the aggregate: Use MAX instead of MIN to find the newest incident in each group. +Get incident details: To get the actual incident record (e.g., its number), you would need to perform a secondary GlideRecord query based on the aggregated data. +Dependencies +This script uses standard ServiceNow APIs (GlideAggregate, gs). No external libraries are required. diff --git a/Core ServiceNow APIs/GlideAggregate/Find Oldest Open Incidents per Group/script.js b/Core ServiceNow APIs/GlideAggregate/Find Oldest Open Incidents per Group/script.js new file mode 100644 index 0000000000..b626b6c141 --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/Find Oldest Open Incidents per Group/script.js @@ -0,0 +1,11 @@ +var ga = new GlideAggregate('incident'); + ga.addActiveQuery(); + ga.addAggregate('MIN', 'opened_at'); + ga.groupBy('assignment_group'); + ga.query(); + + while (ga.next()) { + var group = ga.assignment_group.getDisplayValue(); + var oldestIncidentDate = ga.getAggregate('MIN', 'opened_at'); + gs.info("Oldest open incident for " + group + " was created on: " + oldestIncidentDate); + } diff --git a/Core ServiceNow APIs/GlideAggregate/Get top 5 CIs with most number of Open Incidents/README.md b/Core ServiceNow APIs/GlideAggregate/Get top 5 CIs with most number of Open Incidents/README.md new file mode 100644 index 0000000000..c3012906c7 --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/Get top 5 CIs with most number of Open Incidents/README.md @@ -0,0 +1,13 @@ +Use-case: +**Fetch Top 5 CIs with the most number of Open Incidents along with the count** + +Type of Script writted: **Background Script** + +**How the code works:** +The code uses the GlideAggregate API to efficiently calculate and retrieve the results - +1. A GlideAggregate query is initiated on the Incident table. The query is restricted to only active Incidents. +2. The query instructs the database to COUNT records grouped by the configuration item(cmdb_ci). +3. Furthermore, the records are instructed to be in descending order of number of incidents related to one CI, also a limit + of 5 records are applied to be fetched. +4. The query is executed and a loop is iterated over these 5 records to fetch and print + the CI name and its corresponding incident count. diff --git a/Core ServiceNow APIs/GlideAggregate/Get top 5 CIs with most number of Open Incidents/getCIwithmostActiveInc.js b/Core ServiceNow APIs/GlideAggregate/Get top 5 CIs with most number of Open Incidents/getCIwithmostActiveInc.js new file mode 100644 index 0000000000..2abf6fdb8b --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/Get top 5 CIs with most number of Open Incidents/getCIwithmostActiveInc.js @@ -0,0 +1,24 @@ +var countOfCI = 5; +var inc = new GlideAggregate('incident'); +inc.addActiveQuery(); +inc.addAggregate('COUNT', 'cmdb_ci'); +inc.groupBy('cmdb_ci'); +inc.orderByAggregate('COUNT', 'cmdb_ci'); +inc.setLimit(countOfCI); +inc.query(); +gs.info('---Top ' + countOfCI + ' CIs with Most Open Incidents---'); + + +while (inc.next()) { + var ciName; + var ciSysID = inc.cmdb_ci; + var count = inc.getAggregate('COUNT', 'cmdb_ci'); + var ci = new GlideRecord('cmdb_ci'); + if (ci.get(ciSysID)) { //retrieving the CI record + ciName = ci.name.toString(); + } else { + ciName = 'Invalid CI'; + } + + gs.info('. CI: ' + ciName + ' | Count of Inc: ' + count); +} diff --git a/Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group/PercentileMetrics.js b/Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group/PercentileMetrics.js new file mode 100644 index 0000000000..2aab7892a0 --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group/PercentileMetrics.js @@ -0,0 +1,82 @@ +// Script Include: PercentileMetrics +// Purpose: Compute percentile resolution times by group using nearest-rank selection. +// Scope: global or scoped. Client callable false. + +var PercentileMetrics = Class.create(); +PercentileMetrics.prototype = { + initialize: function() {}, + + /** + * Compute percentiles for incident resolution times by group. + * @param {Object} options + * - windowDays {Number} lookback window (default 30) + * - groupField {String} field to group by (default 'assignment_group') + * - percentiles {Array} e.g. [0.5, 0.9] + * - table {String} table name (default 'incident') + * @returns {Array} [{ group: , count: N, avgMins: X, p: { '0.5': v, '0.9': v } }] + */ + resolutionPercentiles: function(options) { + var opts = options || {}; + var table = opts.table || 'incident'; + var groupField = opts.groupField || 'assignment_group'; + var windowDays = Number(opts.windowDays || 30); + var pct = Array.isArray(opts.percentiles) && opts.percentiles.length ? opts.percentiles : [0.5, 0.9]; + + // Build date cutoff for resolved incidents + var cutoff = new GlideDateTime(); + cutoff.addDaysUTC(-windowDays); + + // First pass: find candidate groups with counts and avg + var ga = new GlideAggregate(table); + ga.addQuery('resolved_at', '>=', cutoff); + ga.addQuery('state', '>=', 6); // resolved/closed states + ga.addAggregate('COUNT'); + ga.addAggregate('AVG', 'calendar_duration'); // average of resolution duration + ga.groupBy(groupField); + ga.query(); + + var results = []; + while (ga.next()) { + var groupId = ga.getValue(groupField); + var count = parseInt(ga.getAggregate('COUNT'), 10) || 0; + if (!groupId || count === 0) continue; + + // Second pass: ordered sample to pick percentile ranks + var ordered = new GlideRecord(table); + ordered.addQuery('resolved_at', '>=', cutoff); + ordered.addQuery('state', '>=', '6'); + ordered.addQuery(groupField, groupId); + ordered.addNotNullQuery('closed_at'); + // Approx resolution minutes using dateDiff: closed_at - opened_at in minutes + ordered.addQuery('opened_at', 'ISNOTEMPTY'); + ordered.addQuery('closed_at', 'ISNOTEMPTY'); + ordered.orderBy('closed_at'); // for stability + ordered.query(); + + var durations = []; + while (ordered.next()) { + var opened = String(ordered.getValue('opened_at')); + var closed = String(ordered.getValue('closed_at')); + var mins = gs.dateDiff(opened, closed, true) / 60; // seconds -> minutes + durations.push(mins); + } + durations.sort(function(a, b) { return a - b; }); + + var pvals = {}; + pct.forEach(function(p) { + var rank = Math.max(1, Math.ceil(p * durations.length)); // nearest-rank + pvals[String(p)] = durations.length ? Math.round(durations[rank - 1]) : 0; + }); + + results.push({ + group: groupId, + count: count, + avgMins: Math.round(parseFloat(ga.getAggregate('AVG', 'calendar_duration')) / 60), + p: pvals + }); + } + return results; + }, + + type: 'PercentileMetrics' +}; diff --git a/Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group/README.md b/Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group/README.md new file mode 100644 index 0000000000..140d0371e0 --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group/README.md @@ -0,0 +1,27 @@ +# Incident resolution percentile by assignment group + +## What this solves +Leaders often ask for P50 or P90 of incident resolution time by assignment group. Out-of-box reports provide averages, but percentiles are more meaningful for skewed distributions. This utility computes configurable percentiles from incident resolution durations. + +## Where to use +- Script Include callable from Background Scripts, Scheduled Jobs, or Flow Actions +- Example Background Script is included + +## How it works +- Uses `GlideAggregate` to get candidate groups with resolved incidents in a time window +- For each group, queries resolved incidents ordered by resolution duration (ascending) +- Picks percentile ranks (for example 0.5, 0.9) using nearest-rank method +- Returns a simple object per group with count, average minutes, and requested percentiles + +## Configure +- `WINDOW_DAYS`: number of days to look back (default 30) +- `GROUP_FIELD`: field to group by (default `assignment_group`) +- Percentiles array (for example `[0.5, 0.9]`) + +## References +- GlideAggregate API + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideAggregate/concept/c_GlideAggregateAPI.html +- GlideRecord API + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideRecord/concept/c_GlideRecordAPI.html +- GlideDateTime API + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideDateTime/concept/c_GlideDateTimeAPI.html diff --git a/Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group/example_background_usage.js b/Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group/example_background_usage.js new file mode 100644 index 0000000000..a39ae0118d --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group/example_background_usage.js @@ -0,0 +1,13 @@ +// Background Script: example usage for PercentileMetrics +(function() { + var util = new PercentileMetrics(); + var out = util.resolutionPercentiles({ + windowDays: 30, + groupField: 'assignment_group', + percentiles: [0.5, 0.9, 0.95] + }); + + out.forEach(function(r) { + gs.info('Group=' + r.group + ' count=' + r.count + ' avg=' + r.avgMins + 'm P50=' + r.p['0.5'] + 'm P90=' + r.p['0.9'] + 'm P95=' + r.p['0.95'] + 'm'); + }); +})(); diff --git a/Core ServiceNow APIs/GlideAggregate/LicensedUserCount/licensed_user_count_by_role.js b/Core ServiceNow APIs/GlideAggregate/LicensedUserCount/licensed_user_count_by_role.js new file mode 100644 index 0000000000..180942c820 --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/LicensedUserCount/licensed_user_count_by_role.js @@ -0,0 +1,23 @@ +(function() { + // Purpose: Count how many users hold each licensed role + // Roles: sys_approver, itil, business_stakeholder, admin + + var roles = ['sys_approver', 'itil', 'business_stakeholder', 'admin']; + + for (var i = 0; i < roles.length; i++) { + var roleName = roles[i]; + + var ga = new GlideAggregate('sys_user_has_role'); + ga.addQuery('role.name', roleName); + ga.addAggregate('COUNT'); + ga.query(); + + if (ga.next()) { + var count = parseInt(ga.getAggregate('COUNT'), 10); + gs.info(roleName + ': ' + count + ' licensed users'); + } else { + gs.info(roleName + ': no users found.'); + } + } + +})(); diff --git a/Core ServiceNow APIs/GlideAggregate/LicensedUserCount/readme.md b/Core ServiceNow APIs/GlideAggregate/LicensedUserCount/readme.md new file mode 100644 index 0000000000..d5617f2bc2 --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/LicensedUserCount/readme.md @@ -0,0 +1,14 @@ +# Licensed User Count by Role Using GlideAggregate + +# Overview +This script counts how many **licensed users** hold specific ServiceNow roles using the `GlideAggregate` API. +It’s useful for **license compliance**, **role audits**, and **access management reporting**. + +The licensed roles analyzed: +- sys_approver +- itil +- business_stakeholder +- admin + +# Objective +To provide a simple, fast, and accurate way to count licensed users per role directly at the database level using `GlideAggregate`. diff --git a/Core ServiceNow APIs/GlideAggregate/SLA Compliance Ratio by Assignment Group/readme.md b/Core ServiceNow APIs/GlideAggregate/SLA Compliance Ratio by Assignment Group/readme.md new file mode 100644 index 0000000000..a85f5490ec --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/SLA Compliance Ratio by Assignment Group/readme.md @@ -0,0 +1,23 @@ +Overview + +This script calculates the SLA breach percentage for each assignment group based on closed incidents in ServiceNow. +It leverages GlideAggregate to count both total SLAs and breached SLAs efficiently, providing key SLA performance insights. + +Useful for: + • SLA dashboards + • Support performance tracking + • Service improvement reports + +Objective + +To determine, for each assignment group: + • How many SLAs were closed + • How many of those breached + • The resulting SLA compliance percentage + +Script Logic + 1. Query the task_sla table. + 2. Filter for closed SLAs linked to incidents. + 3. Aggregate total SLAs (COUNT) and breached SLAs (COUNT, 'breach', 'true'). + 4. Group results by assignment group. + 5. Calculate breach percentage. diff --git a/Core ServiceNow APIs/GlideAggregate/SLA Compliance Ratio by Assignment Group/script.js b/Core ServiceNow APIs/GlideAggregate/SLA Compliance Ratio by Assignment Group/script.js new file mode 100644 index 0000000000..a31caa5a59 --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/SLA Compliance Ratio by Assignment Group/script.js @@ -0,0 +1,18 @@ +(function() { + var ga = new GlideAggregate('task_sla'); + ga.addEncodedQuery('task.sys_class_name=incident^active=false'); + ga.addAggregate('COUNT'); // All SLAs + ga.addAggregate('COUNT', 'breach', 'true'); // breached SLAs + ga.groupBy('task.assignment_group'); + ga.query(); + + gs.info('SLA Compliance Ratio by Group'); + + while (ga.next()) { + var total = parseInt(ga.getAggregate('COUNT')); + var breached = parseInt(ga.getAggregate('COUNT', 'breach', 'true')); + var rate = breached ? ((breached / total) * 100).toFixed(2) : 0; + gs.info(ga.getDisplayValue('task.assignment_group') + ': ' + rate + '% breached (' + breached + '/' + total + ')'); + } + +})(); diff --git a/Core ServiceNow APIs/GlideAggregate/SimpleGlideAggregate/Readme.md b/Core ServiceNow APIs/GlideAggregate/SimpleGlideAggregate/Readme.md new file mode 100644 index 0000000000..e1992df515 --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/SimpleGlideAggregate/Readme.md @@ -0,0 +1,40 @@ +SimpleGlideAggregate Utility +**Overview** +SimpleGlideAggregate is a developer utility Script Include for ServiceNow that provides a simplified, chainable API around the native GlideAggregate class. It abstracts complexities of writing aggregation queries and returns results in an easy-to-use JavaScript object format. +Because OOTB glideAggregate API is little bit different so I tried to create a new function with a simper version. +**Purpose** +Simplify aggregate queries such as COUNT, SUM, MIN, and MAX for developers, especially those less familiar with GlideAggregate methods. +Provide an intuitive interface for common aggregation operations with chaining support. +Facilitate viewing aggregate results alongside individual records matching the same criteria for better analysis. + +**Sample Usage of the functions :** + var sga = new SimpleGlideAggregate('incident'); + + // Build query and add all supported aggregates + var results = sga + .addQuery('active', true) // Filter: active incidents only + .addQuery('priority', '>=', 2) // Priority 2 or higher + .count() // Count matching records + .sum('duration') // Sum of duration field instead of impact + .min('priority') // Minimum priority value in results + .max('sys_updated_on') // Most recent update timestamp + .execute(); + + gs.info('Aggregate Results:'); + gs.info('Count: ' + results.COUNT); + gs.info('Sum of Duration: ' + (results.SUM_duration !== undefined ? results.SUM_duration : 'N/A')); + gs.info('Minimum Priority: ' + (results.MIN_priority !== undefined ? results.MIN_priority : 'N/A')); + gs.info('Most Recent Update (max sys_updated_on timestamp): ' + (results.MAX_sys_updated_on !== undefined ? results.MAX_sys_updated_on : 'N/A')); + + // Optionally fetch some matching record details to complement the aggregate data + var gr = new GlideRecord('incident'); + gr.addQuery('active', true); + gr.addQuery('priority', '>=', 2); + gr.orderByDesc('sys_updated_on'); + gr.setLimit(5); + gr.query(); + + gs.info('Sample Matching Incidents:'); + while (gr.next()) { + gs.info('Number: ' + gr.getValue('number') + ', Priority: ' + gr.getValue('priority') + ', Updated: ' + gr.getValue('sys_updated_on')); + } diff --git a/Core ServiceNow APIs/GlideAggregate/SimpleGlideAggregate/SimpleGlideAggregate.js b/Core ServiceNow APIs/GlideAggregate/SimpleGlideAggregate/SimpleGlideAggregate.js new file mode 100644 index 0000000000..0334807b61 --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/SimpleGlideAggregate/SimpleGlideAggregate.js @@ -0,0 +1,102 @@ +var SimpleGlideAggregate = Class.create(); +SimpleGlideAggregate.prototype = { + initialize: function(tableName) { + if (!tableName) { + throw new Error("Table name is required."); + } + this._table = tableName; + this._ga = new GlideAggregate(tableName); + this._fields = []; + this._conditionsAdded = false; + }, + + /** + * Adds a query condition. + * Usage: addQuery('priority', '=', '1') or addQuery('active', true) + */ + addQuery: function(field, operator, value) { + if (value === undefined) { + this._ga.addQuery(field, operator); + } else { + this._ga.addQuery(field, operator, value); + } + this._conditionsAdded = true; + return this; + }, + + /** + * Adds COUNT aggregate. + */ + count: function() { + this._fields.push({type: 'COUNT', field: null}); + return this; + }, + + /** + * Adds SUM aggregate on a field. + */ + sum: function(field) { + if (!field) throw new Error("Field name required for sum."); + this._fields.push({type: 'SUM', field: field}); + return this; + }, + + /** + * Adds MIN aggregate on a field. + */ + min: function(field) { + if (!field) throw new Error("Field name required for min."); + this._fields.push({type: 'MIN', field: field}); + return this; + }, + + /** + * Adds MAX aggregate on a field. + */ + max: function(field) { + if (!field) throw new Error("Field name required for max."); + this._fields.push({type: 'MAX', field: field}); + return this; + }, + + /** + * Executes the aggregate query and returns results as an object. + * Keys are aggregate type or type_field (for field aggregates). + */ + execute: function() { + var self = this; + + if (this._fields.length === 0) { + throw new Error("At least one aggregate function must be added."); + } + + this._fields.forEach(function(agg) { + if (agg.field) { + self._ga.addAggregate(agg.type, agg.field); + } else { + self._ga.addAggregate(agg.type); + } + }); + + this._ga.query(); + + var results = {}; + if (this._ga.next()) { + this._fields.forEach(function(agg) { + var key = agg.field ? agg.type + '_' + agg.field : agg.type; + var value = agg.field ? self._ga.getAggregate(agg.type, agg.field) : self._ga.getAggregate(agg.type); + results[key] = agg.type === 'COUNT' ? parseInt(value, 10) : parseFloat(value); + }); + } else { + // No rows matched, all aggregates 0 or null + this._fields.forEach(function(agg) { + var key = agg.field ? agg.type + '_' + agg.field : agg.type; + results[key] = 0; + }); + } + + return results; + }, + + type: 'SimpleGlideAggregate' +}; diff --git a/Core ServiceNow APIs/GlideAjax/Check Weekend - Client Side/README.md b/Core ServiceNow APIs/GlideAjax/Check Weekend - Client Side/README.md new file mode 100644 index 0000000000..3f0696495c --- /dev/null +++ b/Core ServiceNow APIs/GlideAjax/Check Weekend - Client Side/README.md @@ -0,0 +1,19 @@ + +# ServiceNow Weekend Checker (Client-Side Utility) + +A reusable client-side script to detect weekends (Saturday/Sunday) and modify ServiceNow form behaviour accordingly. + +# Overview + +This project contains a simple, reusable utility for determining if the current date (based on the user’s browser timezone) falls on a weekend. +It’s ideal for Client Scripts, Catalog Client Scripts, or Service Portal widgets. + +# Features + +- ✅ Lightweight — no dependencies +- ✅ Works across all client script contexts +- ✅ Includes helper method for automatic info messages + +## ⚙️ Usage +Create the Script Include present in the WeekendChecker.js file +Use the Client-Side Script to call GlideAjax and determine if the day/date is a weekend or not. diff --git a/Core ServiceNow APIs/GlideAjax/Check Weekend - Client Side/WeekendChecker.js b/Core ServiceNow APIs/GlideAjax/Check Weekend - Client Side/WeekendChecker.js new file mode 100644 index 0000000000..b3b624bb92 --- /dev/null +++ b/Core ServiceNow APIs/GlideAjax/Check Weekend - Client Side/WeekendChecker.js @@ -0,0 +1,28 @@ +//Script Include +var DateUtilityAjax = Class.create(); +DateUtilityAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, { +//Returns true if the current server date is a weekend + isWeekend: function() { + var gdt = new GlideDateTime(); + var dayOfWeek = gdt.getDayOfWeekLocalTime(); // Sunday = 1, Monday = 2, ..., Saturday = 7 in Servicenow + return (dayOfWeek === 1 || dayOfWeek === 7); + }, + + type: 'DateUtilityAjax' +}); + +//Client Script - GlideAjax +function onLoad() { + var ga = new GlideAjax('DateUtilityAjax'); + ga.addParam('sysparm_name', 'isWeekend'); + + ga.getXMLAnswer(function(answer) { + var isWeekend = (answer === 'true'); + + if (isWeekend) { + g_form.addInfoMessage('Server reports it’s the weekend - some actions are restricted.'); + } else { + g_form.addInfoMessage('Weekday detected - normal operations available.'); + } + }); +} diff --git a/Core ServiceNow APIs/GlideDate/Extract and Convert Date in a Text or String to GlideDate Format.js b/Core ServiceNow APIs/GlideDate/Convert text date to GlideDate Format/Extract and Convert Date in a Text or String to GlideDate Format.js similarity index 100% rename from Core ServiceNow APIs/GlideDate/Extract and Convert Date in a Text or String to GlideDate Format.js rename to Core ServiceNow APIs/GlideDate/Convert text date to GlideDate Format/Extract and Convert Date in a Text or String to GlideDate Format.js diff --git a/Core ServiceNow APIs/GlideDate/README.md b/Core ServiceNow APIs/GlideDate/Convert text date to GlideDate Format/README.md similarity index 100% rename from Core ServiceNow APIs/GlideDate/README.md rename to Core ServiceNow APIs/GlideDate/Convert text date to GlideDate Format/README.md diff --git a/Core ServiceNow APIs/GlideDateTime/Business time utilities (add, diff, next open, in schedule)/BusinessTimeUtils.js b/Core ServiceNow APIs/GlideDateTime/Business time utilities (add, diff, next open, in schedule)/BusinessTimeUtils.js new file mode 100644 index 0000000000..7f5eeb61d8 --- /dev/null +++ b/Core ServiceNow APIs/GlideDateTime/Business time utilities (add, diff, next open, in schedule)/BusinessTimeUtils.js @@ -0,0 +1,136 @@ +var BusinessTimeUtils = Class.create(); +BusinessTimeUtils.prototype = { + initialize: function() {}, + + /** + * Add working hours to a start date, respecting schedule and holidays. + * @param {String} scheduleSysId - sys_id of the GlideSchedule + * @param {Number} hoursToAdd - working hours to add (can be fractional) + * @param {GlideDateTime|String} startGdt - start time; if string, must be ISO/Glide-friendly + * @param {String} [timeZone] - optional IANA TZ, else schedule/instance TZ + * @returns {Object} { ok:Boolean, due:GlideDateTime|null, minutesAdded:Number, message:String } + */ + addWorkingHours: function(scheduleSysId, hoursToAdd, startGdt, timeZone) { + var result = { ok: false, due: null, minutesAdded: 0, message: '' }; + try { + this._assertSchedule(scheduleSysId); + var start = this._toGdt(startGdt); + var msToAdd = Math.round(Number(hoursToAdd) * 60 * 60 * 1000); + if (!isFinite(msToAdd) || msToAdd <= 0) { + result.message = 'hoursToAdd must be > 0'; + return result; + } + + var sched = new GlideSchedule(scheduleSysId, timeZone || ''); + var due = sched.add(new GlideDateTime(start), msToAdd); // returns GlideDateTime + + // How many working minutes were added according to the schedule + var mins = Math.round(sched.duration(start, due) / 60000); + + result.ok = true; + result.due = due; + result.minutesAdded = mins; + return result; + } catch (e) { + result.message = String(e); + return result; + } + }, + + /** + * Calculate working minutes between two times using the schedule. + * @returns {Object} { ok:Boolean, minutes:Number, message:String } + */ + workingMinutesBetween: function(scheduleSysId, startGdt, endGdt, timeZone) { + var out = { ok: false, minutes: 0, message: '' }; + try { + this._assertSchedule(scheduleSysId); + var start = this._toGdt(startGdt); + var end = this._toGdt(endGdt); + if (start.after(end)) { + out.message = 'start must be <= end'; + return out; + } + var sched = new GlideSchedule(scheduleSysId, timeZone || ''); + out.minutes = Math.round(sched.duration(start, end) / 60000); + out.ok = true; + return out; + } catch (e) { + out.message = String(e); + return out; + } + }, + + /** + * Find the next time that is inside the schedule window at or after fromGdt. + * @returns {Object} { ok:Boolean, nextOpen:GlideDateTime|null, message:String } + */ + nextOpen: function(scheduleSysId, fromGdt, timeZone) { + var out = { ok: false, nextOpen: null, message: '' }; + try { + this._assertSchedule(scheduleSysId); + var from = this._toGdt(fromGdt); + var sched = new GlideSchedule(scheduleSysId, timeZone || ''); + + // If already inside schedule, return the same timestamp + if (sched.isInSchedule(from)) { + out.ok = true; + out.nextOpen = new GlideDateTime(from); + return out; + } + + // Move forward minute by minute until we hit an in-schedule time, with a sane cap + var probe = new GlideDateTime(from); + var limitMinutes = 24 * 60 * 30; // cap search to 30 days + for (var i = 0; i < limitMinutes; i++) { + probe.addSecondsUTC(60); + if (sched.isInSchedule(probe)) { + out.ok = true; + out.nextOpen = new GlideDateTime(probe); + return out; + } + } + out.message = 'No open window found within 30 days'; + return out; + } catch (e) { + out.message = String(e); + return out; + } + }, + + /** + * Check if a time is inside the schedule. + * @returns {Object} { ok:Boolean, inSchedule:Boolean, message:String } + */ + isInSchedule: function(scheduleSysId, whenGdt, timeZone) { + var out = { ok: false, inSchedule: false, message: '' }; + try { + this._assertSchedule(scheduleSysId); + var when = this._toGdt(whenGdt); + var sched = new GlideSchedule(scheduleSysId, timeZone || ''); + out.inSchedule = sched.isInSchedule(when); + out.ok = true; + return out; + } catch (e) { + out.message = String(e); + return out; + } + }, + + // ---------- helpers ---------- + + _toGdt: function(val) { + if (val instanceof GlideDateTime) return new GlideDateTime(val); + if (typeof val === 'string' && val) return new GlideDateTime(val); + if (!val) return new GlideDateTime(); // default now + throw 'Unsupported datetime value'; + }, + + _assertSchedule: function(sysId) { + if (!sysId) throw 'scheduleSysId is required'; + var gr = new GlideRecord('cmn_schedule'); + if (!gr.get(sysId)) throw 'Schedule not found: ' + sysId; + }, + + type: 'BusinessTimeUtils' +}; diff --git a/Core ServiceNow APIs/GlideDateTime/Business time utilities (add, diff, next open, in schedule)/README.md b/Core ServiceNow APIs/GlideDateTime/Business time utilities (add, diff, next open, in schedule)/README.md new file mode 100644 index 0000000000..3642bf3d0f --- /dev/null +++ b/Core ServiceNow APIs/GlideDateTime/Business time utilities (add, diff, next open, in schedule)/README.md @@ -0,0 +1,31 @@ +# Business time utilities (add, diff, next open, in schedule) + +## What this solves +Teams repeatedly reimplement the same business-time maths. This utility wraps `GlideSchedule` with four practical helpers so you can: +- Add N working hours to a start date +- Calculate working minutes between two dates +- Find the next open time inside a schedule +- Check if a specific time is inside the schedule window + +All functions return simple objects that are easy to log, test, and consume in Flows or Rules. + +## Where to use +- Script Include in global or scoped app +- Call from Business Rules, Flow Actions, or Background Scripts + +## Functions +- `addWorkingHours(scheduleSysId, hoursToAdd, startGdt, tz)` +- `workingMinutesBetween(scheduleSysId, startGdt, endGdt, tz)` +- `nextOpen(scheduleSysId, fromGdt, tz)` +- `isInSchedule(scheduleSysId, whenGdt, tz)` + +## Notes +- The schedule determines both business hours and holidays. +- `tz` is optional; if omitted, the schedule’s TZ or instance default applies. +- All inputs accept either `GlideDateTime` or ISO strings (UTC-safe). + +## References +- GlideSchedule API + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideSchedule/concept/c_GlideScheduleAPI.html +- GlideDateTime API + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideDateTime/concept/c_GlideDateTimeAPI.html diff --git a/Core ServiceNow APIs/GlideDateTime/Business time utilities (add, diff, next open, in schedule)/example_background_usage.js b/Core ServiceNow APIs/GlideDateTime/Business time utilities (add, diff, next open, in schedule)/example_background_usage.js new file mode 100644 index 0000000000..82b8075838 --- /dev/null +++ b/Core ServiceNow APIs/GlideDateTime/Business time utilities (add, diff, next open, in schedule)/example_background_usage.js @@ -0,0 +1,21 @@ +// Background Script demo for BusinessTimeUtils +(function() { + var SCHEDULE_SYS_ID = 'PUT_YOUR_SCHEDULE_SYS_ID_HERE'; + var TZ = 'Europe/London'; + + var util = new BusinessTimeUtils(); + + var start = new GlideDateTime(); // now + var addRes = util.addWorkingHours(SCHEDULE_SYS_ID, 16, start, TZ); + gs.info('Add 16h ok=' + addRes.ok + ', due=' + (addRes.due ? addRes.due.getDisplayValue() : addRes.message)); + + var end = new GlideDateTime(addRes.due || start); + var diffRes = util.workingMinutesBetween(SCHEDULE_SYS_ID, start, end, TZ); + gs.info('Working minutes between start and due: ' + diffRes.minutes); + + var openRes = util.nextOpen(SCHEDULE_SYS_ID, new GlideDateTime(), TZ); + gs.info('Next open ok=' + openRes.ok + ', at=' + (openRes.nextOpen ? openRes.nextOpen.getDisplayValue() : openRes.message)); + + var inRes = util.isInSchedule(SCHEDULE_SYS_ID, new GlideDateTime(), TZ); + gs.info('Is now in schedule: ' + inRes.inSchedule); +})(); diff --git a/Core ServiceNow APIs/GlideDateTime/Calculate Due date using user defined schedules/README.md b/Core ServiceNow APIs/GlideDateTime/Calculate Due date using user defined schedules/README.md new file mode 100644 index 0000000000..bd51a663e2 --- /dev/null +++ b/Core ServiceNow APIs/GlideDateTime/Calculate Due date using user defined schedules/README.md @@ -0,0 +1,16 @@ +**Description:** +This Script Include calculates a future due date by adding a specified number of business days to a given start date, based on a defined schedule. +This can be used anywhere within the server side scripts like fix scripts, background scripts, UI Action (server script). + +**Pre-requisite:** +A schedule record with valid schedule entries should be created in the cmn_schedule table +A business hours value per day need to be configured +In this sample, the business hours per day is configured as 8 hours i.e 9AM - 5PM. + +**Sample:** +var daysToAdd = 4; // No of days need to be added +var script = new CaclculateDueDate().calculateDueDate(new GlideDateTime(),daysToAdd); // Passing the current date and daysToAdd value to script include +gs.print(script); + +**Output:** +*** Script: 2025-10-13 13:56:07 diff --git a/Core ServiceNow APIs/GlideDateTime/Calculate Due date using user defined schedules/script.js b/Core ServiceNow APIs/GlideDateTime/Calculate Due date using user defined schedules/script.js new file mode 100644 index 0000000000..eaa08bebec --- /dev/null +++ b/Core ServiceNow APIs/GlideDateTime/Calculate Due date using user defined schedules/script.js @@ -0,0 +1,20 @@ +var CaclculateDueDate = Class.create(); +CaclculateDueDate.prototype = { + initialize: function() {}, + + calculateDueDate: function(date, days_to_add) { + var business_hour_per_day = 8; // This can be stored in the system property (Value in Hours) and reused + var duration_script = new DurationCalculator(); // OOB Script include + var tz = gs.getSysTimeZone(); // Get the system timezone + + duration_script.setSchedule('c798c1dfc3907e1091ea5242b40131c8', tz); // Sys id of the schedule + duration_script.setStartDateTime(new GlideDateTime(date)); + var total_duration = days_to_add * (business_hour_per_day * 60 * 60); // Converting the days to seconds + duration_script.calcDuration(total_duration); + + var calculated_due_date = duration_script.getEndDateTime(); + return calculated_due_date.getDisplayValue(); + }, + + type: 'CaclculateDueDate' +}; diff --git a/Core ServiceNow APIs/GlideDateTime/Convert UTC Time To Local Time/readme.md b/Core ServiceNow APIs/GlideDateTime/Convert UTC Time To Local Time/readme.md new file mode 100644 index 0000000000..f4ccab6387 --- /dev/null +++ b/Core ServiceNow APIs/GlideDateTime/Convert UTC Time To Local Time/readme.md @@ -0,0 +1,21 @@ +## Overview +This script converts a UTC date/time field in ServiceNow to the local time of the user that the script runs under using GlideDateTime. +This distinction is important in certain contexts, such as asynchronous business rules, scheduled jobs, or background scripts, where the executing user may differ from the record owner. +It is useful for notifications, reports, dashboards, or any situation where users need localized timestamps that reflect the correct timezone. + +## Table and Field Example +Table: incident +Field: opened_at (stored in UTC) + +## How It Works +The script queries the incident table for the most recent active incident. +Retrieves the opened_at field (in UTC). +Creates a GlideDateTime object to convert this UTC timestamp into the local time of the executing user. +Logs both the original UTC time and the converted local time. + +## Key Notes +Conversion is always based on the timezone of the user executing the script. +In asynchronous operations (background scripts, scheduled jobs, async business rules), this is the system user running the script. + +## Reference +https://developer.servicenow.com/dev.do#!/reference/api/zurich/server_legacy/c_GlideDateTimeAPI diff --git a/Core ServiceNow APIs/GlideDateTime/Convert UTC Time To Local Time/script.js b/Core ServiceNow APIs/GlideDateTime/Convert UTC Time To Local Time/script.js new file mode 100644 index 0000000000..ae875178a8 --- /dev/null +++ b/Core ServiceNow APIs/GlideDateTime/Convert UTC Time To Local Time/script.js @@ -0,0 +1,21 @@ +(function() { + var gr = new GlideRecord('incident'); + gr.addQuery('active', true); + gr.orderByDesc('opened_at'); + gr.setLimit(1); // Example: take the latest active incident + gr.query(); + + if (gr.next()) { + // GlideDateTime object from UTC field + var utcDateTime = gr.opened_at; + + // Convert to user's local time zone + var localTime = new GlideDateTime(utcDateTime); + var displayValue = localTime.getDisplayValue(); // Returns local time in user's timezone + + gs.info('UTC Time: ' + utcDateTime); + gs.info('Local Time: ' + displayValue); + } else { + gs.info('No active incidents found.'); + } +})(); diff --git a/Core ServiceNow APIs/GlideDateTime/Find Incidents Older Than X Days/readme.md b/Core ServiceNow APIs/GlideDateTime/Find Incidents Older Than X Days/readme.md new file mode 100644 index 0000000000..2e0b4cd29f --- /dev/null +++ b/Core ServiceNow APIs/GlideDateTime/Find Incidents Older Than X Days/readme.md @@ -0,0 +1,10 @@ +## Overview +This script retrieves incidents that were opened more than X days ago using **GlideDateTime** and **GlideRecord**. +Useful for reporting, escalations, notifications, and cleanup tasks. + +## Table and Field +- **Table:** `incident` +- **Field:** `opened_at` + +## Parameters +- **X (number of days):** Defines the threshold for old incidents (e.g., 30 days). diff --git a/Core ServiceNow APIs/GlideDateTime/Find Incidents Older Than X Days/script.js b/Core ServiceNow APIs/GlideDateTime/Find Incidents Older Than X Days/script.js new file mode 100644 index 0000000000..315bf52367 --- /dev/null +++ b/Core ServiceNow APIs/GlideDateTime/Find Incidents Older Than X Days/script.js @@ -0,0 +1,18 @@ +(function() { + var days = 30; // Change this to your required number of days + + // Calculate the date X days ago + var cutoffDate = new GlideDateTime(); + cutoffDate.addDaysUTC(-days); + + // Query incidents opened before the cutoff date + var gr = new GlideRecord('incident'); + gr.addQuery('opened_at', '<', cutoffDate); + gr.query(); + + gs.info('Incidents opened more than ' + days + ' days ago:'); + + while (gr.next()) { + gs.info('Incident Number: ' + gr.number + ', Opened At: ' + gr.opened_at.getDisplayValue()); + } +})(); diff --git a/Core ServiceNow APIs/GlideElement/Display base table for each field/README.md b/Core ServiceNow APIs/GlideElement/Display base table for each field/README.md new file mode 100644 index 0000000000..5fd064a21d --- /dev/null +++ b/Core ServiceNow APIs/GlideElement/Display base table for each field/README.md @@ -0,0 +1,32 @@ +# Display Base Table for Each Field + +This code snippet demonstrates how to identify the base table where each field originates from in ServiceNow's table inheritance hierarchy. + +## Functionality + +The script uses `GlideRecordUtil` to retrieve all parent tables for a given table, then iterates through each field in a record to display which base table the field was defined in using the `getBaseTableName()` method from the GlideElement API. + +## Use Case + +This is particularly useful when working with extended tables to understand: +- Which fields are inherited from parent tables +- Which fields are defined locally on the current table +- The complete table inheritance structure + +## Example Output + +For a table like `db_image`, the output shows each field name alongside its originating table: +``` +Fields Base table +sys_id sys_metadata +sys_created_on sys_metadata +image db_image +name db_image +... +``` + +## Related Methods + +- `getBaseTableName()` - Returns the name of the table where the field was originally defined +- `GlideRecordUtil.getTables()` - Returns parent tables in the inheritance hierarchy +- `GlideRecordUtil.getFields()` - Returns an array of all field names for a GlideRecord diff --git a/Core ServiceNow APIs/GlideElement/Display base table for each field/displayBaseTablesForEachField.js b/Core ServiceNow APIs/GlideElement/Display base table for each field/displayBaseTablesForEachField.js new file mode 100644 index 0000000000..94a9a58522 --- /dev/null +++ b/Core ServiceNow APIs/GlideElement/Display base table for each field/displayBaseTablesForEachField.js @@ -0,0 +1,19 @@ +var tableName = 'db_image'; + +var gru = new GlideRecordUtil(); + +var parentTables = gru.getTables(tableName); +gs.print("Parent tables: " + parentTables); + +var gr = new GlideRecord(tableName); +gr.setlimit(1); +gr.query(); + +if(gr.next()){ + var fieldNames = gru.getFields(gr); + gs.print('Fields\t\tBase table'); + for (var i = 0; i < fieldNames.length; i++) { + gs.print(fieldNames[i] + "\t\t" + gr[fieldNames[i]].getBaseTableName()); + } +} + diff --git a/Core ServiceNow APIs/GlideElement/Smart Field Validation and Dependent Field Derivation Using getError() and setError()/README.md b/Core ServiceNow APIs/GlideElement/Smart Field Validation and Dependent Field Derivation Using getError() and setError()/README.md new file mode 100644 index 0000000000..98d6e02d7b --- /dev/null +++ b/Core ServiceNow APIs/GlideElement/Smart Field Validation and Dependent Field Derivation Using getError() and setError()/README.md @@ -0,0 +1,35 @@ +# Smart Field Validation and Dependent Field Derivation Using GlideElement.getError() + +This project demonstrates how to use `GlideElement.setError()` and `GlideElement.getError()` +to perform validation in one Business Rule and field derivation in another, without repeating logic. + +## 📘 Overview + +This snippet demonstrates how to share validation state and error messages between multiple Business Rules using `GlideElement.setError()` and `GlideElement.getError()` in ServiceNow. + +By propagating validation context across Business Rules, developers can: + +- Avoid repeated validation logic. +- Trigger dependent field updates only when a field passes validation. +- Maintain consistent and clean data flow between sequential rules. + +This technique is especially useful when different validation or derivation rules are split by purpose or owned by different teams. + +--- + +## 🧠 Concept + +When one Business Rule sets an error on a field using `setError()`, the error message persists in memory for that record during the same transaction. +A later Business Rule (executing at a higher order) can then retrieve that message using `getError()` and make data-driven decisions. + +### Flow: +1. BR #1 (`Validate Short Description`) checks text length. +2. BR #2 (`Derive Dependent Fields`) runs only if no validation error exists. +3. Category, Subcategory, and Impact are derived dynamically. + +## 🚀 Benefits + +- ✅ Reduces redundant validation checks +- ✅ Improves rule execution efficiency +- ✅ Keeps logic modular and maintainable +- ✅ Provides better visibility and control in field validations diff --git a/Core ServiceNow APIs/GlideElement/Smart Field Validation and Dependent Field Derivation Using getError() and setError()/br_derive_dependent_fields.js b/Core ServiceNow APIs/GlideElement/Smart Field Validation and Dependent Field Derivation Using getError() and setError()/br_derive_dependent_fields.js new file mode 100644 index 0000000000..24fa39decf --- /dev/null +++ b/Core ServiceNow APIs/GlideElement/Smart Field Validation and Dependent Field Derivation Using getError() and setError()/br_derive_dependent_fields.js @@ -0,0 +1,45 @@ +// Name: Derive Dependent Fields +// Table: Incident +// When: before insert or before update +// Order: 200 + +(function executeRule(current, previous /*null when async*/) { + + // Only proceed if short_description changed or new record + if (!(current.operation() === 'insert' || current.short_description.changes())) { + return; + } + + var errorMsg = current.short_description.getError(); + + if (errorMsg) { + gs.info('[BR:200 - Derive] Skipping field derivation due to prior error → ' + errorMsg); + return; + } + + // Proceed only if no prior validation error + var desc = current.getValue('short_description').toLowerCase(); + + // Example 1: Derive category + if (desc.includes('server')) { + current.category = 'infrastructure'; + current.subcategory = 'server issue'; + } else if (desc.includes('email')) { + current.category = 'communication'; + current.subcategory = 'email problem'; + } else if (desc.includes('login')) { + current.category = 'access'; + current.subcategory = 'authentication'; + } else { + current.category = 'inquiry'; + current.subcategory = 'general'; + } + + // Example 2: Derive impact + if (desc.includes('critical') || desc.includes('outage')) { + current.impact = 1; // High + } else { + current.impact = 3; // Low + } + +})(current, previous); diff --git a/Core ServiceNow APIs/GlideElement/Smart Field Validation and Dependent Field Derivation Using getError() and setError()/br_validate_short_description.js b/Core ServiceNow APIs/GlideElement/Smart Field Validation and Dependent Field Derivation Using getError() and setError()/br_validate_short_description.js new file mode 100644 index 0000000000..2fa2e64e84 --- /dev/null +++ b/Core ServiceNow APIs/GlideElement/Smart Field Validation and Dependent Field Derivation Using getError() and setError()/br_validate_short_description.js @@ -0,0 +1,16 @@ +// Name: Validate Short Description +// Table: Incident +// When: before insert or before update +// Order: 100 + +(function executeRule(current, previous /*null when async*/) { + var short_desc = current.getValue('short_description'); + + // Validate only for new records or when field changes + if (current.operation() === 'insert' || current.short_description.changes()) { + if (!short_desc || short_desc.trim().length < 40) { + current.short_description.setError('Short description must be at least 40 characters long.'); + current.setAbortAction(true); + } + } +})(current, previous); diff --git a/Core ServiceNow APIs/GlideHTTPRequest/README.md b/Core ServiceNow APIs/GlideHTTPRequest/Retrieve table records via GlideHTTPRequest/README.md similarity index 100% rename from Core ServiceNow APIs/GlideHTTPRequest/README.md rename to Core ServiceNow APIs/GlideHTTPRequest/Retrieve table records via GlideHTTPRequest/README.md diff --git a/Core ServiceNow APIs/GlideHTTPRequest/glidehttprequest.js b/Core ServiceNow APIs/GlideHTTPRequest/Retrieve table records via GlideHTTPRequest/glidehttprequest.js similarity index 100% rename from Core ServiceNow APIs/GlideHTTPRequest/glidehttprequest.js rename to Core ServiceNow APIs/GlideHTTPRequest/Retrieve table records via GlideHTTPRequest/glidehttprequest.js diff --git a/Core ServiceNow APIs/GlideJsonPath/Basic-Example/README.md b/Core ServiceNow APIs/GlideJsonPath/Basic-Example/README.md new file mode 100644 index 0000000000..c2a062042b --- /dev/null +++ b/Core ServiceNow APIs/GlideJsonPath/Basic-Example/README.md @@ -0,0 +1,9 @@ +# Querying JSON with JSONPath to extract values + +GlideJsonPath is a class which can be used to use JSONPath in ServiceNow. It can be useful when working with JSON Payloads, especially highly nested or in general complex structures. + +References: +- [RFC 9535: JSONPath: Query Expressions for JSON](https://datatracker.ietf.org/doc/rfc9535/) +- [Play with JSONPath outside of ServiceNow](https://jsonpath.com/) +- [Good Examples to start with](https://restfulapi.net/json-jsonpath/) +- [ServiceNow API Documentation](https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideJsonPath/concept/GlideJsonPathAPI.html) \ No newline at end of file diff --git a/Core ServiceNow APIs/GlideJsonPath/Basic-Example/examples.js b/Core ServiceNow APIs/GlideJsonPath/Basic-Example/examples.js new file mode 100644 index 0000000000..03740bb770 --- /dev/null +++ b/Core ServiceNow APIs/GlideJsonPath/Basic-Example/examples.js @@ -0,0 +1,53 @@ +// Run in background script +var json = { + "store": + { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 19.95 + } + } +}; + +var path1 = 'store.book[0].author'; // The author of the first book +var path2 = 'store.book[*].author'; // All authors +var path3 = 'store..price'; // All prices +var path4 = '$..book[?(@.price<10)]'; // All books cheaper than 10 +var path5 = '$..book[?(@.isbn)]'; // All books with an ISBN number +var path6 = '$..*'; // All members of JSON structure + +var gjp = new GlideJsonPath(JSON.stringify(json)); +gs.info('Path: ' + path1 + ' Result: ' + gjp.read(path1)); +gs.info('Path: ' + path2 + ' Result: ' + gjp.read(path2)); +gs.info('Path: ' + path3 + ' Result: ' + gjp.read(path3)); +gs.info('Path: ' + path4 + ' Result: ' + gjp.read(path4)); +gs.info('Path: ' + path5 + ' Result: ' + gjp.read(path5)); +gs.info('Path: ' + path6 + ' Result: ' + gjp.read(path6)); \ No newline at end of file diff --git a/Core ServiceNow APIs/GlideJsonPath/Create Critical P1 Incident from Alert using GlideJsonPath/README.md b/Core ServiceNow APIs/GlideJsonPath/Create Critical P1 Incident from Alert using GlideJsonPath/README.md new file mode 100644 index 0000000000..84814e38fd --- /dev/null +++ b/Core ServiceNow APIs/GlideJsonPath/Create Critical P1 Incident from Alert using GlideJsonPath/README.md @@ -0,0 +1,17 @@ +Create Critical P1 Incident from Alert This script provides the server-side logic for a Scripted REST API endpoint in ServiceNow. +It allows external monitoring tools to send alert data via a POST request, which is then used to automatically create a high-priority, P1 incident. +Overview The API endpoint performs the following actions: Receives a JSON Payload: Accepts a POST request containing a JSON payload with alert details (severity, description, source, CI). Parses Data: Uses the GlideJsonPath API to efficiently extract the necessary alert information from the JSON body. Validates Request: Ensures that the severity is CRITICAL and the description is present. It sends an appropriate error response for invalid or incomplete data. Creates Incident: If the data is valid, it creates a new incident record in the incident table. Sets Incident Fields: Automatically populates the incident's short_description, description, source, and sets the impact, urgency, and priority to 1 - High/Critical. Associates CI: If a ci_sys_id is provided in the payload, it links the incident to the correct Configuration Item. Logs Activity: Logs the successful creation of the incident in the system log for tracking and auditing purposes. Responds to Sender: Sends a JSON response back to the external system, confirming success or failure and providing the new incident's number and sys_id. Expected JSON payload The external system should send a POST request with a JSON body structured like this: json { "alert": { "severity": "CRITICAL", "description": "The primary database server is down. Users are unable to log in.", "source": "Dynatrace", "configuration_item": "DB_Server_01", "ci_sys_id": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" } } Use code with caution. + +Installation As a Scripted REST API Resource Create the Scripted REST API: Navigate to System Web Services > Scripted REST APIs. +Click New and fill in the details: Name: CriticalAlertIncident API ID: critical_alert_incident Save the record. + +Create the Resource: On the Resources related list of the API record, click New. +Name: PostCriticalIncident HTTP Method: POST Relative Path: / Copy and paste the provided script into the Script field. Configure Security: Ensure appropriate authentication is configured for the API, such as OAuth or Basic Auth, to secure the endpoint. Customization Change Priority/Impact: Modify the grIncident.setValue() lines to set different priority or impact levels based on the payload (e.g., if (severity == 'MAJOR') { grIncident.setValue('priority', 2); }). Add Additional Fields: Extend the script to parse and set other incident fields, such as assignment_group, caller_id, or category, based on data from the incoming payload. Enrich Incident Data: Perform a lookup on the CI to fetch additional information and add it to the incident description or other fields. Handle Different Severity Levels: Add if/else logic to handle different severity values (e.g., MAJOR, MINOR) from the source system, creating incidents with different priorities accordingly. + +Dependencies This script requires the GlideJsonPath API, which is available in Jakarta and later releases. +The API endpoint must be secured with appropriate authentication to prevent unauthorized access. + +Considerations + +Security: This API endpoint is a powerful integration point. +Ensure that it is properly secured and that only trusted sources are allowed to create incidents. Error Handling: The script includes robust error handling for common failures (missing data, insertion failure) but should be extended to handle specific use cases as needed. Testing: Thoroughly test the endpoint with a variety of payloads, including valid data, missing data, and invalid data, to ensure it behaves as expected. diff --git a/Core ServiceNow APIs/GlideJsonPath/Create Critical P1 Incident from Alert using GlideJsonPath/script.js b/Core ServiceNow APIs/GlideJsonPath/Create Critical P1 Incident from Alert using GlideJsonPath/script.js new file mode 100644 index 0000000000..2e55276b70 --- /dev/null +++ b/Core ServiceNow APIs/GlideJsonPath/Create Critical P1 Incident from Alert using GlideJsonPath/script.js @@ -0,0 +1,78 @@ +try { + // Get the JSON payload from the request body. + var requestBody = request.body.dataString; + + // Use GlideJsonPath to parse the JSON payload efficiently. + var gjp = new GlideJsonPath(requestBody); + + // Extract key information from the JSON payload. + var severity = gjp.read("$.alert.severity"); + var shortDescription = gjp.read("$.alert.description"); + var source = gjp.read("$.alert.source"); + var ciName = gjp.read("$.alert.configuration_item"); + var ciSysId = gjp.read("$.alert.ci_sys_id"); + + // Validate that mandatory fields are present. + if (!shortDescription || severity != 'CRITICAL') { + response.setStatus(400); // Bad Request + response.setBody({ + "status": "error", + "message": "Missing mandatory alert information or severity is not critical." + }); + return; + } + + // Use GlideRecordSecure for added security and ACL enforcement. + var grIncident = new GlideRecordSecure('incident'); + grIncident.initialize(); + + // Set incident field values from the JSON payload. + grIncident.setValue('short_description', 'INTEGRATION ALERT: [' + source + '] ' + shortDescription); + grIncident.setValue('description', 'A critical alert has been received from ' + source + '.\n\nAlert Details:\nSeverity: ' + severity + '\nDescription: ' + shortDescription + '\nCI Name: ' + ciName); + grIncident.setValue('source', source); + grIncident.setValue('impact', 1); // Set Impact to '1 - High' + grIncident.setValue('urgency', 1); // Set Urgency to '1 - High' + grIncident.setValue('priority', 1); // Set Priority to '1 - Critical' + + // If a CI sys_id is provided, set the Configuration Item. + if (ciSysId) { + grIncident.setValue('cmdb_ci', ciSysId); + } + + // Insert the new incident record and store its sys_id. + var newIncidentSysId = grIncident.insert(); + + if (newIncidentSysId) { + // Get the incident number for the successful response. + var incNumber = grIncident.getRecord().getValue('number'); + + // Log the successful incident creation. + gs.info('Critical P1 incident ' + incNumber + ' created from alert from ' + source); + + // Prepare the success response. + var responseBody = { + "status": "success", + "message": "Critical incident created successfully.", + "incident_number": incNumber, + "incident_sys_id": newIncidentSysId + }; + response.setStatus(201); // Created + response.setBody(responseBody); + } else { + // Handle database insertion failure. + response.setStatus(500); // Internal Server Error + response.setBody({ + "status": "error", + "message": "Failed to create the incident record." + }); + } + + } catch (ex) { + // Handle any exceptions during processing. + gs.error('An error occurred during critical alert incident creation: ' + ex); + response.setStatus(500); + response.setBody({ + "status": "error", + "message": "An internal server error occurred." + }); + } diff --git a/Core ServiceNow APIs/GlideModal/Add HTML Input Field in GlideModal Window/README.md b/Core ServiceNow APIs/GlideModal/Add HTML Input Field in GlideModal Window/README.md new file mode 100644 index 0000000000..3c3a3c188b --- /dev/null +++ b/Core ServiceNow APIs/GlideModal/Add HTML Input Field in GlideModal Window/README.md @@ -0,0 +1,26 @@ +# Add HTML Input Field in GlideModal Window - ServiceNow + +## Use Case +This snippet demonstrates how to include HTML input fields, including rich text editors, inside a GlideModal window in ServiceNow. + +## Real-Life Example of Use +In ServiceNow ITSM, support agents often need to add detailed notes or updates quickly without losing their workflow context. For instance, when investigating complex incidents, agents can click the "Add Details" button to open a modal with rich text input to document findings, attach formatted comments, or paste troubleshooting steps. This modal dialog prevents navigation away from the incident form, speeding up data entry and improving information capture quality. + +## Why This Use Case is Unique and Valuable (Simple) +- Lets users enter rich text and HTML inputs right inside a popup window (GlideModal) without leaving the current page. +- Makes data entry faster and easier by avoiding navigation away from the form. +- Supports complex inputs like formatted text using editors such as TinyMCE. +- Helps improve quality and detail of notes and comments on records. +- Can be reused for different input forms or workflows in ServiceNow. +- Works smoothly within the ServiceNow platform UI for a consistent user experience. + +## Steps to Implement +1. Create a UI Page named `rich_text_modal` with appropriate input fields (string and rich text). +2. Create a UI Action (e.g., "Add Details") on the Incident table that opens the `rich_text_modal` UI Page within a GlideModal. +3. Open an incident record and click the "Add Details" button to see the modal with the HTML input fields. + +## Compatibility +This UI Page and UI Action is compatible with all standard ServiceNow instances without requiring ES2021 features. + +## Files +`UI Page` , `UI Action` - are the files implementing the logic. diff --git a/Core ServiceNow APIs/GlideModal/Add HTML Input Field in GlideModal Window/UI Action b/Core ServiceNow APIs/GlideModal/Add HTML Input Field in GlideModal Window/UI Action new file mode 100644 index 0000000000..3bc9466298 --- /dev/null +++ b/Core ServiceNow APIs/GlideModal/Add HTML Input Field in GlideModal Window/UI Action @@ -0,0 +1,7 @@ +function test() +{ +var dialog = new GlideModal('rich_text_modal'); +dialog.setTitle('Rich Text Editor'); +dialog.setSize('500','500'); +dialog.render(); +} diff --git a/Core ServiceNow APIs/GlideModal/Add HTML Input Field in GlideModal Window/UI page b/Core ServiceNow APIs/GlideModal/Add HTML Input Field in GlideModal Window/UI page new file mode 100644 index 0000000000..938614a4ab --- /dev/null +++ b/Core ServiceNow APIs/GlideModal/Add HTML Input Field in GlideModal Window/UI page @@ -0,0 +1,37 @@ + + + + +
+

+ + +
+ + + + +
+
+
+ +
+ +
+ diff --git a/Core ServiceNow APIs/GlideQuery/Conditional Field Selection/README.md b/Core ServiceNow APIs/GlideQuery/Conditional Field Selection/README.md new file mode 100644 index 0000000000..1bf21ae9ff --- /dev/null +++ b/Core ServiceNow APIs/GlideQuery/Conditional Field Selection/README.md @@ -0,0 +1,24 @@ +# Conditional Field Selection with GlideQuery + +This snippet demonstrates how to dynamically select different sets of fields based on conditions using GlideQuery. This pattern is useful when you need to optimize queries by selecting only the fields you actually need based on runtime conditions, or when building flexible APIs that return different data sets based on user permissions or preferences. + +## Use Cases + +- **Permission-based field selection**: Select different fields based on user roles or permissions +- **Performance optimization**: Only fetch expensive fields when needed +- **API flexibility**: Return different data sets based on request parameters +- **Conditional aggregations**: Include summary fields only when specific conditions are met + +## Key Benefits + +- **Reduced data transfer**: Only fetch the fields you need +- **Performance optimization**: Avoid expensive field calculations when unnecessary +- **Security**: Dynamically exclude sensitive fields based on permissions +- **Maintainable code**: Centralized logic for field selection patterns + +## Examples Included + +1. **Role-based field selection**: Different fields for different user roles +2. **Performance-optimized queries**: Conditional inclusion of expensive fields +3. **Dynamic field arrays**: Building field lists programmatically +4. **Chained conditional selection**: Multiple condition-based selections \ No newline at end of file diff --git a/Core ServiceNow APIs/GlideQuery/Conditional Field Selection/conditional_field_selection.js b/Core ServiceNow APIs/GlideQuery/Conditional Field Selection/conditional_field_selection.js new file mode 100644 index 0000000000..6e2055fead --- /dev/null +++ b/Core ServiceNow APIs/GlideQuery/Conditional Field Selection/conditional_field_selection.js @@ -0,0 +1,205 @@ +// Conditional Field Selection with GlideQuery +// Demonstrates dynamically selecting different fields based on runtime conditions + +/** + * Example 1: Role-based Field Selection + * Select different incident fields based on user's role + */ +function getIncidentsByRole(userRole, assignedTo) { + // Define base fields that everyone can see + let baseFields = ['number', 'short_description', 'state', 'priority', 'sys_created_on']; + + // Define additional fields based on role + let additionalFields = []; + + if (userRole === 'admin' || userRole === 'security_admin') { + additionalFields = ['caller_id', 'assigned_to', 'assignment_group', 'work_notes', 'comments']; + } else if (userRole === 'itil') { + additionalFields = ['caller_id', 'assigned_to', 'assignment_group']; + } else if (userRole === 'agent') { + additionalFields = ['assigned_to', 'assignment_group']; + } + + // Combine base and additional fields + let fieldsToSelect = baseFields.concat(additionalFields); + + return new GlideQuery('incident') + .where('assigned_to', assignedTo) + .where('state', '!=', 7) // Not closed + .select(fieldsToSelect) + .orderByDesc('sys_created_on') + .toArray(50); +} + +/** + * Example 2: Performance-optimized Field Selection + * Only include expensive fields when specifically requested + */ +function getTasksWithOptionalFields(options) { + options = options || {}; + + // Always include these lightweight fields + let fields = ['sys_id', 'number', 'short_description', 'state']; + + // Conditionally add more expensive fields + if (options.includeUserDetails) { + fields.push('caller_id.name', 'caller_id.email', 'assigned_to.name'); + } + + if (options.includeTimeTracking) { + fields.push('work_start', 'work_end', 'business_duration'); + } + + if (options.includeApprovalInfo) { + fields.push('approval', 'approval_history'); + } + + if (options.includeRelatedRecords) { + fields.push('parent.number', 'caused_by.number'); + } + + let query = new GlideQuery('task') + .where('active', true) + .select(fields); + + if (options.assignmentGroup) { + query.where('assignment_group', options.assignmentGroup); + } + + return query.toArray(100); +} + +/** + * Example 3: Dynamic Field Array Building + * Build field selection based on table structure and permissions + */ +function getDynamicFieldSelection(tableName, userPermissions, includeMetadata) { + let fields = []; + + // Always include sys_id + fields.push('sys_id'); + + // Add fields based on table type + if (tableName === 'incident' || tableName === 'sc_request') { + fields.push('number', 'short_description', 'state', 'priority'); + + if (userPermissions.canViewCaller) { + fields.push('caller_id'); + } + + if (userPermissions.canViewAssignment) { + fields.push('assigned_to', 'assignment_group'); + } + } else if (tableName === 'cmdb_ci') { + fields.push('name', 'operational_status', 'install_status'); + + if (userPermissions.canViewConfiguration) { + fields.push('ip_address', 'fqdn', 'serial_number'); + } + } + + // Add metadata fields if requested + if (includeMetadata) { + fields.push('sys_created_on', 'sys_created_by', 'sys_updated_on', 'sys_updated_by'); + } + + return new GlideQuery(tableName) + .select(fields) + .limit(100) + .toArray(); +} + +/** + * Example 4: Chained Conditional Selection with Method Chaining + * Demonstrate building complex queries with multiple conditions + */ +function getConditionalIncidentData(filters) { + let query = new GlideQuery('incident'); + + // Build base field list + let fields = ['sys_id', 'number', 'short_description', 'state']; + + // Apply filters and modify field selection accordingly + if (filters.priority && filters.priority.length > 0) { + query.where('priority', 'IN', filters.priority); + fields.push('priority'); // Include priority field when filtering by it + } + + if (filters.assignmentGroup) { + query.where('assignment_group', filters.assignmentGroup); + fields.push('assignment_group', 'assigned_to'); // Include assignment fields + } + + if (filters.dateRange) { + query.where('sys_created_on', '>=', filters.dateRange.start) + .where('sys_created_on', '<=', filters.dateRange.end); + fields.push('sys_created_on'); // Include date when filtering by it + } + + if (filters.includeComments) { + fields.push('comments', 'work_notes'); + } + + if (filters.includeResolution) { + fields.push('close_code', 'close_notes', 'resolved_by'); + } + + return query.select(fields) + .orderByDesc('sys_created_on') + .toArray(filters.limit || 50); +} + +/** + * Example 5: Security-conscious Field Selection + * Exclude sensitive fields based on user context + */ +function getSecureUserData(requestingUser, targetUserId) { + let baseFields = ['sys_id', 'name', 'user_name', 'active']; + + // Check if requesting user can see additional details + if (gs.hasRole('user_admin') || requestingUser === targetUserId) { + // Full access - include all standard fields + return new GlideQuery('sys_user') + .where('sys_id', targetUserId) + .select(['sys_id', 'name', 'user_name', 'email', 'phone', 'department', 'title', 'manager', 'active']) + .toArray(1); + } else if (gs.hasRole('hr_admin')) { + // HR access - include HR-relevant fields but not IT details + return new GlideQuery('sys_user') + .where('sys_id', targetUserId) + .select(['sys_id', 'name', 'user_name', 'department', 'title', 'manager', 'active']) + .toArray(1); + } else { + // Limited access - only public information + return new GlideQuery('sys_user') + .where('sys_id', targetUserId) + .select(baseFields) + .toArray(1); + } +} + +// Usage Examples: + +// Role-based selection +var adminIncidents = getIncidentsByRole('admin', gs.getUserID()); + +// Performance-optimized query +var tasksWithDetails = getTasksWithOptionalFields({ + includeUserDetails: true, + includeTimeTracking: false, + assignmentGroup: 'hardware' +}); + +// Dynamic field building +var dynamicData = getDynamicFieldSelection('incident', { + canViewCaller: true, + canViewAssignment: false +}, true); + +// Complex conditional query +var filteredIncidents = getConditionalIncidentData({ + priority: [1, 2], + assignmentGroup: 'network', + includeComments: true, + limit: 25 +}); \ No newline at end of file diff --git a/Core ServiceNow APIs/GlideRecord/Archiving Old Incident Records to Improve Performance/readme.md b/Core ServiceNow APIs/GlideRecord/Archiving Old Incident Records to Improve Performance/readme.md new file mode 100644 index 0000000000..6fe761fa84 --- /dev/null +++ b/Core ServiceNow APIs/GlideRecord/Archiving Old Incident Records to Improve Performance/readme.md @@ -0,0 +1,8 @@ +## Purpose +This document explains how to archive old incident records from the `incident` table to an archive table `ar_incident` to improve performance, while preserving historical data for reporting and audit purposes. +## Solution Overview +Use **ServiceNow Archive Rules** to automatically move incidents to an archive table based on specific conditions: +- Incidents that are **closed**. +- Incidents that are **inactive** (`active = false`). +- Incidents that were closed **150 days ago or earlier**. +The records are moved to the archive table `ar_incident`, which preserves all necessary fields for historical reference. diff --git a/Core ServiceNow APIs/GlideRecord/Archiving Old Incident Records to Improve Performance/script.js b/Core ServiceNow APIs/GlideRecord/Archiving Old Incident Records to Improve Performance/script.js new file mode 100644 index 0000000000..f75c926f73 --- /dev/null +++ b/Core ServiceNow APIs/GlideRecord/Archiving Old Incident Records to Improve Performance/script.js @@ -0,0 +1,14 @@ +var gr = new GlideRecord('incident'); +gr.addQuery('state', 7); // Closed +gr.addQuery('active', false); +gr.addQuery('closed_at', '<=', gs.daysAgo(150)); +gr.query(); +while (gr.next()) { + var ar = new GlideRecord('ar_incident'); //ar_incident is the new table for storing archive data + ar.initialize(); + ar.short_description = gr.short_description; + ar.description = gr.description; + // Copy other necessary fields + ar.insert(); + gr.deleteRecord(); // deleting from incident table if record in inserted in the archived table +} diff --git a/Core ServiceNow APIs/GlideRecord/CheckDuplicate-Server/readme.md b/Core ServiceNow APIs/GlideRecord/CheckDuplicate-Server/readme.md new file mode 100644 index 0000000000..2f8e0eec15 --- /dev/null +++ b/Core ServiceNow APIs/GlideRecord/CheckDuplicate-Server/readme.md @@ -0,0 +1,25 @@ +Scan all Servers (cmdb_ci_server). For each one, check if there is another CI in cmdb_ci_computer with the same name but not a server (sys_class_name != cmdb_ci_server). + +If found, log the server name and the duplicate CI’s class; keep a running duplicate count; finally log the total. + +*******Descriton**** +1. var gr = new GlideRecord("cmdb_ci_server"); +2. Creates a record set for Server CIs. + + +gr.addEncodedQuery("sys_class_name=cmdb_ci_server"); +3. Redundant: you’re already targeting the cmdb_ci_server table which is a class table. This filter doesn’t harm, but it’s unnecessary. + + +while (gr.next()) { ... } +4. Loops through each server CI. + + +5.Inside loop: + +Query cmdb_ci_computer for records with the same name but where sys_class_name != cmdb_ci_server. +6. If found, log the duplicate and increment dupCount. + + + +7. Finally logs total dupCount. diff --git a/Core ServiceNow APIs/GlideRecord/CheckDuplicate-Server/script.js b/Core ServiceNow APIs/GlideRecord/CheckDuplicate-Server/script.js new file mode 100644 index 0000000000..39b9812167 --- /dev/null +++ b/Core ServiceNow APIs/GlideRecord/CheckDuplicate-Server/script.js @@ -0,0 +1,17 @@ +var dupCount = 0; +var gr = new GlideRecord("cmdb_ci_server"); +//gr.addQuery("name", "value"); +gr.addEncodedQuery("sys_class_name=cmdb_ci_server"); +gr.query(); +while (gr.next()) { + var dup = new GlideRecord("cmdb_ci_computer"); + dup.addQuery("name", gr.name); + dup.addQuery("sys_class_name", "!=", "cmdb_ci_server"); + dup.query(); + if (dup.next()) { + gs.log("\t" + gr.name + "\t" + dup.sys_class_name); + dupCount++; + } + +} +gs.log("dup count=" + dupCount); diff --git a/Core ServiceNow APIs/GlideRecord/Compare_2_records/README.md b/Core ServiceNow APIs/GlideRecord/Compare_2_records/README.md new file mode 100644 index 0000000000..07367d9014 --- /dev/null +++ b/Core ServiceNow APIs/GlideRecord/Compare_2_records/README.md @@ -0,0 +1,35 @@ +# Compare Two Records Using GlideRecord (Global Scope) + +This snippet compares two records from the same table in ServiceNow field-by-field using the **GlideRecord API**. +It’s useful for debugging, verifying data after imports, or checking differences between two similar records. + +--- + +## Working +The script: +1. Retrieves two records using their `sys_id`. +2. Includes or Excludes the system fields. +2. Iterates over all fields in the record. +3. Logs any fields where the values differ. + +--- + +## Scope +This script is designed to run in the Global scope. +If used in a scoped application, ensure that the target table is accessible from that scope (cross-scope access must be allowed). + +## Usage +Run this script in a **Background Script** or **Fix Script**: + +```js +compareRecords('incident', 'sys_id_1_here', 'sys_id_2_here', false); +``` +--- +## Example Output +```js +short_description: "Printer not working" vs "Printer offline" +state: "In Progress" vs "Resolved" +priority: "2" vs "3" +Comparison complete. +``` + diff --git a/Core ServiceNow APIs/GlideRecord/Compare_2_records/compareRecords.js b/Core ServiceNow APIs/GlideRecord/Compare_2_records/compareRecords.js new file mode 100644 index 0000000000..2988299c0d --- /dev/null +++ b/Core ServiceNow APIs/GlideRecord/Compare_2_records/compareRecords.js @@ -0,0 +1,53 @@ +/** + * Compare two records in a ServiceNow table field-by-field. + * Logs all field differences between the two records, including display values. + * + * Parameters: + * @param {string} table - Table name + * @param {string} sys_id1 - sys_id of first record + * @param {string} sys_id2 - sys_id of second record + * @param {boolean} includeSystemFields - true to compare system fields, false to skip them + * + * Usage: + * compareRecords('incident', 'sys_id_1_here', 'sys_id_2_here', true/false); + */ + +function compareRecords(table, sys_id1, sys_id2, includeSystemFields) { + var rec1 = new GlideRecord(table); + var rec2 = new GlideRecord(table); + + if (!rec1.get(sys_id1) || !rec2.get(sys_id2)) { + gs.error('One or both sys_ids are invalid for table: ' + table); + return; + } + + var fields = rec1.getFields(); + gs.info('Comparing records in table: ' + table); + + for (var i = 0; i < fields.size(); i++) { + var field = fields.get(i); + var fieldName = field.getName(); + + if( !includeSystemFields && fieldName.startsWith('sys_') ) { + continue; + } + + var val1 = rec1.getValue(fieldName); + var val2 = rec2.getValue(fieldName); + + var disp1 = rec1.getDisplayValue(fieldName); + var disp2 = rec2.getDisplayValue(fieldName); + + if (val1 != val2) { + gs.info( + fieldName + ': Backend -> "' + val1 + '" vs "' + val2 + '", ' + + 'Display -> "' + disp1 + '" vs "' + disp2 + '"' + ); + } + } + + gs.info('Comparison complete.'); +} + +// Example call +compareRecords('incident', 'sys_id_1_here', 'sys_id_2_here', false); diff --git a/Core ServiceNow APIs/GlideRecord/Conditional Batch Update/README.md b/Core ServiceNow APIs/GlideRecord/Conditional Batch Update/README.md new file mode 100644 index 0000000000..7a4d900643 --- /dev/null +++ b/Core ServiceNow APIs/GlideRecord/Conditional Batch Update/README.md @@ -0,0 +1,30 @@ +# GlideRecord Conditional Batch Update + +## Description +This snippet updates multiple records in a ServiceNow table based on a GlideRecord encoded query. +It logs all updated records and provides a safe, controlled way to perform batch updates. + +## Prerequisites +- Server-side context (Background Script, Script Include, Business Rule) +- Access to the table +- Knowledge of GlideRecord and encoded queries + +## Note +- Works in Global Scope +- Server-side execution only +- Logs updated records for verification +- Can be used for maintenance, bulk updates, or automated scripts + +## Usage +```javascript +// Update all active low-priority incidents to priority=2 and state=2 +batchUpdate('incident', 'active=true^priority=5', {priority: 2, state: 2}); +``` + +## Sample Output +``` +Updated record: abc123 +Updated record: def456 +Updated record: ghi789 +Batch update completed. Total records updated: 3 +``` \ No newline at end of file diff --git a/Core ServiceNow APIs/GlideRecord/Conditional Batch Update/batchUpdate.js b/Core ServiceNow APIs/GlideRecord/Conditional Batch Update/batchUpdate.js new file mode 100644 index 0000000000..4be2bba0d7 --- /dev/null +++ b/Core ServiceNow APIs/GlideRecord/Conditional Batch Update/batchUpdate.js @@ -0,0 +1,35 @@ +/** + * Update multiple records in a table based on an encoded query with field-level updates. + * Logs all updated records for verification. + * + * @param {string} table - Name of the table + * @param {string} encodedQuery - GlideRecord encoded query to select records + * @param {object} fieldUpdates - Key-value pairs of fields to update + */ +function batchUpdate(table, encodedQuery, fieldUpdates) { + if (!table || !encodedQuery || !fieldUpdates || typeof fieldUpdates !== 'object') { + gs.error('Table, encodedQuery, and fieldUpdates (object) are required.'); + return; + } + + var gr = new GlideRecord(table); + gr.addEncodedQuery(encodedQuery); + gr.query(); + + var count = 0; + while (gr.next()) { + for (var field in fieldUpdates) { + if (gr.isValidField(field)) { + gr.setValue(field, fieldUpdates[field]); + } else { + gs.warn('Invalid field: ' + field + ' in table ' + table); + } + } + + gr.update(); + gs.info('Updated record: ' + gr.getValue('sys_id')); + count++; + } + + gs.info('Batch update completed. Total records updated: ' + count); +} diff --git a/Core ServiceNow APIs/GlideRecord/Fetch active incidents assigned to a specific group/README.md b/Core ServiceNow APIs/GlideRecord/Fetch active incidents assigned to a specific group/README.md new file mode 100644 index 0000000000..1e2554d340 --- /dev/null +++ b/Core ServiceNow APIs/GlideRecord/Fetch active incidents assigned to a specific group/README.md @@ -0,0 +1,2 @@ +This script basically built to pull active incidents for a particular group in order to act quickly. +Demo script retrieves all active incidents assigned to the "Network Support" group. diff --git a/Core ServiceNow APIs/GlideRecord/Fetch active incidents assigned to a specific group/code.js b/Core ServiceNow APIs/GlideRecord/Fetch active incidents assigned to a specific group/code.js new file mode 100644 index 0000000000..e5b58d7550 --- /dev/null +++ b/Core ServiceNow APIs/GlideRecord/Fetch active incidents assigned to a specific group/code.js @@ -0,0 +1,9 @@ + +var gr = new GlideRecord('incident'); + gr.addQuery('active', true); + gr.addQuery('assignment_group.name', 'Network Support'); + gr.query(); + + while (gr.next()) { + gs.info('Incident Number: ' + gr.getValue('number')); + } diff --git a/Core ServiceNow APIs/GlideRecord/Field Level Audit/README.md b/Core ServiceNow APIs/GlideRecord/Field Level Audit/README.md new file mode 100644 index 0000000000..3369d89e5a --- /dev/null +++ b/Core ServiceNow APIs/GlideRecord/Field Level Audit/README.md @@ -0,0 +1,36 @@ +# GlideRecord Field-Level Audit + +## Description +This snippet compares two GlideRecord objects field by field and logs all differences. +It is useful for debugging, auditing updates, or validating changes in Business Rules, Script Includes, or Background Scripts. + +## Prerequisites +- Server-side context (Background Script, Business Rule, Script Include) +- Two GlideRecord objects representing the original and updated records +- Access to the table(s) involved + +## Note +- Works in Global Scope +- Server-side execution only +- Logs all fields with differences to system logs +- Does not modify any records +## Usage +```javascript +// Load original record +var oldRec = new GlideRecord('incident'); +oldRec.get('sys_id_here'); + +// Load updated record +var newRec = new GlideRecord('incident'); +newRec.get('sys_id_here'); + +// Compare and log differences +fieldLevelAudit(oldRec, newRec); +``` + +## Output +``` +Field changed: priority | Old: 5 | New: 2 +Field changed: state | Old: 1 | New: 3 +Field changed: short_description | Old: 'Old description' | New: 'New description' +``` \ No newline at end of file diff --git a/Core ServiceNow APIs/GlideRecord/Field Level Audit/fieldLevelAudit.js b/Core ServiceNow APIs/GlideRecord/Field Level Audit/fieldLevelAudit.js new file mode 100644 index 0000000000..5020556293 --- /dev/null +++ b/Core ServiceNow APIs/GlideRecord/Field Level Audit/fieldLevelAudit.js @@ -0,0 +1,25 @@ +/** + * Compare two GlideRecord objects field by field and log differences. + * + * @param {GlideRecord} grOld - Original record before changes + * @param {GlideRecord} grNew - Updated record to compare against + */ +function fieldLevelAudit(grOld, grNew) { + if (!grOld || !grNew) { + gs.error('Both old and new GlideRecord objects are required.'); + return; + } + + var fields = grOld.getFields(); + fields.forEach(function(f) { + var name = f.getName(); + var oldValue = grOld.getValue(name); + var newValue = grNew.getValue(name); + + if (oldValue != newValue) { + gs.info('Field changed: ' + name + + ' | Old: ' + oldValue + + ' | New: ' + newValue); + } + }); +} diff --git a/Core ServiceNow APIs/GlideRecord/Get Reference Record/README.md b/Core ServiceNow APIs/GlideRecord/Get Reference Record/README.md index 5735162344..c6a9bbf547 100644 --- a/Core ServiceNow APIs/GlideRecord/Get Reference Record/README.md +++ b/Core ServiceNow APIs/GlideRecord/Get Reference Record/README.md @@ -1,3 +1,19 @@ -# Get Reference Record +# Get Reference Record with GlideRecord -If you need a GlideRecord object for a reference item, then getRefRecord() is the method to use. You must call isValidRecord() after getting the reference record as getRefRecord() does not throw errors for empty values. isValidRecord() will be true if your reference record was found. +This folder contains examples demonstrating how to retrieve and work with reference records using `getRefRecord()` in ServiceNow server-side scripting. + +## Overview + +`getRefRecord()` is used to retrieve the full GlideRecord object of a reference field. This allows access to additional fields from the referenced record, such as `name`, `email`, or other attributes beyond the display value. + +Because `getRefRecord()` does not throw an error when the reference field is empty or invalid, it is important to use `isValidRecord()` to verify that the reference was successfully retrieved before accessing its fields. + +## Script Descriptions + +- get_assignment_group_from_incident.js retrieves the assignment group from an incident record and prints its name if the group exists. +- get_requested_by_user.js retrieves a change request by `sys_id`, then accesses the `requested_by` user record. If valid, it prints the user's username and email. + +## Best Practices + +- Always use `isValidRecord()` after calling `getRefRecord()` to ensure the reference is valid. +- Use `getRefRecord()` when you need to access fields from a referenced record, not just its display value. diff --git a/Core ServiceNow APIs/GlideRecord/Get Reference Record/Readme_Soumyadeep.md b/Core ServiceNow APIs/GlideRecord/Get Reference Record/Readme_Soumyadeep.md deleted file mode 100644 index 698348475d..0000000000 --- a/Core ServiceNow APIs/GlideRecord/Get Reference Record/Readme_Soumyadeep.md +++ /dev/null @@ -1,3 +0,0 @@ -This script will glide for a particular record whose sys id has been provided -It will fetch the record and then use getRefRecord() to fetch the requested by user details. -If the user is a valid record, then it will fetch the user_name and email. diff --git a/Core ServiceNow APIs/GlideRecord/Get Reference Record/script.js b/Core ServiceNow APIs/GlideRecord/Get Reference Record/get_assignment_group_from_incident.js similarity index 100% rename from Core ServiceNow APIs/GlideRecord/Get Reference Record/script.js rename to Core ServiceNow APIs/GlideRecord/Get Reference Record/get_assignment_group_from_incident.js diff --git a/Core ServiceNow APIs/GlideRecord/Get Reference Record/GetRecordRequestedBy_Soumyadeep.js b/Core ServiceNow APIs/GlideRecord/Get Reference Record/get_requested_by_user.js similarity index 100% rename from Core ServiceNow APIs/GlideRecord/Get Reference Record/GetRecordRequestedBy_Soumyadeep.js rename to Core ServiceNow APIs/GlideRecord/Get Reference Record/get_requested_by_user.js diff --git a/Core ServiceNow APIs/GlideRecord/Performance Optimization Techniques/README.md b/Core ServiceNow APIs/GlideRecord/Performance Optimization Techniques/README.md new file mode 100644 index 0000000000..453cf8be93 --- /dev/null +++ b/Core ServiceNow APIs/GlideRecord/Performance Optimization Techniques/README.md @@ -0,0 +1,44 @@ +# GlideRecord Performance Optimization Techniques + +This collection provides advanced techniques for optimizing GlideRecord queries and database operations in ServiceNow. + +## Overview + +Performance optimization is crucial for maintaining responsive ServiceNow applications, especially when dealing with large datasets or complex queries. These snippets demonstrate best practices for efficient GlideRecord usage. + +## Key Performance Principles + +- **Minimize Database Roundtrips**: Use efficient query patterns +- **Proper Indexing**: Leverage indexed fields in queries +- **Batch Operations**: Process multiple records efficiently +- **Query Optimization**: Use appropriate query methods +- **Memory Management**: Handle large datasets responsibly + +## Snippets Included + +1. **optimized_batch_processing.js** - Efficient batch processing techniques +2. **indexed_field_queries.js** - Leveraging database indexes +3. **chunked_data_processing.js** - Processing large datasets in chunks +4. **query_performance_comparison.js** - Performance comparison examples +5. **memory_efficient_operations.js** - Memory-conscious GlideRecord usage + +## Performance Monitoring + +Always measure performance using: +- `gs.log()` with timestamps for execution time +- Database query metrics in Developer Tools +- Performance Analytics for production monitoring + +## Best Practices Summary + +1. Always query on indexed fields when possible +2. Use `setLimit()` for large result sets +3. Avoid using `getRowCount()` on large tables +4. Use `chooseWindow()` for pagination +5. Consider using `GlideAggregate` for statistical queries +6. Implement proper error handling and logging + +## Related Documentation + +- [ServiceNow GlideRecord API Documentation](https://developer.servicenow.com/dev.do#!/reference/api/tokyo/server/no-namespace/c_GlideRecordScopedAPI) +- [Query Performance Best Practices](https://docs.servicenow.com/bundle/tokyo-platform-administration/page/administer/managing-data/concept/query-performance.html) diff --git a/Core ServiceNow APIs/GlideRecord/Performance Optimization Techniques/indexed_field_queries.js b/Core ServiceNow APIs/GlideRecord/Performance Optimization Techniques/indexed_field_queries.js new file mode 100644 index 0000000000..d108f267ef --- /dev/null +++ b/Core ServiceNow APIs/GlideRecord/Performance Optimization Techniques/indexed_field_queries.js @@ -0,0 +1,278 @@ +/** + * Indexed Field Query Optimization + * + * This snippet demonstrates how to leverage database indexes for optimal query performance + * in ServiceNow GlideRecord operations. + * + * Use Case: High-performance queries on large tables + * Performance Benefits: Faster query execution, reduced database load + * + * @author ServiceNow Community + * @version 1.0 + */ + +// Method 1: Using Indexed Fields for Optimal Performance +function queryWithIndexedFields() { + var gr = new GlideRecord('incident'); + + // GOOD: Query on indexed fields (state, priority, assignment_group are typically indexed) + gr.addQuery('state', 'IN', '2,3'); // Work in Progress, On Hold + gr.addQuery('priority', '<=', '3'); // High priority and above + gr.addQuery('assignment_group', '!=', ''); // Has assignment group + + // GOOD: Use sys_created_on for date ranges (indexed timestamp) + gr.addQuery('sys_created_on', '>=', gs.daysAgoStart(7)); + gr.addQuery('sys_created_on', '<=', gs.daysAgoEnd(1)); + + // GOOD: Order by indexed field + gr.orderBy('sys_created_on'); + + gr.query(); + + var results = []; + while (gr.next()) { + results.push({ + number: gr.getDisplayValue('number'), + state: gr.getDisplayValue('state'), + priority: gr.getDisplayValue('priority') + }); + } + + return results; +} + +// Method 2: Avoiding Non-Indexed Field Queries +function avoidNonIndexedQueries() { + // BAD EXAMPLE - Don't do this on large tables + function badQueryExample() { + var gr = new GlideRecord('incident'); + + // BAD: Contains query on non-indexed text field + gr.addQuery('description', 'CONTAINS', 'network issue'); + + // BAD: Complex wildcard search + gr.addQuery('short_description', 'STARTSWITH', 'Unable'); + + gr.query(); + return gr.getRowCount(); // Also bad on large tables + } + + // GOOD ALTERNATIVE - Use indexed fields and full-text search + function goodQueryAlternative() { + var gr = new GlideRecord('incident'); + + // GOOD: Use category/subcategory (often indexed) + gr.addQuery('category', 'network'); + + // GOOD: Use indexed fields first to narrow results + gr.addQuery('state', 'IN', '1,2,3'); + gr.addQuery('sys_created_on', '>=', gs.daysAgoStart(30)); + + // Then filter on text if needed (smaller result set) + gr.addQuery('short_description', 'CONTAINS', 'unable'); + + gr.query(); + + var results = []; + while (gr.next()) { + results.push(gr.getUniqueValue()); + } + + return results; + } + + return goodQueryAlternative(); +} + +// Method 3: Compound Index Optimization +function optimizeCompoundIndexes() { + var gr = new GlideRecord('task'); + + // GOOD: Query fields in order that matches compound indexes + // Many tables have compound indexes on (table, state, assigned_to) + gr.addQuery('state', '!=', '7'); // Not closed + gr.addQuery('assigned_to', '!=', ''); // Has assignee + + // Add additional filters after indexed ones + gr.addQuery('priority', '<=', '3'); + + // Order by indexed field for better performance + gr.orderBy('sys_updated_on'); + + gr.query(); + + return gr.getRowCount(); +} + +// Method 4: Reference Field Optimization +function optimizeReferenceQueries() { + // GOOD: Query reference fields by sys_id (indexed) + function queryByReferenceId() { + var groupSysId = getAssignmentGroupSysId('Hardware'); + + var gr = new GlideRecord('incident'); + gr.addQuery('assignment_group', groupSysId); // Uses index + gr.addQuery('state', '!=', '7'); // Not closed + gr.query(); + + return gr.getRowCount(); + } + + // LESS OPTIMAL: Query by reference field display value + function queryByDisplayValue() { + var gr = new GlideRecord('incident'); + gr.addQuery('assignment_group.name', 'Hardware'); // Less efficient + gr.addQuery('state', '!=', '7'); + gr.query(); + + return gr.getRowCount(); + } + + // BEST: Combine both approaches + function optimizedReferenceQuery() { + // First get the sys_id using indexed query + var groupGR = new GlideRecord('sys_user_group'); + groupGR.addQuery('name', 'Hardware'); + groupGR.query(); + + if (groupGR.next()) { + var groupSysId = groupGR.getUniqueValue(); + + // Then use sys_id in main query (indexed) + var gr = new GlideRecord('incident'); + gr.addQuery('assignment_group', groupSysId); + gr.addQuery('state', '!=', '7'); + gr.query(); + + return gr.getRowCount(); + } + + return 0; + } + + return optimizedReferenceQuery(); +} + +// Method 5: Date Range Optimization +function optimizeDateRangeQueries() { + // GOOD: Use built-in date functions with indexed timestamps + function efficientDateQuery() { + var gr = new GlideRecord('incident'); + + // Use sys_created_on (indexed) with built-in functions + gr.addQuery('sys_created_on', '>=', gs.daysAgoStart(30)); + gr.addQuery('sys_created_on', '<=', gs.daysAgoEnd(1)); + + // Add other indexed filters + gr.addQuery('state', '!=', '7'); + + gr.query(); + return gr.getRowCount(); + } + + // LESS OPTIMAL: Complex date calculations + function lessOptimalDateQuery() { + var gr = new GlideRecord('incident'); + + // Complex date calculation (harder to optimize) + var thirtyDaysAgo = new GlideDateTime(); + thirtyDaysAgo.addDaysUTC(-30); + + gr.addQuery('sys_created_on', '>=', thirtyDaysAgo); + gr.query(); + + return gr.getRowCount(); + } + + return efficientDateQuery(); +} + +// Method 6: Query Performance Analysis +function analyzeQueryPerformance() { + var queries = [ + { + name: 'Indexed Query', + query: function() { + var gr = new GlideRecord('incident'); + gr.addQuery('state', '2'); + gr.addQuery('priority', '1'); + gr.setLimit(100); + gr.query(); + return gr.getRowCount(); + } + }, + { + name: 'Non-Indexed Query', + query: function() { + var gr = new GlideRecord('incident'); + gr.addQuery('description', 'CONTAINS', 'test'); + gr.setLimit(100); + gr.query(); + return gr.getRowCount(); + } + } + ]; + + queries.forEach(function(queryTest) { + var startTime = new Date().getTime(); + var result = queryTest.query(); + var endTime = new Date().getTime(); + var executionTime = endTime - startTime; + + gs.log(queryTest.name + ':'); + gs.log(' Execution time: ' + executionTime + 'ms'); + gs.log(' Result count: ' + result); + gs.log(' Performance rating: ' + (executionTime < 100 ? 'Good' : executionTime < 500 ? 'Fair' : 'Poor')); + }); +} + +// Helper function +function getAssignmentGroupSysId(groupName) { + var gr = new GlideRecord('sys_user_group'); + gr.addQuery('name', groupName); + gr.query(); + + if (gr.next()) { + return gr.getUniqueValue(); + } + + return ''; +} + +// Method 7: Index-Aware Pagination +function indexAwarePagination(pageSize, pageNumber) { + pageSize = pageSize || 50; + pageNumber = pageNumber || 0; + + var gr = new GlideRecord('incident'); + + // Use indexed fields for filtering + gr.addQuery('state', 'IN', '1,2,3'); + gr.addQuery('sys_created_on', '>=', gs.daysAgoStart(90)); + + // Order by indexed field for consistent pagination + gr.orderBy('sys_created_on'); + gr.orderByDesc('sys_id'); // Secondary sort for tie-breaking + + // Use chooseWindow for efficient pagination + gr.chooseWindow(pageNumber * pageSize, (pageNumber + 1) * pageSize); + + gr.query(); + + var results = []; + while (gr.next()) { + results.push({ + sys_id: gr.getUniqueValue(), + number: gr.getDisplayValue('number'), + short_description: gr.getDisplayValue('short_description'), + state: gr.getDisplayValue('state') + }); + } + + return { + data: results, + page: pageNumber, + pageSize: pageSize, + hasMore: results.length === pageSize + }; +} diff --git a/Core ServiceNow APIs/GlideRecord/Performance Optimization Techniques/optimized_batch_processing.js b/Core ServiceNow APIs/GlideRecord/Performance Optimization Techniques/optimized_batch_processing.js new file mode 100644 index 0000000000..76b9f8fc7e --- /dev/null +++ b/Core ServiceNow APIs/GlideRecord/Performance Optimization Techniques/optimized_batch_processing.js @@ -0,0 +1,230 @@ +/** + * Optimized Batch Processing with GlideRecord + * + * This snippet demonstrates efficient techniques for processing large numbers of records + * while maintaining good performance and avoiding timeout issues. + * + * Use Case: Bulk updates, data migration, or mass record processing + * Performance Benefits: Reduced memory usage, better transaction management, timeout prevention + * + * @author ServiceNow Community + * @version 1.0 + */ + +// Method 1: Chunked Processing with Limit +function processRecordsInChunks() { + var tableName = 'incident'; + var chunkSize = 500; // Adjust based on your needs and system performance + var processedCount = 0; + var totalProcessed = 0; + + // Log start time for performance monitoring + var startTime = new Date().getTime(); + gs.log('Starting batch processing at: ' + new Date()); + + do { + var gr = new GlideRecord(tableName); + + // Use indexed fields for better performance + gr.addQuery('state', 'IN', '1,2,3'); // Open states + gr.addQuery('sys_created_on', '>=', gs.daysAgoStart(30)); // Last 30 days + + // Set limit for this chunk + gr.setLimit(chunkSize); + gr.orderBy('sys_created_on'); // Consistent ordering for pagination + + // Skip already processed records + gr.chooseWindow(totalProcessed, totalProcessed + chunkSize); + + gr.query(); + + processedCount = 0; + + while (gr.next()) { + try { + // Your processing logic here + updateIncidentPriority(gr); + processedCount++; + totalProcessed++; + + // Log progress every 100 records + if (processedCount % 100 === 0) { + gs.log('Processed ' + totalProcessed + ' records so far...'); + } + + } catch (error) { + gs.error('Error processing record ' + gr.getUniqueValue() + ': ' + error.message); + // Continue processing other records + } + } + + // Yield execution to prevent timeout (if in scheduled job) + gs.sleep(100); // Brief pause between chunks + + } while (processedCount === chunkSize); // Continue if we got a full chunk + + // Log completion statistics + var endTime = new Date().getTime(); + var executionTime = (endTime - startTime) / 1000; + + gs.log('Batch processing completed:'); + gs.log('- Total records processed: ' + totalProcessed); + gs.log('- Execution time: ' + executionTime + ' seconds'); + gs.log('- Average records per second: ' + (totalProcessed / executionTime).toFixed(2)); +} + +// Method 2: Optimized Bulk Update with Batch Commits +function optimizedBulkUpdate() { + var gr = new GlideRecord('task'); + + // Use compound query with indexed fields + gr.addQuery('state', 'IN', '1,2'); + gr.addQuery('priority', '4'); + gr.addQuery('sys_updated_on', '<', gs.daysAgoStart(7)); + + // Set reasonable limit to prevent memory issues + gr.setLimit(1000); + gr.query(); + + var updateCount = 0; + var batchSize = 50; + + // Start transaction for batch processing + var transaction = new GlideTransaction(); + + try { + while (gr.next()) { + // Update the record + gr.priority = '3'; // Increase priority + gr.comments = 'Priority auto-updated due to age'; + gr.update(); + + updateCount++; + + // Commit in batches to manage transaction size + if (updateCount % batchSize === 0) { + transaction.commit(); + transaction = new GlideTransaction(); // Start new transaction + gs.log('Committed batch of ' + batchSize + ' updates. Total: ' + updateCount); + } + } + + // Commit remaining records + if (updateCount % batchSize !== 0) { + transaction.commit(); + } + + gs.log('Bulk update completed. Total records updated: ' + updateCount); + + } catch (error) { + transaction.rollback(); + gs.error('Bulk update failed and rolled back: ' + error.message); + throw error; + } +} + +// Method 3: Memory-Efficient Large Dataset Processing +function processLargeDatasetEfficiently() { + var tableName = 'cmdb_ci'; + var processedTotal = 0; + var hasMoreRecords = true; + var lastSysId = ''; + + while (hasMoreRecords) { + var gr = new GlideRecord(tableName); + + // Use sys_id for cursor-based pagination (most efficient) + if (lastSysId) { + gr.addQuery('sys_id', '>', lastSysId); + } + + // Add your business logic filters + gr.addQuery('install_status', 'IN', '1,6'); // Installed or Reserved + + gr.orderBy('sys_id'); // Consistent ordering + gr.setLimit(200); // Smaller chunks for large tables + gr.query(); + + var currentBatchCount = 0; + + while (gr.next()) { + try { + // Your processing logic + processConfigurationItem(gr); + + currentBatchCount++; + processedTotal++; + lastSysId = gr.getUniqueValue(); + + } catch (error) { + gs.error('Error processing CI ' + gr.getUniqueValue() + ': ' + error.message); + } + } + + // Check if we have more records to process + hasMoreRecords = (currentBatchCount === 200); + + gs.log('Processed batch: ' + currentBatchCount + ' records. Total: ' + processedTotal); + + // Small delay to prevent overwhelming the system + gs.sleep(50); + } + + gs.log('Large dataset processing completed. Total records: ' + processedTotal); +} + +// Helper function example +function updateIncidentPriority(incidentGR) { + // Example business logic + if (incidentGR.getValue('business_impact') == '1' && incidentGR.getValue('urgency') == '1') { + incidentGR.priority = '1'; // Critical + incidentGR.update(); + } +} + +function processConfigurationItem(ciGR) { + // Example CI processing logic + ciGR.last_discovered = new GlideDateTime(); + ciGR.update(); +} + +// Method 4: Performance Monitoring Wrapper +function monitoredBatchOperation(operationName, operationFunction) { + var startTime = new Date().getTime(); + var memoryBefore = gs.getProperty('glide.script.heap.size', 'Unknown'); + + gs.log('Starting operation: ' + operationName); + gs.log('Memory before: ' + memoryBefore); + + try { + var result = operationFunction(); + + var endTime = new Date().getTime(); + var executionTime = (endTime - startTime) / 1000; + var memoryAfter = gs.getProperty('glide.script.heap.size', 'Unknown'); + + gs.log('Operation completed: ' + operationName); + gs.log('Execution time: ' + executionTime + ' seconds'); + gs.log('Memory after: ' + memoryAfter); + + return result; + + } catch (error) { + var endTime = new Date().getTime(); + var executionTime = (endTime - startTime) / 1000; + + gs.error('Operation failed: ' + operationName); + gs.error('Execution time before failure: ' + executionTime + ' seconds'); + gs.error('Error details: ' + error.message); + + throw error; + } +} + +// Example usage of performance monitoring +function exampleMonitoredOperation() { + monitoredBatchOperation('Incident Priority Update', function() { + processRecordsInChunks(); + return 'Success'; + }); +} diff --git a/Core ServiceNow APIs/GlideRecord/Performance Optimization Techniques/query_performance_comparison.js b/Core ServiceNow APIs/GlideRecord/Performance Optimization Techniques/query_performance_comparison.js new file mode 100644 index 0000000000..dc12866238 --- /dev/null +++ b/Core ServiceNow APIs/GlideRecord/Performance Optimization Techniques/query_performance_comparison.js @@ -0,0 +1,424 @@ +/** + * Query Performance Comparison and Analysis + * + * This snippet provides tools to compare different query approaches and measure their performance + * to help developers choose the most efficient methods. + * + * Use Case: Performance testing, query optimization, development best practices + * Performance Benefits: Data-driven optimization decisions, performance monitoring + * + * @author ServiceNow Community + * @version 1.0 + */ + +// Performance Testing Framework +var QueryPerformanceTester = Class.create(); +QueryPerformanceTester.prototype = { + + initialize: function(tableName) { + this.tableName = tableName || 'incident'; + this.results = []; + }, + + // Method to test different query approaches + testQuery: function(testName, queryFunction, iterations) { + iterations = iterations || 1; + var times = []; + var results = []; + + gs.log('Starting performance test: ' + testName); + + for (var i = 0; i < iterations; i++) { + var startTime = new Date().getTime(); + + try { + var result = queryFunction(); + var endTime = new Date().getTime(); + var executionTime = endTime - startTime; + + times.push(executionTime); + results.push(result); + + } catch (error) { + gs.error('Error in test "' + testName + '", iteration ' + (i + 1) + ': ' + error.message); + return null; + } + } + + var avgTime = times.reduce(function(a, b) { return a + b; }) / times.length; + var minTime = Math.min.apply(null, times); + var maxTime = Math.max.apply(null, times); + + var testResult = { + name: testName, + iterations: iterations, + averageTime: avgTime, + minimumTime: minTime, + maximumTime: maxTime, + resultCount: results[0] || 0, + allTimes: times + }; + + this.results.push(testResult); + + gs.log('Test completed: ' + testName); + gs.log('Average time: ' + avgTime + 'ms'); + gs.log('Result count: ' + (results[0] || 0)); + + return testResult; + }, + + // Compare multiple query approaches + compareQueries: function(queryTests, iterations) { + iterations = iterations || 3; + + gs.log('Starting query performance comparison with ' + iterations + ' iterations each'); + + var self = this; + queryTests.forEach(function(test) { + self.testQuery(test.name, test.query, iterations); + }); + + this.printComparison(); + return this.results; + }, + + // Print comparison results + printComparison: function() { + if (this.results.length === 0) { + gs.log('No test results to compare'); + return; + } + + gs.log('\n=== Query Performance Comparison Results ==='); + + // Sort by average time + var sortedResults = this.results.slice().sort(function(a, b) { + return a.averageTime - b.averageTime; + }); + + sortedResults.forEach(function(result, index) { + var rank = index + 1; + var performance = rank === 1 ? 'BEST' : rank === sortedResults.length ? 'WORST' : 'GOOD'; + + gs.log('\nRank #' + rank + ' (' + performance + '): ' + result.name); + gs.log(' Average Time: ' + result.averageTime.toFixed(2) + 'ms'); + gs.log(' Min/Max Time: ' + result.minimumTime + 'ms / ' + result.maximumTime + 'ms'); + gs.log(' Result Count: ' + result.resultCount); + + if (index > 0) { + var percentSlower = ((result.averageTime - sortedResults[0].averageTime) / sortedResults[0].averageTime * 100); + gs.log(' Performance: ' + percentSlower.toFixed(1) + '% slower than best'); + } + }); + + gs.log('\n=== Recommendations ==='); + gs.log('Use "' + sortedResults[0].name + '" for best performance'); + if (sortedResults.length > 1) { + gs.log('Avoid "' + sortedResults[sortedResults.length - 1].name + '" if possible'); + } + } +}; + +// Example 1: Comparing Different Query Approaches +function compareBasicQueryMethods() { + var tester = new QueryPerformanceTester('incident'); + + var queryTests = [ + { + name: 'Indexed Field Query (Optimal)', + query: function() { + var gr = new GlideRecord('incident'); + gr.addQuery('state', '2'); // Work in Progress (indexed) + gr.addQuery('priority', '1'); // Critical (indexed) + gr.setLimit(100); + gr.query(); + return gr.getRowCount(); + } + }, + { + name: 'Multiple OR Conditions', + query: function() { + var gr = new GlideRecord('incident'); + gr.addQuery('state', 'IN', '1,2,3'); // Multiple states + gr.addQuery('priority', 'IN', '1,2'); // High priorities + gr.setLimit(100); + gr.query(); + return gr.getRowCount(); + } + }, + { + name: 'Text Search (Non-Indexed)', + query: function() { + var gr = new GlideRecord('incident'); + gr.addQuery('short_description', 'CONTAINS', 'network'); // Text search + gr.setLimit(100); + gr.query(); + return gr.getRowCount(); + } + }, + { + name: 'Reference Field Display Value', + query: function() { + var gr = new GlideRecord('incident'); + gr.addQuery('caller_id.name', 'CONTAINS', 'John'); // Reference display value + gr.setLimit(100); + gr.query(); + return gr.getRowCount(); + } + } + ]; + + return tester.compareQueries(queryTests, 3); +} + +// Example 2: Date Query Performance Comparison +function compareDateQueryMethods() { + var tester = new QueryPerformanceTester('incident'); + + var queryTests = [ + { + name: 'Built-in Date Functions (Optimal)', + query: function() { + var gr = new GlideRecord('incident'); + gr.addQuery('sys_created_on', '>=', gs.daysAgoStart(30)); + gr.addQuery('sys_created_on', '<=', gs.daysAgoEnd(1)); + gr.setLimit(100); + gr.query(); + return gr.getRowCount(); + } + }, + { + name: 'GlideDateTime Calculations', + query: function() { + var gr = new GlideRecord('incident'); + var thirtyDaysAgo = new GlideDateTime(); + thirtyDaysAgo.addDaysUTC(-30); + var oneDayAgo = new GlideDateTime(); + oneDayAgo.addDaysUTC(-1); + + gr.addQuery('sys_created_on', '>=', thirtyDaysAgo); + gr.addQuery('sys_created_on', '<=', oneDayAgo); + gr.setLimit(100); + gr.query(); + return gr.getRowCount(); + } + }, + { + name: 'String Date Comparison', + query: function() { + var gr = new GlideRecord('incident'); + var today = new Date(); + var thirtyDaysAgo = new Date(today.getTime() - (30 * 24 * 60 * 60 * 1000)); + + gr.addQuery('sys_created_on', '>=', thirtyDaysAgo.toISOString()); + gr.setLimit(100); + gr.query(); + return gr.getRowCount(); + } + } + ]; + + return tester.compareQueries(queryTests, 5); +} + +// Example 3: Pagination Method Comparison +function comparePaginationMethods() { + var tester = new QueryPerformanceTester('incident'); + var pageSize = 50; + var pageNumber = 2; + + var queryTests = [ + { + name: 'chooseWindow() Method (Optimal)', + query: function() { + var gr = new GlideRecord('incident'); + gr.addQuery('state', '!=', '7'); + gr.orderBy('sys_created_on'); + gr.chooseWindow(pageNumber * pageSize, (pageNumber + 1) * pageSize); + gr.query(); + + var count = 0; + while (gr.next()) { count++; } + return count; + } + }, + { + name: 'setLimit() with Offset Simulation', + query: function() { + var gr = new GlideRecord('incident'); + gr.addQuery('state', '!=', '7'); + gr.orderBy('sys_created_on'); + gr.setLimit(pageSize * (pageNumber + 1)); + gr.query(); + + var count = 0; + var skip = pageSize * pageNumber; + while (gr.next()) { + if (skip > 0) { + skip--; + continue; + } + count++; + } + return count; + } + } + ]; + + return tester.compareQueries(queryTests, 3); +} + +// Example 4: Aggregate vs Manual Counting +function compareCountingMethods() { + var tester = new QueryPerformanceTester('incident'); + + var queryTests = [ + { + name: 'GlideAggregate COUNT (Optimal)', + query: function() { + var ga = new GlideAggregate('incident'); + ga.addQuery('state', '!=', '7'); + ga.addAggregate('COUNT'); + ga.query(); + + if (ga.next()) { + return parseInt(ga.getAggregate('COUNT')); + } + return 0; + } + }, + { + name: 'GlideRecord getRowCount()', + query: function() { + var gr = new GlideRecord('incident'); + gr.addQuery('state', '!=', '7'); + gr.query(); + return gr.getRowCount(); + } + }, + { + name: 'Manual Counting with Loop', + query: function() { + var gr = new GlideRecord('incident'); + gr.addQuery('state', '!=', '7'); + gr.query(); + + var count = 0; + while (gr.next()) { count++; } + return count; + } + } + ]; + + return tester.compareQueries(queryTests, 3); +} + +// Example 5: Complex Query Optimization +function compareComplexQueries() { + var tester = new QueryPerformanceTester('incident'); + + var queryTests = [ + { + name: 'Optimized Complex Query', + query: function() { + var gr = new GlideRecord('incident'); + + // Start with most selective indexed fields + gr.addQuery('state', 'IN', '1,2,3'); + gr.addQuery('priority', '<=', '3'); + gr.addQuery('sys_created_on', '>=', gs.daysAgoStart(30)); + + // Add less selective filters after + gr.addQuery('assignment_group', '!=', ''); + + gr.orderBy('sys_created_on'); + gr.setLimit(100); + gr.query(); + + return gr.getRowCount(); + } + }, + { + name: 'Non-Optimized Complex Query', + query: function() { + var gr = new GlideRecord('incident'); + + // Start with less selective fields + gr.addQuery('short_description', 'CONTAINS', 'issue'); + gr.addQuery('assignment_group', '!=', ''); + gr.addQuery('state', 'IN', '1,2,3'); + gr.addQuery('priority', '<=', '3'); + + gr.setLimit(100); + gr.query(); + + return gr.getRowCount(); + } + } + ]; + + return tester.compareQueries(queryTests, 3); +} + +// Comprehensive Performance Analysis +function runCompletePerformanceAnalysis() { + gs.log('=== Starting Comprehensive Query Performance Analysis ==='); + + var allResults = []; + + gs.log('\n1. Basic Query Methods Comparison:'); + allResults.push(compareBasicQueryMethods()); + + gs.log('\n2. Date Query Methods Comparison:'); + allResults.push(compareDateQueryMethods()); + + gs.log('\n3. Pagination Methods Comparison:'); + allResults.push(comparePaginationMethods()); + + gs.log('\n4. Counting Methods Comparison:'); + allResults.push(compareCountingMethods()); + + gs.log('\n5. Complex Query Optimization:'); + allResults.push(compareComplexQueries()); + + gs.log('\n=== Performance Analysis Complete ==='); + + return { + basicQueries: allResults[0], + dateQueries: allResults[1], + pagination: allResults[2], + counting: allResults[3], + complexQueries: allResults[4] + }; +} + +// Quick Performance Check for Development +function quickPerformanceCheck(tableName, testQuery) { + var startTime = new Date().getTime(); + + var gr = new GlideRecord(tableName); + testQuery(gr); // Apply query function + gr.query(); + + var count = 0; + while (gr.next()) { count++; } + + var endTime = new Date().getTime(); + var executionTime = endTime - startTime; + + var performance = { + executionTime: executionTime, + resultCount: count, + performance: executionTime < 100 ? 'Excellent' : + executionTime < 500 ? 'Good' : + executionTime < 1000 ? 'Fair' : 'Poor' + }; + + gs.log('Quick Performance Check Results:'); + gs.log('- Execution Time: ' + executionTime + 'ms'); + gs.log('- Result Count: ' + count); + gs.log('- Performance Rating: ' + performance.performance); + + return performance; +} diff --git a/Core ServiceNow APIs/GlideRecord/Safe Bulk Delete/README.md b/Core ServiceNow APIs/GlideRecord/Safe Bulk Delete/README.md new file mode 100644 index 0000000000..069bc4b7d0 --- /dev/null +++ b/Core ServiceNow APIs/GlideRecord/Safe Bulk Delete/README.md @@ -0,0 +1,32 @@ +# GlideRecord Bulk Delete with Safety Checks + +## Description +This snippet allows you to safely delete multiple records from a ServiceNow table based on an encoded query. +It logs all records that match the query so you can review them before actually deleting anything. +Helps prevent accidental mass deletion of important data. + +## Note +- Works in Global Scope by default +- Can be executed in Background Scripts or Script Includes +- **ALWAYS REVIEW LOGS BEFORE ENABLING DELETION** +## Prerequisites +- Server-side context (Background Script, Business Rule, Script Include) +- Access to the target table +- Basic understanding of GlideRecord and Encoded Queries + +## Usage +```javascript +// Logs all active low-priority incidents that would be deleted +safeDelete('incident', 'active=true^priority=5'); + +// To perform actual deletion, uncomment gr.deleteRecord() inside the function +``` + +## Output +``` +Records matching query: 3 +Record sys_id: 12345abcdef would be deleted. +Record sys_id: 23456bcdef would be deleted. +Record sys_id: 34567cdefg would be deleted. +Bulk delete preview complete. Verify logs before enabling deletion. +``` diff --git a/Core ServiceNow APIs/GlideRecord/Safe Bulk Delete/safeDelete.js b/Core ServiceNow APIs/GlideRecord/Safe Bulk Delete/safeDelete.js new file mode 100644 index 0000000000..24417876c2 --- /dev/null +++ b/Core ServiceNow APIs/GlideRecord/Safe Bulk Delete/safeDelete.js @@ -0,0 +1,33 @@ +/** + * Safely delete multiple records from a ServiceNow table. + * Logs all affected records before deletion to prevent accidental data loss. + * Uncomment gr.deleteRecord() to perform the actual deletion. + * + * @param {string} table - The table name + * @param {string} encodedQuery - GlideRecord encoded query for filtering records + */ +function safeDelete(table, encodedQuery) { + if (!table || !encodedQuery) { + gs.error('Both table name and encoded query are required.'); + return; + } + + var gr = new GlideRecord(table); + gr.addEncodedQuery(encodedQuery); + gr.query(); + + var count = gr.getRowCount(); + gs.info('Records matching query: ' + count); + + if (count === 0) { + gs.info('No records found. Nothing to delete.'); + return; + } + + while (gr.next()) { + gs.info('Record sys_id: ' + gr.getValue('sys_id') + ' would be deleted.'); + // gr.deleteRecord(); // Uncomment this line to actually delete + } + + gs.info('Bulk delete preview complete. Verify logs before enabling deletion.'); +} diff --git a/Integration/Data Export to ML Pipeline/Export Data for ML Training/README.md b/Integration/Data Export to ML Pipeline/Export Data for ML Training/README.md new file mode 100644 index 0000000000..95896815f8 --- /dev/null +++ b/Integration/Data Export to ML Pipeline/Export Data for ML Training/README.md @@ -0,0 +1,31 @@ +# Export ServiceNow Data to ML Pipeline + +## Overview +This snippet shows how to export incident data from ServiceNow and feed it into an external ML pipeline for analysis and predictions. + +## What It Does +- **Script Include**: Queries incidents from ServiceNow +- **Scripted REST API**: Exposes data as JSON endpoint +- **Python Script**: Consumes data, preprocesses it, and runs basic ML operations +- **Result Storage**: Sends predictions back to ServiceNow + +## Use Cases +- Predict incident resolution time +- Classify tickets automatically +- Detect anomalies in service data +- Smart assignment recommendations + +## Files +- `data_export_script_include.js` - Server-side Script Include to query incident data +- `export_data_rest_api.js` - Scripted REST API to expose data as JSON endpoint + +## How to Use +1. Create a Script Include in ServiceNow named `MLDataExporter` using `data_export_script_include.js` +2. Create a Scripted REST API with base path `/api/ml_export` and resource `/incidents` using `export_data_rest_api.js` +3. Call the endpoint: `GET /api/ml_export/incidents?limit=100` +4. External ML systems can fetch formatted incident data via this REST endpoint + +## Requirements +- ServiceNow instance with REST API access +- Python 3.8+ with requests library +- API credentials (username/password or OAuth token) diff --git a/Integration/Data Export to ML Pipeline/Export Data for ML Training/data_export_script_include.js b/Integration/Data Export to ML Pipeline/Export Data for ML Training/data_export_script_include.js new file mode 100644 index 0000000000..5d17fb021c --- /dev/null +++ b/Integration/Data Export to ML Pipeline/Export Data for ML Training/data_export_script_include.js @@ -0,0 +1,49 @@ +// Script Include: MLDataExporter +// Purpose: Query incident data for ML pipeline consumption +// Usage: var exporter = new MLDataExporter(); var data = exporter.getIncidentData(limit); + +var MLDataExporter = Class.create(); +MLDataExporter.prototype = { + initialize: function() {}, + + // Extract incident records suitable for ML training + getIncidentData: function(limit) { + limit = limit || 100; + var incidents = []; + + // Query incidents from database + var gr = new GlideRecord('incident'); + gr.addQuery('active', 'true'); + gr.addQuery('state', '!=', ''); // exclude blank states + gr.setLimit(limit); + gr.query(); + + while (gr.next()) { + // Extract fields relevant for ML analysis + incidents.push({ + id: gr.getValue('sys_id'), + description: gr.getValue('description'), + short_description: gr.getValue('short_description'), + category: gr.getValue('category'), + priority: gr.getValue('priority'), + impact: gr.getValue('impact'), + urgency: gr.getValue('urgency'), + state: gr.getValue('state'), + created_on: gr.getValue('sys_created_on'), + resolution_time: this._calculateResolutionTime(gr) + }); + } + + return incidents; + }, + + // Calculate resolution time in hours (useful ML feature) + _calculateResolutionTime: function(gr) { + var created = new GlideDateTime(gr.getValue('sys_created_on')); + var resolved = new GlideDateTime(gr.getValue('sys_updated_on')); + var diff = GlideDateTime.subtract(created, resolved); + return Math.abs(diff / (1000 * 60 * 60)); // convert to hours + }, + + type: 'MLDataExporter' +}; diff --git a/Integration/Data Export to ML Pipeline/Export Data for ML Training/export_data_rest_api.js b/Integration/Data Export to ML Pipeline/Export Data for ML Training/export_data_rest_api.js new file mode 100644 index 0000000000..e8addc377a --- /dev/null +++ b/Integration/Data Export to ML Pipeline/Export Data for ML Training/export_data_rest_api.js @@ -0,0 +1,41 @@ +// Scripted REST API Resource: ML Data Export +// Base Path: /api/ml_export +// Resource Path: /incidents +// HTTP Method: GET +// Parameters: ?limit=100&offset=0 + +(function process(request, response) { + try { + // Get query parameters + var limit = request.getParameter('limit') || 100; + var offset = request.getParameter('offset') || 0; + + // Use the Script Include to fetch data + var exporter = new MLDataExporter(); + var incidents = exporter.getIncidentData(limit); + + // Prepare response with metadata + var result = { + status: 'success', + count: incidents.length, + data: incidents, + timestamp: new GlideDateTime().toString() + }; + + response.setContentType('application/json'); + response.setStatus(200); + response.getStreamWriter().writeString(JSON.stringify(result)); + + } catch (error) { + // Error handling for ML pipeline + response.setStatus(500); + response.setContentType('application/json'); + var error_response = { + status: 'error', + message: error.toString(), + timestamp: new GlideDateTime().toString() + }; + response.getStreamWriter().writeString(JSON.stringify(error_response)); + gs.log('ML Export API Error: ' + error.toString(), 'MLDataExport'); + } +})(request, response); diff --git a/Integration/GraphQL Integration API/CI Resolver.js b/Integration/GraphQL Integration API/Incident GraphQL resolvers/CI Resolver.js similarity index 100% rename from Integration/GraphQL Integration API/CI Resolver.js rename to Integration/GraphQL Integration API/Incident GraphQL resolvers/CI Resolver.js diff --git a/Integration/GraphQL Integration API/GraphQL schema.js b/Integration/GraphQL Integration API/Incident GraphQL resolvers/GraphQL schema.js similarity index 100% rename from Integration/GraphQL Integration API/GraphQL schema.js rename to Integration/GraphQL Integration API/Incident GraphQL resolvers/GraphQL schema.js diff --git a/Integration/GraphQL Integration API/Incident Resolver.js b/Integration/GraphQL Integration API/Incident GraphQL resolvers/Incident Resolver.js similarity index 100% rename from Integration/GraphQL Integration API/Incident Resolver.js rename to Integration/GraphQL Integration API/Incident GraphQL resolvers/Incident Resolver.js diff --git a/Integration/GraphQL Integration API/README.md b/Integration/GraphQL Integration API/Incident GraphQL resolvers/README.md similarity index 100% rename from Integration/GraphQL Integration API/README.md rename to Integration/GraphQL Integration API/Incident GraphQL resolvers/README.md diff --git a/Integration/GraphQL Integration API/User Resolver.js b/Integration/GraphQL Integration API/Incident GraphQL resolvers/User Resolver.js similarity index 100% rename from Integration/GraphQL Integration API/User Resolver.js rename to Integration/GraphQL Integration API/Incident GraphQL resolvers/User Resolver.js diff --git a/Integration/ITSM/README.md b/Integration/ITSM/Bulk task_ci REST API/README.md similarity index 100% rename from Integration/ITSM/README.md rename to Integration/ITSM/Bulk task_ci REST API/README.md diff --git a/Integration/ITSM/liveCItoTAsk.js b/Integration/ITSM/Bulk task_ci REST API/liveCItoTAsk.js similarity index 100% rename from Integration/ITSM/liveCItoTAsk.js rename to Integration/ITSM/Bulk task_ci REST API/liveCItoTAsk.js diff --git a/Integration/Import Sets Debug/README.md b/Integration/Import Sets Debug/Debug import set payloads/README.md similarity index 100% rename from Integration/Import Sets Debug/README.md rename to Integration/Import Sets Debug/Debug import set payloads/README.md diff --git a/Integration/Import Sets Debug/debugImportSet.js b/Integration/Import Sets Debug/Debug import set payloads/debugImportSet.js similarity index 100% rename from Integration/Import Sets Debug/debugImportSet.js rename to Integration/Import Sets Debug/Debug import set payloads/debugImportSet.js diff --git a/Integration/Import Sets/Import sets overview/ModelManufacture.README.md b/Integration/Import Sets/Import sets overview/ModelManufacture.README.md new file mode 100644 index 0000000000..616626a4d4 --- /dev/null +++ b/Integration/Import Sets/Import sets overview/ModelManufacture.README.md @@ -0,0 +1,16 @@ +When importing or processing Configuration Items (CIs), especially hardware assets, missing model or manufacturer data can cause CI creation failures or incomplete relationships. +This script handles that automatically by: +* Checking if a manufacturer already exists in the core_company table. +* Checking if a model already exists in the cmdb_model hierarchy. +* Creating the manufacturer and/or model records if they are missing. + +How It Works + +1. Extracts model and manufacturer names from the data source (source.u_model and source.u_manufacturer). +2. Calls getOrCreateManufacturer(): + * Searches the core_company table by name. + * Creates a new company record if not found. +3. Calls getOrCreateModel(): + * Searches the cmdb_model table (including child tables). + * If no match exists, inserts a new record in cmdb_hardware_product_model. +4. Logs each action (found, created, or failed) for debugging and auditing. diff --git a/Integration/Import Sets/Import sets overview/ModelManufacture.js b/Integration/Import Sets/Import sets overview/ModelManufacture.js new file mode 100644 index 0000000000..d678de1478 --- /dev/null +++ b/Integration/Import Sets/Import sets overview/ModelManufacture.js @@ -0,0 +1,70 @@ + var modelName = source.u_model; + var manufacturerName = source.u_manufacturer; + +function getOrCreateModel(modelName, manufacturerName) { + try { + var manufacturerSysId = getOrCreateManufacturer(manufacturerName); + if (!manufacturerSysId) { + gs.error("MODEL SCRIPT: Failed to find or create manufacturer: " + manufacturerName); + return null; + } + + // Query cmdb_model to check if any existing model (including child tables) exists + var modelGr = new GlideRecord('cmdb_model'); + modelGr.addQuery('name', modelName); + modelGr.addQuery('manufacturer', manufacturerSysId); + modelGr.query(); + + if (modelGr.next()) { + gs.info("MODEL SCRIPT: Found existing model: " + modelGr.getUniqueValue()); + return modelGr.getUniqueValue(); + } else { + // Create in child table: cmdb_hardware_product_model + var newModel = new GlideRecord('cmdb_hardware_product_model'); + newModel.initialize(); + newModel.setValue('name', modelName); + newModel.setValue('manufacturer', manufacturerSysId); + var newModelSysId = newModel.insert(); + + if (newModelSysId) { + gs.info("MODEL SCRIPT: Created new hardware model: " + newModelSysId); + return newModelSysId; + } else { + gs.error("MODEL SCRIPT: Failed to insert new model for " + modelName); + return null; + } + } + } catch (e) { + gs.error("MODEL SCRIPT: Error in getOrCreateModel(): " + (e.message || e)); + return null; + } + } + + function getOrCreateManufacturer(name) { + try { + var companyGr = new GlideRecord('core_company'); + companyGr.addQuery('name', name); + companyGr.query(); + + if (companyGr.next()) { + gs.info("MODEL SCRIPT: Found manufacturer: " + companyGr.getUniqueValue()); + return companyGr.getUniqueValue(); + } else { + companyGr.initialize(); + companyGr.setValue('name', name); + //companyGr.setValue('manufacturer', true); // Ensure it’s marked as manufacturer + var newMfrSysId = companyGr.insert(); + + if (newMfrSysId) { + gs.info("MODEL SCRIPT: Created new manufacturer: " + newMfrSysId); + return newMfrSysId; + } else { + gs.error("MODEL SCRIPT: Failed to insert new manufacturer: " + name); + return null; + } + } + } catch (e) { + gs.error("MODEL SCRIPT: Error in getOrCreateManufacturer(): " + (e.message || e)); + return null; + } + } diff --git a/Integration/Import Sets/README.md b/Integration/Import Sets/Import sets overview/README.md similarity index 100% rename from Integration/Import Sets/README.md rename to Integration/Import Sets/Import sets overview/README.md diff --git a/Integration/Import Sets/Import sets overview/TriggerDataSource.README.md b/Integration/Import Sets/Import sets overview/TriggerDataSource.README.md new file mode 100644 index 0000000000..5b434ce011 --- /dev/null +++ b/Integration/Import Sets/Import sets overview/TriggerDataSource.README.md @@ -0,0 +1,4 @@ +The triggerDataSource() function eliminates the need for manually executing a Data Source from the UI.
It programmatically triggers the import of a predefined Data Source record and loads the associated data into an Import Set table. +This function is typically used in: +* Scheduled Script Executions +* Flow Designer Actions. diff --git a/Integration/Import Sets/Import sets overview/TriggerDataSource.js b/Integration/Import Sets/Import sets overview/TriggerDataSource.js new file mode 100644 index 0000000000..e784d890f3 --- /dev/null +++ b/Integration/Import Sets/Import sets overview/TriggerDataSource.js @@ -0,0 +1,14 @@ +triggerDataSource: function() { + + var dataSourceSysId = gs.getProperty('ds.tag.based.sys.id'); //Store the sysId of DataSource from system property + + var grDataSource = new GlideRecord('sys_data_source'); + if (grDataSource.get(dataSourceSysId)) { + var loader = new GlideImportSetLoader(); //OOB Method to load + var importSetRec = loader.getImportSetGr(grDataSource); + var ranload = loader.loadImportSetTable(importSetRec, grDataSource); + importSetRec.state = "loaded"; + importSetRec.update(); + return importSetRec.getUniqueValue(); + } +}, diff --git a/Integration/Mail Scripts/Redact PII from outbound email body/README.md b/Integration/Mail Scripts/Redact PII from outbound email body/README.md new file mode 100644 index 0000000000..b929b39b3e --- /dev/null +++ b/Integration/Mail Scripts/Redact PII from outbound email body/README.md @@ -0,0 +1,22 @@ +# Redact PII from outbound email body + +## What this solves +Notifications sometimes leak personal data into emails. This mail script replaces common identifiers in the email body with redacted tokens before send. + +## Where to use +Notification or Email Script record, Advanced view, "Mail script" field. Invoke the function to get a safe body string and print it. + +## How it works +- Applies regex patterns to the email text for emails, phone numbers, IP addresses, NI number style patterns, and 16-digit card-like numbers +- Replaces matches with descriptive placeholders +- Leaves HTML tags intact by operating on the plain text portion you pass in + +## Configure +- Extend or tighten patterns for your organisation +- Toggle specific scrubs on or off in the config block + +## References +- Email Scripts + https://www.servicenow.com/docs/bundle/zurich-platform-administration/page/administer/notification/reference/email-scripts.html +- Notifications + https://www.servicenow.com/docs/bundle/zurich-platform-administration/page/administer/notification/concept/c_EmailNotifications.html diff --git a/Integration/Mail Scripts/Redact PII from outbound email body/mail_redact_pii.js b/Integration/Mail Scripts/Redact PII from outbound email body/mail_redact_pii.js new file mode 100644 index 0000000000..c3c9f50cea --- /dev/null +++ b/Integration/Mail Scripts/Redact PII from outbound email body/mail_redact_pii.js @@ -0,0 +1,54 @@ +// Mail Script: Redact PII from outbound email body +// Usage inside a Notification (Advanced view): +// var safe = redactPii(current.short_description + '\n\n' + current.description); +// template.print(safe); + +(function() { + function redactPii(text) { + if (!text) return ''; + + // Config: toggle specific redactions + var cfg = { + email: true, + phone: true, + ip: true, + niNumber: true, + card16: true + }; + + var out = String(text); + + // Email addresses + if (cfg.email) { + out = out.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '[redacted email]'); + } + + // Phone numbers (UK leaning, permissive, 7+ digits ignoring separators) + if (cfg.phone) { + out = out.replace(/\b(?:\+?\d{1,3}[\s-]?)?(?:\(?\d{3,5}\)?[\s-]?)?\d{3,4}[\s-]?\d{3,4}\b/g, '[redacted phone]'); + } + + // IPv4 addresses + if (cfg.ip) { + out = out.replace(/\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b/g, '[redacted ip]'); + } + + // National Insurance number style (AA 00 00 00 A) simplified - UK Specific + if (cfg.niNumber) { + out = out.replace(/\b([A-CEGHJ-PR-TW-Z]{2}\s*\d{2}\s*\d{2}\s*\d{2}\s*[A-D])\b/gi, '[redacted ni]'); + } + + // 16 consecutive digits that look like a card (permit separators) + if (cfg.card16) { + out = out.replace(/\b(?:\d[ -]?){13,19}\b/g, function(match) { + var digits = match.replace(/[ -]/g, ''); + return digits.length >= 13 && digits.length <= 19 ? '[redacted card]' : match; + }); + } + + return out; + } + + // Expose function to the mail template scope + this.redactPii = redactPii; +}).call(this); diff --git a/Integration/RESTMessageV2/Aadhaar Verification/Readme.md b/Integration/RESTMessageV2/Aadhaar Verification/Readme.md new file mode 100644 index 0000000000..21702357eb --- /dev/null +++ b/Integration/RESTMessageV2/Aadhaar Verification/Readme.md @@ -0,0 +1,83 @@ +# Aadhaar Integration - ServiceNow Integration + +A **production-ready ServiceNow Script Include** for **Aadhaar verification** and **eKYC**, utilizing a **Connection & Credential Alias** for secure, secret-free integration. + +⚠️ **Compliance Warning:** Adherence to **UIDAI regulations**, the **IT Act**, **DPDP**, and your provider's terms is mandatory. Always obtain **explicit user consent** before performing any verification. + +--- + +## 🎯 Key Features + +This script provides a complete flow for secure Aadhaar integration: + +| Feature | Methods | Description | +| :--------------------- | :------------------------- | :------------------------------------------------------------------------------- | +| **OTP eKYC Flow** | `sendOtp()`, `verifyOtp()` | Multi-step process with built-in session management (expiry, replay protection). | +| **Demographic Check** | `verifyDemographic()` | Verify identity (Name, DOB, Gender) _without_ OTP. Returns a match score. | +| **Document Retrieval** | `getDocument()` | Downloads Aadhaar XML/PDF (supports encrypted documents). | +| **Status Tracking** | `checkStatus()` | Queries the status of any ongoing verification session. | + +### 🔒 Security & Compliance Built-in + +- **No Hardcoded Secrets:** Uses **Connection & Credential Alias** (`aadhaar_api`). +- **Data Masking:** Hides Aadhaar/mobile numbers in logs (e.g., `XXXX-XXXX-1234`). +- **Audit Logging:** Comprehensive logging of purpose, actor, consent, and outcome. +- **Consent Tracking:** Persistent tracking of the user's consent statement and timestamp. + +--- + +## 🛠️ Setup & Configuration + +1. **Create Connection & Credential Alias:** + + - **Name:** `aadhaar_api` (or your chosen alias). + - **Base URL:** Your provider's base endpoint (`https://api.provider.tld`). + - **Auth:** Configure credentials (API Key, OAuth, Basic) as per your provider. + +2. **Configure Endpoints (In Script):** Map your provider's API paths inside the Script Include: + ```javascript + var endpoints = { + otpSend: "/aadhaar/otp/send", + otpVerify: "/aadhaar/otp/verify", + // ... and others + }; + ``` +3. **Set Timeouts & Retries:** Configure `timeoutMs` (default 8000) and `retries` (default 2) to manage reliability. + +--- + +## 🚀 Usage Examples + +Use from a **Background Script** or any server-side logic: + +```javascript +var sa = new SmartAadhaar(); + +// 1. Send OTP & Get Session ID +var s1 = sa.sendOtp({ + uid: "123412341234", + purpose: "eKYC for onboarding", + consent: true, +}); + +// 2. Verify OTP (using session_id from s1) +var s2 = sa.verifyOtp({ + uid: "123412341234", + otp: "123456", + session_id: s1.data.session_id, +}); +if (s2.ok) { + gs.info("eKYC success for: " + s2.data.name); +} +``` + +## API Reference + +All methods return a normalized envelope: +`{ ok:Boolean, code:Number, message:String, data:Object }` + +| Method | Purpose | Required Parameters | +| :--------------------- | :------------------------ | :----------------------------- | +| `sendOtp(p)` | Initiate OTP flow | `uid`, `purpose`, `consent` | +| `verifyOtp(p)` | Verify OTP and fetch eKYC | `uid`, `otp`, `session_id` | +| `verifyDemographic(p)` | Demographic match w/o OTP | `uid`, `name`, `dob`, `gender` | diff --git a/Integration/RESTMessageV2/Aadhaar Verification/script.js b/Integration/RESTMessageV2/Aadhaar Verification/script.js new file mode 100644 index 0000000000..6df5040766 --- /dev/null +++ b/Integration/RESTMessageV2/Aadhaar Verification/script.js @@ -0,0 +1,745 @@ +/** + * Name: Aadhaar + * Purpose: Complete Aadhaar verification wrapper via REST using Connection & Credential Alias + * + * Features: + * - Send OTP to Aadhaar-linked mobile + * - Verify OTP and retrieve eKYC data + * - Demographic verification (name, DOB, gender) + * - Document download/retrieval + * - Session management + * - Rate limiting protection + * - Comprehensive error handling + * + * Setup: + * 1. Create Connection & Credential Alias: 'aadhaar_api' + * 2. Configure base endpoint (e.g., https://api.provider.com/v1) + * 3. Add API key/token in credential + * 4. Adjust paths based on your provider's API specification + * + * Security Notes: + * - Never log sensitive data (OTP, full Aadhaar, personal info) + * - Use encrypted fields for storing verification results + * - Implement proper ACLs on tables using this script + * - Audit all verification attempts + */ + +var Aadhaar = Class.create(); +Aadhaar.prototype = { + initialize: function () { + this.alias = "aadhaar_api"; + this.timeoutMs = 15000; + this.retries = 2; + this.sessionStore = {}; + + // API endpoints - adjust based on your provider + this.endpoints = { + sendOtp: "/aadhaar/otp/send", + verifyOtp: "/aadhaar/otp/verify", + verifyDemo: "/aadhaar/demographic/verify", + getDocument: "/aadhaar/document/download", + checkStatus: "/aadhaar/status", + }; + + // Validation patterns + this.patterns = { + aadhaar: /^[2-9]{1}[0-9]{11}$/, + mobile: /^[6-9]\d{9}$/, + otp: /^\d{6}$/, + dob: /^\d{4}-\d{2}-\d{2}$/, + gender: /^(M|F|O)$/i, + }; + }, + + /** + * Step 1: Send OTP to Aadhaar-linked mobile number + * @param {Object} params + * - uid: Aadhaar number (12 digits) + * - captcha: Optional captcha if required by provider + * - consentGiven: Boolean, must be true + * @returns {Object} { ok, code, message, data: { txnId, mobile, validUntil } } + */ + sendOtp: function (params) { + try { + params = params || {}; + + // Input validation + if (!params.uid) { + return this._err("Aadhaar number is required", 400); + } + + if (!params.consentGiven) { + return this._err( + "User consent is required for Aadhaar verification", + 403 + ); + } + + var uid = this._sanitizeAadhaar(params.uid); + if (!this.patterns.aadhaar.test(uid)) { + return this._err("Invalid Aadhaar number format", 400); + } + + var payload = { + aadhaar_number: uid, + consent: "Y", + timestamp: new GlideDateTime().getDisplayValue(), + }; + + if (params.captcha) { + payload.captcha = String(params.captcha); + } + + // Make API call with retry logic + var result = this._executeWithRetry( + "POST", + this.endpoints.sendOtp, + payload + ); + + if (result.ok) { + var responseData = result.data || {}; + + // Store session info (transaction ID) + var txnId = + responseData.transaction_id || + responseData.txnId || + responseData.txn_id; + if (txnId) { + this._storeSession(txnId, { + uid: this._maskAadhaar(uid), + createdAt: new GlideDateTime().getNumericValue(), + stage: "otp_sent", + }); + } + + // Audit log + this._auditLog("OTP_SENT", uid, true, txnId); + + return { + ok: true, + code: result.code, + message: "OTP sent successfully", + data: { + txnId: txnId, + mobile: this._maskMobile(responseData.mobile || responseData.phone), + validUntil: responseData.valid_until || responseData.otpExpiry, + message: responseData.message || "OTP sent to registered mobile", + }, + }; + } + + this._auditLog("OTP_SEND_FAILED", uid, false); + return result; + } catch (e) { + gs.error("[Aadhaar] sendOtp error: " + e); + return this._err("Error sending OTP: " + e, 500); + } + }, + + /** + * Step 2: Verify OTP and retrieve eKYC data + * @param {Object} params + * - txnId: Transaction ID from sendOtp + * - otp: 6-digit OTP + * - shareCode: Optional share code for offline verification + * @returns {Object} { ok, code, message, data: { verified, kycData, documentUrl } } + */ + verifyOtp: function (params) { + try { + params = params || {}; + + // Input validation + if (!params.txnId) { + return this._err("Transaction ID is required", 400); + } + + if (!params.otp) { + return this._err("OTP is required", 400); + } + + var otp = String(params.otp).replace(/\D/g, ""); + if (!this.patterns.otp.test(otp)) { + return this._err("Invalid OTP format. Must be 6 digits", 400); + } + + // Check session + var session = this._getSession(params.txnId); + if (!session) { + return this._err("Invalid or expired transaction", 404); + } + + var payload = { + transaction_id: params.txnId, + otp: otp, + timestamp: new GlideDateTime().getDisplayValue(), + }; + + if (params.shareCode) { + payload.share_code = String(params.shareCode); + } + + // Make API call + var result = this._executeWithRetry( + "POST", + this.endpoints.verifyOtp, + payload + ); + + if (result.ok) { + var responseData = result.data || {}; + + // Update session + this._updateSession(params.txnId, { + stage: "otp_verified", + verifiedAt: new GlideDateTime().getNumericValue(), + }); + + // Parse eKYC data + var kycData = this._parseKycData( + responseData.kyc || responseData.data || responseData + ); + + // Audit log (without sensitive data) + this._auditLog("OTP_VERIFIED", session.uid, true, params.txnId); + + return { + ok: true, + code: result.code, + message: "OTP verified successfully", + data: { + verified: true, + txnId: params.txnId, + kycData: kycData, + documentUrl: responseData.document_url || responseData.documentUrl, + verificationTimestamp: new GlideDateTime().getDisplayValue(), + }, + }; + } + + this._auditLog("OTP_VERIFY_FAILED", session.uid, false, params.txnId); + this._clearSession(params.txnId); + return result; + } catch (e) { + gs.error("[Aadhaar] verifyOtp error: " + e); + return this._err("Error verifying OTP: " + e, 500); + } + }, + + /** + * Alternative: Demographic Verification (without OTP) + * Verifies Aadhaar details against provided demographic information + * @param {Object} params + * - uid: Aadhaar number + * - name: Full name + * - dob: Date of birth (YYYY-MM-DD) + * - gender: M/F/O + * - pincode: Optional 6-digit pincode + * @returns {Object} { ok, code, message, data: { matched, score, details } } + */ + verifyDemographic: function (params) { + try { + params = params || {}; + + // Input validation + var validationResult = this._validateDemographicParams(params); + if (!validationResult.valid) { + return this._err(validationResult.message, 400); + } + + var uid = this._sanitizeAadhaar(params.uid); + + var payload = { + aadhaar_number: uid, + name: this._sanitizeName(params.name), + dob: params.dob, + gender: params.gender.toUpperCase(), + consent: "Y", + timestamp: new GlideDateTime().getDisplayValue(), + }; + + if (params.pincode && /^\d{6}$/.test(params.pincode)) { + payload.pincode = params.pincode; + } + + // Make API call + var result = this._executeWithRetry( + "POST", + this.endpoints.verifyDemo, + payload + ); + + if (result.ok) { + var responseData = result.data || {}; + + // Audit log + this._auditLog("DEMO_VERIFIED", uid, true); + + return { + ok: true, + code: result.code, + message: "Demographic verification completed", + data: { + matched: + responseData.match === true || responseData.status === "matched", + score: responseData.match_score || responseData.score || 0, + details: { + nameMatch: responseData.name_match, + dobMatch: responseData.dob_match, + genderMatch: responseData.gender_match, + addressMatch: responseData.address_match, + }, + verificationId: responseData.verification_id || responseData.ref_id, + timestamp: new GlideDateTime().getDisplayValue(), + }, + }; + } + + this._auditLog("DEMO_VERIFY_FAILED", uid, false); + return result; + } catch (e) { + gs.error("[Aadhaar] verifyDemographic error: " + e); + return this._err("Error in demographic verification: " + e, 500); + } + }, + + /** + * Download/Retrieve Aadhaar Document (XML/PDF) + * @param {Object} params + * - txnId: Transaction ID from successful verification + * - format: 'xml' or 'pdf' (default: 'xml') + * - password: Optional password for encrypted document + * @returns {Object} { ok, code, message, data: { documentId, downloadUrl, format } } + */ + getDocument: function (params) { + try { + params = params || {}; + + if (!params.txnId) { + return this._err("Transaction ID is required", 400); + } + + var session = this._getSession(params.txnId); + if (!session || session.stage !== "otp_verified") { + return this._err("Invalid transaction or OTP not verified", 403); + } + + var format = (params.format || "xml").toLowerCase(); + if (!["xml", "pdf"].includes(format)) { + return this._err('Invalid format. Use "xml" or "pdf"', 400); + } + + var payload = { + transaction_id: params.txnId, + format: format, + timestamp: new GlideDateTime().getDisplayValue(), + }; + + if (params.password) { + payload.password = params.password; + } + + var result = this._executeWithRetry( + "POST", + this.endpoints.getDocument, + payload + ); + + if (result.ok) { + var responseData = result.data || {}; + + this._auditLog("DOCUMENT_RETRIEVED", session.uid, true, params.txnId); + + return { + ok: true, + code: result.code, + message: "Document retrieved successfully", + data: { + documentId: responseData.document_id || responseData.docId, + downloadUrl: responseData.download_url || responseData.url, + format: format, + expiresAt: responseData.expires_at || responseData.expiry, + size: responseData.size, + checksum: responseData.checksum, + }, + }; + } + + return result; + } catch (e) { + gs.error("[Aadhaar] getDocument error: " + e); + return this._err("Error retrieving document: " + e, 500); + } + }, + + /** + * Check verification status + * @param {String} txnId - Transaction ID + * @returns {Object} Status information + */ + checkStatus: function (txnId) { + try { + if (!txnId) { + return this._err("Transaction ID is required", 400); + } + + var session = this._getSession(txnId); + if (!session) { + return this._err("Transaction not found or expired", 404); + } + + var payload = { + transaction_id: txnId, + }; + + var result = this._executeWithRetry( + "GET", + this.endpoints.checkStatus + "/" + txnId, + null + ); + + if (result.ok) { + return { + ok: true, + code: result.code, + message: "Status retrieved", + data: { + txnId: txnId, + stage: session.stage, + status: result.data.status, + createdAt: new GlideDateTime(session.createdAt).getDisplayValue(), + lastUpdated: result.data.updated_at, + }, + }; + } + + return result; + } catch (e) { + gs.error("[Aadhaar] checkStatus error: " + e); + return this._err("Error checking status: " + e, 500); + } + }, + + // ========== UTILITY METHODS ========== + + /** + * Execute HTTP request with retry logic + */ + _executeWithRetry: function (method, path, bodyObj) { + var attempt = 0; + var lastError; + + while (attempt <= this.retries) { + attempt++; + + try { + var response = this._makeRequest(method, path, bodyObj); + var code = response.status; + + if (code >= 200 && code < 300) { + var body = this._safeParse(response.body); + return { ok: true, code: code, message: "success", data: body }; + } + + // Client errors - don't retry + if (code >= 400 && code < 500 && ![429].includes(code)) { + var errorBody = this._safeParse(response.body); + return this._err( + errorBody.message || errorBody.error || "API error", + code, + errorBody + ); + } + + // Server errors or rate limit - retry + if ([429, 500, 502, 503, 504].includes(code)) { + lastError = { code: code, body: response.body }; + if (attempt > this.retries) { + return this._err("Service temporarily unavailable", code); + } + } else { + return this._err("Unexpected response code: " + code, code); + } + } catch (e) { + lastError = e; + if (attempt > this.retries) { + return this._err("Request failed: " + e, 0); + } + } + + // Exponential backoff + if (attempt <= this.retries) { + gs.sleep(this._backoff(attempt)); + } + } + + return this._err("Failed after " + (this.retries + 1) + " attempts", 0); + }, + + /** + * Make HTTP request + */ + _makeRequest: function (method, path, bodyObj) { + var r = new sn_ws.RESTMessageV2(); + r.setHttpMethod(method.toUpperCase()); + r.setEndpoint(this._resolveEndpoint(path)); + r.setRequestHeader("Content-Type", "application/json"); + r.setRequestHeader("Accept", "application/json"); + r.setEccParameter("skip_sensor", true); + + if (bodyObj && method.toUpperCase() !== "GET") { + r.setRequestBody(JSON.stringify(bodyObj)); + } + + r.setHttpTimeout(this.timeoutMs); + r.setAuthenticationProfile("connection_alias", this.alias); + + var res = r.execute(); + return { + status: res.getStatusCode(), + body: res.getBody(), + headers: res.getHeaders(), + }; + }, + + /** + * Resolve endpoint path + */ + _resolveEndpoint: function (path) { + if (!path || path.charAt(0) !== "/") { + path = "/" + (path || ""); + } + return path; + }, + + /** + * Validate demographic parameters + */ + _validateDemographicParams: function (params) { + if (!params.uid) { + return { valid: false, message: "Aadhaar number is required" }; + } + + if (!params.name || params.name.trim().length < 2) { + return { valid: false, message: "Valid name is required" }; + } + + if (!params.dob || !this.patterns.dob.test(params.dob)) { + return { valid: false, message: "Valid DOB required (YYYY-MM-DD)" }; + } + + if (!params.gender || !this.patterns.gender.test(params.gender)) { + return { valid: false, message: "Valid gender required (M/F/O)" }; + } + + var uid = this._sanitizeAadhaar(params.uid); + if (!this.patterns.aadhaar.test(uid)) { + return { valid: false, message: "Invalid Aadhaar number format" }; + } + + return { valid: true }; + }, + + /** + * Parse and standardize KYC data + */ + _parseKycData: function (rawData) { + if (!rawData) return null; + + return { + name: rawData.name || rawData.full_name, + dob: rawData.dob || rawData.date_of_birth, + gender: rawData.gender || rawData.sex, + address: { + line1: rawData.address_line1 || rawData.house, + line2: rawData.address_line2 || rawData.street, + city: rawData.city || rawData.vtc, + district: rawData.district || rawData.dist, + state: rawData.state || rawData.state_name, + pincode: rawData.pincode || rawData.zip, + country: rawData.country || "India", + }, + photo: rawData.photo || rawData.photo_base64, + email: rawData.email, + mobile: rawData.mobile || rawData.phone, + aadhaarLastFour: rawData.aadhaar_last_4 || rawData.uid_last_4, + }; + }, + + /** + * Session management + */ + _storeSession: function (txnId, data) { + var key = "aadhaar_session_" + txnId; + gs.getSession().putProperty(key, JSON.stringify(data)); + this.sessionStore[txnId] = data; + }, + + _getSession: function (txnId) { + if (this.sessionStore[txnId]) { + return this.sessionStore[txnId]; + } + var key = "aadhaar_session_" + txnId; + var stored = gs.getSession().getProperty(key); + if (stored) { + try { + return JSON.parse(stored); + } catch (e) { + return null; + } + } + return null; + }, + + _updateSession: function (txnId, data) { + var session = this._getSession(txnId); + if (session) { + Object.keys(data).forEach(function (key) { + session[key] = data[key]; + }); + this._storeSession(txnId, session); + } + }, + + _clearSession: function (txnId) { + var key = "aadhaar_session_" + txnId; + gs.getSession().clearProperty(key); + delete this.sessionStore[txnId]; + }, + + /** + * Audit logging + */ + _auditLog: function (action, uid, success, txnId) { + var log = new GlideRecord("sys_audit"); + log.initialize(); + log.tablename = "aadhaar_verification"; + log.documentkey = txnId || "N/A"; + log.fieldname = "verification_action"; + log.oldvalue = action; + log.newvalue = success ? "SUCCESS" : "FAILED"; + log.user = gs.getUserID(); + log.insert(); + + // Also log to system log (without sensitive data) + gs.info( + "[Aadhaar] " + + action + + " - " + + (success ? "Success" : "Failed") + + " - TxnId: " + + (txnId || "N/A") + ); + }, + + /** + * Data sanitization + */ + _sanitizeAadhaar: function (uid) { + return String(uid).replace(/\D/g, "").substring(0, 12); + }, + + _sanitizeName: function (name) { + return String(name) + .trim() + .replace(/[^a-zA-Z\s.]/g, "") + .substring(0, 100); + }, + + _maskAadhaar: function (uid) { + if (!uid || uid.length < 12) return "XXXX"; + return "XXXX-XXXX-" + uid.substring(8); + }, + + _maskMobile: function (mobile) { + if (!mobile || mobile.length < 10) return "XXXXXX"; + return "XXXXXX" + mobile.substring(mobile.length - 4); + }, + + /** + * Common utilities + */ + _backoff: function (n) { + return 300 * Math.pow(2, n - 1) + Math.floor(Math.random() * 100); + }, + + _safeParse: function (s) { + if (!s) return {}; + try { + return JSON.parse(s); + } catch (e) { + return { raw: s }; + } + }, + + _err: function (msg, code, data) { + return { + ok: false, + code: code || 0, + message: msg, + data: data || {}, + timestamp: new GlideDateTime().getDisplayValue(), + }; + }, + + type: "Aadhaar", +}; + +/* ========== USAGE EXAMPLES ========== + +// Example 1: Complete OTP-based verification flow +var aadhaar = new Aadhaar(); + +// Step 1: Send OTP +var otpResult = aadhaar.sendOtp({ + uid: '123456789012', + consentGiven: true +}); + +if (otpResult.ok) { + gs.info('OTP sent to: ' + otpResult.data.mobile); + var txnId = otpResult.data.txnId; + + // Step 2: Verify OTP (after user provides OTP) + var verifyResult = aadhaar.verifyOtp({ + txnId: txnId, + otp: '123456' + }); + + if (verifyResult.ok) { + var kycData = verifyResult.data.kycData; + gs.info('Name: ' + kycData.name); + gs.info('DOB: ' + kycData.dob); + + // Step 3: Download document (optional) + var docResult = aadhaar.getDocument({ + txnId: txnId, + format: 'xml' + }); + + if (docResult.ok) { + gs.info('Document URL: ' + docResult.data.downloadUrl); + } + } +} + +// Example 2: Demographic verification (no OTP) +var demoResult = aadhaar.verifyDemographic({ + uid: '123456789012', + name: 'John Doe', + dob: '1990-01-15', + gender: 'M' +}); + +if (demoResult.ok && demoResult.data.matched) { + gs.info('Demographic verification successful'); + gs.info('Match score: ' + demoResult.data.score); +} + +// Example 3: Check status +var statusResult = aadhaar.checkStatus(txnId); +if (statusResult.ok) { + gs.info('Current stage: ' + statusResult.data.stage); +} + +*/ diff --git a/Integration/RESTMessageV2/Auth2 client credentials token cache with auto-refresh/OAuthClientCredsHelper.js b/Integration/RESTMessageV2/Auth2 client credentials token cache with auto-refresh/OAuthClientCredsHelper.js new file mode 100644 index 0000000000..a47c962c08 --- /dev/null +++ b/Integration/RESTMessageV2/Auth2 client credentials token cache with auto-refresh/OAuthClientCredsHelper.js @@ -0,0 +1,147 @@ +/** + * Script Include: OAuthClientCredsHelper + * Purpose: Perform OAuth 2.0 client-credentials token acquisition and caching, + * and wrap RESTMessageV2 calls with automatic token injection and refresh. + * + * SECURITY: Store clientSecret in a secure location (Credentials or encrypted property). + */ +var OAuthClientCredsHelper = Class.create(); +OAuthClientCredsHelper.prototype = { + initialize: function() {}, + + /** + * Execute an API request with Bearer token and one-shot auto-refresh on 401. + * Returns an object {status, body, headers, refreshed:Boolean} + */ + request: function(options) { + var token = this.getToken(options); // may fetch or use cached + + var res = this._call(options, token); + if (res.status !== 401) return res; + + // If 401, refresh token once and retry + var refreshed = this.getToken(this._forceRefresh(options)); + var retry = this._call(options, refreshed); + retry.refreshed = true; + return retry; + }, + + /** + * Get a cached token or fetch a new one if expired/near-expiry. + * Returns the access_token string. + */ + getToken: function(options) { + this._assert(['tokenUrl', 'clientId', 'clientSecret', 'propPrefix'], options); + var now = new Date().getTime(); + + var tokenKey = options.propPrefix + '.access_token'; + var expiryKey = options.propPrefix + '.expires_at'; + + var cached = gs.getProperty(tokenKey, ''); + var expiresAt = parseInt(gs.getProperty(expiryKey, '0'), 10) || 0; + + // 60-second safety buffer + var bufferMs = 60 * 1000; + if (cached && expiresAt > (now + bufferMs)) { + return cached; + } + + // Need a fresh token + var fresh = this._fetchToken(options); + gs.setProperty(tokenKey, fresh.access_token); + gs.setProperty(expiryKey, String(fresh.expires_at)); + return fresh.access_token; + }, + + // ------------------ internals ------------------ + + _call: function(options, accessToken) { + var r = new sn_ws.RESTMessageV2(); + r.setEndpoint(options.resource); + r.setHttpMethod((options.method || 'GET').toUpperCase()); + r.setRequestHeader('Authorization', 'Bearer ' + accessToken); + // Extra headers + Object.keys(options.headers || {}).forEach(function(k) { + r.setRequestHeader(k, options.headers[k]); + }); + + if (options.body && /^(POST|PUT|PATCH)$/i.test(options.method || 'GET')) { + r.setRequestBody(typeof options.body === 'string' ? options.body : JSON.stringify(options.body)); + // set content type if caller didn't + if (!options.headers || !options.headers['Content-Type']) { + r.setRequestHeader('Content-Type', 'application/json'); + } + } + + var resp = r.execute(); + return { + status: resp.getStatusCode(), + body: resp.getBody(), + headers: this._collectHeaders(resp), + refreshed: false + }; + }, + + _fetchToken: function(options) { + // RFC 6749 client-credentials: POST x-www-form-urlencoded + var r = new sn_ws.RESTMessageV2(); + r.setEndpoint(options.tokenUrl); + r.setHttpMethod('POST'); + r.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + + var params = [ + 'grant_type=client_credentials', + 'client_id=' + encodeURIComponent(options.clientId), + 'client_secret=' + encodeURIComponent(options.clientSecret) + ]; + if (options.scope) params.push('scope=' + encodeURIComponent(options.scope)); + if (options.audience) params.push('audience=' + encodeURIComponent(options.audience)); + + r.setRequestBody(params.join('&')); + + var resp = r.execute(); + var status = resp.getStatusCode(); + var body = resp.getBody(); + if (status < 200 || status >= 300) { + throw 'Token endpoint HTTP ' + status + ': ' + body; + } + + var json; + try { json = JSON.parse(body); } + catch (e) { throw 'Invalid token JSON: ' + e.message; } + + var access = json.access_token; + var ttlSec = Number(json.expires_in || 3600); + if (!access) throw 'Token response missing access_token'; + + var now = new Date().getTime(); + var expiresAt = now + (ttlSec * 1000); + return { access_token: access, expires_at: expiresAt }; + }, + + _collectHeaders: function(resp) { + var map = {}; + var names = resp.getAllHeaders(); + for (var i = 0; i < names.size(); i++) { + var name = String(names.get(i)); + map[name] = resp.getHeader(name); + } + return map; + }, + + _forceRefresh: function(options) { + // Nudge cache by setting expiry in the past + var expiryKey = options.propPrefix + '.expires_at'; + gs.setProperty(expiryKey, '0'); + return options; + }, + + _assert: function(keys, obj) { + keys.forEach(function(k) { + if (!obj || typeof obj[k] === 'undefined' || obj[k] === null || obj[k] === '') + throw 'Missing option: ' + k; + }); + }, + + type: 'OAuthClientCredsHelper' +}; diff --git a/Integration/RESTMessageV2/Auth2 client credentials token cache with auto-refresh/README.md b/Integration/RESTMessageV2/Auth2 client credentials token cache with auto-refresh/README.md new file mode 100644 index 0000000000..4022c4006e --- /dev/null +++ b/Integration/RESTMessageV2/Auth2 client credentials token cache with auto-refresh/README.md @@ -0,0 +1,41 @@ +# OAuth 2.0 client-credentials token cache with auto-refresh + +## What this solves +When integrating with external APIs, teams often re-implement the OAuth 2.0 client-credentials flow and forget to cache tokens or handle 401 refreshes. This helper: +- Requests an access token from your token endpoint +- Caches the token in a system property with an expiry timestamp +- Adds the Bearer token to RESTMessageV2 requests +- If the call returns 401 (expired token), refreshes once and retries + +## Where to use +Script Include in global or scoped apps. Call from Business Rules, Scheduled Jobs, Flow Actions, or Background Scripts. + +## How it works +- `getToken(options)` fetches or retrieves a cached token; stores `access_token` and `expires_at` (epoch ms) in system properties. +- `request(options)` executes a resource call with Authorization header; on HTTP 401 it refreshes the token and retries once. +- Token expiry has a 60-second buffer to avoid race on near-expiry tokens. + +## Security notes +- For production, store `client_secret` in a secure location (Credentials table or encrypted system property) and **do not** hardcode secrets in scripts. +- This snippet reads/writes system properties under a chosen prefix. Ensure only admins can read/write them. + +## Options +For `getToken` and `request`: +- `tokenUrl`: OAuth token endpoint URL +- `clientId`: OAuth client id +- `clientSecret`: OAuth client secret +- `scope`: optional scope string +- `audience`: optional audience parameter (some providers require it) +- `propPrefix`: system property prefix for cache (e.g. `x_acme.oauth.sample`) +- `resource` (request only): target API URL +- `method` (request only): GET/POST/etc (default GET) +- `headers` (request only): object of extra headers +- `body` (request only): request body for POST/PUT/PATCH + +## References +- RESTMessageV2 API + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/RESTMessageV2/concept/c_RESTMessageV2API.html +- Direct RESTMessageV2 example + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/RESTMessageV2/reference/r_DirectRESTMessageV2Example.html +- OAuth 2.0 profiles in ServiceNow (concept) + https://www.servicenow.com/docs/bundle/zurich-integrate-applications/page/integrate/outbound-rest/concept/c_oauth2-authentication.html diff --git a/Integration/RESTMessageV2/Auth2 client credentials token cache with auto-refresh/example_background_usage.js b/Integration/RESTMessageV2/Auth2 client credentials token cache with auto-refresh/example_background_usage.js new file mode 100644 index 0000000000..3a942346dd --- /dev/null +++ b/Integration/RESTMessageV2/Auth2 client credentials token cache with auto-refresh/example_background_usage.js @@ -0,0 +1,23 @@ +// Background Script example: call an API with automatic OAuth bearer handling +(function() { + var helper = new OAuthClientCredsHelper(); + + var options = { + // Token settings + tokenUrl: 'https://auth.example.com/oauth2/token', + clientId: 'YOUR_CLIENT_ID', + clientSecret: 'YOUR_CLIENT_SECRET', // store securely in real environments + scope: 'read:things', // optional + audience: '', // optional (for some IdPs) + propPrefix: 'x_acme.oauth.sample', // system property prefix for cache + + // Resource request + resource: 'https://api.example.com/v1/things?limit=25', + method: 'GET', + headers: { 'Accept': 'application/json' } + }; + + var res = helper.request(options); + gs.info('Status: ' + res.status + ', refreshed=' + res.refreshed); + gs.info('Body: ' + res.body); +})(); diff --git a/Integration/RESTMessageV2/Currency Conversion - Using CurrencyFreaks API/README.md b/Integration/RESTMessageV2/Currency Conversion - Using CurrencyFreaks API/README.md new file mode 100644 index 0000000000..419e15c0e7 --- /dev/null +++ b/Integration/RESTMessageV2/Currency Conversion - Using CurrencyFreaks API/README.md @@ -0,0 +1,19 @@ +# Currency Conversion- Using CurrencyFreaks API +## Overview +This API allows to convert an amount from USD to any selected currency in real-time using live exchange rates fetched from the CurrencyFreaks API. + +## Configuration Steps +### Get Your CurrencyFreaks API Key +1. Go to https://currencyfreaks.com +2. Sign up for a free account. +3. Navigate to Dasboard ->API Keys. +4. Copy your API key - you'll need it in ServiceNow. + +### Create a REST Message in ServiceNow +- Name: CurrencyFreaks API +- Endpoint: https://api.currencyfreaks.com/v2.0/rates/latest?apikey=${apikey}&symbols=${symbols} +- HTTP Method: GET + +### Example Response +```json +{"date":"2025-10-30 00:00:00+00","base":"USD","rates":{"EUR":"0.861846","SAR":"3.7502","KWD":"0.30678","INR":"88.4075"}} diff --git a/Integration/RESTMessageV2/Currency Conversion - Using CurrencyFreaks API/script.js b/Integration/RESTMessageV2/Currency Conversion - Using CurrencyFreaks API/script.js new file mode 100644 index 0000000000..0a4895f911 --- /dev/null +++ b/Integration/RESTMessageV2/Currency Conversion - Using CurrencyFreaks API/script.js @@ -0,0 +1,22 @@ +var symbols ="INR,EUR,KWD,SAR"; //Enter symbol name like SAR,AED +var apiKey =""; // Paste your CurrencyFreaks APIKEY here +getExchangeReate(apiKey, symbols); + +function getExchangeReate(apiKey,symbols){ + try { + + var r = new sn_ws.RESTMessageV2('ExchangeRate API', 'Default GET'); + r.setStringParameterNoEscape('symbols', symbols); + r.setStringParameterNoEscape('apikey', apiKey); + + + var response = r.execute(); + var responseBody = response.getBody(); + var httpStatus = response.getStatusCode(); + gs.print("Status: " +httpStatus); + gs.print("Result:" +responseBody); //It will show conversion from USD the selected currency +} +catch(ex) { + var message = ex.message; +} +} diff --git a/Integration/RESTMessageV2/DynamicOutboundEnpoints/README.md b/Integration/RESTMessageV2/DynamicOutboundEnpoints/README.md new file mode 100644 index 0000000000..9f9723f418 --- /dev/null +++ b/Integration/RESTMessageV2/DynamicOutboundEnpoints/README.md @@ -0,0 +1,27 @@ +This is a server-side Script Include that contains the core logic. It reads the endpoint configurations from a System Property, parses the JSON, and returns the appropriate URL based on the current instance's name. + +System Property: x_my_scope.api.endpoints +This property stores a JSON object containing the endpoint URLs for each environment. It must be created and populated in each instance that uses the utility. + +Sample JSON object: +{ + "dev": "https://dev-instance.example.com/api", + "test": "https://test-instance.example.com/api", + "prod": "https://prod-instance.example.com/api" +} + +Usage: +var endpointConfig = new EndpointConfig(); +var endpointUrl = endpointConfig.getEndpoint(); +if (endpointUrl) +{ +gs.info("Endpoint URL: " + endpointUrl); +//Use the endpointUrl in your REST call + var request = new sn_ws.RESTMessageV2(); + request.setEndpoint(endpointUrl); +// ... rest of your integration logic +} else +{ +gs.error("Failed to retrieve endpoint URL."); +} + diff --git a/Integration/RESTMessageV2/DynamicOutboundEnpoints/scriptinclude.js b/Integration/RESTMessageV2/DynamicOutboundEnpoints/scriptinclude.js new file mode 100644 index 0000000000..0dd7cbb606 --- /dev/null +++ b/Integration/RESTMessageV2/DynamicOutboundEnpoints/scriptinclude.js @@ -0,0 +1,54 @@ + +//Create a sample system Property called x_my_scope.api.endpoints having below object as example. make sure your company instance includes those key such as dev,prod,test or modify it with your instance name + +// { +// "dev": "https://dev-instance.example.com/api", +// "test": "https://test-instance.example.com/api", +// "prod": "https://prod-instance.example.com/api" +// } + +var EndpointConfig = Class.create(); +EndpointConfig.prototype = { + initialize: function() { + // No hardcoded object here. It will be fetched from the System Property. + }, + + getEndpoint: function() { + var propertyName = 'x_my_scope.api.endpoints'; + var endpointObjectStr = gs.getProperty(propertyName); + if (gs.nil(endpointObjectStr)) { + gs.error("EndpointConfig: System property '" + propertyName + "' not found or is empty."); + return null; + } + + try { + var endpoints = JSON.parse(endpointObjectStr); + var instanceName = gs.getProperty('instance_name'); + var environmentKey; + + if (instanceName.includes('dev')) { + environmentKey = 'dev'; + } else if (instanceName.includes('test') || instanceName.includes('uat')) { + environmentKey = 'test'; + } else if (instanceName.includes('prod')) { + environmentKey = 'prod'; + } else { + gs.error("EndpointConfig: Could not determine environment for instance '" + instanceName + "'."); + return null; + } + + if (endpoints.hasOwnProperty(environmentKey)) { + return endpoints[environmentKey]; + } else { + gs.error("EndpointConfig: Configuration not found for environment '" + environmentKey + "'."); + return null; + } + + } catch (ex) { + gs.error("EndpointConfig: Failed to parse JSON from system property '" + propertyName + "'. Exception: " + ex); + return null; + } + }, + + type: 'EndpointConfig' +}; diff --git a/Integration/RESTMessageV2/External ML Model Integration/Call ML Prediction API/README.md b/Integration/RESTMessageV2/External ML Model Integration/Call ML Prediction API/README.md new file mode 100644 index 0000000000..76c63c245d --- /dev/null +++ b/Integration/RESTMessageV2/External ML Model Integration/Call ML Prediction API/README.md @@ -0,0 +1,44 @@ +# Integrate ServiceNow with External ML Model API + +## Overview +Call an external ML API from ServiceNow to get AI predictions for incidents and auto-update records. + +## What It Does +- Sends incident data to external ML API via REST call +- Receives predictions (resolution time, category, priority, etc.) +- Automatically updates incident record with predictions +- Includes error handling and logging + +## Use Cases +- Predict how long an incident will take to resolve +- Auto-suggest the right category/priority +- Recommend best assignment group +- Get risk scores for changes + +## Files +- `ml_prediction_script_include.js` - Script Include that calls ML API + +## How to Use +1. Create Script Include in ServiceNow named `MLPredictionClient` +2. Copy code from `ml_prediction_script_include.js` +3. Update `ML_API_URL` and `API_KEY` with your ML service details +4. Call it from a Business Rule or Client Script to get predictions +5. Store results back in incident fields + +## Example Usage +```javascript +var mlClient = new MLPredictionClient(); +var prediction = mlClient.predictIncident({ + description: incident.description, + category: incident.category, + priority: incident.priority +}); + +incident.estimated_resolution_time = prediction.predicted_resolution_time; +incident.update(); +``` + +## Requirements +- ServiceNow instance +- External ML API endpoint (REST) +- API key or token diff --git a/Integration/RESTMessageV2/External ML Model Integration/Call ML Prediction API/ml_prediction_script_include.js b/Integration/RESTMessageV2/External ML Model Integration/Call ML Prediction API/ml_prediction_script_include.js new file mode 100644 index 0000000000..1adf1a9c0e --- /dev/null +++ b/Integration/RESTMessageV2/External ML Model Integration/Call ML Prediction API/ml_prediction_script_include.js @@ -0,0 +1,43 @@ +// Script Include: MLPredictionClient +// Calls external ML API to get incident predictions + +var MLPredictionClient = Class.create(); +MLPredictionClient.prototype = { + initialize: function() { + this.ML_API_URL = 'https://your-ml-api.com/predict'; + this.API_KEY = 'your-api-key-here'; + }, + + predictIncident: function(incidentData) { + try { + var request = new RESTMessageV2(); + request.setEndpoint(this.ML_API_URL); + request.setHttpMethod('POST'); + request.setRequestHeader('Authorization', 'Bearer ' + this.API_KEY); + request.setRequestHeader('Content-Type', 'application/json'); + + // Send incident details to ML API + var payload = { + description: incidentData.description, + category: incidentData.category, + priority: incidentData.priority + }; + request.setRequestBody(JSON.stringify(payload)); + + // Get prediction from external ML service + var response = request.execute(); + var result = JSON.parse(response.getBody()); + + return { + estimated_hours: result.estimated_hours, + predicted_category: result.category, + confidence: result.confidence + }; + } catch (error) { + gs.log('ML API Error: ' + error, 'MLPredictionClient'); + return null; + } + }, + + type: 'MLPredictionClient' +}; diff --git a/Integration/RESTMessageV2/GET with backoff, telemetry, and simple pagination/README.md b/Integration/RESTMessageV2/GET with backoff, telemetry, and simple pagination/README.md new file mode 100644 index 0000000000..58bd0bc209 --- /dev/null +++ b/Integration/RESTMessageV2/GET with backoff, telemetry, and simple pagination/README.md @@ -0,0 +1,21 @@ +# RESTMessageV2 GET with backoff, telemetry, and simple pagination + +## What this solves +External APIs frequently throttle with HTTP 429 or intermittently return 5xx. This helper retries safely, honours Retry-After, logs simple telemetry, and follows a links.next pagination model. + +## Where to use +Script Include can be called from Scheduled Jobs, Flow Actions, Business Rules, or Background Scripts. + +## How it works +- Executes RESTMessageV2 requests +- On 429 or 5xx, sleeps using Retry-After or exponential backoff +- Collects minimal telemetry about attempts and total sleep time +- Appends items from json.items and follows json.links.next + +## References +- RESTMessageV2 API + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/RESTMessageV2/concept/c_RESTMessageV2API.html +- Direct RESTMessageV2 example + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/RESTMessageV2/reference/r_DirectRESTMessageV2Example.html +- Inbound rate limiting and Retry-After header + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/inbound-rest/concept/inbound-REST-API-rate-limiting.html diff --git a/Integration/RESTMessageV2/GET with backoff, telemetry, and simple pagination/RestGetWithBackoff.js b/Integration/RESTMessageV2/GET with backoff, telemetry, and simple pagination/RestGetWithBackoff.js new file mode 100644 index 0000000000..0dace322d6 --- /dev/null +++ b/Integration/RESTMessageV2/GET with backoff, telemetry, and simple pagination/RestGetWithBackoff.js @@ -0,0 +1,135 @@ +/** + * Script Include: RestGetWithBackoff + * Purpose: Safely perform RESTMessageV2 GET requests with retry handling, + * exponential backoff, and simple pagination support. + * + * Example usage (Background Script): + * var helper = new RestGetWithBackoff(); + * var data = helper.getAll({ + * endpoint: 'https://api.example.com/v1/items', + * headers: { 'Authorization': 'Bearer ${token}' }, + * maxRetries: 4, + * baseDelayMs: 750 + * }); + * gs.info('Fetched ' + data.length + ' records'); + */ + +var RestGetWithBackoff = Class.create(); +RestGetWithBackoff.prototype = { + initialize: function() {}, + + /** + * Main entry point to fetch all pages of results. + * Handles retries, pagination, and aggregates results. + * @param {Object} options - endpoint, headers, maxRetries, baseDelayMs + * @returns {Array} all items combined from paginated responses + */ + getAll: function(options) { + var url = options.endpoint; // Initial API endpoint + var headers = options.headers || {}; // Optional request headers + var maxRetries = options.maxRetries || 5; // Maximum retry attempts per page + var baseDelayMs = options.baseDelayMs || 500;// Base delay for exponential backoff + + var items = []; // Array to collect all items across pages + var attempts = 0; // Total number of REST calls + var totalSleepMs = 0; // Total delay time across retries + + // Continue fetching until there are no more pages (links.next = null) + while (url) { + // Execute the REST call (with internal retry logic) + var res = this._execute('get', url, headers, maxRetries, baseDelayMs); + attempts += res.attempts; // Count total attempts made + totalSleepMs += res.sleptMs; // Sum total sleep time used in retries + + // If non-success HTTP code, throw to stop execution + if (res.status < 200 || res.status >= 300) + throw 'HTTP ' + res.status + ' for ' + url + ': ' + res.body; + + // Parse and validate JSON body + var json = this._safeJson(res.body); + + // If body contains an 'items' array, append to results + if (Array.isArray(json.items)) items = items.concat(json.items); + + // Get next page link if available (standard 'links.next' pattern) + url = json && json.links && json.links.next ? json.links.next : null; + } + + // Log a completion summary + gs.info('REST helper complete. items=' + items.length + + ', attempts=' + attempts + + ', sleptMs=' + totalSleepMs); + + return items; + }, + + /** + * Executes a REST call with retry and exponential backoff. + * Retries on HTTP 429 (Too Many Requests) or 5xx errors. + * @returns {Object} status, body, attempts, sleptMs + */ + _execute: function(method, url, headers, maxRetries, baseDelayMs) { + var attempt = 0; + var sleptMs = 0; + + while (true) { + attempt++; + + // Build the RESTMessageV2 object + var r = new sn_ws.RESTMessageV2(); + r.setEndpoint(url); + r.setHttpMethod(method.toUpperCase()); + + // Apply custom headers (for example, auth tokens or content type) + Object.keys(headers).forEach(function(k) { r.setRequestHeader(k, headers[k]); }); + + // Execute the request + var resp = r.execute(); + var status = resp.getStatusCode(); + var body = resp.getBody(); + + // Success range (2xx) + if (status >= 200 && status < 300) { + return { status: status, body: body, attempts: attempt, sleptMs: sleptMs }; + } + + // Handle 429 (rate limit) or transient 5xx server errors + if (status === 429 || status >= 500) { + // Stop retrying if max reached + if (attempt >= maxRetries) { + return { status: status, body: body, attempts: attempt, sleptMs: sleptMs }; + } + + // Honour Retry-After header if present; otherwise exponential delay + var retryAfter = Number(resp.getHeader('Retry-After')) || 0; + var delayMs = retryAfter > 0 ? retryAfter * 1000 : Math.pow(2, attempt) * baseDelayMs; + + // Log retry details for visibility in system logs + gs.info('Retrying ' + url + ' after ' + delayMs + + ' ms due to HTTP ' + status + ' (attempt ' + attempt + ')'); + + gs.sleep(delayMs); // Wait before retrying + sleptMs += delayMs; + continue; + } + + // Non-retryable failure (e.g., 4xx not including 429) + return { status: status, body: body, attempts: attempt, sleptMs: sleptMs }; + } + }, + + /** + * Safe JSON parser that throws descriptive error on invalid JSON. + * @param {String} body - raw HTTP response text + * @returns {Object} parsed JSON + */ + _safeJson: function(body) { + try { + return JSON.parse(body || '{}'); + } catch (e) { + throw 'Invalid JSON: ' + e.message; + } + }, + + type: 'RestGetWithBackoff' +}; diff --git a/Integration/RESTMessageV2/GET with backoff, telemetry, and simple pagination/example_background_usage.js b/Integration/RESTMessageV2/GET with backoff, telemetry, and simple pagination/example_background_usage.js new file mode 100644 index 0000000000..191116d457 --- /dev/null +++ b/Integration/RESTMessageV2/GET with backoff, telemetry, and simple pagination/example_background_usage.js @@ -0,0 +1,11 @@ +// Background Script usage example for RestGetWithBackoff +(function() { + var helper = new RestGetWithBackoff(); + var data = helper.getAll({ + endpoint: 'https://api.example.com/v1/things?limit=100', + headers: { 'Authorization': 'Bearer ${token}' }, + maxRetries: 4, + baseDelayMs: 750 + }); + gs.info('Fetched ' + data.length + ' items total'); +})(); diff --git a/Integration/RESTMessageV2/Smart Incident Categorizer AI/README.md b/Integration/RESTMessageV2/Smart Incident Categorizer AI/README.md new file mode 100644 index 0000000000..dd1c32522c --- /dev/null +++ b/Integration/RESTMessageV2/Smart Incident Categorizer AI/README.md @@ -0,0 +1,20 @@ +# Smart Incident Categorizer using AI + +## Description +Automatically categorizes incidents using OpenAI GPT-3.5 based on description content. + +## Use Case +- Auto-assigns category when incidents are created without category +- Reduces manual categorization effort +- Improves consistency in incident classification + +## Setup +1. Create system property: `openai.api.key` with your OpenAI API key +2. Create Business Rule on `incident` table +3. Set to run `before insert` when category is empty + +## Categories +Returns one of: network, hardware, software, database, security, email + +## Testing +Create incident without category - verify auto-assignment in work notes. diff --git a/Integration/RESTMessageV2/Smart Incident Categorizer AI/smart-incident.js b/Integration/RESTMessageV2/Smart Incident Categorizer AI/smart-incident.js new file mode 100644 index 0000000000..b26e665eb0 --- /dev/null +++ b/Integration/RESTMessageV2/Smart Incident Categorizer AI/smart-incident.js @@ -0,0 +1,53 @@ +// Business Rule: Smart Incident Categorizer AI +// Table: incident +// When: before, insert +// Filter Conditions: Category is empty + +(function executeRule(current, previous) { + if (current.isNewRecord() && !current.category) { + var categorizer = new SmartIncidentCategorizer(); + var suggestedCategory = categorizer.categorizeIncident(current.short_description + ' ' + current.description); + + if (suggestedCategory) { + current.category = suggestedCategory; + current.work_notes = 'Category auto-assigned by AI: ' + suggestedCategory; + } + } +})(current, previous); + +var SmartIncidentCategorizer = Class.create(); +SmartIncidentCategorizer.prototype = { + categorizeIncident: function(description) { + try { + var openai = new sn_ws.RESTMessageV2(); + openai.setHttpMethod('POST'); + openai.setEndpoint('https://api.openai.com/v1/chat/completions'); + openai.setRequestHeader('Authorization', 'Bearer ' + gs.getProperty('openai.api.key')); + openai.setRequestHeader('Content-Type', 'application/json'); + + var payload = { + model: "gpt-3.5-turbo", + messages: [{ + role: "system", + content: "You are an IT service desk categorizer. Return only one of these categories: network, hardware, software, database, security, email" + }, { + role: "user", + content: "Categorize this incident: " + description + }], + max_tokens: 10, + temperature: 0.1 + }; + + openai.setRequestBody(JSON.stringify(payload)); + var response = openai.execute(); + + if (response.getStatusCode() == 200) { + var result = JSON.parse(response.getBody()); + return result.choices[0].message.content.trim().toLowerCase(); + } + } catch (e) { + gs.error('AI Categorizer Error: ' + e.message); + } + return null; + } +}; diff --git a/Integration/RESTMessageV2/UPS Tracking/README.md b/Integration/RESTMessageV2/UPS Tracking/README.md new file mode 100644 index 0000000000..371bff9c86 --- /dev/null +++ b/Integration/RESTMessageV2/UPS Tracking/README.md @@ -0,0 +1,13 @@ +This script calls the UPS tracking API. + +UPS Developer Account: +Sign up at https://developer.ups.com +Create an App to get credentials +1. Client ID +2. Client Secret + +How to use: +1. Replace YOUR_CLIENT_ID and YOUR_CLIENT_SECRET with your UPS credentials. +2. Use the sandbox URL (wwwcie.ups.com) for testing and production URL (onlinetools.ups.com) for live data. +3. You can move this logic into a Script Include and call it from a Flow, Business Rule, or Catalog Client Script. +4. For security, store credentials in a Connection & Credential Alias and reference them in the script instead of hardcoding. diff --git a/Integration/RESTMessageV2/UPS Tracking/trackUPS.js b/Integration/RESTMessageV2/UPS Tracking/trackUPS.js new file mode 100644 index 0000000000..d6128c1590 --- /dev/null +++ b/Integration/RESTMessageV2/UPS Tracking/trackUPS.js @@ -0,0 +1,43 @@ +(function executeUPSLookup() { + try { + var trackingNumber = '1Z12345E1512345676'; // replace or pass as a variable + + // Step 1: Get OAuth token from UPS + var tokenRequest = new sn_ws.RESTMessageV2(); + tokenRequest.setEndpoint('https://wwwcie.ups.com/security/v1/oauth/token'); + tokenRequest.setHttpMethod('POST'); + tokenRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + tokenRequest.setBasicAuth('YOUR_CLIENT_ID', 'YOUR_CLIENT_SECRET'); + tokenRequest.setRequestBody('grant_type=client_credentials'); + + var tokenResponse = tokenRequest.execute(); + var tokenBody = tokenResponse.getBody(); + var tokenObj = JSON.parse(tokenBody); + var accessToken = tokenObj.access_token; + + gs.info('UPS OAuth token retrieved successfully.'); + + // Step 2: Use token to request tracking info + var trackingRequest = new sn_ws.RESTMessageV2(); + trackingRequest.setEndpoint('https://wwwcie.ups.com/api/track/v1/details/' + trackingNumber); + trackingRequest.setHttpMethod('GET'); + trackingRequest.setRequestHeader('Authorization', 'Bearer ' + accessToken); + trackingRequest.setRequestHeader('transId', gs.generateGUID()); + trackingRequest.setRequestHeader('transactionSrc', 'ServiceNow'); + + var trackingResponse = trackingRequest.execute(); + var trackingBody = trackingResponse.getBody(); + var trackingObj = JSON.parse(trackingBody); + + gs.info('UPS Tracking Info: ' + JSON.stringify(trackingObj, null, 2)); + + // Example: log current status + if (trackingObj.trackResponse && trackingObj.trackResponse.shipment) { + var shipment = trackingObj.trackResponse.shipment[0]; + var status = shipment.package[0].activity[0].status.description; + gs.info('Current Status: ' + status); + } + } catch (ex) { + gs.error('Error pulling UPS tracking info: ' + ex.message); + } +})(); diff --git a/Integration/Rest Integration Send Attachment Payload/Send attachment payload via REST/DataLoaderFromIntuneAPI.js b/Integration/Rest Integration Send Attachment Payload/Send attachment payload via REST/DataLoaderFromIntuneAPI.js new file mode 100644 index 0000000000..2938c0e999 --- /dev/null +++ b/Integration/Rest Integration Send Attachment Payload/Send attachment payload via REST/DataLoaderFromIntuneAPI.js @@ -0,0 +1,36 @@ +getDevices: function(importSetTable) { + var endPoint = null; + var isEndofDevices = false; + + while (!isEndofDevices) { + // Call REST Message Method + var url = new sn_ws.RESTMessageV2('Servicenow-Rest', 'GetDevice'); + + if (endPoint !== null) { + url.setEndpoint(endPoint); // Set the endpoint from REST Message method + } + + var response = url.execute(); // Execute endpoint + var responseBody = response.getBody(); // Capture the response body + var httpStatus = response.getStatusCode(); // Capture response status code (200 for success) + + // Parse the response to JSON format + var parsedResponse = JSON.parse(responseBody); + var deviceData = parsedResponse.value; + + // Loop through each record and insert data into import set table + for (var i = 0; i < deviceData.length; i++) { + importSetTable.insert(deviceData[i]); + } + + if (parsedResponse["@odata.nextLink"]) { // Pagination : Check if response has next link + // Copy next data link to endPoint variable + endPoint = parsedResponse["@odata.nextLink"]; + } else { + isEndofDevices = true; + } + } // End of while loop + }, +Calling from datasource + + var data = new Utils().getDevices(import_set_table); diff --git a/Integration/Rest Integration Send Attachment Payload/Send attachment payload via REST/DataloaderFromIntuneAPI_README.md b/Integration/Rest Integration Send Attachment Payload/Send attachment payload via REST/DataloaderFromIntuneAPI_README.md new file mode 100644 index 0000000000..b5d5b8951b --- /dev/null +++ b/Integration/Rest Integration Send Attachment Payload/Send attachment payload via REST/DataloaderFromIntuneAPI_README.md @@ -0,0 +1,5 @@ +The getDevices() function automates the process of: +Calling a REST Message defined in ServiceNow. +Retrieving device data in JSON format. +Handling pagination using the @odata.nextLink attribute. +Inserting each record into a specified Import Set table for further transformation. diff --git a/Integration/Rest Integration Send Attachment Payload/README.md b/Integration/Rest Integration Send Attachment Payload/Send attachment payload via REST/README.md similarity index 100% rename from Integration/Rest Integration Send Attachment Payload/README.md rename to Integration/Rest Integration Send Attachment Payload/Send attachment payload via REST/README.md diff --git a/Integration/Rest Integration Send Attachment Payload/attachment_payload_script.js b/Integration/Rest Integration Send Attachment Payload/Send attachment payload via REST/attachment_payload_script.js similarity index 100% rename from Integration/Rest Integration Send Attachment Payload/attachment_payload_script.js rename to Integration/Rest Integration Send Attachment Payload/Send attachment payload via REST/attachment_payload_script.js diff --git a/Integration/Scripted REST Api/Create Catalog Items Dynamically/README.md b/Integration/Scripted REST Api/Create Catalog Items Dynamically/README.md new file mode 100644 index 0000000000..2ebed13ca1 --- /dev/null +++ b/Integration/Scripted REST Api/Create Catalog Items Dynamically/README.md @@ -0,0 +1,100 @@ + +# ServiceNow Catalog Builder API +**Automate Catalog Item Creation with a Single REST Call** + +--- + +## Overview + +The **ServiceNow Catalog Builder API** is a custom **Scripted REST API** that dynamically creates Service Catalog Items in your instance — including variables and choices — from a simple JSON payload. + +This API eliminates the repetitive manual work of configuring Catalog Items one by one, and makes it possible to **automate catalog creation programmatically** or **integrate it with CI/CD pipelines, GitHub workflows, or external systems**. + +--- + +## Key Features + +Automatically create **Catalog Items** in `sc_cat_item` +Dynamically generate **Variables** and **Choices** +Supports **category mapping** and **item ownership** +Extensible design for **flows, icons, and attachments** +Developer-friendly — fully JSON-driven + +--- + +## Use Case + +This API is perfect for: +- **Admin Automation:** Auto-build standard catalog forms during environment setup. +- **RPA / CI Pipelines:** Integrate with DevOps or GitHub Actions to deploy catalog definitions. +- **Dynamic Service Portals:** Allow external apps or portals to create items on demand. + +Example: +A company wants to auto-create 10 new service catalog items from a GitHub configuration file. +Using this API, they simply call one REST endpoint for each definition — no manual clicks needed. + +--- + +## Scripted REST API Details + +| Property | Value | +|-----------|--------| +| **Name** | Catalog Builder API | +| **API ID** | `x_demo.catalog_creator` | +| **Resource Path** | `/create` | +| **Method** | POST | +| **Authentication** | Basic Auth / OAuth | +| **Tables Used** | `sc_cat_item`, `item_option_new`, `question_choice` | + +--- + +## Logic Flow + +1. **Receive JSON input** with item name, category, and variables. +2. **Create a new record** in `sc_cat_item`. +3. **Loop through variables** and create them in `item_option_new`. +4. If the variable type is `select_box`, create **choices** automatically. +5. Return a JSON response with the new item’s `sys_id` and success message. + +--- + +## Example Input (POST Body) + +```json +{ + "name": "Request New Laptop", + "category": "Hardware", + "short_description": "Laptop provisioning request form", + "description": "Allows employees to request a new laptop with model and RAM options.", + "owner": "admin", + "variables": [ + { + "name": "Laptop Model", + "type": "select_box", + "choices": "Dell,HP,Lenovo" + }, + { + "name": "RAM Size", + "type": "select_box", + "choices": "8GB,16GB,32GB" + }, + { + "name": "Business Justification", + "type": "multi_line_text" + } + ] +} + + +## Example Output: +{ + "catalog_sys_id": "b2f6329cdb6d0010355b5fb4ca9619e2", + "message": "Catalog item created successfully!" +} +After the API call: +A new Catalog Item appears under Maintain Items. +The item contains: +Short Description: Laptop provisioning form +Variables: Laptop Model, RAM Size, Business Justification +Choices: Auto-populated for select boxes +The item is active and ready to use in the catalog. diff --git a/Integration/Scripted REST Api/Create Catalog Items Dynamically/catalog.js b/Integration/Scripted REST Api/Create Catalog Items Dynamically/catalog.js new file mode 100644 index 0000000000..d52fafb054 --- /dev/null +++ b/Integration/Scripted REST Api/Create Catalog Items Dynamically/catalog.js @@ -0,0 +1,120 @@ +// Scenario : As a ServiceNow Admin or Developer managing dozens of similar request forms (like “Request Laptop”, “Request Mobile”, “Request Access”, etc.). +// Manually creating each catalog item is repetitive. + +// This code will Automate Catalog Item Creation with a Single REST Call +//Script: POST /api/x_demo/catalog_creator/create + +(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) { + + var body = request.body.data; + var result = {}; + + try { + // 1. Create Catalog Item + var catItem = new GlideRecord('sc_cat_item'); + catItem.initialize(); + catItem.name = body.name; + catItem.short_description = body.short_description || ''; + catItem.description = body.description || ''; + catItem.category = getCategorySysId(body.category); + catItem.owning_group = getOwner(body.owner); + catItem.active = true; + var catSysId = catItem.insert(); + + result.catalog_sys_id = catSysId; + + // 2. Create Variables + if (body.variables && body.variables.length > 0) { + for (var i = 0; i < body.variables.length; i++) { + var v = body.variables[i]; + + var variable = new GlideRecord('item_option_new'); + variable.initialize(); + variable.cat_item = catSysId; + variable.name = v.name.toLowerCase().replace(/ /g, '_'); + variable.question_text = v.name; + variable.type = getType(v.type); + variable.order = (i + 1) * 100; + var varSysId = variable.insert(); + + // Add choices for select box variables + if (v.choices && v.choices.length > 0) { + var choices = v.choices.split(','); + for (var j = 0; j < choices.length; j++) { + var choice = new GlideRecord('question_choice'); + choice.initialize(); + choice.question = varSysId; + choice.value = choices[j].trim(); + choice.label = choices[j].trim(); + choice.insert(); + } + } + } + } + + result.message = "Catalog item created successfully!"; + response.setStatus(201); + + } catch (e) { + gs.error("Error creating catalog item: " + e); + result.message = e.toString(); + response.setStatus(500); + } + + response.setBody(result); + + + function getCategorySysId(categoryName) { + var cat = new GlideRecord('sc_category'); + cat.addQuery('title', categoryName); + cat.query(); + if (cat.next()) return cat.sys_id; + return null; + } + + function getOwner(ownerName) { + var usr = new GlideRecord('sys_user'); + usr.addQuery('user_name', ownerName); + usr.query(); + if (usr.next()) return usr.sys_id; + return gs.getUserID(); + } + + function getType(typeName) { + var map = { + "single_line_text": 1, + "multi_line_text": 2, + "select_box": 3, + "reference": 8, + "checkbox": 5 + }; + return map[typeName] || 1; + } + +})(request, response); + +//Example JSON +//{ + "name": "Request New Laptop", + "category": "Hardware", + "short_description": "Laptop provisioning form", + "description": "Allows employees to request a new laptop.", + "owner": "admin", + "variables": [ + { + "name": "Laptop Model", + "type": "select_box", + "choices": "Dell,HP,Lenovo" + }, + { + "name": "RAM Size", + "type": "select_box", + "choices": "8GB,16GB,32GB" + }, + { + "name": "Business Justification", + "type": "multi_line_text" + } + ] +} + diff --git a/Integration/Scripted REST Api/DomainSeperation/README.md b/Integration/Scripted REST Api/DomainSeperation/README.md new file mode 100644 index 0000000000..7c93b75a92 --- /dev/null +++ b/Integration/Scripted REST Api/DomainSeperation/README.md @@ -0,0 +1,119 @@ +# ServiceNow Scripted REST API for creating incdents in the correct company/domain + +## Overview + +The API allows authenticated users to create new **Incident** records within their own domain and company context. + +> **DISCLAIMER** +> This script was developed and tested on a **ServiceNow Personal Developer Instance (PDI)**. +> It is intended for **educational and demonstration purposes only**. +> Please **test thoroughly in a dedicated development environment** before deploying to production. + +--- + +## Features + +- Creates a new Incident record for the currently logged-in user. +- Automatically assigns the user's domain and company to the incident. +- Returns the generated incident number and domain in the response. + +--- + +## Prerequisites & Dependencies + +Before using or testing this Scripted REST API, ensure the following conditions are met: + +1. **Domain Separation Plugin** + + - The **Domain Separation** plugin must be activated on your instance. + - This enables `sys_domain` references and ensures incidents are created within the correct domain context. + +2. **Core Data Setup** + + - Ensure valid entries exist in the **core_company** table. + - Each company should have an associated **domain** record in the **sys_domain** table. + - These relationships are critical for correct domain assignment during incident creation. + +3. **User Configuration** + + - The user invoking this API must: + - Belong to a specific domain. + - Have the **snc_platform_rest_api_access** role to access Scripted REST APIs. + - Users must also have ACL permissions to: + - **Read** from the `sys_user` table. + - **Insert** into the `incident` table. + +4. **Instance Configuration** + - Tested and validated on a **ServiceNow Personal Developer Instance (PDI)**. + - Other environments should be configured with equivalent domain and company data for consistent results. + +--- + +## Information + +- **Author**: Anasuya Rampalli ([anurampalli](https://github.com/anurampalli)) +- **Version**: 1.0 +- **Date**: 2025-10-08 +- **Context**: Scripted REST API (`create` function) +- **Tested On**: ServiceNow Personal Developer Instance (PDI) + +--- + +## Expected Request Format + +```json +POST /api/your_namespace/your_endpoint +Content-Type: application/json + +{ + "short_description": "Issue description text" +} +``` +```` + +--- + +## Response Examples + +### Success + +```json +{ + "status": "success", + "incident_id": "INC0012345", + "domain": "TOP/Child Domain" +} +``` + +### Error + +```json +{ + "error": { + "message": "User Not Authenticated", + "detail": "Required to provide Auth information" + }, + "status": "failure" +} +``` + +--- + +## How It Works + +1. Extracts the `short_description` from the incoming JSON payload. +2. Identifies the authenticated user via `gs.getUserID()`. +3. Retrieves the user's domain and company using `sys_user`. +4. Creates a new `incident` record with the user's domain, company, and description. +5. Returns the incident number and domain in the response. + +--- + +## Testing Tips + +- Use a valid ServiceNow PDI with Scripted REST API enabled. +- Ensure the user is authenticated before making requests. +- Check the `incident` table for newly created records. + +--- + diff --git a/Integration/Scripted REST Api/DomainSeperation/create.js b/Integration/Scripted REST Api/DomainSeperation/create.js new file mode 100644 index 0000000000..c3ec7e62a4 --- /dev/null +++ b/Integration/Scripted REST Api/DomainSeperation/create.js @@ -0,0 +1,93 @@ +/** + * + * This script is provided for **educational and demonstration purposes only**. + * Please thoroughly **test in a dedicated development environment** + * before deploying to production. + * + * ----------------------------------------------------------------------------- + * Script Purpose: + * Creates a new Incident record under the same domain and company as the + * currently logged-in user. Returns the generated incident number and domain. + * ----------------------------------------------------------------------------- + * + * @author Anasuya Rampalli (anurampalli) + * @version 1.0 + * @date 2025-10-08 + * @tested On ServiceNow PDI (Personal Developer Instance) + * @context Scripted REST API (process function) + */ + +/** + * Processes the incoming REST API request and creates an Incident + * for the authenticated user within their domain. + * + * @param {RESTAPIRequest} request - The incoming REST API request object containing JSON payload. + * @param {RESTAPIResponse} response - The response object used to send results back to the client. + * + * Expected JSON Body: + * { + * "short_description": "Issue description text" + * } + * + * Response Example (Success): + * { + * "status": "success", + * "incident_id": "INC0012345", + * "domain": "TOP/Child Domain" + * } + * + * Response Example (Error): + * { + * "error": { + * "message": "User Not Authenticated", + * "detail": "Required to provide Auth information" + * }, + * "status": "failure" + * } + */ +(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) { + var body = request.body.data; + var companyName = body.company; + var shortDesc = body.short_description; + //gs.info(gs.getUserID()); + var userSysId = gs.getUserID(); + var result = {}; + + try { + // looup user + var grUser = new GlideRecord("sys_user"); + grUser.addQuery("sys_id", userSysId.toString()); + grUser.query(); + if (grUser.next()) { + var domain = grUser.sys_domain; + // Create new incident + var grIncident = new GlideRecord("incident"); + grIncident.initialize(); + grIncident.short_description = shortDesc; + grIncident.caller_id = userSysId; + gs.info("COMPANY: " + grUser.company.getDisplayValue()); + grIncident.company = grUser.company; + grIncident.sys_domain = grUser.sys_domain; // domain reference comes from core_company + grIncident.insert(); + + let correlationId = grIncident.number; + gs.info( + "Domain Indcident API: inserted incident number: " + correlationId + ); + result.status = "success"; + result.incident_id = correlationId; + result.domain = grUser.sys_domain.getDisplayValue(); + } else { + response.setStatus(404); + result.status = "error"; + result.message = "User not found: " + companyName; + } + } catch (e) { + response.setStatus(500); + result.status = "error"; + result.message = e.message; + } + + response.setBody(result); +})(request, response); + diff --git a/Integration/Scripted REST Api/Group Membership API/README.md b/Integration/Scripted REST Api/Group Membership API/README.md new file mode 100644 index 0000000000..e0591b695c --- /dev/null +++ b/Integration/Scripted REST Api/Group Membership API/README.md @@ -0,0 +1,72 @@ +# Group Membership API- Scripted REST API +## Overview +This API provides a simple, secure way to reterive all members of a specified user group in ServiceNow. It allows integrations, Service Portal widgets, or external systems to query group membership without giving direct access to user tables + +### API Details +- **API Name**: Group Membership API +- **API ID**: group_membership_api +- **ResourceName**: Members +- **Relative Path**: /members +- **HTTP Method**: GET +- **Query Parameter**: groupName (required) + +## Request Format + +### Example Request +GET https://.service-now.com/api/1819147/group_membership_api/members?groupName=Hardware + +### Example Response +```json +{ + { + "result": { + "groupName": "Hardware", + "totalMembers": 7, + "member": [ + { + "userName": "beth.anglin", + "displayName": "Beth Anglin", + "email": "beth.anglin@example.com", + "active": "true" + }, + { + "userName": "itil", + "displayName": "ITIL User", + "email": "itil@example.com", + "active": "true" + }, + { + "userName": "bow.ruggeri", + "displayName": "Bow Ruggeri", + "email": "bow.ruggeri@example.com", + "active": "true" + }, + { + "userName": "david.dan", + "displayName": "David Dan", + "email": "david.dan@example.com", + "active": "true" + }, + { + "userName": "david.loo", + "displayName": "David Loo", + "email": "david.loo@example.com", + "active": "true" + }, + { + "userName": "don.goodliffe", + "displayName": "Don Goodliffe", + "email": "don.goodliffe@example.com", + "active": "true" + }, + { + "userName": "fred.luddy", + "displayName": "Fred Luddy", + "email": "fred.luddy@example.com", + "active": "true" + } + ] + } +} + +} diff --git a/Integration/Scripted REST Api/Group Membership API/group_membership.js b/Integration/Scripted REST Api/Group Membership API/group_membership.js new file mode 100644 index 0000000000..322e95caf7 --- /dev/null +++ b/Integration/Scripted REST Api/Group Membership API/group_membership.js @@ -0,0 +1,38 @@ +(function process( /*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) { + var groupName = request.queryParams.groupName; + var members = []; + if (!groupName) { + response.setStatus(400); + return { + error: "groupName query parameter is required" + }; + } + var grGrp = new GlideRecord('sys_user_group'); + grGrp.addQuery('name', groupName); + grGrp.query(); + if (!grGrp.next()) { + response.setStatus(400); + return { + error: "Group name doesn't found" + }; + } + var grGrpMem = new GlideRecord('sys_user_grmember'); + grGrpMem.addQuery("group.name", groupName); + grGrpMem.query(); + while (grGrpMem.next()) { + members.push({ + userName: grGrpMem.user.user_name.toString(), + displayName: grGrpMem.user.name.toString(), + email: grGrpMem.user.email.toString(), + active: grGrpMem.user.active.toString() + }); + } + return { + groupName: groupName.toString(), + totalMembers: members.length, + member: members + }; + + + +})(request, response); diff --git a/Integration/Scripted REST Api/MID Server status JSON endpoint/README.md b/Integration/Scripted REST Api/MID Server status JSON endpoint/README.md new file mode 100644 index 0000000000..e2d8d78170 --- /dev/null +++ b/Integration/Scripted REST Api/MID Server status JSON endpoint/README.md @@ -0,0 +1,26 @@ +# MID Server status JSON endpoint + +## What this solves +Operations teams often need a quick machine-readable view of MID Server health for dashboards and monitors. This Scripted REST API returns a compact JSON array of MID Servers with their status, last update time, and a simple "stale" flag if the record has not changed recently. + +## Where to use +Create a Scripted REST API with a single Resource and paste this script as the Resource Script. Call it from monitoring tools, dashboards, or widgets. + +## How it works +- Queries `ecc_agent` for active MID Servers +- Returns `name`, `status`, `sys_id`, `sys_updated_on`, and a computed `stale` boolean based on a configurable `minutes_stale` query parameter (default 15) +- Uses `gs.dateDiff` to compute minutes since last update + +## Configure +- Pass `minutes_stale` as a query parameter to override the default, for example `...?minutes_stale=30` +- Extend the payload as needed (for example add `version`, `ip_address`) if available in your instance + +## References +- Scripted REST APIs + https://www.servicenow.com/docs/bundle/zurich-application-development/page/build/applications/task/create-scripted-rest-api.html +- MID Server overview + https://www.servicenow.com/docs/bundle/zurich-servicenow-platform/page/product/mid-server/concept/c_MIDServer.html +- GlideRecord API + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideRecord/concept/c_GlideRecordAPI.html +- GlideDateTime and dateDiff + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideDateTime/concept/c_GlideDateTimeAPI.html diff --git a/Integration/Scripted REST Api/MID Server status JSON endpoint/mid_server_status_api.js b/Integration/Scripted REST Api/MID Server status JSON endpoint/mid_server_status_api.js new file mode 100644 index 0000000000..46c91e1876 --- /dev/null +++ b/Integration/Scripted REST Api/MID Server status JSON endpoint/mid_server_status_api.js @@ -0,0 +1,44 @@ +// Scripted REST API Resource Script: MID Server status JSON endpoint +// Method: GET +// Path: /mid/status + +(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) { + try { + // Configurable staleness threshold in minutes via query param + var q = request.queryParams || {}; + var minutesStale = parseInt((q.minutes_stale && q.minutes_stale[0]) || '15', 10); + if (!isFinite(minutesStale) || minutesStale <= 0) minutesStale = 15; + + var now = new GlideDateTime(); + + var out = []; + var gr = new GlideRecord('ecc_agent'); // MID Server table + gr.addActiveQuery(); + gr.orderBy('name'); + gr.query(); + + while (gr.next()) { + var updated = String(gr.getValue('sys_updated_on') || ''); + var minutesSince = 0; + if (updated) { + // gs.dateDiff returns seconds when third arg is true + minutesSince = Math.floor(gs.dateDiff(updated, now.getValue(), true) / 60); + } + + out.push({ + sys_id: gr.getUniqueValue(), + name: gr.getDisplayValue('name') || gr.getValue('name'), + status: gr.getDisplayValue('status') || gr.getValue('status'), // Up, Down, etc. + sys_updated_on: gr.getDisplayValue('sys_updated_on'), + minutes_since_update: minutesSince, + stale: minutesSince >= minutesStale + }); + } + + response.setStatus(200); + response.setBody(out); + } catch (e) { + response.setStatus(500); + response.setBody({ error: String(e) }); + } +})(request, response); diff --git a/Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/HmacUtils.js b/Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/HmacUtils.js new file mode 100644 index 0000000000..88e29d03cb --- /dev/null +++ b/Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/HmacUtils.js @@ -0,0 +1,35 @@ +// Script Include: HmacUtils +// Purpose: Compute HMAC SHA-256 and constant-time compare. + +var HmacUtils = Class.create(); +HmacUtils.prototype = { + initialize: function() {}, + + hmacSha256Hex: function(secret, message) { + var mac = Packages.javax.crypto.Mac.getInstance('HmacSHA256'); + var key = new Packages.javax.crypto.spec.SecretKeySpec( + new Packages.java.lang.String(secret).getBytes('UTF-8'), + 'HmacSHA256' + ); + mac.init(key); + var raw = mac.doFinal(new Packages.java.lang.String(message).getBytes('UTF-8')); + + var sb = new Packages.java.lang.StringBuilder(); + for (var i = 0; i < raw.length; i++) { + var hex = Packages.java.lang.Integer.toHexString((raw[i] & 0xff) | 0x100).substring(1); + sb.append(hex); + } + return sb.toString(); + }, + + constantTimeEquals: function(a, b) { + var A = String(a || ''); + var B = String(b || ''); + if (A.length !== B.length) return false; + var diff = 0; + for (var i = 0; i < A.length; i++) diff |= A.charCodeAt(i) ^ B.charCodeAt(i); + return diff === 0; + }, + + type: 'HmacUtils' +}; diff --git a/Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/README.md b/Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/README.md new file mode 100644 index 0000000000..6cc7fc80a1 --- /dev/null +++ b/Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/README.md @@ -0,0 +1,26 @@ +# Webhook receiver with HMAC SHA-256 validation + +## What this solves +Inbound webhooks should be verified to ensure the payload really came from the sender. This receiver validates an `X-Signature` header containing an HMAC SHA-256 of the request body using a shared secret. Invalid signatures return HTTP 401. + +## Where to use +- Scripted REST API resource script +- Include the `HmacUtils` Script Include in the same app or global + +## How it works +- Reads raw request body and the `X-Signature` header +- Computes HMAC SHA-256 using the shared secret +- Compares in constant time to avoid timing attacks +- If valid, inserts the payload into a target table or queues it for processing + +## Configure +- Set `SHARED_SECRET` (prefer credentials or encrypted properties) +- Update `TARGET_TABLE` for successful inserts + +## References +- Scripted REST APIs + https://www.servicenow.com/docs/bundle/zurich-application-development/page/build/applications/task/create-scripted-rest-api.html +- REST API request/response objects + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideHTTPRequest/concept/c_scripted-rest-api-request.html +- Java crypto (used server-side) + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/Script/server_apis/concept/java-use.html diff --git a/Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/WebhookHmacReceiver.js b/Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/WebhookHmacReceiver.js new file mode 100644 index 0000000000..9972c552a5 --- /dev/null +++ b/Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/WebhookHmacReceiver.js @@ -0,0 +1,43 @@ +// Scripted REST API Resource Script: Webhook receiver with HMAC validation +(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) { + var SHARED_SECRET = gs.getProperty('x_acme.webhook.secret', ''); + var TARGET_TABLE = 'x_acme_inbound_webhook'; // replace with your table + + try { + var body = request.body && request.body.data ? request.body.data : ''; + var signature = request.getHeader('X-Signature') || ''; // hex HMAC hash + + if (!SHARED_SECRET) { + response.setStatus(500); + response.setBody({ error: 'Server not configured' }); + return; + } + if (!signature || !body) { + response.setStatus(400); + response.setBody({ error: 'Missing signature or body' }); + return; + } + + var util = new HmacUtils(); + var expected = util.hmacSha256Hex(SHARED_SECRET, body); + + if (!util.constantTimeEquals(expected, signature)) { + response.setStatus(401); + response.setBody({ error: 'Invalid signature' }); + return; + } + + // Valid payload: insert a record for processing + var rec = new GlideRecord(TARGET_TABLE); + rec.initialize(); + rec.payload = body; + rec.signature = signature; + rec.insert(); + + response.setStatus(200); + response.setBody({ ok: true }); + } catch (e) { + response.setStatus(500); + response.setBody({ error: String(e) }); + } +})(request, response); diff --git a/Integration/Scripted SOAP Incident Creation/README.md b/Integration/Scripted SOAP Incident Creation/Scripted SOAP incident creation/README.md similarity index 100% rename from Integration/Scripted SOAP Incident Creation/README.md rename to Integration/Scripted SOAP Incident Creation/Scripted SOAP incident creation/README.md diff --git a/Integration/Scripted SOAP Incident Creation/Scripted SOAP incident creation.js b/Integration/Scripted SOAP Incident Creation/Scripted SOAP incident creation/Scripted SOAP incident creation.js similarity index 100% rename from Integration/Scripted SOAP Incident Creation/Scripted SOAP incident creation.js rename to Integration/Scripted SOAP Incident Creation/Scripted SOAP incident creation/Scripted SOAP incident creation.js diff --git a/Modern Development/ECMASCript 2021/README.md b/Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/README.md similarity index 100% rename from Modern Development/ECMASCript 2021/README.md rename to Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/README.md diff --git a/Modern Development/ECMASCript 2021/arrowfunctions.js b/Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/arrowfunctions.js similarity index 100% rename from Modern Development/ECMASCript 2021/arrowfunctions.js rename to Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/arrowfunctions.js diff --git a/Modern Development/ECMASCript 2021/class.js b/Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/class.js similarity index 100% rename from Modern Development/ECMASCript 2021/class.js rename to Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/class.js diff --git a/Modern Development/ECMASCript 2021/const.js b/Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/const.js similarity index 100% rename from Modern Development/ECMASCript 2021/const.js rename to Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/const.js diff --git a/Modern Development/ECMASCript 2021/defaultparms.js b/Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/defaultparms.js similarity index 100% rename from Modern Development/ECMASCript 2021/defaultparms.js rename to Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/defaultparms.js diff --git a/Modern Development/ECMASCript 2021/destructuring.js b/Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/destructuring.js similarity index 100% rename from Modern Development/ECMASCript 2021/destructuring.js rename to Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/destructuring.js diff --git a/Modern Development/ECMASCript 2021/forof.js b/Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/forof.js similarity index 100% rename from Modern Development/ECMASCript 2021/forof.js rename to Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/forof.js diff --git a/Modern Development/ECMASCript 2021/javascriptmode.png b/Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/javascriptmode.png similarity index 100% rename from Modern Development/ECMASCript 2021/javascriptmode.png rename to Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/javascriptmode.png diff --git a/Modern Development/ECMASCript 2021/let.js b/Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/let.js similarity index 100% rename from Modern Development/ECMASCript 2021/let.js rename to Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/let.js diff --git a/Modern Development/ECMASCript 2021/map.js b/Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/map.js similarity index 100% rename from Modern Development/ECMASCript 2021/map.js rename to Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/map.js diff --git a/Modern Development/ECMASCript 2021/openrecord.png b/Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/openrecord.png similarity index 100% rename from Modern Development/ECMASCript 2021/openrecord.png rename to Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/openrecord.png diff --git a/Modern Development/ECMASCript 2021/set.js b/Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/set.js similarity index 100% rename from Modern Development/ECMASCript 2021/set.js rename to Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/set.js diff --git a/Modern Development/ECMASCript 2021/spread.js b/Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/spread.js similarity index 100% rename from Modern Development/ECMASCript 2021/spread.js rename to Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/spread.js diff --git a/Modern Development/ECMASCript 2021/symbol.js b/Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/symbol.js similarity index 100% rename from Modern Development/ECMASCript 2021/symbol.js rename to Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/symbol.js diff --git a/Modern Development/ECMASCript 2021/templatestringsandliterals.js b/Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/templatestringsandliterals.js similarity index 100% rename from Modern Development/ECMASCript 2021/templatestringsandliterals.js rename to Modern Development/ECMASCript 2021/Server-side ECMAScript 2021 examples/templatestringsandliterals.js diff --git a/Modern Development/Service Portal Widgets/Animated Notification Badge/README.md b/Modern Development/Service Portal Widgets/Animated Notification Badge/README.md new file mode 100644 index 0000000000..ef11e2b90d --- /dev/null +++ b/Modern Development/Service Portal Widgets/Animated Notification Badge/README.md @@ -0,0 +1,18 @@ +# 🔔 Animated Notification Badge + +This snippet demonstrates how to create an animated notification badge using native ServiceNow client-side capabilities, without relying on direct DOM manipulation or inline styles. +It uses AngularJS and CSS to apply a pulsating animation to the badge, ideal for Portal widgets that require attention-grabbing indicators. + +![Demo of animated badge](./animated-badge.gif) + +## 📦 Files + +- `notification-badge.html` – Badge markup with conditional visibility +- `notification-badge.css` – Keyframe animation and badge styling +- `notification-badge.js` – Logic to trigger or reset badge visibility + +## 🚀 How to Use + +1. Copy the HTML, CSS, and client script into your custom Portal widget. +2. Bind the badge visibility to a condition (e.g., number of unread messages). +3. Use the `animate__pulse` class to trigger attention-grabbing animations. diff --git a/Modern Development/Service Portal Widgets/Animated Notification Badge/animated-badge.gif b/Modern Development/Service Portal Widgets/Animated Notification Badge/animated-badge.gif new file mode 100644 index 0000000000..a8c0c2ea47 Binary files /dev/null and b/Modern Development/Service Portal Widgets/Animated Notification Badge/animated-badge.gif differ diff --git a/Modern Development/Service Portal Widgets/Animated Notification Badge/notification-badge.css b/Modern Development/Service Portal Widgets/Animated Notification Badge/notification-badge.css new file mode 100644 index 0000000000..d394774228 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Animated Notification Badge/notification-badge.css @@ -0,0 +1,35 @@ +.notification-wrapper { + position: relative; + display: inline-block; +} + +.notification-icon { + font-size: 24px; + color: #333; +} + +.notification-badge { + position: absolute; + top: -6px; + right: -6px; + background-color: #e74c3c; + color: white; + font-size: 12px; + padding: 2px 6px; + border-radius: 50%; + font-weight: bold; + animation-duration: 0.6s; + animation-fill-mode: both; +} + +.notification-pulse { + animation-name: pulseScale; + animation-iteration-count: infinite; + animation-timing-function: ease-in-out; +} + +@keyframes pulseScale { + 0% { transform: scale(1); } + 50% { transform: scale(1.2); } + 100% { transform: scale(1); } +} diff --git a/Modern Development/Service Portal Widgets/Animated Notification Badge/notification-badge.html b/Modern Development/Service Portal Widgets/Animated Notification Badge/notification-badge.html new file mode 100644 index 0000000000..ae74f9e960 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Animated Notification Badge/notification-badge.html @@ -0,0 +1,9 @@ +
+ + + {{ c.badgeCount }} + +
diff --git a/Modern Development/Service Portal Widgets/Animated Notification Badge/notification-badge.js b/Modern Development/Service Portal Widgets/Animated Notification Badge/notification-badge.js new file mode 100644 index 0000000000..9fc4d51b4d --- /dev/null +++ b/Modern Development/Service Portal Widgets/Animated Notification Badge/notification-badge.js @@ -0,0 +1,6 @@ +api.controller=function($scope) { + var c = this; + + c.badgeCount = 3; + c.hasNewNotification = c.badgeCount > 0; +}; diff --git a/Modern Development/Service Portal Widgets/Catalog Item Explorer/README.md b/Modern Development/Service Portal Widgets/Catalog Item Explorer/README.md index 15f3ac438d..5700ff90ba 100644 --- a/Modern Development/Service Portal Widgets/Catalog Item Explorer/README.md +++ b/Modern Development/Service Portal Widgets/Catalog Item Explorer/README.md @@ -24,3 +24,8 @@ The primary goal of the Catalog Item Explorer widget is to serve as a valuable l 7. **Quick Search Placeholder:** Define the placeholder message for the Quick Search field to align with your portal's user interface. 8. **Widget Title:** Customize the title of the widget to match its purpose within your Service Portal. 9. **Copyright Display:** Choose whether to display copyright information in the bottom right corner of the widget. + +## Latest Update (v1.21): +- **Support for the external URL content items. +- **The default target window changed to "_self" (same window). +- **Option to open an item in the new window added at the end of the row. diff --git a/Modern Development/Service Portal Widgets/Catalog Item Explorer/client_script.js b/Modern Development/Service Portal Widgets/Catalog Item Explorer/client_script.js index 7638ce2cb4..12d88efebb 100644 --- a/Modern Development/Service Portal Widgets/Catalog Item Explorer/client_script.js +++ b/Modern Development/Service Portal Widgets/Catalog Item Explorer/client_script.js @@ -1,151 +1,152 @@ -api.controller = function ($scope, $window) { - /* widget controller */ - var c = this; +api.controller = function($scope, $window) { + /* widget controller */ + var c = this; - /* Variable and Service Initizalization */ - setWidgetState("initial", c.data.catalogCategories); - - /* Function to be called when "Show All Items" has been clicked */ - c.showAllItems = function () { + /* Variable and Service Initizalization */ setWidgetState("initial", c.data.catalogCategories); - c.filteredCatalogItems = c.displayItems = c.data.catalogItems; - c.isShowAllSelected = true; - c.data.currentPage = resetCurrentPage(); - c.isMultiplePage = checkMultiPage(c.filteredCatalogItems.length, c.data.itemsPerPage); - }; - - /* Function to be called when "Quick Search" is active */ - c.quickSearch = function () { - if ($scope.searchText.length == 0) { - setWidgetState("initial", c.data.catalogCategories); - return; + + /* Function to be called when "Show All Items" has been clicked */ + c.showAllItems = function() { + setWidgetState("initial", c.data.catalogCategories); + c.filteredCatalogItems = c.displayItems = c.data.catalogItems; + c.isShowAllSelected = true; + c.data.currentPage = resetCurrentPage(); + c.isMultiplePage = checkMultiPage(c.filteredCatalogItems.length, c.data.itemsPerPage); + }; + + /* Function to be called when "Quick Search" is active */ + c.quickSearch = function() { + if ($scope.searchText.length == 0) { + setWidgetState("initial", c.data.catalogCategories); + return; + } + + setWidgetState("default-selected", c.data.catalogCategories); + c.data.currentPage = resetCurrentPage(); + c.filteredCatalogItems = c.displayItems = $scope.searchText.length > 0 ? quickSearch(c.data.catalogItems, $scope.searchText) : []; + c.isMultiplePage = checkMultiPage(c.filteredCatalogItems.length, c.data.itemsPerPage); + }; + + /* Function to be called when category letter has been clicked */ + c.selectCategory = function(category) { + setWidgetState("default", c.data.catalogCategories); + category.selected = true; + c.data.currentPage = resetCurrentPage(); + c.filteredCatalogItems = selectCategory(c.data.catalogItems, category); + c.isMultiplePage = checkMultiPage(c.filteredCatalogItems.length, c.data.itemsPerPage); + c.displayItems = calculateDisplayCatalogItems(c.filteredCatalogItems, c.data.currentPage, c.data.itemsPerPage); + }; + + /* Function to be called when reset button has been pressed*/ + c.resetState = function() { + setWidgetState("initial", c.data.catalogCategories); + }; + + /* Function to generate URL and define the target window */ + c.openUrl = function (itemId, externalUrl, openInNewWindow) { + var fullLink = ""; + fullLink = c.data.defaultCatalogLink + itemId; + + /* If external URL provided then replace the output with it */ + if (externalUrl) { fullLink = externalUrl; } + + /* Define the target window */ + var target = openInNewWindow ? '_blank' : '_self'; + $window.open(fullLink, target); + }; + + /* Pagination */ + + /* Function to be called by the form element when another page has been selected */ + c.pageChanged = function() { + c.displayItems = calculateDisplayCatalogItems(c.filteredCatalogItems, c.data.currentPage, c.data.itemsPerPage); + }; + + /* Functions */ + + /* If it is a quick seach then we are giving filtered array based on the condition */ + function quickSearch(items, searchText) { + return items.filter(function(item) { + try { + /* First we need to check that values are not null, otherwise assign them with empty space to avoid app crash */ + var itemName = item.name != null ? item.name.toLowerCase() : ""; + var itemDescription = item.description != null ? item.description.toLowerCase() : ""; + + /* Return item if quick search text we placed in our input field is contained in the item name or description */ + return (itemName).indexOf(searchText.toLowerCase()) != -1 || (itemDescription).indexOf(searchText.toLowerCase()) != -1; + } catch (error) { + console.log("Something went wrong while filtering searching by item name or description"); + } + }); } - setWidgetState("default-selected", c.data.catalogCategories); - c.data.currentPage = resetCurrentPage(); - c.filteredCatalogItems = c.displayItems = $scope.searchText.length > 0 ? quickSearch(c.data.catalogItems, $scope.searchText) : []; - c.isMultiplePage = checkMultiPage(c.filteredCatalogItems.length, c.data.itemsPerPage); - }; - - /* Function to be called when category letter has been clicked */ - c.selectCategory = function (category) { - setWidgetState("default", c.data.catalogCategories); - category.selected = true; - c.data.currentPage = resetCurrentPage(); - c.filteredCatalogItems = selectCategory(c.data.catalogItems, category); - c.isMultiplePage = checkMultiPage(c.filteredCatalogItems.length, c.data.itemsPerPage); - c.displayItems = calculateDisplayCatalogItems(c.filteredCatalogItems, c.data.currentPage, c.data.itemsPerPage); - }; - - /* Function to be called when reset button has been pressed*/ - c.resetState = function () { - setWidgetState("initial", c.data.catalogCategories); - }; - - /* Function to make the whole row clickable */ - c.openUrl = function (itemId, externalUrl) { - - var fullLink = ""; - fullLink = c.data.defaultCatalogLink + itemId; - - /* If external URL provided then replace the output with it */ - if (externalUrl) { fullLink = externalUrl }; - - $window.open(fullLink, "_blank"); - }; - - /* Pagination */ - - /* Function to be called by the form element when another page has been selected */ - c.pageChanged = function () { - c.displayItems = calculateDisplayCatalogItems(c.filteredCatalogItems, c.data.currentPage, c.data.itemsPerPage); - }; - - /* Functions */ - - /* If it is a quick seach then we are giving filtered array based on the condition */ - function quickSearch(items, searchText) { - return items.filter(function (item) { - try { - /* First we need to check that values are not null, otherwise assign them with empty space to avoid app crash */ - var itemName = item.name != null ? item.name.toLowerCase() : ""; - var itemDescription = item.description != null ? item.description.toLowerCase() : ""; - - /* Return item if quick search text we placed in our input field is contained in the item name or description */ - return (itemName).indexOf(searchText.toLowerCase()) != -1 || (itemDescription).indexOf(searchText.toLowerCase()) != -1; - } catch (error) { - console.log("Something went wrong while filtering searching by item name or description"); - } - }); - } - - /* If it is a quick seach then we are giving filtered array based on the condition */ - function selectCategory(items, category) { - return items.filter(function (item) { - return (item.name.toLowerCase()).substring(0, 1) == category.letter.toLowerCase(); - }); - } - - /* Function to reset the category selection to default state (all are non-selected) */ - function resetSelected(items) { - for (var i = 0; i < items.length; i++) { - items[i].selected = false; + /* If it is a quick seach then we are giving filtered array based on the condition */ + function selectCategory(items, category) { + return items.filter(function(item) { + return (item.name.toLowerCase()).substring(0, 1) == category.letter.toLowerCase(); + }); + } + + /* Function to reset the category selection to default state (all are non-selected) */ + function resetSelected(items) { + for (var i = 0; i < items.length; i++) { + items[i].selected = false; + } + c.isShowAllSelected = false; } - c.isShowAllSelected = false; - } - - /* Function to reset quick search text in the input field */ - function resetQuickSearchText() { - $scope.searchText = ""; - } - - /* Function that accumulates reset of selected category and quick search text */ - function setWidgetState(state, items) { - /* Default state is intended to clear quick search text and reset category selection only */ - if (state == "default") { - resetSelected(items); - resetQuickSearchText(); - - return c.data.msgDefaultState; + + /* Function to reset quick search text in the input field */ + function resetQuickSearchText() { + $scope.searchText = ""; } - /* Default-Selected is intended to reset the category selection state only e.g. for All items category selection */ - if (state == "default-selected") { - resetSelected(items); + /* Function that accumulates reset of selected category and quick search text */ + function setWidgetState(state, items) { + /* Default state is intended to clear quick search text and reset category selection only */ + if (state == "default") { + resetSelected(items); + resetQuickSearchText(); + + return c.data.msgDefaultState; + } + + /* Default-Selected is intended to reset the category selection state only e.g. for All items category selection */ + if (state == "default-selected") { + resetSelected(items); + + return c.data.msgCategoryReset; + } + + /* Initial is intended to bring the widget to the initial state same as after pager reload */ + if (state == "initial") { + resetQuickSearchText(); + resetSelected(items); + c.filteredCatalogItems = c.data.catalogItems; + c.displayItems = []; + c.isShowAllSelected = false; + c.isMultiplePage = false; + + return "Initialization has completed"; + } + } - return c.data.msgCategoryReset; + /* Function to flag multipaging which is used by pagination to display page selector */ + function checkMultiPage(itemsToDisplay, numOfPages) { + return Math.ceil(itemsToDisplay / numOfPages) > 1 ? true : false; } - /* Initial is intended to bring the widget to the initial state same as after pager reload */ - if (state == "initial") { - resetQuickSearchText(); - resetSelected(items); - c.filteredCatalogItems = c.data.catalogItems; - c.displayItems = []; - c.isShowAllSelected = false; - c.isMultiplePage = false; + /* Function to reset the current page to 1 everytime the category changes */ + function resetCurrentPage() { + return 1; + } + + /* Function to prepare the list of items to display based on the selected page */ + function calculateDisplayCatalogItems(filteredItemsArray, currentPage, itemsPerPage) { + return filteredItemsArray.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage); + } - return "Initialization has completed"; + /* Debug - Logs */ + if (c.data.isDebugEnabled) { + console.log(c); } - } - - /* Function to flag multipaging which is used by pagination to display page selector */ - function checkMultiPage(itemsToDisplay, numOfPages) { - return Math.ceil(itemsToDisplay / numOfPages) > 1 ? true : false; - } - - /* Function to reset the current page to 1 everytime the category changes */ - function resetCurrentPage() { - return 1; - } - - /* Function to prepare the list of items to display based on the selected page */ - function calculateDisplayCatalogItems(filteredItemsArray, currentPage, itemsPerPage) { - return filteredItemsArray.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage); - } - - /* Debug - Logs */ - if (c.data.isDebugEnabled) { - console.log(c); - } }; diff --git a/Modern Development/Service Portal Widgets/Catalog Item Explorer/css.scss b/Modern Development/Service Portal Widgets/Catalog Item Explorer/css.scss index a2ec3cce47..39f984658b 100644 --- a/Modern Development/Service Portal Widgets/Catalog Item Explorer/css.scss +++ b/Modern Development/Service Portal Widgets/Catalog Item Explorer/css.scss @@ -3,18 +3,18 @@ justify-content: center; flex-wrap: wrap; width: 100%; - padding: 10px 0; + padding: 1rem 0; margin: 0; } .catalog-category { - font-size: 25px; + font-size: 2.4rem; font-weight: 600; } .category-letter:hover { transform: scale(1.4); - border-radius: 10px; + border-radius: 1rem; cursor: pointer; } @@ -36,12 +36,35 @@ color: #428BCA; } +.list-group-item { + margin:0; + display: flex; + align-items: center; +} + .main-column { + flex: 55%; cursor: pointer; } +.item-type-column { + flex: 25%; + text-align: center; + font-size: 1.2rem; +} + +.external-redirect-cell { + flex: 10%; + text-align: center; +} + +.panels-container { + display: flex; + justify-content: center; +} + .panel-footer, .panel-heading { - height: 40px; + height: 4rem; display: flex; justify-content: space-between; align-items: center; diff --git a/Modern Development/Service Portal Widgets/Catalog Item Explorer/script.js b/Modern Development/Service Portal Widgets/Catalog Item Explorer/script.js index 66b8eb63d9..ba4e2d0768 100644 --- a/Modern Development/Service Portal Widgets/Catalog Item Explorer/script.js +++ b/Modern Development/Service Portal Widgets/Catalog Item Explorer/script.js @@ -22,10 +22,11 @@ /* Get Catalog ID */ var catalogsId = $sp.getParameter("used_catalog") || options.used_catalog; - /* Get all catalog items */ + /* Get all catalog items which are active and not marked hidden on service portal */ var catalogItems = new GlideRecordSecure('sc_cat_item'); catalogItems.addQuery('sc_catalogs', 'IN', catalogsId); catalogItems.addQuery('active', true); + catalogItems.addQuery('hide_sp', false); catalogItems.orderBy('name'); catalogItems.query(); @@ -51,6 +52,7 @@ itemId: catalogItems.getUniqueValue(), name: catalogItems.getValue('name'), description: catalogItems.getValue('short_description'), + type: catalogItems.getDisplayValue('sys_class_name'), externalUrl: extUrl }); } @@ -59,39 +61,37 @@ data.catalogCategories = getUniqueFirstLetters(data.catalogItems); function getUniqueFirstLetters(strings) { - /* Create an empty array to store the first letters */ - var firstLetters = []; + /* Create an object to store unique first letters */ + var firstLettersMap = {}; - /* Iterate over the input array of strings */ - for (var i = 0; i < strings.length; i++) { - /* Get the first letter of the current string */ - var firstLetter = strings[i].name.charAt(0); - var exists = false; + /* Iterate over the input array of strings */ + for (var i = 0; i < strings.length; i++) { + /* Get the first letter of the current string and convert it to uppercase */ + var firstLetter = strings[i].name.charAt(0).toUpperCase(); - /* Check if the letter already exists in the array */ - for (var j = 0; j < firstLetters.length; j++) { - if (firstLetters[j].letter === firstLetter.toUpperCase()) { - exists = true; - break; - } - } + /* Use the letter as a key in the object to ensure uniqueness */ + if (!firstLettersMap[firstLetter]) { + firstLettersMap[firstLetter] = true; + } + } - /* Check if the first letter already exist in the array */ - if (!exists) { - /* If not add it */ - firstLetters.push({ - letter: firstLetter, - selected: false - }); - } - } + /* Convert the object keys to an array of objects */ + var firstLetters = []; + for (var letter in firstLettersMap) { + if (firstLettersMap.hasOwnProperty(letter)) { + firstLetters.push({ + letter: letter, + selected: false + }); + } + } - /* Sort the array of objects, otherwise the simplier version of sort might be used */ - firstLetters.sort(function (a, b) { - return a.letter.localeCompare(b.letter); - }); + /* Sort the array of objects */ + firstLetters.sort(function (a, b) { + return a.letter.localeCompare(b.letter); + }); - /* Return the sorted array of unique first letters */ - return firstLetters; + /* Return the sorted array of unique first letters */ + return firstLetters; } })(); diff --git a/Modern Development/Service Portal Widgets/Catalog Item Explorer/template.html b/Modern Development/Service Portal Widgets/Catalog Item Explorer/template.html index 856787e696..053eeb6e71 100644 --- a/Modern Development/Service Portal Widgets/Catalog Item Explorer/template.html +++ b/Modern Development/Service Portal Widgets/Catalog Item Explorer/template.html @@ -31,22 +31,29 @@
    -
  • -
    -
    - {{item.name}} -
    - -
    - {{item.description}} -
    -
    -
    -
  • -
+
  • +
    +
    + {{item.name}} +
    + +
    + {{item.description}} +
    +
    +
    + +
    + {{item.type}} +
    + +
    + +
    +
  • + -
    +
    @@ -54,5 +61,5 @@
    + {{c.filteredCatalogItems.length}}© 2025 Ivan Betev
    diff --git a/Modern Development/Service Portal Widgets/Change Calendar Report/Body HTML template.html b/Modern Development/Service Portal Widgets/Change Calendar Report/Body HTML template.html new file mode 100644 index 0000000000..5be1019208 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Change Calendar Report/Body HTML template.html @@ -0,0 +1,8 @@ +
    + +

    {{c.title}}

    + +
    + {{::c.initialMessage}} +
    +
    diff --git a/Modern Development/Service Portal Widgets/Change Calendar Report/CSS b/Modern Development/Service Portal Widgets/Change Calendar Report/CSS new file mode 100644 index 0000000000..57bac6813a --- /dev/null +++ b/Modern Development/Service Portal Widgets/Change Calendar Report/CSS @@ -0,0 +1,52 @@ +.report-widget-wrap { + background:#fff; + padding:15px; + margin: 0 0 15px 0; + } + + .report-widget-title { + padding: $sp-space--xl; + font-weight:bold; + margin-top: 0; + margin-bottom: 0; + font-family: $now-sp-font-family-sans-serif; + color: $text-color; + font-size: $font-size-h4; + + } + + .highcharts-container g.highcharts-button *, + .highcharts-container image.hc-image { + transition: fill-opacity 0.3s linear, stroke-opacity 0.3s linear, opacity 0.3s linear; + fill-opacity: 0; + stroke-opacity: 0; + opacity:0; + } + + .highcharts-container:hover g.highcharts-button *, + .highcharts-container:hover image.hc-image { + fill-opacity: 1; + stroke-opacity: 1; + opacity:1; + } + + .highcharts-legend-item span::after, + .highcharts-legend-item::after { + content: "\200E"; + } + + table.wide .pivot_cell, + table.wide .pivot_caption, + table.wide .pivot_caption_dark { + padding: 3px 5px; + } + .highlight-wrap { + display: none; + } + + .fc-week-number { + width: 42px; + background-color: #ededed; + } + + diff --git a/Modern Development/Service Portal Widgets/Change Calendar Report/Client Controller b/Modern Development/Service Portal Widgets/Change Calendar Report/Client Controller new file mode 100644 index 0000000000..811bc18e51 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Change Calendar Report/Client Controller @@ -0,0 +1,101 @@ +function($scope, $uibModal, $timeout, spUtil) { + var c = this; + var reportId = c.options.report_id || ''; + c.rectangleId = c.widget.rectangle_id || c.data.rectangleId; + c.showTitle = (c.options.show_title === true || c.options.show_title === 'true'); + c.title = c.options.title || ''; + + if (c.options.widget_parameters) { + c.initialMessage = c.data.ch.i18n.building; + window.chartHelpers = window.chartHelpers || {}; + $.extend(window.chartHelpers, c.data.ch); + + $timeout(function() { + var targetEl = $("#report-widget-" + c.rectangleId); + embedReportById(targetEl, reportId); + + $timeout(function() { + targetEl.off('click', 'a[href*="change_request.do?sys_id"]'); + + targetEl.on('click', 'a[href*="change_request.do?sys_id"]', function(event) { + event.preventDefault(); + var href = $(this).attr('href') || ''; + var match = href.match(/sys_id=([a-f0-9]{32})/i); + var sysId = match ? match[1] : ''; + + var modalData = { + number: '', + short_description: '', + description: '', + sys_id: sysId + }; + + // Open modal immediately + $uibModal.open({ + controller: function($scope, $uibModalInstance, $sce) { + $scope.data = modalData; + + $scope.getTrustedDescription = function() { + if (!$scope.data.description) return ''; + var text = $scope.data.description; + + // Convert line breaks to
    + text = text.replace(/\n/g, '
    '); + + // Convert URLs to links + var urlRegex = /(\bhttps?:\/\/[^\s<]+)/gi; + text = text.replace(urlRegex, function(url) { + return '' + url + ''; + }); + + // Convert emails to mailto links + var emailRegex = /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6})/gi; + text = text.replace(emailRegex, function(email) { + return '' + email + ''; + }); + + return $sce.trustAsHtml(text); + }; + + $scope.close = function() { + $uibModalInstance.dismiss('cancel'); + }; + }, + resolve: { + $sce: function() { + return angular.injector(['ng']).get('$sce'); + } + }, + template: '' + + '' + + '' + }); + + // Populate modal data via server + spUtil.get(c.widget.sys_id, { + action: 'getChangeDetails', + sys_id: sysId + }).then(function(response) { + if (response.data.changeDetails && !response.data.changeDetails.error) { + modalData.number = response.data.changeDetails.number; + modalData.short_description = response.data.changeDetails.short_description; + modalData.description = response.data.changeDetails.description; + } + }); + }); + }, 1000); + }); + } else { + c.initialMessage = c.data.ch.i18n.selectReport; + } +} diff --git a/Modern Development/Service Portal Widgets/Change Calendar Report/README.md b/Modern Development/Service Portal Widgets/Change Calendar Report/README.md new file mode 100644 index 0000000000..6659f3c44b --- /dev/null +++ b/Modern Development/Service Portal Widgets/Change Calendar Report/README.md @@ -0,0 +1,148 @@ +# Report IT Change Request: Change Calendar Widget + +A **Service Portal widget** for displaying interactive **Change Request Calendar Reports** in ServiceNow. +This widget embeds a ServiceNow report, allows visual exploration of change data, and enhances user experience through color-coded legends and a modal view for detailed record insights. + +--- + +## Features + +* **Report Embedding:** Displays a selected ServiceNow report dynamically in the portal. +* **Interactive Legend:** Color legend automatically updates based on selected highlight field (`risk`, `type`, `state`). +* **Change Request Details Modal:** Clicking a change number opens a modal showing detailed record information (number, description, risk, state, start and end dates). +* **Dynamic Color Mapping:** Fetches `sys_ui_style` and `sys_choice` data to visualize change request status colors. +* **Accessible & Responsive UI:** Fully keyboard-accessible with clear color indicators and responsive design. + +--- + +## Configuration + +### **Widget Options** + +| Option | Type | Description | +| ------------ | ------------------------ | ------------------------------------- | +| `report_id` | Reference (`sys_report`) | Select the ServiceNow report to embed | +| `show_title` | Boolean | Toggle visibility of report title | + +### **Installation Steps** + +1. Import the widget XML into your ServiceNow instance via **Studio** or **Update Set**. +2. Add the widget to a Service Portal page (e.g., Change Dashboard). +3. In widget options: + + * Select your desired **report** (`sys_report`). + * Enable “Show Title” if required. +4. Save and reload the page — the report will render dynamically. + +--- + +## Color Legend + +* Automatically generated from `sys_ui_style` table for elements `type`, `state`, and `risk`. +* Displays color-coded labels for visual clarity. +* Updates automatically when the highlight field dropdown changes. + +**Example:** + +| Element | Color | Meaning | +| ------- | ------------ | ------------------ | +| State | 🔵 Implement | Change in progress | +| Risk | 🟡 High | Requires review | +| Type | 🟢 Normal | Standard change | + +--- + +## Modal Preview of Change Details + +Clicking a change number in the calendar opens a modal window with: + +| Field | Description | +| ------------------- | ---------------------------- | +| Change Number | Linked record reference | +| Short Description | Summary of change | +| Description | Detailed explanation | +| Type / Risk / State | Key metadata fields | +| Planned Start & End | Change implementation window | + +--- + +## Technical Overview + +| Component | Technology | Purpose | +| ----------------- | --------------------------------------- | -------------------------------------------------- | +| **Client Script** | AngularJS + jQuery + `$uibModal` | Event handling, modal logic, legend updates | +| **Server Script** | GlideRecord API | Fetch report and change details securely | +| **CSS** | Custom SCSS / SP Variables | Responsive layout, color blocks, and accessibility | +| **Template** | Angular bindings (`ng-if`, `ng-repeat`) | Dynamic rendering of legend and report | + +--- + +## Security & Performance + +* Uses `spUtil.get()` for secure data retrieval via the widget server script. +* Enforces ACL-based record access (`change_request` table). +* Sanitizes HTML using `$sce.trustAsHtml` for safe modal rendering. +* Optimized DOM operations and `$timeout` to reduce UI latency. + +--- + +## Dependencies + +* **ServiceNow Studio or App Engine Studio** +* **Service Portal Enabled** +* **Change Management Application** (`change_request` table) +* **Performance Analytics & Reporting Plugin** (`com.snc.pa.sp.widget`) + +Optional: + +* **Color Mapping in `sys_ui_style`** +* **Active `sys_report` record** + +--- + +## Example Use Case + +> The Change Manager wants a visual, color-coded view of all scheduled changes for the month. +> Using the **Report ITS Change Request** widget, they embed their “Change Calendar by Risk” report into the Service Portal. +> They can quickly filter changes, view color-coded statuses, and open detailed records—all from one place. + +--- + +## Testing Scenarios + +| Test | Expected Result | +| -------------------------- | ----------------------------------------------------- | +| Load widget without report | Displays “Select a report in widget options!” message | +| Click on change link | Modal opens with record details | +| Change highlight dropdown | Legend updates to reflect new color group | +| No matching record | Displays “Record not found” in modal | + +--- + +## Future Enhancements + +* Filter changes by assignment group or service. +* Add “Export as PDF” or “Add to Calendar” options. +* Integrate with CAB meeting module for review visualization. +* Replace jQuery with native AngularJS `$element` bindings for performance. + +--- + +## Contributors + +* **Developer:** Admin / ServiceNow Platform Engineer +* **Maintainers:** Performance Analytics & Reporting Widget Team +* **Scope:** Global (`x_snc_pa.sp.widget`) + +--- +Please find the screenshot below + +![WhatsApp Image 2025-10-26 at 09 59 27 (1)](https://github.com/user-attachments/assets/a2e024cf-87be-4f29-9c5a-aee3e2dffbfd) + +![WhatsApp Image 2025-10-26 at 09 59 27 (2)](https://github.com/user-attachments/assets/bd610e94-08ac-47be-842d-e8c59dadce70) + +![WhatsApp Image 2025-10-26 at 09 59 27](https://github.com/user-attachments/assets/1333e974-6b56-48b8-b1c2-340e0a35e0af) + +image + + diff --git a/Modern Development/Service Portal Widgets/Change Calendar Report/Server Side Script b/Modern Development/Service Portal Widgets/Change Calendar Report/Server Side Script new file mode 100644 index 0000000000..b79847115a --- /dev/null +++ b/Modern Development/Service Portal Widgets/Change Calendar Report/Server Side Script @@ -0,0 +1,130 @@ +(function() { + options.report_id = options.report_id || ''; + + if (options.report_id !== '') { + var reportGr = new GlideRecord('sys_report'); + reportGr.get(options.report_id); + if (reportGr.canRead()) + options.title = reportGr.getDisplayValue('title'); + } + + var chartHelpers = chartHelpers || {}; + chartHelpers.i18n = chartHelpers.i18n || {}; + + chartHelpers.i18n.selectReport = gs.getMessage('Select a report in widget options!'); + chartHelpers.i18n.building = gs.getMessage('Building chart, please wait...'); + chartHelpers.i18n.total = gs.getMessage('Total'); + chartHelpers.i18n.maxCells = gs.getMessage('The size of the pivot table is too big. Use filters to reduce it or switch to a modern browser.'); + chartHelpers.i18n.chartGenerationError = gs.getMessage('An error occurred while generating chart. Please try again later.'); + + chartHelpers.i18n.showAsHeatmap = gs.getMessage('Show data as a heatmap visualization'); + chartHelpers.i18n.showAsMarkers = gs.getMessage('Show data using latitude and longitude'); + chartHelpers.i18n.saveAsJpg = gs.getMessage('Save as JPEG'); + chartHelpers.i18n.saveAsPng = gs.getMessage('Save as PNG'); + chartHelpers.i18n.highlightBasedOn = gs.getMessage('Highlight based on:'); + chartHelpers.i18n.isRTL = GlideI18NStyle().getDirection().equals('rtl'); + chartHelpers.i18n.weekNumberTitle = gs.getMessage('Week'); + chartHelpers.i18n.weekNumberTitleShort = gs.getMessage('Week'); + chartHelpers.i18n.seeMoreEvents = gs.getMessage('See {0} more events'); + chartHelpers.i18n.viewEventsInList = gs.getMessage('View {0} events in a list'); + chartHelpers.i18n.viewAllEventsInList = gs.getMessage('View all events in a list'); + chartHelpers.i18n.viewAllRecords = gs.getMessage('View all records'); + chartHelpers.i18n.none = gs.getMessage('None'); + chartHelpers.i18n.plusMany = gs.getMessage('+ many'); + chartHelpers.i18n.plusMore = gs.getMessage('+ {0} more'); + chartHelpers.i18n.buttonText = { + prevYear: "", + nextYear: "", + today: gs.getMessage('today'), + year: gs.getMessage('year'), + month: gs.getMessage('month'), + week: gs.getMessage('week'), + day: gs.getMessage('day') + }; + chartHelpers.i18n.allDayHtml = gs.getMessage('all-day'); + chartHelpers.i18n.daysNames = [ + gs.getMessage('Sunday'), + gs.getMessage('Monday'), + gs.getMessage('Tuesday'), + gs.getMessage('Wednesday'), + gs.getMessage('Thursday'), + gs.getMessage('Friday'), + gs.getMessage('Saturday') + ]; + chartHelpers.i18n.dayNamesShort = [ + gs.getMessage('Sun'), + gs.getMessage('Mon'), + gs.getMessage('Tue'), + gs.getMessage('Wed'), + gs.getMessage('Thu'), + gs.getMessage('Fri'), + gs.getMessage('Sat') + ]; + chartHelpers.i18n.monthNames = [ + gs.getMessage('January'), + gs.getMessage('February'), + gs.getMessage('March'), + gs.getMessage('April'), + gs.getMessage('May'), + gs.getMessage('June'), + gs.getMessage('July'), + gs.getMessage('August'), + gs.getMessage('September'), + gs.getMessage('October'), + gs.getMessage('November'), + gs.getMessage('December') + ]; + chartHelpers.i18n.monthNamesShort = [ + gs.getMessage('Jan'), + gs.getMessage('Feb'), + gs.getMessage('Mar'), + gs.getMessage('Apr'), + gs.getMessage('May'), + gs.getMessage('Jun'), + gs.getMessage('Jul'), + gs.getMessage('Aug'), + gs.getMessage('Sep'), + gs.getMessage('Oct'), + gs.getMessage('Nov'), + gs.getMessage('Dec') + ]; + chartHelpers.i18n.none = gs.getMessage('-- None --'); + chartHelpers.i18n.groupBy = gs.getMessage('Group by'); + chartHelpers.i18n.groupByTitle = gs.getMessage('Select a different group by field'); + chartHelpers.i18n.stackBy = gs.getMessage('Stacked by'); + chartHelpers.i18n.stackByTitle = gs.getMessage('Select a different stacked by field'); + chartHelpers.device = {}; + chartHelpers.device.type = GlideMobileExtensions.getDeviceType(); + + chartHelpers.systemParams = { + firstDay: (gs.getProperty("glide.ui.date_format.first_day_of_week", 2) - 1) % 7, + defaultDate: SNC.ReportUtil.getNowTimeInUSFormat(), + maxEventsDisplayedPerCell: gs.getProperty("glide.report.calendar.max_events_displayed_per_cell", 3), + maxMoreEventsPerDay: gs.getProperty("glide.report.calendar.max_more_events_per_day", 30), + defaultEventDuration: gs.getProperty("glide.report.calendar.default_event_duration", "01:00:00"), + maxDaysBack: gs.getProperty("glide.report.calendar.max_days_back", 30), + enablePreviewOnHover: gs.getProperty("glide.report.calendar.enable_preview_on_hover", true) + }; + + data.rectangleId = gs.generateGUID(); + data.ch = chartHelpers; + +//Passing Change Details to Client Controller to show data in the modal on Click +// From here + if (input && input.action === 'getChangeDetails' && input.sys_id) { + var gr = new GlideRecord('change_request'); + if (gr.get(input.sys_id)) { + data.changeDetails = { + number: gr.getValue('number'), + short_description: gr.getValue('short_description') || 'No short description', + description:gr.getValue('description') ||'No description' + }; + } else { + data.changeDetails = { + error: 'Record not found' + }; + } + } +// Till here + +})(); diff --git a/Modern Development/Service Portal Widgets/Custom attachment variable/README.md b/Modern Development/Service Portal Widgets/Custom attachment variable/README.md new file mode 100644 index 0000000000..726d5e9c36 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Custom attachment variable/README.md @@ -0,0 +1,12 @@ +# Attachment variable widget + +This widget lets you use the oob attachment picker from the oob catalog item widget as a custom type variable giving you more control over attachment behaviour, you can for example use ui policies to hide and show the attachment picker. + +## Usage example + +Create new widget [sp_widget] +Copy template.html in the Body HTML template field +Copy controller.js in the Client controller field +Add custom type variable using newly created widget positioned as last variable +Set hide attachment for catalog item as true +Open cat item in portal and adjust widget as needed diff --git a/Modern Development/Service Portal Widgets/Custom attachment variable/controller.js b/Modern Development/Service Portal Widgets/Custom attachment variable/controller.js new file mode 100644 index 0000000000..ce5871e7b9 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Custom attachment variable/controller.js @@ -0,0 +1,78 @@ +api.controller = function ($scope, nowAttachmentHandler, spUtil, spAttachmentUpload, $timeout, cabrillo, spModal) { + var c = this; + c.isNative = cabrillo.isNative() + /* + * change table and guid if you want to attach to some other record + */ + $scope.table = $scope.page.g_form.recordTableName; + $scope.guid = $scope.$parent.c.getAttachmentGuid(); + // + $scope.data.maxAttachmentSize = 24; + var ah = $scope.attachmentHandler = new nowAttachmentHandler(setAttachments, appendError); + ah.setParams($scope.table, $scope.guid, 1024 * 1024 * $scope.data.maxAttachmentSize); + + // implement logic to show drag and drop picker or clip icon with text + $scope.showDragAndDrop = function () { + if (true) + return true; + else + return false; + } + /* + * callback function called after attachment action happens + * e.g. implement mandatory attachment + */ + function setAttachments(attachments, action) { + if (!angular.equals($scope.attachments, attachments)) + $scope.attachments = attachments; + if (action === "added") { + // custom attachment added logic + } + if (action === "renamed") { + // custom attachment renamed logic + } + if (action === "deleted") { + // custom attachment deleted logic + } + spUtil.get($scope, { + action: "from_attachment" + }); + } + /* + * callback function called on error + */ + function appendError(error) { + spUtil.addErrorMessage(error.msg + error.fileName); + } + + // drag & drop handler + $scope.dropFiles = function (files) { + if (files && files.length > 0) { + $scope.attachmentUploadInProgress = true; + $scope.totalFilesBeingUploaded++; + spAttachmentUpload.uploadAttachments($scope.attachmentHandler, files); + } + $timeout(function () { + if ($scope.attachmentUploadInProgress != false) + spUtil.addInfoMessage("The attachment upload is in progress. Note that some actions are deactivated during the file upload process"); + }, 2000); + $scope.$on('attachment.upload.idle', function () { + $scope.attachmentUploadInProgress = false; + $scope.totalFilesBeingUploaded = 0; + }); + }; + //confirm delete dialog + $scope.confirmDeleteAttachment = function (attachment) { + if (c.isNative) { + if (confirm("delete attachment?")) { + $scope.data.attachment_action_in_progress = true; + $scope.attachmentHandler.deleteAttachment(attachment); + } + } else { + spModal.confirm("delete attachment?").then(function () { + $scope.data.attachment_action_in_progress = true; + $scope.attachmentHandler.deleteAttachment(attachment); + }); + } + } +}; diff --git a/Modern Development/Service Portal Widgets/Custom attachment variable/template.html b/Modern Development/Service Portal Widgets/Custom attachment variable/template.html new file mode 100644 index 0000000000..cb042ede35 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Custom attachment variable/template.html @@ -0,0 +1,31 @@ +
    +
    +
    + + + + +
    +
    + + + ${Uploading attachments} +
    +
    +
    +
    +
    +
    + +
    diff --git a/Modern Development/Service Portal Widgets/Dynamic Table and Record Selector/Client Side.js b/Modern Development/Service Portal Widgets/Dynamic Table and Record Selector/Client Side.js new file mode 100644 index 0000000000..24828cef73 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Dynamic Table and Record Selector/Client Side.js @@ -0,0 +1,94 @@ +api.controller=function($scope, $http, spUtil, $rootScope, $timeout, spModal) { + /* widget controller */ + var c = this; + + // Initialize scope variables + $scope.tablename = { + displayValue: '', + value: '', + name: 'tablename' + }; + $scope.record = { + displayValue: '', + value: '', + name: 'record' + }; + $scope.selectedTable = ''; + $scope.TableSysId = ''; + + // Handle field changes (table/record) + $scope.$on("field.change", function(evt, parms) { + if (parms.field.name === 'tablename') { + // Get sys_id of selected table → fetch actual table name & label + var sysId = parms.newValue; + var url = '/api/now/table/sys_db_object/' + sysId + '?sysparm_fields=name,label'; + $http.get(url).then(function(res) { + if (res.data.result) { + $scope.selectedTable = res.data.result.name; + $scope.selectedTableLabel = res.data.result.label; + c.getDisplayField($scope.selectedTable, sysId); // fetch display field + } + }); + } else if (parms.field.name === 'record') { + // Save selected record sys_id + $scope.TableSysId = parms.newValue; + } + }); + + // Get display field for a table (recursive if needed) + c.getDisplayField = function(tableName, tablesysId) { + var url = '/api/now/table/sys_dictionary' + + '?sysparm_query=name=' + tableName + '^display=true' + + '&sysparm_fields=element' + + '&sysparm_limit=1'; + + $http.get(url).then(function(response) { + if (response.data.result && response.data.result.length > 0) { + // Found display field + $scope.recorddisplayValue = response.data.result[0].element; + } else { + // Check parent table + var parentsysIdUrl = '/api/now/table/sys_db_object/' + tablesysId + '?sysparm_fields=super_class'; + $http.get(parentsysIdUrl).then(function(parentRes) { + var parentTable = parentRes.data.result.super_class.value; + + if (!parentTable) { + // No parent - fallback checks + var nameCheckUrl = '/api/now/table/sys_dictionary' + + '?sysparm_query=name=' + tableName + '^element=name' + + '&sysparm_fields=element&sysparm_limit=1'; + + $http.get(nameCheckUrl).then(function(nameRes) { + if (nameRes.status == 200) { + $scope.recorddisplayValue = 'name'; + } else { + var numberCheckUrl = '/api/now/table/sys_dictionary' + + '?sysparm_query=name=' + tableName + '^element=number' + + '&sysparm_fields=element&sysparm_limit=1'; + + $http.get(numberCheckUrl).then(function(numberRes) { + if (numberRes.status == 200) { + $scope.recorddisplayValue = 'number'; + } else { + $scope.recorddisplayValue = 'sys_id'; // Final fallback + } + }); + } + }); + + } else { + // Parent exists - recurse + var parentNameUrl = '/api/now/table/sys_db_object/' + parentTable + '?sysparm_fields=name'; + $http.get(parentNameUrl).then(function(parentResname) { + var parentTableName = parentResname.data.result.name; + c.getDisplayField(parentTableName, parentTable); // recursive lookup + }); + } + }); + } + }, function(error) { + spModal.alert("Error fetching display field: " + error); + }); + }; + +}; diff --git a/Modern Development/Service Portal Widgets/Dynamic Table and Record Selector/HTML.html b/Modern Development/Service Portal Widgets/Dynamic Table and Record Selector/HTML.html new file mode 100644 index 0000000000..65ec867a99 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Dynamic Table and Record Selector/HTML.html @@ -0,0 +1,26 @@ +
    +
    + + + +
    + + +
    + + + +
    +
    diff --git a/Modern Development/Service Portal Widgets/Dynamic Table and Record Selector/README.md b/Modern Development/Service Portal Widgets/Dynamic Table and Record Selector/README.md new file mode 100644 index 0000000000..7aaeb51c75 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Dynamic Table and Record Selector/README.md @@ -0,0 +1,78 @@ +Widget Name: Dynamic Table and Record Selector + +Overview: +This ServiceNow Service Portal widget allows users to dynamically select any table and then choose a record from that table. The widget automatically determines which field should be shown as the display field for the selected table. It also handles parent table inheritance and provides fallback options for display fields. + +Main Features: + +Lists all tables from the sys_db_object table. + +Automatically finds the correct display field (field with display=true). + +Supports parent table lookup if the child table does not have a display field. + +Provides fallback checks for fields named "name", "number", or defaults to "sys_id". + +Uses ServiceNow REST APIs to fetch metadata and record data dynamically. + +Works with the standard sn-record-picker directive in Service Portal. + +How It Works: + +The first record picker displays all tables from sys_db_object using the label field. + +When the user selects a table, the widget fetches the actual table name and label using the sys_id. + +The controller calls the getDisplayField function to determine which field should be displayed in the record picker. + +It checks sys_dictionary for a field with display=true. + +If found, that field is used as the display field. + +If not found, it checks if the table has a parent (super_class). + +If a parent exists, it recursively checks the parent table. + +If there is no parent, it uses fallback checks for "name", then "number", and finally "sys_id". + +The second record picker then displays the records from the selected table using the determined display field. + +When the user selects a record, its sys_id is stored in the variable TableSysId. + +Example Flow: + +Select “Incident” from the table picker. + +The widget detects that the display field is “number”. + +The record picker lists incident numbers. + +When a record is selected, its sys_id is saved for further use. + +Technologies Used: + +ServiceNow Service Portal + +AngularJS (spUtil, spModal) + +ServiceNow REST API: + +/api/now/table/sys_db_object + +/api/now/table/sys_dictionary + +Use Cases: + +Creating dynamic reference selectors for any table. + +Building tools that link or map data between tables. + +CMDB record selection where tables may have inheritance. + +Generic admin utilities or catalog forms needing flexible input. + +File Components: + +HTML Template: Contains two sn-record-picker elements for selecting table and record. + +Client Controller (JS): Handles field change events, fetches table metadata, determines display fields, and manages recursion logic. diff --git a/Modern Development/Service Portal Widgets/Emoji Replacer Widget/CSS-SCSS.css b/Modern Development/Service Portal Widgets/Emoji Replacer Widget/CSS-SCSS.css new file mode 100644 index 0000000000..e5ffa58d62 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Emoji Replacer Widget/CSS-SCSS.css @@ -0,0 +1,9 @@ +.card{ + max-width:600px; + margin: auto; + box-shadow: 0 2px 6px rgba(0,0,0,0.1); + border-radius: 12px; +} +textarea{ + resize: none; +} diff --git a/Modern Development/Service Portal Widgets/Emoji Replacer Widget/Client Script.js b/Modern Development/Service Portal Widgets/Emoji Replacer Widget/Client Script.js new file mode 100644 index 0000000000..19e59def9f --- /dev/null +++ b/Modern Development/Service Portal Widgets/Emoji Replacer Widget/Client Script.js @@ -0,0 +1,25 @@ +api.controller=function($scope) { + /* widget controller */ + var c = this; + + c.emojiMap ={ + ':smile:' :'😊', + ':sad:':'😓', + ":heart:":'❤️', + ":thumbsup:":'👍', + ":laugh:":"😀", + ":wink:":"😉", + ":clap:":"👏", + ":party:" :"🥳" + }; + + c.replaceEmojis = function(){ + var text = $scope.data.inputText || ''; + + for(var key in c.emojiMap){ + var regex = new RegExp(key.replace(/([.*+?^${}()|\[\]\/\\])/g,"\\$1"),'g'); + text = text.replace(regex,c.emojiMap[key]); + } + c.outputText= text; + } +}; diff --git a/Modern Development/Service Portal Widgets/Emoji Replacer Widget/HTML.html b/Modern Development/Service Portal Widgets/Emoji Replacer Widget/HTML.html new file mode 100644 index 0000000000..009621b664 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Emoji Replacer Widget/HTML.html @@ -0,0 +1,18 @@ +
    +

    😊 + Emoji Replacer +

    +

    + Type something using emoji shortcuts like :smile:,:heart:, or:thumbsup: +

    + +
    +
    + Output Preview: +
    +
    + +
    +
    +
    diff --git a/Modern Development/Service Portal Widgets/Emoji Replacer Widget/README.md b/Modern Development/Service Portal Widgets/Emoji Replacer Widget/README.md new file mode 100644 index 0000000000..2d23d33050 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Emoji Replacer Widget/README.md @@ -0,0 +1,22 @@ +## Emoji Replacer Widget + +This widget enhances the user experience by automatically converting emojis code into visual emojis while typing - adding personality and clarity to text communication. +## How It works +- User types in a text box: +- "Great job team!:tada::thumbsup:" +- Script will detects matching emoji code using regex. +- The widget replaces them with real emojis: +- "Great job team!🎉👍 +## Available Emoji in Widget + ":smile:" :😊, + ":sad:":😓, + ":heart:":❤️, + ":thumbsup:":👍, + ":laugh:":😀, + ":wink:":😉, + ":clap:":👏, + ":party:":🥳, + ":tada:":🎉 +## Output + +![Emoji Output](emoji.png) diff --git a/Modern Development/Service Portal Widgets/Emoji Replacer Widget/emoji.png b/Modern Development/Service Portal Widgets/Emoji Replacer Widget/emoji.png new file mode 100644 index 0000000000..71a4446a92 Binary files /dev/null and b/Modern Development/Service Portal Widgets/Emoji Replacer Widget/emoji.png differ diff --git a/Modern Development/Service Portal Widgets/Fill survey or item from url/README.md b/Modern Development/Service Portal Widgets/Fill survey or item from url/README.md new file mode 100644 index 0000000000..f31b19f53d --- /dev/null +++ b/Modern Development/Service Portal Widgets/Fill survey or item from url/README.md @@ -0,0 +1,46 @@ +# Auto-fill form from base64encoded data + +This widget automatically populates a form on service portal using base64-encoded JSON passed via a URL parameter. Use the variable name as key or for surveys use the sys_id of the [asmt_assessment_instance_question] + +## How to + +1. Create a new widget and add the server script and client controller from the *Survey or form filler widget.js* file +2. Add the widget for example as a catalog item variable or add it anywhere on your survey taking page such as *take_survey* +3. Encode json of form fields and values key-value pairs and add it as a parameter called data into your url +4. Navigate to page and form autofills + +## To note +For catalog items the variable name is the key, for surveys the key to use is the sys_id of the [asmt_assessment_instance_question] + + +## Example usage + +```javascript +var obj = { + is_this_a_replacement_for_a_lost_or_broken_iphone: "yes", + what_was_the_original_phone_number: "1234567980", + monthly_data_allowance: "Unlimited", + color: "red", + storage: "256", +} + +gs.info(GlideStringUtil.base64Encode(JSON.stringify(obj))) +/* +*** Script: eyJpc190aGlzX2FfcmVwbGFjZW1lbnRfZm9yX2FfbG9zdF9vcl9icm9rZW5faXBob25lIjoieWVzIiwid2hhdF93YXNfdGhlX29yaWdpbmFsX3Bob25lX251bWJlciI6IjEyMzQ1Njc5ODAiLCJtb250aGx5X2RhdGFfYWxsb3dhbmNlIjoiVW5saW1pdGVkIiwiY29sb3IiOiJyZWQiLCJzdG9yYWdlIjoiMjU2In0= +--> +https://{instancename}.service-now.com/esc?id=sc_cat_item&sys_id=ec80c13297968d1021983d1e6253af32&data=eyJpc190aGlzX2FfcmVwbGFjZW1lbnRfZm9yX2FfbG9zdF9vcl9icm9rZW5faXBob25lIjoieWVzIiwid2hhdF93YXNfdGhlX29yaWdpbmFsX3Bob25lX251bWJlciI6IjEyMzQ1Njc5ODAiLCJtb250aGx5X2RhdGFfYWxsb3dhbmNlIjoiVW5saW1pdGVkIiwiY29sb3IiOiJyZWQiLCJzdG9yYWdlIjoiMjU2In0%3D +*/ +var arr = { + "b3bf8ec283683210557ff0d6feaad327": 1, + "bbbf8ec283683210557ff0d6feaad326": 2, + "b7bf8ec283683210557ff0d6feaad327": 3, + "bfbf8ec283683210557ff0d6feaad326": 4, + "fbbf8ec283683210557ff0d6feaad325": "it is good" +} +gs.print(GlideStringUtil.base64Encode(JSON.stringify(arr))) +/* +*** Script: eyJiM2JmOGVjMjgzNjgzMjEwNTU3ZmYwZDZmZWFhZDMyNyI6MSwiYmJiZjhlYzI4MzY4MzIxMDU1N2ZmMGQ2ZmVhYWQzMjYiOjIsImI3YmY4ZWMyODM2ODMyMTA1NTdmZjBkNmZlYWFkMzI3IjozLCJiZmJmOGVjMjgzNjgzMjEwNTU3ZmYwZDZmZWFhZDMyNiI6NCwiZmJiZjhlYzI4MzY4MzIxMDU1N2ZmMGQ2ZmVhYWQzMjUiOiJpdCBpcyBnb29kIn0= +--> +https://{instancename}.service-now.com/esc?id=take_survey&type_id=cf6e97d35d371200964f58e4abb23f18&data=eyJiM2JmOGVjMjgzNjgzMjEwNTU3ZmYwZDZmZWFhZDMyNyI6MSwiYmJiZjhlYzI4MzY4MzIxMDU1N2ZmMGQ2ZmVhYWQzMjYiOjIsImI3YmY4ZWMyODM2ODMyMTA1NTdmZjBkNmZlYWFkMzI3IjozLCJiZmJmOGVjMjgzNjgzMjEwNTU3ZmYwZDZmZWFhZDMyNiI6NCwiZmJiZjhlYzI4MzY4MzIxMDU1N2ZmMGQ2ZmVhYWQzMjUiOiJpdCBpcyBnb29kIn0%3D +*/ +``` diff --git a/Modern Development/Service Portal Widgets/Fill survey or item from url/Survey or form filler widget.js b/Modern Development/Service Portal Widgets/Fill survey or item from url/Survey or form filler widget.js new file mode 100644 index 0000000000..326d0b0ef3 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Fill survey or item from url/Survey or form filler widget.js @@ -0,0 +1,41 @@ +//Server script: +(function () { + //get the data parameter with base64encoded json with field names for keys and matching values + data.encodedData = $sp.getParameter("data"); + try { + //turn the data back into a js object + data.decodedData = JSON.parse(GlideStringUtil.base64Decode(data.encodedData)); + } catch (e) { + //bad json; do nothing + return; + } +})(); + + +//Client controller: +api.controller = function ($scope, $rootScope) { + + var c = this, + g_form = $scope.page.g_form, + answerMap = c.data.decodedData; + // return if the answermap is not a valid object + if (!answerMap instanceof Object) + return; + // if we have g_form set values from the data parameter to form + if (g_form) { + fillAnswers(); + } + //loops through the object with field name keys and corresponding values and sets values + function fillAnswers() { + for (key in answerMap) { + if (!answerMap.hasOwnProperty(key) && !g_form.hasField(key)) + continue; + g_form.setValue(key, answerMap[key]); + } + } + //get gform from the spmodel + $rootScope.$on('spModel.gForm.initialized', function (e, gFormInstance) { + g_form = gFormInstance; + fillAnswers(); + }); +}; diff --git a/Modern Development/Service Portal Widgets/HR Task Progress Bar/CSS.js b/Modern Development/Service Portal Widgets/HR Task Progress Bar/CSS.js new file mode 100644 index 0000000000..0a5abade37 --- /dev/null +++ b/Modern Development/Service Portal Widgets/HR Task Progress Bar/CSS.js @@ -0,0 +1,16 @@ +/*Parent container using flex to adjust width automatically*/ +.parent { + display: flex; + justify-content: space-evenly; + background: cornflowerblue; +} +/*Text (HR task) will be shown in Red colo and green background*/ +.child{ + color:#FF0000; + width:100%; + background: lightgreen; +} +/*single color when task is not in WIP*/ +.child_1{ + width:100%; +} diff --git a/Modern Development/Service Portal Widgets/HR Task Progress Bar/HTML.js b/Modern Development/Service Portal Widgets/HR Task Progress Bar/HTML.js new file mode 100644 index 0000000000..5431ccfe5f --- /dev/null +++ b/Modern Development/Service Portal Widgets/HR Task Progress Bar/HTML.js @@ -0,0 +1,6 @@ + +
    +
    + {{tasks.number}} +
    +
    diff --git a/Modern Development/Service Portal Widgets/HR Task Progress Bar/README.md b/Modern Development/Service Portal Widgets/HR Task Progress Bar/README.md new file mode 100644 index 0000000000..2f01b73095 --- /dev/null +++ b/Modern Development/Service Portal Widgets/HR Task Progress Bar/README.md @@ -0,0 +1,12 @@ +**Steps to add widget to page** +1. Open "hrm_ticket_page" portal page. +2. Create a widget with HTML, CSS, Client, Server code as per this document. +3. Add the widget to top of "hrm_ticket_page" page. + +**Output** +1. If the HR case has associated tasks, those tasks will be shown as progress bar. +2. WIP tasks will be shown with green background and red text. +3. All other state tasks will be shown in black text and blue background. + + +image diff --git a/Modern Development/Service Portal Widgets/HR Task Progress Bar/Server.js b/Modern Development/Service Portal Widgets/HR Task Progress Bar/Server.js new file mode 100644 index 0000000000..84c82854fc --- /dev/null +++ b/Modern Development/Service Portal Widgets/HR Task Progress Bar/Server.js @@ -0,0 +1,15 @@ +(function() { + data.state = ''; + data.taskArr = []; // array to return HR task fields + var recordId = $sp.getParameter('sys_id'); // get sys_id of HR case from URL + var getTask = new GlideRecord('sn_hr_core_task'); + getTask.addEncodedQuery('parent=' + recordId); // encoded Query to get all task related to HR case + getTask.query(); + while (getTask.next()) { + var obj = {}; // object to store HR task values as JSON + obj.number = getTask.getValue('number'); // add HR task number + obj.state = getTask.getValue('state'); // add HR task state + obj.sys_id = getTask.getValue('sys_id'); // add HR task sys_id + data.taskArr.push(obj); + } +})(); diff --git a/Modern Development/Service Portal Widgets/HR Task Progress Bar/client_script.js b/Modern Development/Service Portal Widgets/HR Task Progress Bar/client_script.js new file mode 100644 index 0000000000..2d4e54a21f --- /dev/null +++ b/Modern Development/Service Portal Widgets/HR Task Progress Bar/client_script.js @@ -0,0 +1,10 @@ +api.controller = function(spUtil, $scope) { + /* widget controller */ + var c = this; + // record watcher to show changes on progress bar dynamically + spUtil.recordWatch($scope, "sn_hr_core_task", "active=true", function(name) { + c.data.state = name.data.record.state; + c.server.update(); + + }); +}; diff --git a/Modern Development/Service Portal Widgets/Image icon Menu/Image Menu List.png b/Modern Development/Service Portal Widgets/Image icon Menu/Image Menu List.png new file mode 100644 index 0000000000..74d8d3de54 Binary files /dev/null and b/Modern Development/Service Portal Widgets/Image icon Menu/Image Menu List.png differ diff --git a/Modern Development/Service Portal Widgets/Image icon Menu/Page content.png b/Modern Development/Service Portal Widgets/Image icon Menu/Page content.png new file mode 100644 index 0000000000..32b27ae2f8 Binary files /dev/null and b/Modern Development/Service Portal Widgets/Image icon Menu/Page content.png differ diff --git a/Modern Development/Service Portal Widgets/Image icon Menu/README.md b/Modern Development/Service Portal Widgets/Image icon Menu/README.md new file mode 100644 index 0000000000..5d4723f753 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Image icon Menu/README.md @@ -0,0 +1,36 @@ +# contribution + +Developed a custom Service Portal widget named "Image Icon Menu" to display Quick Links. +This widget features icon-based navigation for key actions such as "Rewards & Recognition" and "Knowledge Article Creation". + + +Function: +----------- + +The widget displays a customizable image-based icon menu with links to various resources like "Rewards & Recognition" or "Knowledge Article Creation." Menu items are dynamically populated based on configuration. + +Widget Structure: +-------------------- + +The widget is composed of the following components: + +HTML template for structure - Dynamically renders up to 2 menu items, each showing: An image (img), A title (span), A hyperlink which Opens the target link. + +CSS for styling - Applies flexbox to align icons and titles, Uses brand color (#01b8f2) and bold font for titles. + +Client-side script for interactive behavior + +Server-side script for backend data handling.Populates data object with: + +Header image and title. + +Titles, images, URLs, and targets for up to 2 menu items, based on widget options. + + + + + + + + + diff --git a/Modern Development/Service Portal Widgets/Image icon Menu/Server.js b/Modern Development/Service Portal Widgets/Image icon Menu/Server.js new file mode 100644 index 0000000000..e6636fcda1 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Image icon Menu/Server.js @@ -0,0 +1,42 @@ +//Serverside code for Menu Item - + + + +(function() { + + + var userID = gs.getUserID(); //Logged in user + var gr = new GlideRecord('sn_hr_core_profile'); + gr.addQuery('user', userID); + gr.query(); + + if (gr.next()) { + data.saltype = gr.getValue('employee_class').toString(); + console.log(data.saltype); + } + + // Widget Header Stuff + + data.imgImage = options.header_image; + data.imgTitle = options.header_title; + + // Menu Item 1 + + data.item_1_TITLE = options.item_1_title; + data.item_1_IMG = options.item_1_img; + data.item_1_URL = options.item_1_url; + data.item_1_TARGET = options.item_1_target; + + + // Menu Item 2 + + data.item_2_TITLE = options.item_2_title; + data.item_2_IMG = options.item_2_img; + data.item_2_URL = options.item_2_url; + data.item_2_TARGET = options.item_2_target; + + + +})(); + + diff --git a/Modern Development/Service Portal Widgets/Image icon Menu/client.js b/Modern Development/Service Portal Widgets/Image icon Menu/client.js new file mode 100644 index 0000000000..916047aeed --- /dev/null +++ b/Modern Development/Service Portal Widgets/Image icon Menu/client.js @@ -0,0 +1,4 @@ +api.controller=function() { + /* widget controller */ + var c = this; +}; diff --git a/Modern Development/Service Portal Widgets/Image icon Menu/iconWidgetinstance.js b/Modern Development/Service Portal Widgets/Image icon Menu/iconWidgetinstance.js new file mode 100644 index 0000000000..eb1f9292c5 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Image icon Menu/iconWidgetinstance.js @@ -0,0 +1,53 @@ +{ +      "header_image": { +            "value": "image_icon.png", +            "displayValue": "image_icon.png" +      }, +      "header_title": { +            "value": "Quick Links", +            "displayValue": "Quick Links" +      }, +      +      +      "item_1_title": { +            "value": "Request Knowledge Articles", +            "displayValue": "Request Knowledge Articles" +      }, +      "item_1_img": { +            "value": "request_ka_icon.png", +            "displayValue": "request_ka_icon.png" +      }, +      "item_1_url": { +            "value": "?id=sc_cat_item&sys_id=657498bf1b00c5d0fd4899f4bd4bcb1e", +            "displayValue": "?id=sc_cat_item&sys_id=657498bf1b00c5d0fd4899f4bd4bcb1e" +      }, +      +       +      "item_1_target": { +            "value": "_blank", +            "displayValue": "_blank" +      }, +      "item_2_target": { +            "value": "_blank", +            "displayValue": "_blank" +      }, +      +     + + "item_2_title": { +            "value": "Rewards & Recognitions", +            "displayValue": "Rewards & Recognitions" +      }, +      "item_2_img": { +            "value": "achv_icon.png", +            "displayValue": "achv_icon.png" +      }, +      "item_2_url": { +            "value": "https://xyz.achievers.com/recent_activity", +            "displayValue": "https://xyz.achievers.com/recent_activity" + + }, + + + +} diff --git a/Modern Development/Service Portal Widgets/Image icon Menu/iconmenu.css b/Modern Development/Service Portal Widgets/Image icon Menu/iconmenu.css new file mode 100644 index 0000000000..97db23f1ba --- /dev/null +++ b/Modern Development/Service Portal Widgets/Image icon Menu/iconmenu.css @@ -0,0 +1,20 @@ +.header-image, .image-icon-item { + width: 56px; + height: 56px; + object-fit: contain; + margin-right: 1rem; +} + +.header-title { + font-size: 20px; +} + +a.list-group-item { + display: flex; + align-items: center; +} + +.image-icon-title { + color: #01b8f2; + font-weight: bold; +} diff --git a/Modern Development/Service Portal Widgets/Image icon Menu/iconmenu.html b/Modern Development/Service Portal Widgets/Image icon Menu/iconmenu.html new file mode 100644 index 0000000000..d5dd3a47c7 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Image icon Menu/iconmenu.html @@ -0,0 +1,25 @@ +
    +
    +

    + + + + {{data.imgTitle || "Image Icon Menu"}} +

    +
    + +
    + diff --git a/Modern Development/Service Portal Widgets/Image icon Menu/image4.png b/Modern Development/Service Portal Widgets/Image icon Menu/image4.png new file mode 100644 index 0000000000..aa81d120df Binary files /dev/null and b/Modern Development/Service Portal Widgets/Image icon Menu/image4.png differ diff --git a/Modern Development/Service Portal Widgets/JSON Beautifier/Client_side.js b/Modern Development/Service Portal Widgets/JSON Beautifier/Client_side.js new file mode 100644 index 0000000000..8def55ccb7 --- /dev/null +++ b/Modern Development/Service Portal Widgets/JSON Beautifier/Client_side.js @@ -0,0 +1,18 @@ +api.controller=function($scope) { + /* widget controller */ +$scope.rawJson = ''; +$scope.formattedJson = ''; +$scope.error = ''; + + $scope.beautifyJSON = function(){ + try{ + $scope.error = ''; + const parsed = JSON.parse($scope.rawJson); + $scope.formattedJson = JSON.stringify(parsed,null,2); + }catch(e){ + $scope.error = 'Invalid JSON' + e.message; + $scope.formattedJson = ''; + } + }; + +}; diff --git a/Modern Development/Service Portal Widgets/JSON Beautifier/HTML.html b/Modern Development/Service Portal Widgets/JSON Beautifier/HTML.html new file mode 100644 index 0000000000..7993db5057 --- /dev/null +++ b/Modern Development/Service Portal Widgets/JSON Beautifier/HTML.html @@ -0,0 +1,21 @@ +
    +

    JSON Beautifier

    +
    + + +
    +
    + +
    +
    +
    + {{error}} +
    +
    +
    + +
    {{formattedJson}}
    +
    +
    diff --git a/Modern Development/Service Portal Widgets/JSON Beautifier/README.md b/Modern Development/Service Portal Widgets/JSON Beautifier/README.md new file mode 100644 index 0000000000..ee2ab4df5e --- /dev/null +++ b/Modern Development/Service Portal Widgets/JSON Beautifier/README.md @@ -0,0 +1,11 @@ +## JSON Beautifier Widget + +The JSON Beautifier widget is a developer-focused tool designed to make working with JSON in ServiceNow fast, easy and efficient. It helps admins, developers and testers handles JSON payloads from APIs, Integration etc. + +## Benefits +- Reduces time spent manually formatting or checking JSON. +- Helps identify error or differences between JSOn payload quickly + +## Output +![A test image](demo.png) + diff --git a/Modern Development/Service Portal Widgets/JSON Beautifier/demo.png b/Modern Development/Service Portal Widgets/JSON Beautifier/demo.png new file mode 100644 index 0000000000..e8a4ab4ef7 Binary files /dev/null and b/Modern Development/Service Portal Widgets/JSON Beautifier/demo.png differ diff --git a/Modern Development/Service Portal Widgets/Konami Code Easter Egg/KonamiCodeEasterEggV2.js b/Modern Development/Service Portal Widgets/Konami Code Easter Egg/KonamiCodeEasterEggV2.js new file mode 100644 index 0000000000..423bc492d6 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Konami Code Easter Egg/KonamiCodeEasterEggV2.js @@ -0,0 +1,50 @@ +function(spModal) { + var c = this; + console.log('Lol what are you doing here?'); + + const KONAMI_CODE = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', + 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a']; + let inputSequence = []; + let timeoutId; + + const handleKeyPress = (e) => { + // Clear timeout to reset sequence if user pauses too long + clearTimeout(timeoutId); + + // Add key to sequence + inputSequence.push(e.key); + + // Keep only the last N keys (length of Konami code) + if (inputSequence.length > KONAMI_CODE.length) { + inputSequence.shift(); + } + + // Check if current sequence matches Konami code + if (inputSequence.join(',') === KONAMI_CODE.join(',')) { + activateCheats(); + inputSequence = []; // Reset after activation + } + + // Reset sequence after 2 seconds of inactivity + timeoutId = setTimeout(() => { + inputSequence = []; + }, 2000); + }; + + const activateCheats = () => { + spModal.open({ + size: 'sm', + title: 'Cheats activated', + message: 'Konami code entered', + buttons: [{ label: '${Close}', cancel: true }] + }); + }; + + document.addEventListener('keydown', handleKeyPress); + + // Cleanup listener when widget is destroyed + c.$onDestroy = function() { + document.removeEventListener('keydown', handleKeyPress); + clearTimeout(timeoutId); + }; +} diff --git a/Modern Development/Service Portal Widgets/Konami Code Easter Egg/README.md b/Modern Development/Service Portal Widgets/Konami Code Easter Egg/README.md index 244cde1c18..b7be3f1f38 100644 --- a/Modern Development/Service Portal Widgets/Konami Code Easter Egg/README.md +++ b/Modern Development/Service Portal Widgets/Konami Code Easter Egg/README.md @@ -1,3 +1,16 @@ # Konami Code Easter Egg Put this code in the client controller of a widget to listen for the Konami Code. By default it just opens a modal notifying the user that the konami code as activated. Modify to do whatever fun things you want. + +## Version 2 + +[KonamiCodeEasterEggV2.js]("Modern Development\Service Portal Widgets\Konami Code Easter Egg\KonamiCodeEasterEggV2.js") is the same code but improved with: + +1. Uses e.key instead of e.keyCode (which is deprecated) with modern arrow key names +2. Automatically tracks only the last N keypresses instead of manual position tracking +3. Resets the sequence if the user pauses too long (more forgiving UX) +4. Removes event listener when widget is destroyed to prevent memory leaks +5. Uses array join comparison instead of position tracking +6. Modern variable declarations for better scoping + +image diff --git a/Modern Development/Service Portal Widgets/Language Selector/README.md b/Modern Development/Service Portal Widgets/Language Selector/README.md new file mode 100644 index 0000000000..7a2b385a54 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Language Selector/README.md @@ -0,0 +1,24 @@ +# Language Selector with Flags + +A language selector widget for the Portal. +The user can change the instance language without having to leave the Portal. + +image +image + + +## What it does +- Displays a dropdown with flags and language names. +- Automatically updates the user's language in the `sys_user` table. +- Reloads the page to apply the new language immediately. + +## Files +- **HTML Template:** renders the dropdown with flag emojis and labels. +- **Client Script:** handles language selection and sends the PATCH request. +- **Server Script:** provides the current user ID and stored language. + +## Example +When the user selects **🇪🇸 Spanish**, the widget updates their user record and reloads the Portal in Spanish. + +## Prerequisites +- The language selected **must be installed and active** in the instance. diff --git a/Modern Development/Service Portal Widgets/Language Selector/language-selector.client.js b/Modern Development/Service Portal Widgets/Language Selector/language-selector.client.js new file mode 100644 index 0000000000..3006b117b8 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Language Selector/language-selector.client.js @@ -0,0 +1,22 @@ +function($http) { + var c = this; + + c.languages = [ + { code: 'en', label: 'English', flag: '🇬🇧' }, + { code: 'pb', label: 'Portuguese (Brazil)', flag: '🇧🇷' }, + { code: 'es', label: 'Spanish', flag: '🇪🇸' }, + { code: 'fr', label: 'French', flag: '🇫🇷' }, + { code: 'de', label: 'German', flag: '🇩🇪' }, + { code: 'it', label: 'Italian', flag: '🇮🇹' } + ]; + + c.userId = c.data.user_id; + c.selected = c.data.language || 'en'; + + c.changeLang = function() { + $http.patch('/api/now/table/sys_user/' + c.userId, { preferred_language: c.selected }) + .then(function(response) { + location.reload(); + }); + }; +} diff --git a/Modern Development/Service Portal Widgets/Language Selector/language-selector.css b/Modern Development/Service Portal Widgets/Language Selector/language-selector.css new file mode 100644 index 0000000000..58d724c2f4 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Language Selector/language-selector.css @@ -0,0 +1,13 @@ +.lang-selector { + display: flex; + align-items: center; + gap: 8px; +} +select, button { + padding: 6px 8px; + border-radius: 6px; + border: 1px solid #ccc; +} +button { + cursor: pointer; +} diff --git a/Modern Development/Service Portal Widgets/Language Selector/language-selector.html b/Modern Development/Service Portal Widgets/Language Selector/language-selector.html new file mode 100644 index 0000000000..5e674246e6 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Language Selector/language-selector.html @@ -0,0 +1,6 @@ +
    + +
    diff --git a/Modern Development/Service Portal Widgets/Language Selector/language-selector.server.js b/Modern Development/Service Portal Widgets/Language Selector/language-selector.server.js new file mode 100644 index 0000000000..90effd0d9c --- /dev/null +++ b/Modern Development/Service Portal Widgets/Language Selector/language-selector.server.js @@ -0,0 +1,9 @@ +(function() { + var user = gs.getUser(); + data.user_id = user.getID(); + + var grUser = new GlideRecord('sys_user'); + if (grUser.get(data.user_id)) { + data.language = grUser.getValue('preferred_language') || 'en'; + } +})(); diff --git a/Modern Development/Service Portal Widgets/Live Ticket Counter Service Portal Widget/Client Controller b/Modern Development/Service Portal Widgets/Live Ticket Counter Service Portal Widget/Client Controller new file mode 100644 index 0000000000..47d31fdea1 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Live Ticket Counter Service Portal Widget/Client Controller @@ -0,0 +1,118 @@ +api.controller = function($scope, $interval, spModal, $window) { + var c = this; + + // Initialize + c.counts = {}; + c.changes = {}; + c.lastUpdate = new Date(); + c.isRefreshing = false; + c.autoRefresh = true; + c.soundEnabled = true; + c.newCritical = false; + var refreshInterval; + + // Load initial data + c.$onInit = function() { + c.counts = c.data.counts || {}; + c.previousCounts = angular.copy(c.counts); + c.startAutoRefresh(); + }; + + // Refresh data + c.refresh = function() { + c.isRefreshing = true; + + c.server.get().then(function(response) { + var newCounts = response.data.counts; + + // Calculate changes + c.changes = { + critical: (newCounts.critical || 0) - (c.counts.critical || 0), + high: (newCounts.high || 0) - (c.counts.high || 0), + medium: (newCounts.medium || 0) - (c.counts.medium || 0), + low: (newCounts.low || 0) - (c.counts.low || 0) + }; + + // Check for new critical tickets + if (c.changes.critical > 0) { + c.newCritical = true; + if (c.soundEnabled) { + c.playAlertSound(); + } + + // Remove pulse animation after 3 seconds + $interval(function() { + c.newCritical = false; + }, 3000, 1); + } + + // Update counts + c.counts = newCounts; + c.lastUpdate = new Date(); + c.isRefreshing = false; + }); + }; + + // Auto-refresh toggle + c.toggleAutoRefresh = function() { + if (c.autoRefresh) { + c.startAutoRefresh(); + } else { + c.stopAutoRefresh(); + } + }; + + // Start auto-refresh + c.startAutoRefresh = function() { + if (refreshInterval) { + $interval.cancel(refreshInterval); + } + + refreshInterval = $interval(function() { + c.refresh(); + }, 30000); // 30 seconds + }; + + // Stop auto-refresh + c.stopAutoRefresh = function() { + if (refreshInterval) { + $interval.cancel(refreshInterval); + } + }; + + // Play sound alert + c.playAlertSound = function() { + var audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSqA0fPTgjMGHm7A7+OZRQ0PVq/m77BdGAg+ltryxnMpBSl+zPLaizsIGGS57OibUBELTqXh8bllHAU2jdXwyH0vBSZ8yfDajkULEFau5u+wXRgIPpXa8sZzKQUpfszy2Ys7CBhkuezom1ARDEyl4fG5ZRwFNo3V8Mh9LwUmfMnw2o5FDBFWrebvsF0YCD6V2vLGcykFKX7M8tmLOwgYZLns6JtQEQxMpeHxuWUcBTaN1fDIfS8FJnzJ8NqORQwRVq3m77BdGAg+ldryx3MpBSl+zPLaizsIGGS57OmbUBEMTKXh8bllHAU2jdXwyH0vBSZ8yfDajkUMEVat5u+wXRgIPpXa8sZzKQUpfszy2Ys7CBhkuezom1ARDEyl4fG5ZRwFNo3V8Mh9LwUmfMnw2o5FDBFWrebvsF0YCD6V2vLGcykFKX7M8tmLOwgYZLns6JtQEQxMpeHxuWUcBTaN1fDIfS8FJnzJ8NqORQwRVq3m77BdGAg='); + audio.play().catch(function(e) { + console.log('Could not play sound:', e); + }); + }; + + // Toggle sound + c.toggleSound = function() { + c.soundEnabled = !c.soundEnabled; + if (c.soundEnabled) { + spModal.alert('🔊 Sound alerts enabled'); + } else { + spModal.alert('🔇 Sound alerts disabled'); + } + }; + + // View tickets by priority + c.viewTickets = function(priority) { + var priorityNames = { + '1': 'Critical', + '2': 'High', + '3': 'Medium', + '4': 'Low' + }; + + // Navigate to filtered list + $window.location.href = '/incident_list.do?sysparm_query=priority=' + priority + '^active=true'; + }; + + // Cleanup on destroy + c.$onDestroy = function() { + c.stopAutoRefresh(); + }; +}; diff --git a/Modern Development/Service Portal Widgets/Live Ticket Counter Service Portal Widget/Live Ticket Counter .css b/Modern Development/Service Portal Widgets/Live Ticket Counter Service Portal Widget/Live Ticket Counter .css new file mode 100644 index 0000000000..21517211d1 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Live Ticket Counter Service Portal Widget/Live Ticket Counter .css @@ -0,0 +1,189 @@ +.ticket-counter-widget { + background: white; + border-radius: 12px; + padding: 20px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + max-width: 800px; + margin: 0 auto; +} + +.widget-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + border-bottom: 2px solid #f0f0f0; + padding-bottom: 15px; +} + +.widget-header h3 { + margin: 0; + font-size: 24px; + color: #333; +} + +.refresh-btn { + background: #007bff; + color: white; + border: none; + padding: 8px 12px; + border-radius: 50%; + font-size: 20px; + cursor: pointer; + transition: all 0.3s ease; +} + +.refresh-btn:hover { + background: #0056b3; + transform: scale(1.1); +} + +.refresh-btn.spinning { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.counter-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 15px; + margin-bottom: 20px; +} + +.counter-card { + background: white; + border-radius: 10px; + padding: 20px; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; + border: 2px solid #e0e0e0; +} + +.counter-card:hover { + transform: translateY(-5px); + box-shadow: 0 5px 15px rgba(0,0,0,0.2); +} + +.counter-card.critical { + border-left: 5px solid #dc3545; +} + +.counter-card.high { + border-left: 5px solid #fd7e14; +} + +.counter-card.medium { + border-left: 5px solid #ffc107; +} + +.counter-card.low { + border-left: 5px solid #28a745; +} + +.counter-icon { + font-size: 32px; + margin-bottom: 10px; +} + +.counter-number { + font-size: 48px; + font-weight: bold; + color: #333; + margin: 10px 0; +} + +.counter-number.pulse { + animation: pulse 1s ease-in-out infinite; +} + +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.1); color: #dc3545; } + 100% { transform: scale(1); } +} + +.counter-label { + font-size: 14px; + color: #666; + font-weight: 600; + text-transform: uppercase; +} + +.counter-change { + margin-top: 5px; + padding: 3px 8px; + background: #ff6b6b; + color: white; + border-radius: 12px; + font-size: 11px; + display: inline-block; + animation: slideIn 0.5s ease; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.widget-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 15px; + border-top: 1px solid #f0f0f0; + font-size: 12px; + color: #666; +} + +.auto-refresh label { + display: flex; + align-items: center; + gap: 5px; + cursor: pointer; +} + +.sound-toggle { + position: absolute; + top: 20px; + right: 70px; +} + +.sound-toggle button { + background: #f0f0f0; + border: none; + padding: 8px 12px; + border-radius: 50%; + font-size: 20px; + cursor: pointer; + transition: all 0.3s ease; +} + +.sound-toggle button.active { + background: #28a745; +} + +.sound-toggle button:hover { + transform: scale(1.1); +} + +@media (max-width: 600px) { + .counter-grid { + grid-template-columns: repeat(2, 1fr); + } + + .widget-footer { + flex-direction: column; + gap: 10px; + } +} diff --git a/Modern Development/Service Portal Widgets/Live Ticket Counter Service Portal Widget/Live Ticket Counter.html b/Modern Development/Service Portal Widgets/Live Ticket Counter Service Portal Widget/Live Ticket Counter.html new file mode 100644 index 0000000000..1fd2d19b9f --- /dev/null +++ b/Modern Development/Service Portal Widgets/Live Ticket Counter Service Portal Widget/Live Ticket Counter.html @@ -0,0 +1,77 @@ +
    +
    +

    🎫 Live Ticket Monitor

    + +
    + +
    + +
    +
    🔴
    +
    + {{c.counts.critical || 0}} +
    +
    Critical
    +
    + +{{c.changes.critical}} new +
    +
    + + +
    +
    🟠
    +
    + {{c.counts.high || 0}} +
    +
    High
    +
    + +{{c.changes.high}} new +
    +
    + + +
    +
    🟡
    +
    + {{c.counts.medium || 0}} +
    +
    Medium
    +
    + +{{c.changes.medium}} new +
    +
    + + +
    +
    🟢
    +
    + {{c.counts.low || 0}} +
    +
    Low
    +
    + +{{c.changes.low}} new +
    +
    +
    + + + + +
    + +
    +
    diff --git a/Modern Development/Service Portal Widgets/Live Ticket Counter Service Portal Widget/README.md b/Modern Development/Service Portal Widgets/Live Ticket Counter Service Portal Widget/README.md new file mode 100644 index 0000000000..642f6c3250 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Live Ticket Counter Service Portal Widget/README.md @@ -0,0 +1,39 @@ +# Live Ticket Counter - Service Portal Widget. + +## Use Case +The Live Ticket Counter widget displays a real-time count of incident tickets categorized by priority (Critical, High, Medium, Low). It visually updates with an animation to indicate refresh activity and allows users to click on any priority count to view a filtered list of tickets of that priority. This helps support teams monitor ticket load dynamically and quickly access relevant tickets for faster incident management. + +## Why it's unique +- Real-time ticket count updates without page refresh +- Visual pulse animation highlights new critical tickets +- Clickable cards open filtered ticket lists by priority +- Beginner-friendly and easy to implement + +## How it is Useful in ServiceNow +- Real-time updates keep support agents and managers instantly informed of critical ticket volume changes, helping prioritize work promptly. +- Visible counts by priority enable supervision to redistribute workloads or escalate issues proactively. +- Agents save time by accessing filtered ticket lists with a click instead of manually searching for tickets by priority. +- The widget's last updated timestamp and auto-refresh toggle give users control and confidence in data freshness. +- As a light, interactive component, it complements existing dashboards and pages without heavy customizations. + +## How to Use + +1. Create Widget + - Go to Service Portal > Widgets + - Click "New" + - Copy-paste HTML, Client Controller, Server Script, and CSS into appropriate sections + +2. Add to Page + - Place the widget on any Service Portal page where ticket monitoring is required + - Widget ID: `live_ticket_counter_sp` + +3. Test and Customize + - View tickets by clicking on priority cards + - Toggle auto-refresh and sound alerts + - Modify styling or priority levels as needed + +## Compatibility +This Html , css , client , server code is compatible with all standard ServiceNow instances without requiring ES2021 features. + +## Files +- `Live Ticket Counter.html`,`Live Ticket Counter .css`,`Server Script`,`Client Script`, — these are files. diff --git a/Modern Development/Service Portal Widgets/Live Ticket Counter Service Portal Widget/Server Script b/Modern Development/Service Portal Widgets/Live Ticket Counter Service Portal Widget/Server Script new file mode 100644 index 0000000000..f74ef31b8b --- /dev/null +++ b/Modern Development/Service Portal Widgets/Live Ticket Counter Service Portal Widget/Server Script @@ -0,0 +1,55 @@ +(function() { + // Get ticket counts by priority + data.counts = {}; + + try { + // Count Critical (Priority 1) + var criticalGR = new GlideAggregate('incident'); + criticalGR.addQuery('priority', '1'); + criticalGR.addQuery('active', true); + criticalGR.addAggregate('COUNT'); + criticalGR.query(); + if (criticalGR.next()) { + data.counts.critical = parseInt(criticalGR.getAggregate('COUNT')); + } + + // Count High (Priority 2) + var highGR = new GlideAggregate('incident'); + highGR.addQuery('priority', '2'); + highGR.addQuery('active', true); + highGR.addAggregate('COUNT'); + highGR.query(); + if (highGR.next()) { + data.counts.high = parseInt(highGR.getAggregate('COUNT')); + } + + // Count Medium (Priority 3) + var mediumGR = new GlideAggregate('incident'); + mediumGR.addQuery('priority', '3'); + mediumGR.addQuery('active', true); + mediumGR.addAggregate('COUNT'); + mediumGR.query(); + if (mediumGR.next()) { + data.counts.medium = parseInt(mediumGR.getAggregate('COUNT')); + } + + // Count Low (Priority 4) + var lowGR = new GlideAggregate('incident'); + lowGR.addQuery('priority', '4'); + lowGR.addQuery('active', true); + lowGR.addAggregate('COUNT'); + lowGR.query(); + if (lowGR.next()) { + data.counts.low = parseInt(lowGR.getAggregate('COUNT')); + } + + } catch (e) { + gs.error('Error in Live Ticket Counter: ' + e.message); + data.counts = { + critical: 0, + high: 0, + medium: 0, + low: 0 + }; + } +})(); diff --git a/Modern Development/Service Portal Widgets/Manage Delegates Widget/README.md b/Modern Development/Service Portal Widgets/Manage Delegates Widget/README.md new file mode 100644 index 0000000000..1dd62ac4d1 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Manage Delegates Widget/README.md @@ -0,0 +1,15 @@ +# My Delegates - Portal Widget +## Overview +Simple Service Portal widget to create, edit, list, and delete OOB Delegate functionality in portal. + +## Files included +- HTML : widget HTML. +- CSS : widget CSS. +- Client Script : widget Client contriller. +- Server Script : widget Server Script. + +## How to use +- Create a new Service Portal widget. +- Copy paste the html, css, client and server scipts to respective fields +- Save and add the widget to a portal page. + diff --git a/Modern Development/Service Portal Widgets/Manage Delegates Widget/delegates.html b/Modern Development/Service Portal Widgets/Manage Delegates Widget/delegates.html new file mode 100644 index 0000000000..154e4be510 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Manage Delegates Widget/delegates.html @@ -0,0 +1,82 @@ +
    +
    +

    Delegates

    +
    +
    +
    + You have no active delegates. +
    + +
      +
    • +

      {{d.delegate_display}}

      +

      + User: {{d.user_display}}
      + Starts: {{d.starts_display || 'N/A'}}
      + Ends: {{d.ends_display || 'N/A'}} +

      +

      + Notifications: + Approvals + Assignments + All + Invitations +

      +
      + +
      +
    • +
    + +
    + +
    +

    Create a New Delegate

    +

    Edit Delegate

    + +
    + + +
    + + +
    + +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    + +
    + + + + +
    + +
    + + + +
    +
    +
    +
    +
    diff --git a/Modern Development/Service Portal Widgets/Manage Delegates Widget/delegates.scss b/Modern Development/Service Portal Widgets/Manage Delegates Widget/delegates.scss new file mode 100644 index 0000000000..e2ff7973d2 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Manage Delegates Widget/delegates.scss @@ -0,0 +1,19 @@ +.delegate-item { + border-left: 4px solid #5bc0de; + margin-bottom: 10px; + padding: 10px 15px; +} +.delegate-item h4 { + margin-top: 0; + font-weight: bold; + color: #333; +} +.delegate-form-container { + margin-top: 20px; + padding-top: 15px; + border-top: 1px solid #eee; +} +.btn-row { margin-top: 8px; } +.form-group { margin-bottom: 15px; } +.panel-heading + .panel-body { padding-top: 20px; } +.checkboxes label { margin-right: 12px; } diff --git a/Modern Development/Service Portal Widgets/Manage Delegates Widget/delegates_client.css b/Modern Development/Service Portal Widgets/Manage Delegates Widget/delegates_client.css new file mode 100644 index 0000000000..d543eb04e1 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Manage Delegates Widget/delegates_client.css @@ -0,0 +1,86 @@ +function($scope) { + var c = this; + c.form = {}; + c.editing = false; + c.delegateField = { displayValue: '', value: '', name: 'delegate' }; + + c.$onInit = function() { + c.data = c.data || {}; + c.form = { + approvals: false, + assignments: false, + notifications: false, + invitations: false + }; + }; + + function pad(n){ return n<10 ? '0'+n : n; } + + function localToGlide(local) { + if (!local) return ''; + var d = new Date(local); + return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()); + } + + c.saveDelegate = function() { + c.form.delegate = c.delegateField.value; + c.form.starts = localToGlide(c.form.starts_local); + c.form.ends = localToGlide(c.form.ends_local); + + c.data.action = 'save_delegate'; + c.data.record = angular.copy(c.form); + + c.server.update().then(function() { + c.data.action = undefined; + c.resetForm(); + c.server.get().then(function(response) { + c.data = response.data; + }); + }, function() { + alert('Failed to save delegate.'); + }); + }; + + c.edit = function(sys_id) { + c.server.get().then(function(response) { + c.data = response.data; + var rec = c.data.delegates.find(function(x){ return x.sys_id === sys_id; }) || {}; + c.editing = true; + c.form = { + sys_id: rec.sys_id, + approvals: !!rec.approvals, + assignments: !!rec.assignments, + notifications: !!rec.notifications, + invitations: !!rec.invitations, + starts_local: rec.starts_value ? new Date(rec.starts_value.replace(' ', 'T')) : null, + ends_local: rec.ends_value ? new Date(rec.ends_value.replace(' ', 'T')) : null + }; + c.delegateField.value = rec.delegate_sys_id || rec.delegate; + c.delegateField.displayValue = rec.delegate_display; + }); + }; + + c.deleteDelegate = function() { + if (!c.form.sys_id) return; + if (!confirm('Delete delegate record?')) return; + c.data.action = 'delete_delegate'; + c.data.sys_id = c.form.sys_id; + c.server.update().then(function() { + c.data.action = undefined; + c.resetForm(); + c.server.get().then(function(response) { c.data = response.data; }); + }); + }; + + c.resetForm = function() { + c.editing = false; + c.form = { + approvals: false, + assignments: false, + notifications: false, + invitations: false + }; + c.delegateField = { displayValue: '', value: '', name: 'delegate' }; + }; +} diff --git a/Modern Development/Service Portal Widgets/Manage Delegates Widget/delegates_server.js b/Modern Development/Service Portal Widgets/Manage Delegates Widget/delegates_server.js new file mode 100644 index 0000000000..a86b684957 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Manage Delegates Widget/delegates_server.js @@ -0,0 +1,69 @@ +(function() { + data.delegates = []; + var currentUser = gs.getUserID(); + + if (input && input.action === 'save_delegate' && input.record) { + var rec = input.record; + var gr; + if (rec.sys_id) { + gr = new GlideRecord('sys_user_delegate'); + if (!gr.get(rec.sys_id)) { + gr.initialize(); + } + } else { + gr = new GlideRecord('sys_user_delegate'); + gr.initialize(); + } + + gr.setValue('user', currentUser); + + if (rec.delegate) gr.setValue('delegate', rec.delegate); + if (rec.starts) gr.setValue('starts', rec.starts); + if (rec.ends) gr.setValue('ends', rec.ends); + + gr.setValue('approvals', rec.approvals ? 'true' : 'false'); + gr.setValue('assignments', rec.assignments ? 'true' : 'false'); + gr.setValue('notifications', rec.notifications ? 'true' : 'false'); + gr.setValue('invitations', rec.invitations ? 'true' : 'false'); + + var id = gr.update(); + data.saved_sys_id = id; + } + + if (input && input.action === 'delete_delegate' && input.sys_id) { + var ddel = new GlideRecord('sys_user_delegate'); + if (ddel.get(input.sys_id)) { + ddel.deleteRecord(); + data.deleted = true; + } else { + data.deleted = false; + } + } + + + var grList = new GlideRecord('sys_user_delegate'); + grList.addQuery('user', currentUser); + grList.orderByDesc('starts'); + grList.query(); + while (grList.next()) { + var obj = {}; + obj.sys_id = grList.getUniqueValue(); + obj.user = grList.getValue('user'); + obj.delegate = grList.getValue('delegate'); + obj.user_display = grList.getDisplayValue('user'); + obj.delegate_display = grList.getDisplayValue('delegate'); + obj.starts_display = grList.getDisplayValue('starts'); + obj.ends_display = grList.getDisplayValue('ends'); + obj.starts_value = grList.getValue('starts'); + obj.ends_value = grList.getValue('ends'); + obj.approvals = grList.getValue('approvals') === 'true' || grList.getValue('approvals') === '1'; + obj.assignments = grList.getValue('assignments') === 'true' || grList.getValue('assignments') === '1'; + obj.notifications = grList.getValue('notifications') === 'true' || grList.getValue('notifications') === '1'; + obj.invitations = grList.getValue('invitations') === 'true' || grList.getValue('invitations') === '1'; + + obj.user_sys_id = obj.user; + obj.delegate_sys_id = obj.delegate; + + data.delegates.push(obj); + } +})(); diff --git a/Modern Development/Service Portal Widgets/My Assets/README.md b/Modern Development/Service Portal Widgets/My Assets/README.md new file mode 100644 index 0000000000..e8f647551e --- /dev/null +++ b/Modern Development/Service Portal Widgets/My Assets/README.md @@ -0,0 +1,22 @@ +# My Assets Widget + +## Overview +Displays assets assigned to the logged-in user in a clean, responsive table. +Data is fetched from the `alm_asset` table using a secure server script. + +## Files +- **HTML** – Defines the widget layout and table structure +- **Server Script** – Retrieves user-specific assets from `alm_asset` +- **CSS** – Adds modern, responsive styling + +## Features +- Responsive table layout +- Record count badge +- Hover and gradient effects +- Empty state message + +## Usage +1. Navigate to **Service Portal > Widgets** in ServiceNow. +2. Create a new widget and paste the HTML, Server Script, Client Script, and CSS. +3. Save the widget and add it to your desired portal page. +4. The widget automatically displays assets assigned to the logged-in user. diff --git a/Modern Development/Service Portal Widgets/My Assets/my_assets.css b/Modern Development/Service Portal Widgets/My Assets/my_assets.css new file mode 100644 index 0000000000..742b626f11 --- /dev/null +++ b/Modern Development/Service Portal Widgets/My Assets/my_assets.css @@ -0,0 +1,41 @@ +.my-assets-widget { + border-radius: 12px; + box-shadow: 0 2px 10px rgba(0,0,0,0.08); + overflow: hidden; +} + +.my-assets-widget .panel-heading { + background: linear-gradient(90deg, #0078d4, #005fa3); + color: #fff; + padding: 12px 16px; + font-weight: 500; +} + +.my-assets-widget .panel-heading .badge { + background-color: #fff; + color: #005fa3; + font-weight: 600; +} + +.my-assets-widget .table { + margin-bottom: 0; +} + +.my-assets-widget .table-hover tbody tr:hover { + background-color: #f2f8ff; + cursor: pointer; +} + +.my-assets-widget .asset-link { + font-weight: 500; + color: #0078d4; + text-decoration: none; +} + +.my-assets-widget .asset-link:hover { + text-decoration: underline; +} + +.my-assets-widget .panel-body.text-center { + padding: 30px; +} diff --git a/Modern Development/Service Portal Widgets/My Assets/my_assets.html b/Modern Development/Service Portal Widgets/My Assets/my_assets.html new file mode 100644 index 0000000000..cf6994ce88 --- /dev/null +++ b/Modern Development/Service Portal Widgets/My Assets/my_assets.html @@ -0,0 +1,45 @@ +
    +
    +

    + My Assets +

    + {{data.recordCount}} Asset(s) +
    + +
    +
    + + + + + + + + + + + + + + + +
    Asset NameAssigned ToAction
    + + + {{asset.display}} + + {{asset.assigned_to}} + + View + +
    +
    +
    + +
    + +

    No assets assigned to you.

    +
    +
    diff --git a/Modern Development/Service Portal Widgets/My Assets/my_assets_server_side.js b/Modern Development/Service Portal Widgets/My Assets/my_assets_server_side.js new file mode 100644 index 0000000000..ea1367f9c8 --- /dev/null +++ b/Modern Development/Service Portal Widgets/My Assets/my_assets_server_side.js @@ -0,0 +1,19 @@ +(function() { + data.userID = gs.getUserID(); + data.assets = []; + + var gr = new GlideRecordSecure('alm_asset'); + gr.addQuery('assigned_to', gs.getUserID()); + gr.orderBy('display_name'); + gr.query(); + + data.recordCount = gr.getRowCount(); + + while (gr.next()) { + data.assets.push({ + display: gr.getDisplayValue('display_name'), + assigned_to: gr.getDisplayValue('assigned_to'), + sysid: gr.getUniqueValue() + }); + } +})(); diff --git a/Modern Development/Service Portal Widgets/My Mentioned Items/CSS.js b/Modern Development/Service Portal Widgets/My Mentioned Items/CSS.js new file mode 100644 index 0000000000..4f2b212d95 --- /dev/null +++ b/Modern Development/Service Portal Widgets/My Mentioned Items/CSS.js @@ -0,0 +1,15 @@ +/* +list css to show border-bottom and padding. +*/ +li{ + padding: 1.2rem 0; + border-bottom: .1rem solid #DADDE2; + list-style:none; +} + +/* +set background color of widget to white. +*/ +.main-cont{ + background:#ffffff; +} diff --git a/Modern Development/Service Portal Widgets/My Mentioned Items/HTML.js b/Modern Development/Service Portal Widgets/My Mentioned Items/HTML.js new file mode 100644 index 0000000000..db385f096e --- /dev/null +++ b/Modern Development/Service Portal Widgets/My Mentioned Items/HTML.js @@ -0,0 +1,14 @@ +
    +
    +

    ${My Mentions}

    +
    +
    +
    +
      +
    • + ${You have been mentioned in }{{item.record}} ${by }{{item.user_from}} +
    • +
    +
    +
    +
    diff --git a/Modern Development/Service Portal Widgets/My Mentioned Items/README.md b/Modern Development/Service Portal Widgets/My Mentioned Items/README.md new file mode 100644 index 0000000000..753a233746 --- /dev/null +++ b/Modern Development/Service Portal Widgets/My Mentioned Items/README.md @@ -0,0 +1,12 @@ +**How to use** +1. Add this widget to portal homepage or any other page. +2. It will display the top 5 records where user is mentioned in. +3. It will also display the user who has mentioned the logged in user. + +**Use Case** +1. User will receive the mentioned items on portal homepage. +2. User can directly go to the record and reply. +3. The link will land the user on tickets page. + +my mentions + diff --git a/Modern Development/Service Portal Widgets/My Mentioned Items/server script.js b/Modern Development/Service Portal Widgets/My Mentioned Items/server script.js new file mode 100644 index 0000000000..6c4490547b --- /dev/null +++ b/Modern Development/Service Portal Widgets/My Mentioned Items/server script.js @@ -0,0 +1,21 @@ +(function() { + /* + This code will display the records wher user is mentioned in (@user in Jurnal fields). + This will also provide the link to record. + Only top 5 mentions will be displayed. + */ + data.mentionArr = []; // array to store mentions. + var mentionRec = new GlideRecord('live_notification'); + mentionRec.addEncodedQuery('user=' + gs.getUserID()); // get only logged-in user's records + mentionRec.orderBy('sys_created_on'); // get by created date. + mentionRec.setLimit(5); + mentionRec.query(); + while (mentionRec.next()) { + tempval = {}; // temp object. + tempval.record = mentionRec.getValue('title'); + tempval.user = mentionRec.user.name.toString(); + tempval.user_from = mentionRec.user_from.name.toString(); + tempval.url = '/' + $sp.getValue('url_suffix') + '?id=ticket&sys_id=' + mentionRec.getValue('document') + '&table=' + mentionRec.getValue('table'); + data.mentionArr.push(tempval); + } +})(); diff --git a/Modern Development/Service Portal Widgets/My Reminders/README.md b/Modern Development/Service Portal Widgets/My Reminders/README.md new file mode 100644 index 0000000000..39b217f69b --- /dev/null +++ b/Modern Development/Service Portal Widgets/My Reminders/README.md @@ -0,0 +1,31 @@ +# Service Portal Reminder Widget + +A simple custom ServiceNow Service Portal widget for viewing and creating personal reminders. + +## Features + +- This uses **ServiceNow's OOB Reminder table**. Table name : 'reminder' +- Displays a list of reminders for the current user. +- Provides a form to create new reminders. +- Allows associating reminders with any Task record. +- Auto-refreshes the list after a new reminder is created. + + +## How to create + +1. Navigate to **Service Portal > Widgets**. +2. Click **Create a new widget**. +3. Set the **Widget Name** (e.g. 'My Reminders') and **ID** (e.g., 'reminder-widget'). +4. Copy and paste the provided HTML, CSS, Client Script, and Server Script into their respective tabs. +5. Save the widget. + +## How to Use + +1. Open your target portal page in the Service Portal Designer. +2. Find your widget in the "Widgets" filter on the left. +3. Drag and dropp the widget onto the page. + +## Screenshots +image +image + diff --git a/Modern Development/Service Portal Widgets/My Reminders/reminders.html b/Modern Development/Service Portal Widgets/My Reminders/reminders.html new file mode 100644 index 0000000000..9688cb7ca0 --- /dev/null +++ b/Modern Development/Service Portal Widgets/My Reminders/reminders.html @@ -0,0 +1,85 @@ +
    +
    +

    My Reminders

    +
    +
    +
    + You have no active reminders. +
    + +
      +
    • +

      {{reminder.subject}}

      +

      + Task: {{reminder.task_display || 'N/A'}}
      + Action: Send an {{reminder.using}} {{reminder.remind_me}} minutes before {{reminder.field_display}}. +

      +

      Notes: {{reminder.notes}}

      +
    • +
    + +
    + +
    +

    Create a New Reminder

    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    + +
    + + +
    + +
    + +
    +
    +
    +
    +
    diff --git a/Modern Development/Service Portal Widgets/My Reminders/reminders.scss b/Modern Development/Service Portal Widgets/My Reminders/reminders.scss new file mode 100644 index 0000000000..29ddb182ab --- /dev/null +++ b/Modern Development/Service Portal Widgets/My Reminders/reminders.scss @@ -0,0 +1,29 @@ +.reminder-item { + border-left: 4px solid #337ab7; + margin-bottom: 10px; + padding: 10px 15px; +} + +.reminder-item h4 { + margin-top: 0; + font-weight: bold; + color: #333; +} + +.reminder-item p { + margin-bottom: 5px; +} + +.reminder-form-container { + margin-top: 20px; + padding-top: 15px; + border-top: 1px solid #eee; +} + +.form-group { + margin-bottom: 15px; +} + +.panel-heading + .panel-body { + padding-top: 20px; +} diff --git a/Modern Development/Service Portal Widgets/My Reminders/reminders_client.js b/Modern Development/Service Portal Widgets/My Reminders/reminders_client.js new file mode 100644 index 0000000000..2718887094 --- /dev/null +++ b/Modern Development/Service Portal Widgets/My Reminders/reminders_client.js @@ -0,0 +1,42 @@ +function($scope) { + var c = this; + + // Object to hold data for the new reminder form + c.newReminder = {}; + + // Special object for the sn-record-picker directive + c.taskField = { + displayValue: '', + value: '', + name: 'task' + }; + + // Function to submit the new reminder + c.createReminder = function() { + // Check if the form is valid before submitting + if ($scope.reminderForm.$invalid) { + return; + } + + // Set the task sys_id from the record picker into our submission object + c.newReminder.task = c.taskField.value; + c.data.newReminder = c.newReminder; + + // Set an action for the server to identify the request + c.data.action = 'create_reminder'; + + // Call the server script to insert the record + c.server.update().then(function(response) { + // Clear the action and the form model after successful submission + c.data.action = undefined; + c.newReminder = {}; + c.taskField.displayValue = ''; + c.taskField.value = ''; + + // Refresh the reminder list by reloading server data + c.server.get().then(function(response) { + c.data = response.data; + }); + }); + }; +} diff --git a/Modern Development/Service Portal Widgets/My Reminders/reminders_server.js b/Modern Development/Service Portal Widgets/My Reminders/reminders_server.js new file mode 100644 index 0000000000..4c8dfe0d90 --- /dev/null +++ b/Modern Development/Service Portal Widgets/My Reminders/reminders_server.js @@ -0,0 +1,38 @@ +(function() { + + var currentUserId = gs.getUserID(); + data.reminders = []; + + + if (input && input.action === 'create_reminder') { + var newReminder = new GlideRecord('reminder'); + newReminder.initialize(); + newReminder.setValue('user', currentUserId); + newReminder.setValue('task', input.newReminder.task); + newReminder.setValue('subject', input.newReminder.subject); + newReminder.setValue('notes', input.newReminder.notes); + newReminder.setValue('remind_me', input.newReminder.remind_me); + newReminder.setValue('field', input.newReminder.field); + newReminder.setValue('using', input.newReminder.using); + newReminder.insert(); + } + + + var reminderGR = new GlideRecord('reminder'); + reminderGR.addQuery('user', currentUserId); + reminderGR.orderByDesc('sys_created_on'); // Show newest first + reminderGR.query(); + + while (reminderGR.next()) { + var reminderObj = {}; + reminderObj.sys_id = reminderGR.getUniqueValue(); + reminderObj.subject = reminderGR.getValue('subject'); + reminderObj.notes = reminderGR.getValue('notes'); + reminderObj.remind_me = reminderGR.getValue('remind_me'); + reminderObj.field_display = reminderGR.getDisplayValue('field'); // Get user-friendly display value + reminderObj.using = reminderGR.getValue('using'); + reminderObj.task_display = reminderGR.getDisplayValue('task'); // Get task number/display value + data.reminders.push(reminderObj); + } + +})(); diff --git a/Modern Development/Service Portal Widgets/Open in Platform/README.md b/Modern Development/Service Portal Widgets/Open in Platform/README.md index ec67b8ff50..d80671260b 100644 --- a/Modern Development/Service Portal Widgets/Open in Platform/README.md +++ b/Modern Development/Service Portal Widgets/Open in Platform/README.md @@ -1,3 +1,19 @@ -Widget will create a button that will only be visable to users with the itil role that will take them to the same record in platform. will work with the form and standard ticket pages (or anywhere with the table and sysId in the url. +**This is an enhancement to the current code** +1. Added "open in Workspace" Button to open the record in workspace(defined in option schema), since it is gaining popularity now. +2. Enhanced the visibility condition so that the button is only visible on pages having sys_id and table in url. +3. Enhanced css to improve visibility of button. +4. This button wll look for the table to workspace mapping (in option schema) and create the URL to open record in respective workspace. If now mapping is found, the record is opened in SOW workspace(default). +5. The button name has been changed to generic title "Open In Workspace" -see also [on Share](https://developer.servicenow.com/connect.do#!/share/contents/6592535_open_in_platform_widget?t=PRODUCT_DETAILS) +**Sample** +{ +"name":"define_workspace", +"type":"json", +"label":"Define Table Workspace JSON Mapping", +"value":{ + "sn_grc_issue":"risk/privacy", // will open issue records in RISK workspace + "sn_si_incident":"sir" // will open security incidents in SIR workspace. +} +} + +Widget will create a button that will only be visible to users with the itil role that will take them to the same record in platform. will work with the form and standard ticket pages (or anywhere with the table and sysId in the url. diff --git a/Modern Development/Service Portal Widgets/Open in Platform/body.html b/Modern Development/Service Portal Widgets/Open in Platform/body.html index 507dbb3fc9..c1e1537cd6 100644 --- a/Modern Development/Service Portal Widgets/Open in Platform/body.html +++ b/Modern Development/Service Portal Widgets/Open in Platform/body.html @@ -1,5 +1,6 @@ diff --git a/Modern Development/Service Portal Widgets/Open in Platform/css.js b/Modern Development/Service Portal Widgets/Open in Platform/css.js new file mode 100644 index 0000000000..ed4e26f29a --- /dev/null +++ b/Modern Development/Service Portal Widgets/Open in Platform/css.js @@ -0,0 +1,7 @@ +/* +This styling will align buttons on 2 ends of the div. +*/ +.btn-cntr{ +display: flex; +justify-content: space-between; +} diff --git a/Modern Development/Service Portal Widgets/Open in Platform/option schema.js b/Modern Development/Service Portal Widgets/Open in Platform/option schema.js new file mode 100644 index 0000000000..fad9e8ec71 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Open in Platform/option schema.js @@ -0,0 +1,23 @@ +[ +{ +"name":"open_in_platform", +"type":"string", +"label":"Name for Plarform Button", +"default_value":"Open in Platform" +}, +{ +"name":"open_in_workspace", +"type":"string", +"label":"Name for Workspace Button", +"default_value":"Open In workspace" +}, +{ +"name":"define_workspace", +"type":"json", +"label":"Define Table Workspace JSON Mapping", +"value":{ + "sn_grc_issue":"risk/privacy", + "sn_si_incident":"sir" +} +} +] diff --git a/Modern Development/Service Portal Widgets/Open in Platform/server.js b/Modern Development/Service Portal Widgets/Open in Platform/server.js index f94e939a90..0ebc6927c3 100644 --- a/Modern Development/Service Portal Widgets/Open in Platform/server.js +++ b/Modern Development/Service Portal Widgets/Open in Platform/server.js @@ -1,11 +1,22 @@ (function() { - data.table = input.table || $sp.getParameter("table"); - data.sys_id = input.sys_id || $sp.getParameter("sys_id"); - - data.url = "/nav_to.do?uri="+data.table+".do?sys_id="+data.sys_id; - - data.role = false; - if (gs.hasRole("itil")){ - data.role = true; - } + /* + Code will get table and sys_id parameter from url and create url for platform and workspace(defined in option schema). + This widget can be used in any page having sys_id and table in url , eg: ticket page. + */ + data.table = $sp.getParameter("table"); // get table from url + data.sys_id = $sp.getParameter("sys_id"); // get sys_id from url + + var tableWorkspaceMapping = JSON.parse(options.define_workspace); // get the table to workspace mapping from instance options. + Object.keys(tableWorkspaceMapping).forEach(function(key) { + if (key == data.table) + data.workspace_url = "now/" + tableWorkspaceMapping[key] + "/record/" + data.table + "/" + data.sys_id; // if table to workspce mapping is found, the create workspace URL. + else + data.workspace_url = "now/sow/record/" + data.table + "/" + data.sys_id; // open in SOW + }); + data.platform_url = "/nav_to.do?uri=" + data.table + ".do?sys_id=" + data.sys_id; + + data.role = false; + if (gs.hasRole("itil") && data.table && data.sys_id) { // only visible to users with itil role and if url has required parameters. + data.role = true; + } })(); diff --git a/Modern Development/Service Portal Widgets/Signature Pad Widget/Client Controller.js b/Modern Development/Service Portal Widgets/Signature Pad Widget/Client Controller.js new file mode 100644 index 0000000000..8a80963f93 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Signature Pad Widget/Client Controller.js @@ -0,0 +1,80 @@ +api.controller = function($scope) { + var c = this; + var canvas, ctx; + var drawing = false; + var lastPos = { x: 0, y: 0 }; + + // Initialize after DOM is ready + c.$onInit = function() { + setTimeout(function() { + canvas = document.getElementById('signature-pad'); + if (!canvas) return; + + // Get 2D drawing context + ctx = canvas.getContext('2d'); + ctx.lineWidth = 2; + ctx.strokeStyle = '#000'; + + // Mouse event listeners + canvas.addEventListener('mousedown', startDraw); + canvas.addEventListener('mousemove', draw); + canvas.addEventListener('mouseup', endDraw); + + // Touch event listeners (for mobile/tablet) + canvas.addEventListener('touchstart', startDraw); + canvas.addEventListener('touchmove', draw); + canvas.addEventListener('touchend', endDraw); + }, 200); + }; + + // Get drawing position based on mouse or touch input + function getPosition(event) { + var rect = canvas.getBoundingClientRect(); + if (event.touches && event.touches[0]) { + return { + x: event.touches[0].clientX - rect.left, + y: event.touches[0].clientY - rect.top + }; + } + return { + x: event.clientX - rect.left, + y: event.clientY - rect.top + }; + } + + // Start drawing when mouse/touch pressed + function startDraw(e) { + drawing = true; + lastPos = getPosition(e); + } + + // Draw line on canvas while dragging + function draw(e) { + if (!drawing) return; + e.preventDefault(); // Prevent page scrolling on touch + var pos = getPosition(e); + ctx.beginPath(); + ctx.moveTo(lastPos.x, lastPos.y); + ctx.lineTo(pos.x, pos.y); + ctx.stroke(); + lastPos = pos; + } + + // Stop drawing when mouse/touch released + function endDraw() { + drawing = false; + } + + // Clear the canvas + c.clearSignature = function() { + if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height); + drawing = false; + }; + + // Convert signature to base64 image and attach + c.attachSignature = function() { + if (!ctx) return alert("Canvas not initialized."); + var data = canvas.toDataURL('image/png'); // Get base64 encoded image + alert("Signature captured successfully. It will be attached after submission.\n\n" + data); + }; +}; diff --git a/Modern Development/Service Portal Widgets/Signature Pad Widget/HTML File.html b/Modern Development/Service Portal Widgets/Signature Pad Widget/HTML File.html new file mode 100644 index 0000000000..9cdfdb1192 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Signature Pad Widget/HTML File.html @@ -0,0 +1,12 @@ +
    + + + + + +
    + + +
    +
    diff --git a/Modern Development/Service Portal Widgets/Signature Pad Widget/README.md b/Modern Development/Service Portal Widgets/Signature Pad Widget/README.md new file mode 100644 index 0000000000..44630829e9 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Signature Pad Widget/README.md @@ -0,0 +1,11 @@ +A lightweight ServiceNow Service Portal widget that allows users to draw, clear, and attach signatures using a element, compatible with both desktop and mobile devices. + +Features + +Draw signature using mouse or touch input. + +Clear the signature pad with one click. + +Convert the signature to a Base64 PNG string for storage or submission. + +No external dependencies (pure JavaScript & HTML5 Canvas). diff --git a/Modern Development/Service Portal Widgets/Stepper/CSS.css b/Modern Development/Service Portal Widgets/Stepper/CSS.css new file mode 100644 index 0000000000..ab493b0f80 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Stepper/CSS.css @@ -0,0 +1,75 @@ +/* Main container styling with padding and rounded corners */ +.stepper-container { + padding: 16px; + border-radius: 8px; +} + +/* Flex container for horizontal stepper layout */ +.stepper { + display: flex; + justify-content: space-between; + align-items: center; +} + +/* Individual step container with flex properties */ +.stepper-step { + display: flex; + align-items: center; + flex: 1; + position: relative; +} + +/* Circular step indicator base styling */ +.stepper-circle { + width: 40px; + height: 40px; + background: #e0e0e0; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 20px; + color: #fff; +} + +/* Green background for completed steps */ +.stepper-circle.completed { + background: #43A047; +} + +/* Blue background with border for active step */ +.stepper-circle.active { + background: #4285f4; + border: 3px solid #1976d2; +} + +/* Step label text styling */ +.stepper-label { + margin-left: 12px; + font-size: 20px; + color: #333; + opacity: 0.7; + font-weight: normal; +} + +/* Enhanced styling for completed and active step labels */ +.stepper-label.completed, +.stepper-label.active { + color: #222; + font-weight: bold; + opacity: 1; +} + +/* Connecting line between steps */ +.stepper-line { + height: 2px; + background: #e0e0e0; + flex: 1; + margin: 0 16px; +} + +/* Hide line after the last step */ +.stepper-step:last-child .stepper-line { + display: none; +} diff --git a/Modern Development/Service Portal Widgets/Stepper/HTML.html b/Modern Development/Service Portal Widgets/Stepper/HTML.html new file mode 100644 index 0000000000..252e3fd30d --- /dev/null +++ b/Modern Development/Service Portal Widgets/Stepper/HTML.html @@ -0,0 +1,28 @@ + +
    + +
    + +
    + +
    + + + + {{$index + 1}} +
    + + + {{step}} + + +
    +
    +
    +
    diff --git a/Modern Development/Service Portal Widgets/Stepper/README.md b/Modern Development/Service Portal Widgets/Stepper/README.md new file mode 100644 index 0000000000..f6ed8058ea --- /dev/null +++ b/Modern Development/Service Portal Widgets/Stepper/README.md @@ -0,0 +1,30 @@ +# Stepper Widget + +This custom widget provides a visually appealing **stepper** (multi-step progress indicator) for ServiceNow Service Portal, allowing you to display progress through steps such as campaign creation or onboarding. + +## Features + +- Shows steps with dynamic titles and highlights the current and completed steps. +- Steps and current step are passed in as widget options. +- Completed steps show a green icon. +- Handles widget options for showing steps and the current step. + +image + + +## Widget Options + +| Option | Type | Description | Example | +|---------------|--------|-----------------------------------------------|------------------------------------------| +| steps | String | Stringified array of step names (JSON array) | `["Step 1", "Step 2", "Step 3"]` | +| currentStep | Number | The current active step (0-based index) | `1` | + +## Usage + +1. Add the widget to your Service Portal page. +2. In the widget options, set: + - **steps** as a JSON string array (e.g., `["Step 1", "Step 2", "Step 3"]`) + - **currentStep** as the index of the current step (e.g., `1`) +image + + diff --git a/Modern Development/Service Portal Widgets/Stepper/Server Script.js b/Modern Development/Service Portal Widgets/Stepper/Server Script.js new file mode 100644 index 0000000000..4d87e7db21 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Stepper/Server Script.js @@ -0,0 +1,9 @@ +(function() { + // Parse and set the steps array from widget options + // If options.steps exists, parse the JSON string; otherwise, use empty array + data.steps = options.steps ? JSON.parse(options.steps) : []; + + // Parse and set the current step index from widget options + // If options.current_step exists, convert to integer; otherwise, default to 0 + data.currentStep = options.current_step ? parseInt(options.current_step) : 0; +})(); diff --git a/Modern Development/Service Portal Widgets/Sticky Notes/CSS-SCSS.css b/Modern Development/Service Portal Widgets/Sticky Notes/CSS-SCSS.css new file mode 100644 index 0000000000..28d91d204b --- /dev/null +++ b/Modern Development/Service Portal Widgets/Sticky Notes/CSS-SCSS.css @@ -0,0 +1,42 @@ +.sticky-notes-widget { + padding: 10px; +} + +.sticky-note { + display: inline-block; + width: 220px; + min-height: 100px; + margin: 8px; + padding: 10px; + border-radius: 6px; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + vertical-align: top; + font-size: 13px; + white-space: pre-wrap; +} + +.note-toolbar { + text-align: right; + margin-bottom: 3px; +} + +.note-content { + font-weight: 500; + color: #333; +} + +.note-meta { + margin-top: 6px; + font-size: 11px; + color: #666; +} + +.add-note-box { + margin-top: 20px; +} + +.note-controls { + display: flex; + gap: 5px; + margin-top: 5px; +} diff --git a/Modern Development/Service Portal Widgets/Sticky Notes/Client Script.js b/Modern Development/Service Portal Widgets/Sticky Notes/Client Script.js new file mode 100644 index 0000000000..51dc3802a0 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Sticky Notes/Client Script.js @@ -0,0 +1,19 @@ +api.controller=function($scope) { +/* widget controller */ +var c = this; + c.add =function(){ + c.data.action = "add"; + c.server.update().then(function(){ + c.data.action = undefined; + c.data.newColor =""; + c.data.text = ""; + }) + } + c.remove =function(i){ + c.data.i =i; + c.data.action = "remove"; + c.server.update().then(function(){ + c.data.action = undefined; + }) + } +} diff --git a/Modern Development/Service Portal Widgets/Sticky Notes/HTML.html b/Modern Development/Service Portal Widgets/Sticky Notes/HTML.html new file mode 100644 index 0000000000..3ef63816ea --- /dev/null +++ b/Modern Development/Service Portal Widgets/Sticky Notes/HTML.html @@ -0,0 +1,24 @@ +
    +

    My Sticky Notes

    +
    +
    + +
    +
    {{n.text}}
    +
    {{n.created_on | date:'short'}}
    +
    +
    +