Pictures/Icon's as selectors in Service Catalog

adamrauh
Kilo Expert

From the Service catalog, when building a catalog item (in this ex, for requesting a monitor cable or converter/extender), what is the best way to use pictures to select an item vs checkboxes, drop downs, etc.

 

Since I don't expect most users to know what mini-display to HDMI may mean, but recognize what it looks like, I'm thinking of ways to user selectable pictures as variables instead of drop-downs, etc. (unless upon being selected, the drop down shows the corresponding jpeg/icon).

 

Any ideas?

6 REPLIES 6

kalyani8
Giga Expert

Hi,

 

Is this feature now available?

 

We have got the same requirement.

 

Thanks,

Kalyani

Danny K
Tera Contributor

No idea if 10 years on anyone's still interested in this but... this requirement came up for me last night, and I couldn't find a solution anywhere. It seems like something that should exist!

Here's a super basic widget you can add to a Custom variable, that will enable you to use images and values from a table in SN to display an image grid on catalog items that users can select from.

The value associated with the image will then be written to another field in the form - ideally hidden - so there is a string value to present in the catalog task views that fulfillers see (as the widget will only render in service portal).


I offer this as a starting point for anyone else looking to figure out a fix for this. It's just a sketch at the moment, but could be helpful to others. It is functional - but your use-case may vary.



HTML template

<div class="image-picker-widget">
  <div class="image-grid" ng-style="gridStyle">
    <div class="image-tile" ng-repeat="img in data.images"
         ng-click="!isReadOnly && selectImage(img)"
         ng-class="{'selected': img === selectedImage, 'readonly': isReadOnly}">
      <img ng-src="{{img.image}}" alt="{{img.value}}" class="selectable-image" ng-style="{'max-width': options.maxImageWidth + 'px'}"/>
      <div class="image-label" ng-if="options.showLabels">
      	{{ img.value }}
    	</div>
    </div>
  </div>
</div>

 
CSS

.image-grid {
  display: grid;
  gap: 12px;
}

