From aa77acf6a1bb71eb546a4f4533343b9067a16709 Mon Sep 17 00:00:00 2001 From: Ivan Betev Date: Fri, 10 Oct 2025 14:23:48 +0200 Subject: [PATCH 1/9] Catalog Item Explorer - Widget Update Functionality update: - Support for the external URL content items. - The default target window changed to "_self" (same window). - Option to open an item in a new window added at the end of the row. --- .../Catalog Item Explorer/template.html | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) 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.type}} +
    + +
    + +
    +
  • + -
    +
    @@ -54,5 +61,5 @@
    + {{c.filteredCatalogItems.length}}© 2025 Ivan Betev
    From ed18c98f8b654c836aa6d7ef4e42e6663e5c976a Mon Sep 17 00:00:00 2001 From: Ivan Betev Date: Fri, 10 Oct 2025 14:24:09 +0200 Subject: [PATCH 2/9] Update css.scss --- .../Catalog Item Explorer/css.scss | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) 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; From 562f55db65007ff70fd184c18ecc9ed3a7332ec9 Mon Sep 17 00:00:00 2001 From: Ivan Betev Date: Fri, 10 Oct 2025 14:24:31 +0200 Subject: [PATCH 3/9] Update script.js --- .../Catalog Item Explorer/script.js | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) 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; } })(); From 465b76da08bb761f4b7f9f7f5b56d1c916b03565 Mon Sep 17 00:00:00 2001 From: Ivan Betev Date: Fri, 10 Oct 2025 14:24:51 +0200 Subject: [PATCH 4/9] Update client_script.js --- .../Catalog Item Explorer/client_script.js | 277 +++++++++--------- 1 file changed, 139 insertions(+), 138 deletions(-) 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); - } }; From 9f771b367a910b3116ec991c9820a4fe140bf1eb Mon Sep 17 00:00:00 2001 From: Ivan Betev Date: Fri, 10 Oct 2025 14:25:14 +0200 Subject: [PATCH 5/9] Update options_schema.json From f47ea54a202aba0cc1a1c8deeae2b9bc947c0b0a Mon Sep 17 00:00:00 2001 From: Ivan Betev Date: Sun, 12 Oct 2025 15:37:31 +0200 Subject: [PATCH 6/9] Create script.js --- .../Hastag Extraction/script.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 Specialized Areas/Regular Expressions/Hastag Extraction/script.js diff --git a/Specialized Areas/Regular Expressions/Hastag Extraction/script.js b/Specialized Areas/Regular Expressions/Hastag Extraction/script.js new file mode 100644 index 0000000000..6e4eb7cbba --- /dev/null +++ b/Specialized Areas/Regular Expressions/Hastag Extraction/script.js @@ -0,0 +1,15 @@ +(function() { + + var demoData = "Quick test for hashtag and mention extraction in ServiceNow. " + + "Let's make sure it catches #Hack4Good #ServiceNow #regex and mentions like @ivan and @servicenow."; + + var tagRegex = /[#@][A-Za-z0-9_]+/g; + var matches = demoData.match(tagRegex); + + if (matches && matches.length > 0) { + gs.info('Found ' + matches.length + ' tags: ' + matches.join(', ')); + } else { + gs.info('No hashtags or mentions found.'); + } + +})(); From 32f3c779ca5b5ce547008fa66b14db3caf0088ec Mon Sep 17 00:00:00 2001 From: Ivan Betev Date: Sun, 12 Oct 2025 15:44:41 +0200 Subject: [PATCH 7/9] Create README.md --- .../Hastag Extraction/README.md | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 Specialized Areas/Regular Expressions/Hastag Extraction/README.md diff --git a/Specialized Areas/Regular Expressions/Hastag Extraction/README.md b/Specialized Areas/Regular Expressions/Hastag Extraction/README.md new file mode 100644 index 0000000000..86987c0ad3 --- /dev/null +++ b/Specialized Areas/Regular Expressions/Hastag Extraction/README.md @@ -0,0 +1,35 @@ +# Hashtag & Mention Extractor for ServiceNow + +A simple yet useful **ServiceNow Background Script** that extracts all hashtags (`#example`) and mentions (`@user`) from any text input using regular expressions. + +This script demonstrates how to apply JavaScript RegEx in server-side ServiceNow logic to parse, analyze, or extend user-generated text - ideal for text-analysis demos or lightweight automation projects. + +--- + +## πŸ’‘ Example Use Cases +- Automatically identify hashtags and mentions in **incident comments**, **knowledge articles**, or **survey feedback**. +- Build internal analytics to track trending topics like `#VPN`, `#email`, or `#network`. + +--- +## πŸš€ How to Run +1. In your ServiceNow instance, navigate to **System Definition β†’ Scripts – Background**. +2. Paste the script from this repository. +3. Click **Run Script**. + +--- + +## πŸ“¦ Reusability +The logic is **self-contained** within a single function block β€” no dependencies or external calls. +You can easily **copy and adjust it** to fit different contexts: +- Use it inside a **Business Rule**, **Script Include**, or **Flow Action Script**. +- Replace the sample `demoData` with a field value (e.g., `current.comments`) to analyze live data. +- Adjust the regex to detect other patterns (emails, keywords, etc.). + +This makes it a **plug-and-play snippet** for any ServiceNow application or table that requires quick text pattern recognition. + +--- + +## πŸ”§ Possible Extensions +- Parse live table data (`sys_journal_field`, `kb_knowledge`) instead of static text. +- Store extracted tags in a custom table for analytics. +- Schedule a nightly β€œTop Tags” report with **Flow Designer** or **PA Widgets**. From 07826f251bb17f968dafc583691efd28c18fca94 Mon Sep 17 00:00:00 2001 From: Ivan Betev Date: Sun, 12 Oct 2025 15:45:54 +0200 Subject: [PATCH 8/9] Rename README.md to README.md --- .../{Hastag Extraction => Hashtag & Mention Extractor}/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Specialized Areas/Regular Expressions/{Hastag Extraction => Hashtag & Mention Extractor}/README.md (100%) diff --git a/Specialized Areas/Regular Expressions/Hastag Extraction/README.md b/Specialized Areas/Regular Expressions/Hashtag & Mention Extractor/README.md similarity index 100% rename from Specialized Areas/Regular Expressions/Hastag Extraction/README.md rename to Specialized Areas/Regular Expressions/Hashtag & Mention Extractor/README.md From 6f2cc7f4bc089e3ca1d355a343b3836306ec6684 Mon Sep 17 00:00:00 2001 From: Ivan Betev Date: Sun, 12 Oct 2025 15:46:16 +0200 Subject: [PATCH 9/9] Rename script.js to script.js --- .../{Hastag Extraction => Hashtag & Mention Extractor}/script.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Specialized Areas/Regular Expressions/{Hastag Extraction => Hashtag & Mention Extractor}/script.js (100%) diff --git a/Specialized Areas/Regular Expressions/Hastag Extraction/script.js b/Specialized Areas/Regular Expressions/Hashtag & Mention Extractor/script.js similarity index 100% rename from Specialized Areas/Regular Expressions/Hastag Extraction/script.js rename to Specialized Areas/Regular Expressions/Hashtag & Mention Extractor/script.js