Configuration Item Icon Assignment Widget, Part III

When to use iterative development? You should use iterative development only on projects that you want to succeed.”
Martin Fowler

I probably could have wrapped all of this up last time, but I had not really given a whole lot of thought to a couple of critical decisions related to the last two items that needed to be coded out, the Save button and the Cancel button. For the Cancel button, the main question is where are you going to land once you leave the page. For the Save button, which is the bigger question, I need to figure out where I am going to store the information now that I have collected it on the screen. Since that’s a more complicated deliberation, let’s focus on the Cancel button first.

One thing that I wanted to do with the Cancel button was to pop up a confirmation that you really did want to bail, but only if you had made any changes that you would be losing by leaving the page. I wasn’t keeping track of whether or not you had made any changes, though, so I needed to fix that first. I created a variable called dataAltered and initialized it to false, and then I set it to true if you added or removed a row or selected an icon. With that in place, I built the function to handle the Cancel button clicks.

$scope.cancel = function() {
	if (dataAltered) {
		spModal.confirm('Discard your recent changes?').then(function(confirmed) {
			if (confirmed) {
				goBack();
			}
		});
	} else {
		goBack();
	}
};

Those of you paying close attention will notice that I still did not address the question of where to go after clicking the Cancel button; I just called a nonexistent function called goBack. That’s basically my way of putting things off until some future time when I am forced to deal with it, and usually I will create the function with a single line of code that pops an alert or write a log record to the let me know that I’ve made it that far. That let’s me test what I have done without having to deal with what I haven’t done. In this particular case, though, that was a short delay, because now it’s time to put real code in the goBack function, which means that it’s time to decide where people are going to land when they hit the Cancel button.

One of my first thoughts was that if this widget was sharing a screen with my Dynamic Service Portal Breadcrumbs widget, I could just go back one step in the chain to the previous screen. In order for that to work, I would need to know if I was sharing the page with that widget, which is not really possible at the moment. Ideally, that widget should announce its presence with some kind rootScope broadcast message that says something like, “Hey, I am here, and oh by the way, here is the URL for the previous page if you want to go back to it.” That actually sounds like a good idea, but it also sounds like a project for another day. I was planning on adding this widget to my existing Tools menu in the main UI, so I didn’t really see a lot of value in investing a lot of effort right now on getting it to work on the portal with breadcrumbs.

Ultimately, I decided to just go to the home page of the main UI, unless of course, we really were running in the portal, and in that case, I would just return to the home page of the portal. Since the main UI pages run inside of an iFrame, it seemed relatively simple enough to detect the difference by looking at the parent window. The final code turned out to be just this:

function goBack() {
	var destination = '?';
	if (window.parent.location.href != window.location.href) {
		destination = '/home.do';
	}
	window.location.href = destination;
}

That takes care of the easy part. Now it is finally time to make a decision on where to store this information. The simplest thing to do seemed to be to just add another column to the sys_db_object table for the name of the icon. That ‘s a fairly foundational table to the whole ServiceNow platform, though, and I really do not like to mess with those if I can avoid it. A preferable alternative would be to create a simple m2m table that had a column for the CI class and another column for the icon name. That’s much better, but there is still a certain amount of work involved in creating the table, saving the data to the table, and then reading the data back in from the table whenever you wanted to use it. What I finally decided to do was to just keep the icon map hard-coded in my Script Include, and then just update the script column of the Script Include whenever the Save button is clicked. Look, Ma — no tables!

My plan was to grab the script, substring off the parts of the script before and after the property list, rebuild the property list from the data on the screen, and then rebuild the script from the before and after pieces and the new property list. There is probably some regex wizardry that would do all of that with a single line of code, but most of that is all voodoo magic as far as I can tell, and I like to build things that can be comprehended by a little bit larger audience. In the end, my approach seemed to have an awful lot of code and variables involved, but it does work, so there you go.

function save() {
	var scriptGR = new GlideRecord('sys_script_include');
	if (scriptGR.get('7b6ebf052f8e18104425fcecf699b6f6')) {
		var original = scriptGR.getValue('script');
		var i = original.indexOf('map: {');
		if (i != -1) {
			var preamble = original.substring(0, i);
			var theRest = original.substring(i);
			i = theRest.indexOf('\n\t},');
			if (i != -1) {
				var postamble = theRest.substring(i);
				var separator = '';
				var newScript = preamble + 'map: {';
				for (i=0; i<data.itemArray.length; i++) {
					newScript += separator + '\n\t\t' + data.itemArray[i].id + ": '" + data.itemArray[i].icon + "'";
					separator = ',';
				}
				newScript += postamble;
				scriptGR.setValue('script', newScript);
				scriptGR.update();
				gs.addInfoMessage('The Configuration Item icon assignments have been saved');
			} else {
				gs.addErrorMessage('The Script Include to update has been corrupted.');
			}
		} else {
			gs.addErrorMessage('The Script Include to update has been corrupted.');
		}
	} else {
		gs.addErrorMessage('The Script Include to update was not found.');
	}
}