.image-tile {
  border: 2px solid transparent;
  cursor: pointer;
  border-radius: 4px;
  transition: border-color 0.2s;
  padding: 4px;
  text-align: center;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.image-tile.selected {
  border-color: #3276b1;
}

.image-tile.readonly {
  cursor: default;
  opacity: 0.6;
  pointer-events: none;
}

.image-label {
    margin-top: 6px;
    font-size: 0.85em;
    color: #444;
    text-align: center;
    word-break: break-word;
  }

.selectable-image {
  width: 100%;
  max-height: 120px;
  object-fit: contain;
}

 

Server Script

(function() {
  data.images = [];

  if (!options.tableName || !options.imageField || !options.valueField) {
    data.error = 'Missing required configuration options';
    return;
  }

  var gr = new GlideRecord(options.tableName);
  if (options.query) {
    gr.addEncodedQuery(options.query);
  }
	
  var sortField = options.sortField || options.valueField;

	gr.orderBy(sortField);
  gr.query();

  while (gr.next()) {
    var imageSysId = gr.getValue(options.imageField);
    var imageUrl = imageSysId ? '/sys_attachment.do?sys_id=' + imageSysId : '';

    data.images.push({
      sys_id: gr.getUniqueValue(),
      image: imageUrl,
      value: gr.getValue(options.valueField),
			sort: gr.getValue(sortField)
    });
  }
})();


Client controller

api.controller=function($scope, $timeout) {
  var c = this;
  
	$scope.selectedImage = null;
	$scope.isReadOnly = false;
  
  // Compute column count from options
  var colCount = parseInt($scope.options.maxColumns, 10);
  if (isNaN(colCount) || colCount < 1) {
    colCount = 3; // default fallback
  }
	// Use that info to figure out column widths
	var minColWidth = $scope.options.minColWidth || 150;
	var columnGap = 12;
	var totalWidth = (minColWidth * colCount) + (columnGap * (colCount - 1));

	$scope.gridStyle = {
		'display': 'grid',
		'grid-template-columns': 'repeat(auto-fit, minmax(' + minColWidth + 'px, 1fr))',
		'gap': columnGap + 'px',
		'max-width': totalWidth + 'px',
		'margin': '0 auto' // center it
	};


  $scope.selectImage = function(img) {
		if ($scope.isReadOnly) return;
    $scope.selectedImage = img;

    var gForm = $scope.page && $scope.page.g_form;

    if (gForm && $scope.options.targetVariable) {
      gForm.setValue($scope.options.targetVariable, img.value);
    } else {
      console.warn('g_form not available or targetVariable not set');
    }
  };
	
	
// Auto-select the matching image on load
  $scope.$watch('data.images', function(images) {
    if (!Array.isArray(images) || images.length === 0) return;
		

		var selectedValue = null;

		// First try to get value from g_form (editable form context)
		var gForm = $scope.page && $scope.page.g_form;

		// Use g_form.isReadOnly — supported in Service Portal
		if (gForm && $scope.options.targetVariable) {
			$scope.isReadOnly = gForm.isReadOnly($scope.options.targetVariable);
			selectedValue = gForm.getValue($scope.options.targetVariable);
		}

		// Fallback for post-submission views
		if (!gForm || !$scope.options.targetVariable) {
			$scope.isReadOnly = true;
		}


		if (gForm && $scope.options.targetVariable) {			
			selectedValue = gForm.getValue($scope.options.targetVariable);
		}

		// Fallback: use data.variable.value (read-only / submitted view)
		if (!selectedValue && $scope.data.variable && $scope.data.variable.value) {
			selectedValue = $scope.data.variable.value;
		}

		if (selectedValue) {
			for (var i = 0; i < images.length; i++) {
				if (images[i].value === selectedValue) {
					$scope.selectedImage = images[i];
					break;
				}
			}
		}

  });
}


Option Schema 

{
  "tableName": {
    "type": "string",
    "label": "Source Table"
  },
  "query": {
    "type": "string",
    "label": "Query"
  },
  "orderField": {
    "type": "string",
    "label": "Field to sort images by"
  },
  "imageField": {
    "type": "string",
    "label": "Image Field"
  },
  "valueField": {
    "type": "string",
    "label": "Value Field"
  },
  "targetVariable": {
    "type": "string",
    "label": "Target Variable Name"
  },
  "maxColumns": {
  "type": "number",
  "label": "Number of Columns",
  "default": 3
  },
  "minColWidth": {
   "type": "number",
  "label": "Minimum column width",
  "default": 50
  },
  "showLabels": {
  "type": "boolean",
  "label": "Show Value Labels Under Images",
  "default": false
}
}

  
How it works : 

On load, the widget goes and retrieves all the relevant data from your source table. Then creates some data objects to use rendering the HTML. It also calculates the styling for the html in the client. Then renders a grid of images.

Images when selected will populate another variable on the form with their associated string value.


Images and their associated values should be stored in a table in ServiceNow.

This table should contain at least two fields.
- An image type field
- A string field to store the value associated with that image.
It could also contain many other values, to allow you to store all relevant images in a single table and use the widget query option to limit the images shown.
Consider having a reference field to the catalog item table and an active flag for example.

The option values for the widget are :

  "tableName": the name of the table storing the images
  "query": an encoded query string to restrict the records returned from the table
  "imageField": the name of the field in the table that stores the image
  "valueField": the name of the field in the table that stores the value to be associated when that image is picked
  "targetVariable": the name of the variable in the catalog item that you want to have set on selection of the image. This should store a string type value - so any compatible variable type works (for example, choice)
Note : this should almost certainly be a hidden field for end-users on the catalog form submission view.
  "sortField": the name of the field in the table that you want to use to sort the images by
  "maxColumns": an integer value that defines the maximum number of columns in the image grid
  "minColWidth": an integer value that limits the minumum width of each column in the image grid
  "showLabels": a boolean value to indicate whether or not the "value" should be displayed underneath each image


To use the widget, create a "Custom" or "Custom with label" type variable. Add your widget name into the "Widget" field.

In the Default value for that variable, add your option values. For example :

{
  "tableName": "my_image_and_value_table",
  "query": "active=true",
  "imageField": "image",
  "valueField": "value",
  "targetVariable": "selected_value",
  "sortField": "order",
  "maxColumns": 4,
  "minColWidth": 150,
  "showLabels": true
}

This will show a max of 4 columns, at least 150px wide, with labels under the images.
Images will be loaded from the "image" field on the "my_image_and_value" table.
Only records with a value of "active" set to "true" will be shown.
They will be sorted based on the "order" value on that table.
When an image in the grid gets selected the variable named "selected_value" will be updated with the value from the "value" field in "my_image_and_value" table record that the image came from.