Configurable Data Table Widget Content Selector, Part II

“Code reuse is the Holy Grail of Software Engineering.”
Douglas Crockford

So far, I’ve hacked up one of the stock Data Table widgets and created one example of a configuration script for my proposed Data Table Content Selector. Now it’s time to actually build the widget itself. Here is how I envision it appearing based on the sample configuration that I put together earlier:

List Content Selector based on earlier configuration

Basically, it contains individual sections for each of the three configurable selections: Perspective, State, and Table. Different configurations would, of course, produce different results, but the concept remains the same regardless of your particular configuration: you make a selection using the widget and then the widget will construct the appropriate URL based on your selection and then take you to that URL. The hacked Data Table from URL Definition widget, which shares the page, will then pick up those URL parameters, which will in turn drive the records that appear in the Data Table. Here is one possible arrangement where the Data Table widget consumes 75% of the screen and the companion selector widget occupies the remaining 25% on the right hand side.

One potential arrangement of the two widgets on the page

Where you place your new widget is entirely up to you. The example above happens to use a 9/3 split to put the selector on the right, but you could also use a 3/9 split to put it on the left, or if you did not want to take away from the width of the Date Table, you could place it above, or below (not recommended), or even put it into some kind of modal pop-up box accessed via a simple link or button. The point is, you can put it wherever you want and it won’t affect the way that it works. Although the two widgets do share the same portal page, they don’t actually communicate with each other directly. There is no broadcasting or listening for messages between the two. The selector simply constructs the appropriate URL and then both widgets draw from the URL parameters to determine what ends up on the screen. Here is another example with a different arrangement and a different configuration:

Another potential arrangement of the two widgets on the page

In order to handle all possible situations, there has to be some default behavior in the event that no URL parameters are provided. Based on the configuration file, default values can be determined for all three selections (perspective, state, and table), and there can even be a default response for an invalid configuration. No matter what the circumstances, you want the page to do something so things are not left broken. To retain the order, configuration parameters are specified in arrays rather than objects. The default then, can always be the first item in the array. For the perspective, which is role based, it would be the first item in the array for which the current user has an applicable role. For tables, which can be different for different perspectives, it would be the first item on the list for the selected perspective. If defaults cannot be determined based on the configuration and the current user, then the appropriate action would be to leave the page entirely and go to some other page such as the home page of the portal. This is preferable to leaving the page up in a broken state.

The client side code, which handles the construction of the new URL, includes all of the code necessary to process both the default responses and the user’s selections:

function($scope, $location) {
	var c = this;

	if (!c.data.table) {
		if (c.data.config.defaults.table) {
			refreshPage(c.data.config.defaults.table, c.data.config.defaults.perspective, c.data.config.defaults.state);
		} else {
			window.location.search = '';
		}
	}

	$scope.selectTable = function(selection) {
		refreshPage(selection, c.data.perspective, c.data.state);
	};

	$scope.selectPerspective = function(selection) {
		refreshPage(c.data.config.table[selection][0].name, selection, c.data.config.state[0].name);
	};

	$scope.selectState = function(selection) {
		refreshPage(c.data.table, c.data.perspective, selection);
	};

	function refreshPage(table, perspective, state) {
		var tableInfo = getTableInfo(table, perspective);
		var newSearch = '?id=' + $location.search()['id'];
		newSearch += '&table=' + tableInfo.name;
		newSearch += '&filter=' + tableInfo[state].filter;
		newSearch += '&fields=' + tableInfo[state].fields;
		newSearch += '&px=' + perspective;
		newSearch += '&sx=' + state;
		window.location.search = newSearch;
	}

	function getTableInfo(table, perspective) {
		var tableInfo = {};
		for (var i=0; i<c.data.config.table[perspective].length; i++) {
			if (c.data.config.table[perspective][i].name == table) {
				tableInfo = c.data.config.table[perspective][i];
			}
		}
		return tableInfo;
	}
}

At the top of the script, we verify that a table has been selected. If not, then we check to make sure that there is a default table. If there is, then we select the default table; otherwise, we return to the home page of the Portal. If a table has already been selected, then we just wait for the user to make another selection, at which time we refresh the page with the selected options.

The server side code is a little more involved. The first thing that we need to do is go out and get the configuration information. Once we have that in hand, we can determine the authorized perspectives, and once those have been established, we can then determine the defaults. After that, it’s just a matter of pulling in the URL parameters and verifying them against the configuration. The final action on the server side is to use the defined filter for the active state to get a row count for every applicable table.

(function() {
	var mdc = new MyDataConfig();
	data.config = mdc.getConfig($sp);
	data.config.authorizedPerspective = getAuthorizedPerspectives();
	establsihDefaults();
	
	if (!input) {
		data.list = [];
		if (data.config.defaults.perspective && $sp.getParameter('table')) {
			data.perspective = data.config.defaults.perspective;
			data.state = data.config.defaults.state;
			data.table = data.config.defaults.table;
			if ($sp.getParameter('px')) {
				for (var i=0; i<data.config.authorizedPerspective.length; i++) {
					if ($sp.getParameter('px') == data.config.authorizedPerspective[i].name) {
						data.perspective = $sp.getParameter('px');
					}
				}
			}
			for (var i=0; i<data.config.table[data.perspective].length; i++) {
				if ($sp.getParameter('table') == data.config.table[data.perspective][i].name) {
					data.table = $sp.getParameter('table');
				}
			}
			if ($sp.getParameter('sx')) {
				for (var i=0; i<data.config.state.length; i++) {
					if ($sp.getParameter('sx') == data.config.state[i].name) {
						data.state = $sp.getParameter('sx');
					}
				}
			}
			for (var i in data.config.table[data.perspective]) {
				getValues(data.config.table[data.perspective][i]);
			}
		}
	}
	
	function getAuthorizedPerspectives() {
		var authorizedPerspective = [];
		for (var i in data.config.perspective) {
			var p = data.config.perspective[i];
			if (p.roles) {
				var role = p.roles.split(',');
				var authorized = false;
				for (var i=0; i<role.length; i++) {
					if (gs.hasRole(role[i])) {
						authorized = true;
					}
				}
				if (authorized) {
					authorizedPerspective.push(p);
				}
			} else {
				authorizedPerspective.push(p);
			}
		}
		return authorizedPerspective;
	}
	
	function establsihDefaults() {
		data.config.defaults = {};
		if (data.config.authorizedPerspective[0]) {
			data.config.defaults.perspective = data.config.authorizedPerspective[0].name;
			for (var i in data.config.state) {
				var s = data.config.state[i];
				if (!data.config.defaults.state) {
					data.config.defaults.state = s.name;
				}
			}
			for (var i in data.config.table[data.config.defaults.perspective]) {
				var t = data.config.table[data.config.defaults.perspective][i];
				if (!data.config.defaults.table) {
					data.config.defaults.table = t.name;
				}
			}
		}
	}
	
	function getValues(tableInfo) {
		var gr = new GlideRecord(tableInfo.name);
		gr.addEncodedQuery(tableInfo[data.state].filter);
		gr.query();
		if (gr.getRowCount() > 0 || tableInfo.name == data.table) {
			data.list.push({
				name: tableInfo.name,
				label: gr.getLabel(),
				value: gr.getRowCount()
			});
		}
	}
})();

That’s all there is to it. For those of you who like to follow along at home, I’ve bundled all of the parts, including the hacked Data Table from URL Definition widget, into an Update Set so that you can give things a spin in your own environment. If you have any ideas for improvements, please feel free to leave them in the Comments — thanks!