That’s a server side script, by the way, so we still need a client side script to send things over to the server. That one is pretty simple, though, and looks like this:

$scope.save = function() {
	c.data.action = 'save';
	c.server.update();
};

In fact, the whole client side script, including the code that we just added to detect changes and handle the two buttons now looks like this in its final form:

function CIIconAssignment($scope, spModal) {
	var c = this;
	var dataAltered = false;

	$scope.addSelected = function() {
		c.server.update();
		$('#snrp').select2("val","");
		dataAltered = true;
	};

	$scope.removeItem = function(inx) {
		spModal.confirm('Remove ' + c.data.itemArray[inx].id + ' from the list?').then(function(confirmed) {
			if (confirmed) {
				c.data.itemArray.splice(inx, 1);
				dataAltered = true;
			}
		});
	};

	$scope.selectIcon = function(inx) {
		spModal.open({
			title: 'Select Icon',
			widget: 'icon-picker',
			buttons: [
				{label: '${Cancel}', cancel: true}
			],
			size: 'sm',
		}).then(function(response) {
			c.data.itemArray[inx].icon = response.selected;
			dataAltered = true;
		});
	};

	$scope.save = function() {
		c.data.action = 'save';
		c.server.update();
	};

	$scope.cancel = function() {
		if (dataAltered) {
			spModal.confirm('Discard your recent changes?').then(function(confirmed) {
				if (confirmed) {
					goBack();
				}
			});
		} else {
			goBack();
		}
	};

	function goBack() {
		var destination = '?';
		if (window.parent.location.href != window.location.href) {
			destination = '/home.do';
		}
		window.location.href = destination;
	}
}

The last thing that I did was grab one of the existing sidebar menu options under my Tools section and use it as model for a new menu option to launch this page. After that, I used the new option to bring up the page and test everything out. Everything seems to work, and it came out looking pretty good, all things considered.

Configuration Item Icon Assignment widget and related menu option

One thing that I did realize during my testing is that there is nothing to stop you from deleting the root cmdb_ci entry, which serves as my default. I don’t really want you doing that, so I added an ng-hide to the delete icon that will remove that option when the item.id is cmdb_ci. You can see in the image above that there is no delete option for that row. That’s much better.

After playing around and testing, I ended up saving my Script Include several times over. Right now, it looks like this:

var ConfigurationItemIconMap = Class.create();
ConfigurationItemIconMap.prototype = {
	initialize: function() {
	},

	getIcon: function(ciClass) {
		var icon = this.map[ciClass];

		if (!icon) {
			ciClass = this.getParentClass(ciClass);
			if (ciClass) {
				icon = this.getIcon(ciClass);
			}
		}

		return icon;
	},

	getParentClass: function(ciClass) {
		var parentClass = '';

		var tableGR = new GlideRecord('sys_db_object');
		if (tableGR.get('name', ciClass)) {
			parentClass = tableGR.super_class.name;
		}

		return parentClass;
	},

	map: {
		cmdb_ci: 'configuration',
		cmdb_ci_aix_server: 'connect-viewdocument',
		cmdb_ci_computer: 'hardware',
		cmdb_ci_config_file: 'document',
		cmdb_ci_database: 'database',
		cmdb_ci_hardware: 'keyboard',
		cmdb_ci_linux_server: 'sp-wishlist-sm',
		cmdb_ci_server: 'server',
		cmdb_ci_spkg: 'software',
		cmdb_ci_unix_server: 'text-underlined',
		cmdb_ci_win_server: 'vtb-freeform'
	},

	type: 'ConfigurationItemIconMap'
};

Of course, that will change the next time that I use the tool, but I like the fact that I did not end up creating any extra columns or tables to pull this off. For those of you who like to play along at home, here is an Update Set containing all of the parts and pieces.

Configuration Item Icon Assignment Widget, Part II

Walking on water and developing software from a specification are easy if both are frozen.”
Edward V. Berard

It wasn’t entirely true that my empty shell CI Icon Assignment widget was completely devoid of all code underneath. I did have to toss in the function that launches the Retina Icon Picker in order to show that in action, but I was able to steal most of that code from the little test page that I had built to try that guy out.

$scope.selectIcon = function(inx) {
	spModal.open({
		title: 'Select Icon',
		widget: 'icon-picker',
		buttons: [
			{label: '${Cancel}', cancel: true}
		],
		size: 'sm',
	}).then(function(response) {
		c.data.itemArray[inx].icon = response.selected;
	}});
};

In my original test page, I had a second response function to handle the Cancel button, but for our purposes here, that’s not really necessary. If you hit the Cancel button on the pop-up on this widget, it will just close and nothing else will happen, which is perfectly fine. The other alteration that I had to make was to include an index argument so that when you did make a selection, it was applied to the appropriate line. Other than that, it’s pretty much what we were working with earlier.

But the icon picker is not the only element on the screen that needs a little code behind it. We’ll need to do something with the Configuration Item picker as well. Selecting a CI from the picker should result in a new row added to the table for the selected CI, and then the picker should be cleared in preparation for another selection to be made. Since we have to go look up the label for our second column, that sounds like a server side operation, so our client side code will be pretty simple:

