From aa77acf6a1bb71eb546a4f4533343b9067a16709 Mon Sep 17 00:00:00 2001 From: Ivan Betev Date: Fri, 10 Oct 2025 14:23:48 +0200 Subject: [PATCH 1/7] 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/7] 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/7] 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/7] 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/7] Update options_schema.json From 65221f2d17a279bf67b8c40706c48882c04f469f Mon Sep 17 00:00:00 2001 From: Ivan Betev Date: Fri, 10 Oct 2025 14:50:19 +0200 Subject: [PATCH 6/7] Update README.md --- .../Service Portal Widgets/Catalog Item Explorer/README.md | 5 +++++ 1 file changed, 5 insertions(+) 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..154b12aa19 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. From b587e60837ba53bea2d11b4d928a2d85337a81c1 Mon Sep 17 00:00:00 2001 From: Ivan Betev Date: Fri, 10 Oct 2025 14:56:28 +0200 Subject: [PATCH 7/7] Update README.md --- .../Service Portal Widgets/Catalog Item Explorer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 154b12aa19..5700ff90ba 100644 --- a/Modern Development/Service Portal Widgets/Catalog Item Explorer/README.md +++ b/Modern Development/Service Portal Widgets/Catalog Item Explorer/README.md @@ -25,7 +25,7 @@ The primary goal of the Catalog Item Explorer widget is to serve as a valuable l 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): +## 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.