$scope.addSelected = function() {
	c.server.update();
	$('#snrp').select2("val","");
};

The first line just kicks the process over to the server side, and then then second line clears out the value of the sn-record-picker so that it is ready for another selection. Over on the server side, we do the work to add the new CI type to the list, as long as it isn’t already there.

if (input) {
	data.itemArray = input.itemArray;
	if (input.classToAdd && input.classToAdd.value) {
		addClassToList(input.classToAdd.value);
	}
}

function addClassToList(key) {
	var foundIt = false;
	for (var i=0; !foundIt && i<data.itemArray.length; i++) {
		if (data.itemArray[i].id == key) {
			foundIt = true;
		}
	}
	if (!foundIt) {
		var thisItem = {};
		thisItem.id = key;
		thisItem.name = getItemName(key);
		data.itemArray.push(thisItem);
	}
	input.classToAdd = {};
}

function getItemName(key) {
	var ciGR = new GlideRecord(key);
	return ciGR.getLabel();
}

That takes care of adding an item to the list. Now we have to add some code to handle removing an item from the list. We put a Delete icon at the end of each line, so we just need to build the code that will run when someone clicks on that icon. Since it is a delete, we will want to pop up a quick confirm dialog before we actually remove the item from the list, just to make sure that they did that on purpose. All of that can be handled on the client side, and fairly simply.

$scope.removeItem = function(inx) {
	spModal.confirm('Remove ' + c.data.itemArray[inx].id + ' from the list?').then(function(confirmed) {
		if (confirmed) {
			c.data.itemArray.splice(inx, 1);
		}
	});
};

To test all of that out, I added a few items to the list, selected an icon for each, and then tried to remove a few. During my testing, I realized that adding enough items to the list scrolls the icon selector off of the page and out of sight, which I didn’t really like, so I did a little rearranging and put the selector on top of the list instead of underneath. With that change, the modal delete confirmation now looks like this:

Delete confirmation pop-up on reconfigured screen layout

So now we have all of the code in place to select a CI, to select an icon for the CI, and to remove a CI from the list. Now all we have left to do is to put some code under the two buttons at the bottom of the screen. Of course, coding the Save button would be much easier if I knew where I was going store this information once I am through building the list, but I haven’t really given much thought to that just yet, since we hadn’t quite gotten to that part until just this moment. Now that writing the code for the buttons is the only thing left to do, it’s time to figure that out. But that could potentially be an entire installment in and of itself, so I think we’ll stop here and save that exercise for another day.

Configuration Item Icon Assignment Widget

“You don’t have to see the whole staircase, just take the first step.”
Martin Luther King, Jr.

The main reason that I wanted to find a way to include an Icon in a pick list is because I wanted to assign different icons to various classes of Configuration Items to help visually distinguish the items based on their type. Now that I know that I can’t just add an icon to a single SELECT statement, I’m not exactly sure how I am going to that, but that’s not today’s issue. When all is said and done, I may not even be able to do what I am hoping to do, but I’m not working on that part right now. I try not to get too distracted by the things that I don’t understand or don’t know how I’m going to accomplish before it’s time. Since I can’t code everything all at once, I don’t need to solve all of the mysteries all at once, either. My theory is that I should be able to figure it out when the time comes, so there is no need to worry about it right now. To keep productive, and to maintain focus, I like to deal with things One Piece at a Time.

Today, I want to build a function that will return the appropriate icon name based on a passed configuration item class, which will give me something to call when it is time to go get the icon associated with a particular item. I want to return an icon name in all circumstances, so if the specific CI class is not mapped to an icon, then I want the function to check the parent class, and basically keep doing that until an icon name is found. If it makes it all the way to the top and there is still no icon, then there needs to be a default, because one way or another, I want to return an icon name no matter what. I will eventually stuff this function into a Script Include of some kind, but for now, I just want to code out the function. Here is what I came up with.

getIcon: function(ciClass) {
	var icon = this.map[ciClass];
	if (!icon) {
		ciClass = this.getParentClass(ciClass);
		if (ciClass) {
			icon = this.getIcon(ciClass);
		}
	}
	return icon;
},

getParentClass: function(ciClass) {
	var parentClass = '';
	var tableGR = new GlideRecord('sys_db_object');
	if (tableGR.get('name', ciClass)) {
		parentClass = tableGR.super_class.name;
	}
	return parentClass;
},

OK, it turns out that it is actually two functions, but you get the idea. This code assumes that there is a map included that associates icon names to CI classes, and that the map is keyed by CI class and returns the icon name. We don’t have to have the fully populated map right now, but to test the code, we will at least need to stub it out with a minimum of one item. That should be fairly simple to do.

map: {
	cmdb_ci: 'configuration'
},

Since the cmdb_ci class is basically the root class of all Configuration Items, defining a value for that class essentially establishes the default. As you crawl up the parentage of any specific CI class, you will eventually find your way to the cmdb_ci class, so that should satisfy my requirement that there should always be a default response from the function call.

The stubbed-out map is a good start, but I want to build up my map using some kind of tool that will allow me to select a CI class and then use my new icon picker to select an appropriate icon for the class. Something that would look like this:

CI/Icon map maintenance tool

Once you added your CI class to the list, then you could click on the little magnifying glass icon to launch our icon picker to select your icon.

Using the Icon Picker to select an icon for a CI class

What you are seeing is just a screen mock up at this point, but putting the screen together is always a good place to start. Here is the HTML that I used to produce the screen image.

<div class="panel">
<div style="width: 100%; padding: 5px 50px;">
  <h2 style="width: 100%; text-align: center;">${Configuration Item Icon Assignment}</h2>
  <table class="table table-hover table-condensed">
    <thead>
      <tr>
        <th style="text-align: center;">Item</th>
        <th style="text-align: center;">Label</th>
        <th style="text-align: center;">Icon</th>
        <th style="text-align: center;">Icon Name</th>
        <th style="text-align: center;">Delete</th>
      </tr>
    </thead>
    <tbody>
      <tr ng-repeat="item in c.data.itemArray track by item.id | orderBy: 'id'" ng-hide="item.removed">
        <td data-th="Item"><input class="form-control" ng-model="item.id" readonly="readonly"/></td>
        <td data-th="Label"><input class="form-control" ng-model="item.name" readonly="readonly"/></td>
        <td data-th="Icon" style="text-align: center;"><span style="font-size: 25px;" class="icon icon-{{item.icon}}"></span></td>
        <td data-th="Icon Name">
          <span class="input-group" style="width: 100%;">
            <input class="form-control" ng-model="item.icon" readonly="readonly"/>
            <span class="input-group-btn" ng-click="selectIcon($index)" aria-hidden="false">
              <button class="btn-ref btn btn-default">
                <span class="icon icon-search" aria-hidden="true"></span>
                <span class="sr-only ng-binding">${Select an icon}</span>
              </button>
            </span>
          </span>
        </td>
        <td data-th="Delete" style="text-align: center;"><img src="/images/delete_row.gif" ng-click="removeItem($index)" alt="Click here to remove this item from the list" title="Click here to remove this item from the list" style="cursor: pointer;"/></td>
      </tr>
    </tbody>
  </table>
  <p>To add another Configuration Item to the list, select an item from below:</p>
  <sn-record-picker
    id="snrp"
    field="data.classToAdd"
    ng-change="addSelected()"
    table="'sys_db_object'"
    default-query="'super_class=72e7251abc002300aadb875973a34b54^ORsuper_class.super_class=72e7251abc002300aadb875973a34b54^ORsuper_class.super_class.super_class=72e7251abc002300aadb875973a34b54^ORsuper_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^ORsuper_class.super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^ORsuper_class.super_class.super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^ORsuper_class.super_class.super_class.super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54'"
    display-field="'label'"
    display-fields="'name'"
    search-fields="'label'"
    value-field="'name'">
  </sn-record-picker>
  <br/>
  <p>To remove an item from the list, click on the Delete icon.</p>
</div>

<div style="width: 100%; padding: 5px 50px; text-align: center;">
  <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
   
  <button ng-click="cancel()" class="btn ng-binding ng-scope" role="button" title="Click here to cancel your changes">Cancel</button>
</div>
</div>

For the sn-record-picker, I used my old friend, the sn-record-picker Helper, but before I got that far, I had to first work out the query. I wanted any CI table (the table name is also the class name), so I was looking for any table that was based on the cmdb_ci table. There may be an easier way to do this, but this works, so I just went with it.

super_class=72e7251abc002300aadb875973a34b54^OR
super_class.super_class=72e7251abc002300aadb875973a34b54^OR
super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^OR
super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^OR
super_class.super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^OR
super_class.super_class.super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^OR
super_class.super_class.super_class.super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54

That’s an ugly, brute force way of doing that, but it gets the job done, which is really all that we are after at this point. The real fun will be putting all of the code behind the screen to both build up the map and then store it somewhere. That’s pretty complicated stuff, and I’m not really sure exactly how I am going to do all of that, so we’ll save that as an exercise for another day.

Retina Icon Picker

“This life’s hard, but it’s harder if you’re stupid.”
George V. Higgins

Nothing is ever easy. Oh sure, it all seems easy in the beginning, but then you get into it and you realize that things are just a little more complicated than they first appeared. It’s always something. But then, as my old boss used to explain to me, if it was easy, then anyone could do it!

All I really wanted to do was to create a SELECT statement with an option list of Retina Icons that included the icon image itself in the drop-down selection list. How hard could that be? Something that might come out looking somewhat like this:

SELECT statement with icon images in the option list

Unfortunately, the current SELECT statement doesn’t support that where SIZE is not greater than 1. I have no idea why it is that there is that distinction, but on a single line SELECT statement, it ignores the CSS for the icon. However, once you add SIZE=”2″ or something else other than 1, it works great. That’s really nice, but that’s not what I was after. I didn’t want to take up more than one line on my form for this, so after searching fruitlessly for a way to get around this annoyance, I finally accepted the reality that I was going to need to look for something else.

Eventually, I decided to stick with the SELECT, but put it in a modal pop-up that could show a dozen or so lines when needed, and then just go away when I was done making my selection. That seemed easy enough. How hard could it be?

To start out, I copied all of the values from that master icon page and used them to create an array of all of the options.

data.option = [
	{"label": "accessibility", "value": "accessibility"},
	{"label": "activity-circle", "value": "activity-circle"},
	{"label": "activity-stream", "value": "activity-stream"},
	{"label": "activity", "value": "activity"},
	{"label": "add-circle-empty", "value": "add-circle-empty"},
	{"label": "add-circle", "value": "add-circle"},
	{"label": "add", "value": "add"},
	{"label": "alert-triangle", "value": "alert-triangle"},
	{"label": "alert", "value": "alert"},
	{"label": "align-center", "value": "align-center"},
	{"label": "align-left", "value": "align-left"},
	{"label": "align-right", "value": "align-right"},
	{"label": "all-apps", "value": "all-apps"},
	{"label": "application-generic", "value": "application-generic"},
	{"label": "archive", "value": "archive"},
	{"label": "arrow-down-rounded", "value": "arrow-down-rounded"},
	{"label": "arrow-down-triangle", "value": "arrow-down-triangle"},
	{"label": "arrow-down", "value": "arrow-down"},
	{"label": "arrow-left-rounded", "value": "arrow-left-rounded"},
	{"label": "arrow-left", "value": "arrow-left"},
	{"label": "arrow-right-rounded", "value": "arrow-right-rounded"},
	{"label": "arrow-right", "value": "arrow-right"},
	{"label": "arrow-up-rounded", "value": "arrow-up-rounded"},
	{"label": "arrow-up", "value": "arrow-up"},
	{"label": "article-document", "value": "article-document"},
	{"label": "barcode", "value": "barcode"},
	{"label": "blog", "value": "blog"},
	{"label": "book-open", "value": "book-open"},
	{"label": "book", "value": "book"},
	{"label": "boolean", "value": "boolean"},
	{"label": "bot", "value": "bot"},
	{"label": "brand-mobile", "value": "brand-mobile"},
	{"label": "brand-now", "value": "brand-now"},
	{"label": "brand-service", "value": "brand-service"},
	{"label": "brand-servicenow", "value": "brand-servicenow"},
	{"label": "calendar", "value": "calendar"},
	{"label": "cards", "value": "cards"},
	{"label": "cart-full", "value": "cart-full"},
	{"label": "cart", "value": "cart"},
	{"label": "catalog", "value": "catalog"},
	{"label": "chart-do", "value": "chart-do"},
	{"label": "chart-pi", "value": "chart-pi"},
	{"label": "check-circle", "value": "check-circle"},
	{"label": "check", "value": "check"},
	{"label": "checkbox-checked", "value": "checkbox-checked"},
	{"label": "checkbox-empty", "value": "checkbox-empty"},
	{"label": "chevron-down", "value": "chevron-down"},
	{"label": "chevron-left", "value": "chevron-left"},
	{"label": "chevron-right-circle-solid", "value": "chevron-right-circle-solid"},
	{"label": "chevron-right-circle", "value": "chevron-right-cicle"},
	{"label": "chevron-right", "value": "chevron-right"},
	{"label": "chevron-up", "value": "chevron-up"},
	{"label": "circle-solid", "value": "circle-solid"},
	{"label": "clear-cache", "value": "clear-cache"},
	{"label": "clear", "value": "clear"},
	{"label": "clockwise", "value": "clockwise"},
	{"label": "code", "value": "code"},
	{"label": "cog-changes", "value": "cog-changes"},
	{"label": "cog-selected", "value": "cog-selected"},
	{"label": "cog", "value": "cog"},
	{"label": "collaboration", "value": "collaboration"},
	{"label": "comment-add", "value": "comment-add"},
	{"label": "comment-hollow", "value": "comment-hollow"},
	{"label": "comment", "value": "comment"},
	{"label": "company-feed", "value": "company-feed"},
	{"label": "condition", "value": "condition"},
	{"label": "configuration", "value": "configuration"},
	{"label": "connect-adduser-sm", "value": "connect-adduser-sm"},
	{"label": "connect-adduser", "value": "connect-adduser"},
	{"label": "connect-close-sm", "value": "connect-close-sm"},
	{"label": "connect-close", "value": "connect-close"},
	{"label": "connect-minimize-sm", "value": "connect-minimize-sm"},
	{"label": "connect-minimize", "value": "connect-minimize"},
	{"label": "connect-newwin", "value": "connect-newwin"},
	{"label": "connect-newwindow-sm", "value": "connect-newwindow-sm"},
	{"label": "connect-viewdocument-sm", "value": "connect-viewdocument-sm"},
	{"label": "connect-viewdocument", "value": "connect-viewdocument"},
	{"label": "connection", "value": "connection"},
	{"label": "console", "value": "console"},
	{"label": "copy", "value": "copy"},
	{"label": "counter-clockwise", "value": "counter-clockwise"},
	{"label": "cross-circle", "value": "cross-circle"},
	{"label": "cross", "value": "cross"},
	{"label": "cursor-move", "value": "cursor-move"},
	{"label": "cursor-select", "value": "cursor-select"},
	{"label": "dashboard", "value": "dashboard"},
	{"label": "database-error", "value": "database-error"},
	{"label": "database", "value": "database"},
	{"label": "date-time", "value": "date-time"},
	{"label": "debug", "value": "debug"},
	{"label": "default-knowledge-base", "value": "default-knowledge-base"},
	{"label": "delete-selected", "value": "delete-selected"},
	{"label": "delete", "value": "delete"},
	{"label": "directions", "value": "directions"},
	{"label": "discovery-connection", "value": "discovery-connection"},
	{"label": "discovery-identification", "value": "discovery-identification"},
	{"label": "discovery-pattern", "value": "discovery-pattern"},
	{"label": "discovery-square", "value": "discovery-square"},
	{"label": "discovery-step", "value": "discovery-step"},
	{"label": "document-all-generic", "value": "document-all-generic"},
	{"label": "document-attachment", "value": "document-attachment"},
	{"label": "document-code", "value": "document-code"},
	{"label": "document-doc", "value": "document-doc"},
	{"label": "document-multiple", "value": "document-multiple"},
	{"label": "document-pdf", "value": "document-pdf"},
	{"label": "document-ppt", "value": "document-ppt"},
	{"label": "document-txt", "value": "document-txt"},
	{"label": "document-xls", "value": "document-xls"},
	{"label": "document-zip", "value": "document-zip"},
	{"label": "document", "value": "document"},
	{"label": "double-chevron-left", "value": "double-chevron-left"},
	{"label": "double-chevron-right", "value": "double-chevron-right"},
	{"label": "download-sourcecode", "value": "download-sourcecode"},
	{"label": "download", "value": "download"},
	{"label": "drag-dots", "value": "drag-dots"},
	{"label": "drag", "value": "drag"},
	{"label": "drawer-selected", "value": "drawer-selected"},
	{"label": "drawer", "value": "drawer"},
	{"label": "edit-syntax", "value": "edit-syntax"},
	{"label": "edit", "value": "edit"},
	{"label": "ellipsis-vertical", "value": "ellipsis-vertical"},
	{"label": "ellipsis", "value": "ellipsis"},
	{"label": "empty-circle", "value": "empty-circle"},
	{"label": "endpoint", "value": "endpoint"},
	{"label": "envelope-open", "value": "envelope-open"},
	{"label": "envelope-subscribe", "value": "envelope-subscribe"},
	{"label": "envelope-unsubscribe", "value": "envelope-unsubscribe"},
	{"label": "error-circle", "value": "error-circle"},
	{"label": "error", "value": "error"},
	{"label": "essentials", "value": "essentials"},
	{"label": "export", "value": "export"},
	{"label": "filter", "value": "filter"},
	{"label": "first", "value": "first"},
	{"label": "fit-width", "value": "fit-width"},
	{"label": "floor-plan", "value": "floor-plan"},
	{"label": "folder", "value": "folder"},
	{"label": "form", "value": "form"},
	{"label": "format", "value": "format"},
	{"label": "fullscreen", "value": "fullscreen"},
	{"label": "glasses", "value": "glasses"},
	{"label": "global", "value": "global"},
	{"label": "hardware", "value": "hardware"},
	{"label": "help", "value": "help"},
	{"label": "history", "value": "history"},
	{"label": "home", "value": "home"},
	{"label": "hr", "value": "hr"},
	{"label": "identification", "value": "identification"},
	{"label": "image", "value": "image"},
	{"label": "indent", "value": "indent"},
	{"label": "info", "value": "info"},
	{"label": "insert-table", "value": "insert-table"},
	{"label": "it", "value": "it"},
	{"label": "key", "value": "key"},
	{"label": "keyboard", "value": "keyboard"},
	{"label": "label-dot", "value": "label-dot"},
	{"label": "label", "value": "label"},
	{"label": "last", "value": "last"},
	{"label": "layout", "value": "layout"},
	{"label": "lightbulb", "value": "lightbulb"},
	{"label": "like", "value": "like"},
	{"label": "link", "value": "link"},
	{"label": "list", "value": "list"},
	{"label": "livefeed", "value": "livefeed"},
	{"label": "loading", "value": "loading"},
	{"label": "location", "value": "location"},
	{"label": "locked", "value": "locked"},
	{"label": "loop", "value": "loop"},
	{"label": "mail", "value": "mail"},
	{"label": "marker", "value": "marker"},
	{"label": "maximize", "value": "maximize"},
	{"label": "menu-arrows", "value": "menu-arrows"},
	{"label": "menu", "value": "menu"},
	{"label": "minimize", "value": "minimize"},
	{"label": "mobile", "value": "mobile"},
	{"label": "move", "value": "move"},
	{"label": "my-feed", "value": "my-feed"},
	{"label": "navigator", "value": "navigator"},
	{"label": "new-above", "value": "new-above"},
	{"label": "new-below", "value": "new-below"},
	{"label": "new-ticket", "value": "new-ticket"},
	{"label": "new-window", "value": "new-window"},
	{"label": "not-started-circle", "value": "not-started-circle"},
	{"label": "notification-bell", "value": "notification-bell"},
	{"label": "number", "value": "number"},
	{"label": "open-document-new-tab", "value": "open-document-new-tab"},
	{"label": "or", "value": "or"},
	{"label": "panel-display-bottom", "value": "panel-display-bottom"},
	{"label": "panel-display-popout", "value": "panel-display-popout"},
	{"label": "panel-display-right", "value": "panel-display-right"},
	{"label": "paperclip", "value": "paperclip"},
	{"label": "pause", "value": "pause"},
	{"label": "percent", "value": "percent"},
	{"label": "phone", "value": "phone"},
	{"label": "phonecall-incoming", "value": "phonecall-incoming"},
	{"label": "phonecall-keypad", "value": "phonecall-keypad"},
	{"label": "phonecall-outgoing", "value": "phonecall-outgoing"},
	{"label": "play", "value": "play"},
	{"label": "poll", "value": "poll"},
	{"label": "pop-in", "value": "pop-in"},
	{"label": "pop-out", "value": "pop-out"},
	{"label": "power", "value": "power"},
	{"label": "preview", "value": "preview"},
	{"label": "print", "value": "print"},
	{"label": "queue", "value": "queue"},
	{"label": "radio-numeric-scale", "value": "radio-numeric-scale"},
	{"label": "radio-scale", "value": "radio-scale"},
	{"label": "redo-action", "value": "redo-action"},
	{"label": "refresh", "value": "refresh"},
	{"label": "remove", "value": "remove"},
	{"label": "replace-all", "value": "replace-all"},
	{"label": "replace", "value": "replace"},
	{"label": "required", "value": "required"},
	{"label": "run-command", "value": "run-command"},
	{"label": "save", "value": "save"},
	{"label": "script-check", "value": "script-check"},
	{"label": "script-comment", "value": "script-comment"},
	{"label": "script", "value": "script"},
	{"label": "search-database", "value": "search-database"},
	{"label": "search", "value": "search"},
	{"label": "select", "value": "select"},
	{"label": "server", "value": "server"},
	{"label": "share", "value": "share"},
	{"label": "software", "value": "software"},
	{"label": "sort-ascending", "value": "sort-ascending"},
	{"label": "sort-descending", "value": "sort-descending"},
	{"label": "sp-wishlist-sm", "value": "sp-wishlist-sm"},
	{"label": "sp-wishlist", "value": "sp-wishlist"},
	{"label": "spell-check", "value": "spell-check"},
	{"label": "split-vertical", "value": "split-vertical"},
	{"label": "star-empty", "value": "star-empty"},
	{"label": "star", "value": "star"},
	{"label": "step-in", "value": "step-in"},
	{"label": "step-out", "value": "step-out"},
	{"label": "step-over", "value": "step-over"},
	{"label": "stop-watch", "value": "stop-watch"},
	{"label": "stop", "value": "stop"},
	{"label": "stream-all-input", "value": "stream-all-input"},
	{"label": "stream-one-input", "value": "stream-one-input"},
	{"label": "string", "value": "string"},
	{"label": "sub-elements", "value": "sub-elements"},
	{"label": "subtract", "value": "subtract"},
	{"label": "success-circle", "value": "success-circle"},
	{"label": "success", "value": "success"},
	{"label": "syntax-check", "value": "syntax-check"},
	{"label": "tab", "value": "tab"},
	{"label": "table-sm", "value": "table-sm"},
	{"label": "table", "value": "table"},
	{"label": "tack", "value": "tack"},
	{"label": "target", "value": "target"},
	{"label": "template", "value": "template"},
	{"label": "text-bold", "value": "text-bold"},
	{"label": "text-italic", "value": "text-italic"},
	{"label": "text-style-add", "value": "text-style-add"},
	{"label": "text-style-clear", "value": "text-style-clear"},
	{"label": "text-underlined", "value": "text-underlined"},
	{"label": "text", "value": "text"},
	{"label": "threshold", "value": "threshold"},
	{"label": "timeline", "value": "timeline"},
	{"label": "today", "value": "today"},
	{"label": "translation", "value": "translation"},
	{"label": "trash", "value": "trash"},
	{"label": "tree-right", "value": "tree-right"},
	{"label": "tree", "value": "tree"},
	{"label": "undo-action", "value": "undo-action"},
	{"label": "undo", "value": "undo"},
	{"label": "unindent", "value": "unindent"},
	{"label": "unlink", "value": "unlink"},
	{"label": "unlocked", "value": "unlocked"},
	{"label": "upload", "value": "upload"},
	{"label": "user-add", "value": "user-add"},
	{"label": "user-group", "value": "user-group"},
	{"label": "user-profile", "value": "user-profile"},
	{"label": "user-selected", "value": "user-selected"},
	{"label": "user-subtract", "value": "user-subtract"},
	{"label": "user", "value": "user"},
	{"label": "vcr-down", "value": "vcr-down"},
	{"label": "vcr-left", "value": "vcr-left"},
	{"label": "vcr-right", "value": "vcr-right"},
	{"label": "vcr-up", "value": "vcr-up"},
	{"label": "video", "value": "video"},
	{"label": "view", "value": "view"},
	{"label": "vtb-flexible-outline", "value": "vtb-flexible-outline"},
	{"label": "vtb-flexible-sm", "value": "vtb-flexible-sm"},
	{"label": "vtb-flexible", "value": "vtb-flexible"},
	{"label": "vtb-freeform-sm", "value": "vtb-freeform-sm"},
	{"label": "vtb-freeform", "value": "vtb-freeform"},
	{"label": "vtb-guided-sm", "value": "vtb-guided-sm"},
	{"label": "vtb-guided", "value": "vtb-guided"},
	{"label": "warning-circle", "value": "warning-circle"},
	{"label": "wishlist-sm", "value": "wishlist-sm"},
	{"label": "wishlist", "value": "wishlist"},
	{"label": "work-note", "value": "work-note"},
	{"label": "workflow-active", "value": "workflow-active"},
	{"label": "workflow-approval-action", "value": "workflow-approval-action"},
	{"label": "workflow-approval-rejected", "value": "workflow-approval-rejected"},
	{"label": "workflow-approved", "value": "workflow-approved"},
	{"label": "workflow-check", "value": "workflow-check"},
	{"label": "workflow-complete", "value": "workflow-complete"},
	{"label": "workflow-late", "value": "workflow-late"},
	{"label": "workflow-on-hold", "value": "workflow-on-hold"},
	{"label": "workflow-pending", "value": "workflow-pending"},
	{"label": "workflow-progress", "value": "workflow-progress"},
	{"label": "workflow-rejected", "value": "workflow-rejected"},
	{"label": "workflow-requested", "value": "workflow-requested"},
	{"label": "workflow-skip", "value": "workflow-skip"},
	{"label": "workflow", "value": "workflow"},
	{"label": "zoom-in", "value": "zoom-in"},
	{"label": "zoom-out", "value": "zoom-out"}
];

Once I had my array, it was just a matter of throwing a little HTML together to build the SELECT statement and to loop through the array to create all of the OPTION statements.

<div>
  <select ng-model="data.selected" size="10">
    <option ng-repeat="opt in data.option" class="icon icon-{{opt.value}}" value="{{opt.value}}">   {{opt.label}}</option>
  </select>
</div>

That was enough to display the results, so I built myself a little test page to launch the widget in a modal popup, just to see how it looked.

Modal pop-up of icon SELECT statement

Well, I’m not sure how that blank option got in there in that first position, but other than that, it looks pretty good. Now I just have to figure out how to get the selection back to the main widget once the operator makes their choice. Functionally, there are a couple of different ways to go here: you could have user click on their selection and then click on an OK button to complete the process, or you could go with the assumption that clicking on an option was final and just close out the window right then and there and pass the choice back to the primary screen. Personally, I lean towards the second option; it’s a little rude, but it saves you an unnecessary extra click. From my perspective, when you make a selection, you’re done and we’re out of here.

The first thing to do then, is to add an ng-change to the SELECT statement.

<div>
  <select ng-model="data.selected" ng-change="iconSelected();" size="10">
    <option ng-repeat="opt in data.option" class="icon icon-{{opt.value}}" value="{{opt.value}}">   {{opt.label}}</option>
  </select>
</div>

Then we have to create the referenced function in the Client controller. The behavior that we want is for the pop-up to return the selection to the caller and then go away. I wasn’t exactly sure how to do that, but I did a little research and it turns out that there are a couple of options here as well. You can send in a shared object as one of the widget options when you first open the spModal window, which gives you a place to store things that both the opener and the opened can access, or you can emulate a button click, and pass information back as an argument to the click function. The syntax on that last one seems a little bizarre, but I tried it and it worked, so I decided to go that route. My function turned out to simply be this:

$scope.iconSelected = function() {
	$scope.$parent.$parent.buttonClicked({selected: c.data.selected});
};

Passing the value of the selected option in the buttonClicked function both returns the data to the caller and closes the modal window. Sweet! To open the window and grab the selected value, the code in my little test page ended up looking like this:

spModal.open({
	title: 'Select Icon',
	widget: 'icon-picker',
	buttons: [
		{label: '${Cancel}', cancel: true}
	],
	size: 'sm',
}).then(function(response) {
	alert(JSON.stringify(response));
}, function () {
	alert('Cancelled');
});

You will notice that there are two response functions, one for a normal completion of the task and a second one to handle the Cancel button or the forced closing of the modal pop-up. Using this temporary test page, we can launch the modal and click around and see what happens. If we select an option, we should get an alert that contains the option value selected.

Alert showing the option selected

Well, that all seems to work. In my little test page, all I do is pop the alert to prove that things are working. In actual use, I would update the value of the target field, but at this point, I’m just focused on building the pop-up selector. Building out a function that will actually use this pop-up selector is a project for another day.