SpeckledFISH-ServiceNow-Custom-Process-Flow-Colors

You’re probably here because at one point you had to answer this customer question “Can we have custom colors for the process flow chevrons on our form?

I had that very question and decided to figure out a way to use CSS through a portal page/widget combination to achieve a configurable, custom solution. Here’s how I accomplished this. Scroll to the bottom for a list of Videos.

Getting the V1.0.1 Update Set:

Download the V1.0.1 Update Set. This is a ZIP File. Unzip to get the XML file.

NOTE: V1.0.1 Released: 30 Dec 2024: Release Notes:
1. For both process flows – checking for a new record to show the process flow with all stages in the ToDo State
2. Updated the Status table’s Status field length to 40.

Note: you may get 2 preview errors. You should be able to “Accept remote update” for both.

PLEASE NOTE: This is for development use only. Use these concepts at your own risk. Take the time to reverse engineer what you’re seeing in this solution before you implement it for any customer.

MY USE CASE: Allow custom colors for a process flow on a form in the advanced UI for a custom scoped application.

MY GOALS:

  1. The original goal was to create a way for a custom, scoped application to be able to have a configurable method to adjust the colors of the process flow stages. But, like with most developers, the more I worked on this, the more I could see it could be enhanced. I eventually stopped at a point and decided to publish. Can it be improved – I’m sure it can and challenge you to come up with your approach.
    1. This initial release is itself a scoped application that can be installed and used to manage process flows for tables across multiple scopes, but if you do this, you are accepting all associated risks.
    2. My initial intent was to figure out a way to build individual process flows within a specific custom scoped application that we are building for a customer and allow the selection of custom colors for each of the process flows stages.
      1. Use this Scoped Application as an engineering exercise to reverse engineer my solution and make it work for your particular use case. Also – improve on it where you can see it can be improved.
  2. I wanted to have 2 process flow versions in the initial release:
    1. A chevron-like process flow that mimics the OOB process flow.
    2. A progress bar-like process flow.
  3. I wanted to use CSS instead of images to create the chevron/stage indicators.
  4. I wanted to allow user configuration for:
    1. Selecting what table to use the custom process flow (select the Application Scope, Table, Field and Sort Order for the tables).
    2. Selecting the HTML colors for the 3 different stage phases: Completed, Current and ToDo–custom for each table’s implementation.
  5. I wanted to exploit the existing UI Formatter and UI Macro combination concept while minimizing the amount of Jelly code needed.
  6. I wanted to use a portal page and widget to create the process flows and pull those into the form via the UI Formatter.
  7. I wanted to automate getting the table’s available process stages that drive the process flow.
  8. I wanted to automate setting the process stage colors based on:
    1. The current stage. Knowing the current stage and the sort order of the stages allows setting which stage is completed, current and still ToDo.

KNOWN ISSUES IN FIRST RELEASE:

  1. This doesn’t work with tables that extend the Task table that use the Task table’s State field to manage the process flow stages.

SUGGESTED USE:

  1. This release is intended to be a development version one installs on a Personal Developer Instance and learns how it works by doing a bit of reverse engineering.

ALTERNATIVE USE CASES/CONSIDERATIONS:

  1. This application uses a simple approach to insert a portal page/widget into an Advanced UI form using the OOB UI Formatter/UI Macro combination. The UI Macro simply uses an iFrame with a dynamically generated src=”” URL to pull a portal page/widget into a table’s form.
  2. Imagine what other data one might want to pull into a form for the user to see?

This is how to create and implement a custom process flow:

Ingredients:

  1. Two Configuration Tables:
    1. Process Flow Configuration: This table allows selecting the table and its choice field then associating html color codes to the 3 states
    2. Table Choice Field Configuration: This table allows the user to identify the table and specific choice field on that table that is used to manage the graphical process flow at the top of the applicable form.
  2. Portal Items:
    1. A portal just for custom process flows — can have no header or footer styling
    1. A portal page (for each process flow type)
      1. Chevron process flow
      2. Progress Bar Process Flow
    2. A portal widget (for each process flow type)
  3. A UI Macro (sys_ui_macro):
    1. One for each process flow type.
  4. A UI Formatter (sys_ui_formatter). UI formatters are table specific and also process flow specific. UI Formatters are what is used to add the process flow to any table’s form view.
  5. Tables that will use a process flow formatter:
    1. This table needs a field on the table that is either a Choice type or a Reference Type

Summary of how this works.

  1. The UI Formatter points to the UI Macro in the “Formatter” field – it’s the API name of the UI Macro with “.xml” added to the end.
    1. For each table that will use a process flow, a UI Formatter needs to be added. Typically these are added at the top of the form above all fields and the css in the referenced UI Macro is specifically coded to eliminate wasted space. If the process flow will be used somewhere else on the form, the css on the UI Macro will need to be adjusted to ensure a good fit on the form.
  2. The UI Macro uses an IFrame which has as its src=”” as a URL that points to the portal and page id, and passes the tableName and the sys_id of the record the user is viewing.
    1. The UI Macro captures the tableName and the SysID, using Jelly code, of the record being viewed and passes that as parameters on the IFrame’s src URL.
      1. The UI macro retrieves the sys_id into a jelly variable and add it to the IFrame’s src url as a parameter
      2. The UI Macro retrieves the tableName into a jelly variabale and add to the url as a parameter
    2. The UI macro contains css styling to ensure the IFrame sits in a good location on the form:
      1. This css uses some negative margins to minimize the wasted space the process flow otherwise consumes.
  3. The portal widget captures the tableName and sys_id of the current record and uses that to determine:
    1. The ToDo, Completed and Current html color codes to use for each state
      1. This is determined by getting all the Configuration records for the particular table
      2. Then compares those with the selected item and sets the chevron colors
        1. Because the choice options are dynamically retrieved from the sys_choice table in the correct sequence, there is code in the portal widget that loops through all the available active choices and compares them with the currently selected value for the applicable choice field on the currently viewed record
        2. If it’s not a match – and we’ve already found the current value, then that stage is complete, otherwise is is a ToDo stage.
        3. if a match – that stage in current and we’ve found the current stage
        4. After we’ve found the current stage, naturally everything after that is ToDo
    2. The widget HTML doesn’t need to be modified
    3. The widget CSS doesn’t need to be modified
    4. The widget client script isn’t used
    5. The widget’s server script dynamically gets what it needs to work, so should not need to be edited.
      1. NOTE: This is where all the magic happens, so spend some time reverse engineering here.
      2. I intentionally stayed away from storing code in script includes to increase the application’s portability – if you chose to implement this custom process flow concept in your own scoped application – I would recommend creating a script include to store the functions in one place rather than having the same functions in each widget’s Server Script.

How to Implement: You can either Download the V1.0.1 update set or create the application from scratch following the below steps.

  1. Determine which of the 2 pre-packaged process flow types you will use (or both):
    1. Chevron
    2. Progress Bar
  2. Create the Configuration Tables:
    1. Process Flow Configuration:
      1. Add to your application’s menu
      2. Application Access:
        1. Accessible from: All application scopes
        2. Caller access: Caller Tracking
          1. Can Read: true
          2. Can Create: true
          3. Can Update: true
          4. Allow access to this table via web services: true.
      3. Controls:
        1. Auto Number
        2. Prefix: PFCONFIG
        3. Number: 1,000
        4. Number of digits: 7
        5. Role: Whatever role you want to manage process flow configurations.
      4. Fields:
        1. Active, active, true/false, default value true
        2. HTML Color Code, html_color_code, string, 60 max char
          1. There’s a hint and Label link on this field about Section 508 Compliance
        3. Process Flow Stage, Choice
          1. ToDo, todo
          2. Current, current
          3. Completed, completed
        4. Application Scope (application_scope), Reference, (sys_scope)
        5. Table, table, reference, Table (sys_db_object)
          1. Reference Qual: Advanced:
            1. javascript:”sys_scope=” + current.getValue(‘application_scope’);
    2. Table Choice Field Configuration
      1. Add to your application’s menu.
      2. Application Access:
        1. Accessible from: All application scopes
        2. Caller access: Caller Tracking
          1. Can Read: true
          2. Can Create: true
          3. Can Update: true
          4. Allow access to this table via web services: true.
      3. Controls:
        1. Auto Number
        2. Prefix: TCFCONFIG
        3. Number: 1,000
        4. Number of digits: 7
        5. Role: Whatever role you want to manage process flow configurations.
      4. Fields:
        1. Active, active, true/false, default value true
        2. Process Flow Field, process_flow_field, reference, Dictionary Entry
          1. Reference Qual: Advanced:
            1. javascript:”sys_scope=” + gs.getCurrentApplicationId() + “^internal_type=choice^name=” + current.table.name;
        3. Application Scope, application_scope, reference, (sys_scope):
        4. Table, table, reference, Table (sys_db_object)
          1. Reference Qual: Advanced:
            1. javascript:”sys_scope=” + current.getValue(‘application_scope’);
        5. Sort Field, sort_field, reference, Dictionary Entry (sys_dictionary)
          1. Reference Qual: Advanced:
            1. javascript:”name=”+ current.process_flow_field.reference;
  3. Create the 2 properties that store the table names of these 2 tables.
    1. The property to store the sys_id of the Process Flow Configuration table
      1. Suffix: ProcessFlowConfigurationTable
      2. Name: Auto generated (use this in the widget server code)
      3. Description: This is the name of the configuration table used to store the process flow stages’ HTML color codes. This property is called in the widgets so the widget code knows which table to query to get the HTML color codes for each process flow state.
      4. Type: string
      5. Value: the scoped database table name.
    2. The property to store the sys_id of the Table Choice Field Configuration table
      1. Suffix: ChoiceFieldConfigurationTable
      2. Name: Auto generated (use this in the widget server code)
      3. Description: This is the name of the configuration table used to find the field in a specified table to useto determine the stages of the process flow.
      4. Type: string
      5. Value: the scoped database table name.
  4. Create a UI Macro for each process flow type you’ll implement
    1. Name the Macro
      1. process_flow_{process_flow_type}. Examples:
        1. process_flow_chevrons
        2. process_flow_progress_bar
    2. Copy the code below into the XML
  5. Create a new Process Flow UI Formatter (sys_ui_formatter)–one for each table where a formatter will be
    1. Name the Formatter “PF Formatter-{table reference} {type} (or however you can within max characters limit–but try to indicate the table and formatter type).
      1. PF Formatter-Cust Order Chevrons
      2. PF Formatter-Cust Order Progress Bar
    2. Formatter: the API value from the Macro above – add “.xml” at the end
    3. Select the applicable table
    4. Type: Formatter
  6. Create the portal – if there is an existing scoped portal that has a header/footer and styling, create a basic portal just for the scoped app’s process flow implementation. This has no styling or header or footer– this is so when the page is injected via the iframe there is no additional formatting.
    1. Update the UI Macro’s iframe src url to use the applicable portal
      1. src=”/portalurl?id=portal_page_id$[AMP]tableName=$[tableName]$[AMP]mySysID=$[mySysID]”
  7. Create the Portal page
    1. Add the css on the page to override some bootstrap css
  8. Create the portal widget
    1. See code below
      1. Adjust the system properties in 2 places
  9. Update the portal page:
    1. Add to a 12-column-wide container on the page
    2. Insert the new widget in the 12-column container
  10. Add a Process Flow Configuration record for the table on which the UI Formatter is used to identify the HTML color code for each stage.
    1. For each table that will use the custom process flow, add a record for each stage: ToDo, Current, Completed
  11. Add a Table Choice Field Configuration record for the table on which the UI Formatter is used – to identify the table and that table’s field that drives the process flow’s status and the sort field for any reference field used to drive the process flow stages.
  12. Add the formatter to the form in the desired location.

SCREENSHOTS AND CODE SNIPPETS:

UI Macros

Chevrons Process Flow:

The Chevrons Process Flow UI Macro

XML CODE:

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">

<g2:evaluate var='jvar_sysID'>
	var currentSysID = current.getValue('sys_id');
	currentSysID;
</g2:evaluate>
<g2:evaluate var='jvar_tableName'>
	var currentTableName = current.getTableName();
	currentTableName;
</g2:evaluate>

<iframe class="customProcessFlowIFrame" scrolling="no" src="/sfpf?id=speckledfish_process_flow_chevrons${AMP}tableName=$[jvar_tableName]${AMP}mySysID=$[jvar_sysID]"></iframe>

<style>
/*  Adjust the margin top and bottom to fit your form and minimize wasted space */
.customProcessFlowIFrame{
	border: none;
	width:100%;
	margin: -20px 0px -60px 0px;
	padding: 0px; 
}
</style>
</j:jelly>

Progress Bar Process Flow:

The Progress Bar Process Flow UI Macro

XML CODE:

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">

<g2:evaluate var='jvar_sysID'>
	var currentSysID = current.getValue('sys_id');
	currentSysID;
</g2:evaluate>
<g2:evaluate var='jvar_tableName'>
	var currentTableName = current.getTableName();
	currentTableName;
</g2:evaluate>

<iframe class="customProcessFlowIFrame" scrolling="no" src="/sfpf?id=speckledfish_process_flow_progress_bar${AMP}tableName=$[jvar_tableName]${AMP}mySysID=$[jvar_sysID]"></iframe>

<style>
/*  Adjust the margin top and bottom to fit your form and minimize wasted space */
.customProcessFlowIFrame{
	border: none;
	width:100%;
	margin: -30px 0px -60px 0px;
	padding: 0px; 
}
</style>
</j:jelly>

UI Formatters. NOTE: a UI Formatter is needed for each table on which you will have a process flow and for each process flow type.

UI Formatter: Table: Customer Order Reference, Process Flow Type: Chevrons
UI Formatter: Table: Customer Order Reference, Process Flow Type: Progress Bar
UI Formatter: Table: Customer Order, Process Flow Type: Chevrons
UI Formatter: Table: Customer Order, Process Flow Type: Progress Bar

Portal Pages (1 for each process flow type)

Portal Page: For the Chevrons Process Flow Type
Portal Page: For the Progress Bar Process Flow Type

The Portal Page’s CSS:

.container{
  margin: 0px !important;
  min-width: 100% !important;
  
}
section.page{
  padding-bottom: 0px !important;
}

Portal Widgets (1 for each process flow type). Include the respective widget on the respective portal page in a 12-column container.

Chevrons Process Flow Type

Process Flow Widget: Chevrons Process Flow Type

Chevrons Process Flow Widget Code:

HTML Template:

<div>
  <div 
       style="display:none;"
       id="processFlowInfo">
    CHEVRONS This is a custom process flow using a process flow formatter, a UI Macro and a custom portal, portal page, and portal widget.
    
  </div>
  <div 
       style="font-size:20px;"
       ng-if="data.buildingStatus">
    Building Status Flow ...
  </div>
  
  <div class="process-flow">
    <div 
         ng-repeat="stage in data.stages"
         class="chevron"
         style="background-color:  {{stage.stageChevronColor}}"
         title="{{stage.stageLabel}} {{stage.youAreHere}}">
      <span><i class="{{stage.stageIconYouAreHere}}"></i> {{stage.stageLabel}} <i class="{{stage.stageIcon}}"></i></span>
      <span ng-if="false">{{stage.stageLabel}}  <i class="{{stage.stageIcon}}"></i></span>
    </div>


  </div>

CSS:

.padding-top{
  padding-top: 0px !important;
}

/* Container for the process flow */
.process-flow {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0; /* No gap between chevrons for seamless look */
  margin: 20px;
  margin: 15px;
}

/* Individual chevron */
.chevron {
  position: relative;
  background-color: #ccc; /* Default background color */
  color: white;
  padding: 10px 20px;
  padding: 10px 10px;
  font-size: 14px;
  font-weight: bold;
  text-align: center;
  text-transform: uppercase;
  clip-path: polygon(0% 0%, 90% 0%, 100% 50%, 90% 100%, 0% 100%, 10% 50%);
  transition: background-color 0.3s ease, color 0.3s ease;
  flex: 1; /* Flex for equal sizing */
}

/* Completed chevron */
.chevron.completed {
  background-color: #4caf50; /* Green for completed */
}

/* Active chevron */
.chevron.active {
  background-color: #2196f3; /* Blue for active */
}

/* Last chevron adjustments */
.chevron:last-child {
  clip-path: polygon(0% 0%, 100% 0%, 100% 50%, 100% 100%, 0% 100%, 10% 50%);
}

/* Add some spacing between chevrons if desired */
.chevron + .chevron {
  margin-left: -10px; /* Slight overlap for seamless transition */
}

Server Script:

(function() {
	/* populate the 'data' object */
	/* e.g., data.table = $sp.getValue('table'); */
	// show a "Building process flow..." message if needed
	data.buildingStatus = true;

	// get the passed-in table name and record sysID
	// tableName is the table for the currently viewed record
	// mySysID is the sys_id of the currently viewed record
	var tableName = $sp.getParameter('tableName');
	var recordSysID = $sp.getParameter("mySysID");

	// get the field that drives the workflow stages
	var choiceFieldColumnNameObj = getTableChoiceFieldColumnNameObj(tableName);
	// this is the field on the record that drives the process flow
	var choiceFieldColumnName = choiceFieldColumnNameObj.choiceField;
	
	// Don't forget folks, we need to know, for reference fields, what field to sort on
	// let's get that now.
	var stagesSortField = choiceFieldColumnNameObj.stagesSortField;

	// now -> we need to know where all the possible choices are for that field
	// that drives the workflow's stages. So, we need to get the field type and the table
	// where we can get the choices. Options are typically:
	// 1. The sys_choice table for field types of 'choice' or 'string'
	// 2. the referenced table for field type of 'reference'
	// get an object that contains the field Type and the table where choices for stages reside
	var fieldTypeValuesTableObj = getFieldTypeAndValuesTable(tableName, choiceFieldColumnName);

	// Dude, get the record on which we're displaying the process flow
	var processFlowRecord = new GlideRecord(tableName);
	var encodedQuery = "sys_id=" + recordSysID;
	processFlowRecord.addEncodedQuery(encodedQuery);
	processFlowRecord.query();
	var stages = [];
	if (processFlowRecord.next()){
		// get the value for the record's current stage (display value and stored value)
		var currentStageDisplay = processFlowRecord.getDisplayValue(choiceFieldColumnName);
		var currentStage = processFlowRecord.getValue(choiceFieldColumnName);
		data.currentStage = currentStageDisplay;

		// Now folks we already have the currently stored value for the field that drives the stages
		// next up -> we need to get all the available stages and their associated sort order
		// so we can populate the process flow with completed, current and ToDo stages
		stages = getStages(currentStage, tableName, choiceFieldColumnName, stagesSortField, fieldTypeValuesTableObj);
		data.buildingStatus = false;
		//stages = getStages(currentStage, tableName, choiceFieldColumnName, fieldTypeValuesTableObj);
	}
	data.stages = stages;

})();

function getStages(currentStage, tableName, choiceFieldColumnName, stagesSortField, fieldTypeValuesTableObj){
	var fieldType = fieldTypeValuesTableObj.fieldType;
	var valuesTable = fieldTypeValuesTableObj.valuesTable;
	console.log("getStages >> fieldType: " + fieldType + " and valuesTable: " + valuesTable);

	var stages = [];
	var stagesGR = new GlideRecord(valuesTable);
	var encodedQuery = "";
	var stageLabelField = "";
	var stageValueField = "";
	var stageSequenceField = "";
	//var stagesGR = new GlideRecord('sys_choice');

	switch (fieldType.toLowerCase()){
		case "reference":
			// query the table where the values are from
			// We need the Display Value field from the valuesTable-why? because this is the field
			// where all the process flow labels come from.
			var tableDisplayFieldColumnName = getTableDisplayValueField(valuesTable);
			stageLabelField = tableDisplayFieldColumnName; //"status";
			stageValueField = "sys_id";
			stageSequenceField = stagesSortField;
			stagesGR.orderBy(stagesSortField);
			encodedQuery = "active=true";
			break;
		case "choice":
		case "string":
			// query the sys_choice table
			stagesGR.orderBy(stagesSortField);
			encodedQuery = "name=" + tableName + "^element=" + choiceFieldColumnName + "^inactive=false";
			stageLabelField = 'label';
			stageValueField = 'value';
			stageSequenceField = stagesSortField;
			break;
	}
	console.log("getStages >> encodedQuery: " + encodedQuery);
	stagesGR.addEncodedQuery(encodedQuery);
	stagesGR.query();
	var numberOfStages = stagesGR.getRowCount();
	var foundCurrent = false;
	var configObj = getTableColorCodes(tableName);
	var chevronCompletedColor = configObj.completed;
	var chevronCurrentColor = configObj.current;
	var chevronToDoColor = configObj.todo;

	var count = 1;

	while(stagesGR.next()){
		var isLastStage = false;
		var stageIcon = "icon-success-circle";
		var stageIconYouAreHere = "";
		var stageLabel = stagesGR.getValue(stageLabelField);
		var stageValue = stagesGR.getValue(stageValueField);
		var stageSequence = stagesGR.getValue(stageSequenceField);
		var youAreHere = "";

		//var stageClass = "completed";
		var stageChevronColor = chevronCompletedColor;
		if (stageValue == currentStage){
			foundCurrent = true;
			stageIcon = "icon-vcr-left";
			stageIconYouAreHere = "icon-vcr-right";
			youAreHere = "- You Are Here.";
			stageChevronColor = chevronCurrentColor;
		} else {
			if (foundCurrent){
				stageChevronColor = chevronToDoColor;
				stageIcon = "";
			}
		}
		if (count == numberOfStages) {
			isLastStage = true;
		}
		var stageObj = {
			'stageCount': count,
			'stageLabel': stageLabel,
			'stageValue': stageValue,
			'youAreHere': youAreHere,
			'stageChevronColor': stageChevronColor,
			'isLastStage': isLastStage,
			'stageIcon': stageIcon,
			'stageIconYouAreHere':stageIconYouAreHere
		};
		stages.push(stageObj);
		count++;
	}

	return stages;

}



function DEP___getStages(currentStage, tableName, choiceFieldColumnName, fieldTypeValuesTableObj){
	var fieldType = fieldTypeValuesTableObj.fieldType;
	var valuesTable = fieldTypeValuesTableObj.valuesTable;
	console.log("getStages >> fieldType: " + fieldType + " and valuesTable: " + valuesTable);

	var stages = [];
	var stagesGR = new GlideRecord(valuesTable);
	var encodedQuery = "";
	var stageLabelField = "";
	var stageValueField = "";
	var stageSequenceField = "";
	//var stagesGR = new GlideRecord('sys_choice');

	switch (fieldType.toLowerCase()){
		case "reference":
			// query the table where the values are from
			// TODO: how to allow determining dynamically the stageLavelField for reference fields
			stagesGR.orderBy('sort_order');
			encodedQuery = "active=true";
			// need valuesTable-specific field name for the stage value
			switch(valuesTable.toLowerCase()){
				case "x_221138_sfpflow_status":
					stageLabelField = "status";
					stageValueField = "sys_id";
					stageSequenceField = "sort_order";
					break;

			}
			break;
		case "choice":
			// query the sys_choice table
			stagesGR.orderBy('sequence');
			encodedQuery = "name=" + tableName + "^element=" + choiceFieldColumnName + "^inactive=false";
			stageLabelField = 'label';
			stageValueField = 'value';
			stageSequenceField = 'sequence';
			break;
	}
	console.log("getStages >> encodedQuery: " + encodedQuery);
	stagesGR.addEncodedQuery(encodedQuery);
	stagesGR.query();
	var numberOfStages = stagesGR.getRowCount();
	var foundCurrent = false;
	var configObj = getTableColorCodes(tableName);
	var chevronCompletedColor = configObj.completed;
	var chevronCurrentColor = configObj.current;
	var chevronToDoColor = configObj.todo;

	var count = 1;

	while(stagesGR.next()){
		var isLastStage = false;
		var stageIcon = "icon-success-circle";
		var stageLabel = stagesGR.getValue(stageLabelField);
		var stageValue = stagesGR.getValue(stageValueField);
		var stageSequence = stagesGR.getValue(stageSequenceField);

		//var stageClass = "completed";
		var stageChevronColor = chevronCompletedColor;
		if (stageValue == currentStage){
			foundCurrent = true;
			stageIcon = "";
			stageChevronColor = chevronCurrentColor;
		} else {
			if (foundCurrent){
				stageChevronColor = chevronToDoColor;
				stageIcon = "";
			}
		}
		if (count == numberOfStages) {
			isLastStage = true;
		}
		var stageObj = {
			'stageCount': count,
			'stageLabel': stageLabel,
			'stageValue': stageValue,
			//'stageClass': stageClass,
			'stageChevronColor': stageChevronColor,
			'isLastStage': isLastStage,
			'stageIcon': stageIcon
		};
		stages.push(stageObj);
		count++;
	}

	return stages;

}


function getTableColorCodes(tableName){
	var processFlowConfigurationTable = gs.getProperty('x_221138_sfpflow.ProcessFlowConfigurationTable');
	var configRecords = new GlideRecord(processFlowConfigurationTable);
	var encodedQuery = "table.name=" + tableName;
	configRecords.addEncodedQuery(encodedQuery);
	configRecords.query();
	var configObj = {};
	while(configRecords.next()){
		var configStage = configRecords.getValue('process_flow_stage');
		var stageColor = configRecords.getValue('html_color_code');
		configObj[configStage] = stageColor;
	}
	return configObj;

}

function getTableChoiceFieldColumnNameObj(tableName){
	// get all the configured choiceField
	var choiceFieldConfigTable = gs.getProperty('x_221138_sfpflow.ChoiceFieldConfigurationTable');

	var choiceFieldConfig = new GlideRecord(choiceFieldConfigTable);
	var encodedQuery = "active=true^table.name=" + tableName;
	// active=true^table=928e4728c37a5210aad0be13e40131c4
	// active=true^table.name=x_221138_sfpflow_customer_order_reference
	choiceFieldConfig.addEncodedQuery(encodedQuery);
	choiceFieldConfig.orderByDesc('sys_updated_on');
	choiceFieldConfig.setLimit(1);
	choiceFieldConfig.query();
	var choiceFieldObj = {};
	if(choiceFieldConfig.next()){
		var configTableName = choiceFieldConfig.table.name;
		//choiceFieldObj = {}; 
		//choiceFieldObj[configTableName] = {}; 
		var tableChoiceFieldColumnName = choiceFieldConfig.process_flow_field.element.toString();
		var stagesSortField = "sequence"; // default to sequence which is the sort order for sys_choice table

		// now if the table choice field is a reference field, we need the real sort field from the reference table
		var tableChoiceFieldType = choiceFieldConfig.process_flow_field.internal_type; 
		//gs.info("\ntableChoiceFieldType: " + tableChoiceFieldType);
		if(tableChoiceFieldType.toLowerCase() == 'reference'){
			stagesSortField = choiceFieldConfig.sort_field.element.toString();
			//gs.info("\nstagesSortField: " + stagesSortField);
		}
		choiceFieldObj.choiceField = tableChoiceFieldColumnName;
		choiceFieldObj.stagesSortField = stagesSortField;
		var tableDisplayFieldColumnName = getTableDisplayValueField(tableName);
		choiceFieldObj.tableDisplayFieldColumnName = tableDisplayFieldColumnName;
		//choiceFieldObj[configTableName]['choiceField'] = tableChoiceFieldColumnName;
		//choiceFieldObj[configTableName]['stagesSortField'] = stagesSortField;
	}
	/*
	var choiceFieldObj = {
		'x_412720_sfplan_fire_sale': 'fire_sale_status',
		'x_412720_sfplan_process_flow_test':'stage'
	}
	*/
	return choiceFieldObj;
	//return choiceFieldObj[tableName];
}
function DEP___getTableChoiceFieldColumnNameObj(tableName){
	// get all the configured choiceField
	var choiceFieldConfigTable = gs.getProperty('x_221138_sfpflow.ChoiceFieldConfigurationTable');

	var choiceFieldConfig = new GlideRecord(choiceFieldConfigTable);
	var encodedQuery = "active=true";
	choiceFieldConfig.addEncodedQuery(encodedQuery);
	choiceFieldConfig.query();
	var choiceFieldObj = {};
	while(choiceFieldConfig.next()){
		var configTableName = choiceFieldConfig.table.name;
		var tableChoiceFieldColumnName = choiceFieldConfig.process_flow_field.element;
		choiceFieldObj[configTableName] = tableChoiceFieldColumnName;
	}
	/*
	var choiceFieldObj = {
		'x_412720_sfplan_fire_sale': 'fire_sale_status',
		'x_412720_sfplan_process_flow_test':'stage'
	}
	*/
	return choiceFieldObj;
}
function getFieldTypeAndValuesTable(tableName, fieldName){
	var fieldTypeValuesTableObj = {};
	var sysDictionary = new GlideRecord('sys_dictionary');
	var encodedQuery = "name=" + tableName + "^element=" + fieldName;

	sysDictionary.addEncodedQuery(encodedQuery);
	sysDictionary.setLimit(1);
	sysDictionary.query();
	var valuesTable = 'sys_choice';
	if (sysDictionary.next()){
		var fieldType = sysDictionary.getValue('internal_type');
		if (fieldType.toLowerCase() == 'reference'){
			var referenceTable = sysDictionary.reference.name;
			valuesTable = referenceTable;

		}
		fieldTypeValuesTableObj.fieldType = fieldType;
		fieldTypeValuesTableObj.valuesTable = valuesTable;
	}
	return fieldTypeValuesTableObj;
}
function getTableDisplayValueField(tableName){
	var sysDictionaryEntry = new GlideRecord('sys_dictionary');
	var encodedQuery = "name=" + tableName + "^display=true";
	sysDictionaryEntry.addEncodedQuery(encodedQuery);
	sysDictionaryEntry.setLimit(1);
	sysDictionaryEntry.query();
	if(sysDictionaryEntry.next()){
		var processFlowLabelFieldColumnName = sysDictionaryEntry.getValue('element');
		return processFlowLabelFieldColumnName;
	}
	return false;

}

Progress Bar Process Flow Type:

Process Flow Widget: Progress Bar Process Flow Type

HTML Template:

<div>
  <div 
       style="display:none;"
       id="processFlowInfo">
    PROGRESS BAR This is a custom process flow using a process flow formatter, a UI Macro and a custom portal, portal page, and portal widget.

  </div>


  <div 
       style="font-size:20px;"
       ng-if="data.buildingStatus">
    Building Status Flow ...
  </div>
  <div class="progress-bar">

    <div class="segment"
         ng-repeat="stage in data.stages"
         style="background-color: {{stage.stageChevronColor}}"
         title="{{stage.stageLabel}} {{stage.youAreHere}}"
         >
      <span><i class="{{stage.stageIconYouAreHere}}"></i> {{stage.stageLabel}} <i class="{{stage.stageIcon}}"></i></span>
    </div>

  </div>

CSS:

.padding-top{
  padding-top: 0px !important;
}


/* Container for the progress bar */
.progress-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background-color: #ddd; /* Background of the progress bar */
  border-radius: 10px;
  overflow: hidden;
  position: relative;
  /*height: 40px; */
  padding: 3px 0px;
  width: 100%;
  margin: 20px auto;
}

/* Each segment */
.segment {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  text-align: center;
  color: #fff;
  font-size: 14px;
  font-weight: bold;
  text-transform: uppercase;
  transition: background-color 0.3s ease;
  background-color: #ccc; /* Default color for segments */
  min-height: 40px;
}

/* Segment border to separate stages */
.segment:not(:last-child)::after {
  content: "";
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background-color: #fff; /* Separator line between segments */
}

/* Completed segment */
.segment.completed {
  background-color: #4caf50; /* Green for completed */
}

/* Active segment */
.segment.active {
  background-color: #2196f3; /* Blue for active */
}

/* Inactive segments */
.segment {
  background-color: #ccc; /* Gray for inactive */
}

Server Script:

(function() {
	/* populate the 'data' object */
	/* e.g., data.table = $sp.getValue('table'); */
	// show a "Building process flow..." message if needed
	data.buildingStatus = true;
	// get the passed-in table name and record sysID
	// tableName is the table for the currently viewed record
	// mySysID is the sys_id of the currently viewed record
	var tableName = $sp.getParameter('tableName');
	var recordSysID = $sp.getParameter("mySysID");
	//data.recordSysID = recordSysID;

	// get the field that drives the workflow stages
	var choiceFieldColumnNameObj = getTableChoiceFieldColumnNameObj(tableName);
	// this is the field on the record that drives the process flow
	var choiceFieldColumnName = choiceFieldColumnNameObj.choiceField;

	// Don't forget folks, we need to know, for reference fields, what field to sort on
	// let's get that now.
	var stagesSortField = choiceFieldColumnNameObj.stagesSortField;

	// now -> we need to know where all the possible choices are for that field
	// that drives the workflow's stages. So, we need to get the field type and the table
	// where we can get the choices. Options are typically:
	// 1. The sys_choice table for field types of 'choice' or 'string'
	// 2. the referenced table for field type of 'reference'
	// get an object that contains the field Type and the table where choices for stages reside
	var fieldTypeValuesTableObj = getFieldTypeAndValuesTable(tableName, choiceFieldColumnName);

	// Dude, get the record on which we're displaying the process flow
	var processFlowRecord = new GlideRecord(tableName);
	var encodedQuery = "sys_id=" + recordSysID;
	processFlowRecord.addEncodedQuery(encodedQuery);
	processFlowRecord.query();
	var stages = [];
	if (processFlowRecord.next()){
		// get the value for the record's current stage (display value and stored value)
		var currentStageDisplay = processFlowRecord.getDisplayValue(choiceFieldColumnName);
		var currentStage = processFlowRecord.getValue(choiceFieldColumnName);
		data.currentStage = currentStageDisplay;

		// Now folks we already have the currently stored value for the field that drives the stages
		// next up -> we need to get all the available stages and their associated sort order
		// so we can populate the process flow with completed, current and ToDo stages
		stages = getStages(currentStage, tableName, choiceFieldColumnName, stagesSortField, fieldTypeValuesTableObj);
		data.buildingStatus = false;
	}
	data.stages = stages;

})();



function getStages(currentStage, tableName, choiceFieldColumnName, stagesSortField, fieldTypeValuesTableObj){
	var fieldType = fieldTypeValuesTableObj.fieldType;
	var valuesTable = fieldTypeValuesTableObj.valuesTable;
	console.log("getStages >> fieldType: " + fieldType + " and valuesTable: " + valuesTable);

	var stages = [];
	var stagesGR = new GlideRecord(valuesTable);
	var encodedQuery = "";
	var stageLabelField = "";
	var stageValueField = "";
	var stageSequenceField = "";
	//var stagesGR = new GlideRecord('sys_choice');

	switch (fieldType.toLowerCase()){
		case "reference":
			// query the table where the values are from
			// We need the Display Value field from the valuesTable-why? because this is the field
			// where all the process flow labels come from.
			var tableDisplayFieldColumnName = getTableDisplayValueField(valuesTable);
			stageLabelField = tableDisplayFieldColumnName; //"status";
			stageValueField = "sys_id";
			stageSequenceField = stagesSortField;
			stagesGR.orderBy(stagesSortField);
			encodedQuery = "active=true";
			break;
		case "choice":
		case "string":
			// query the sys_choice table
			stagesGR.orderBy(stagesSortField);
			encodedQuery = "name=" + tableName + "^element=" + choiceFieldColumnName + "^inactive=false";
			stageLabelField = 'label';
			stageValueField = 'value';
			stageSequenceField = stagesSortField;
			break;
	}
	console.log("getStages >> encodedQuery: " + encodedQuery);
	stagesGR.addEncodedQuery(encodedQuery);
	stagesGR.query();
	var numberOfStages = stagesGR.getRowCount();
	var foundCurrent = false;
	var configObj = getTableColorCodes(tableName);
	var chevronCompletedColor = configObj.completed;
	var chevronCurrentColor = configObj.current;
	var chevronToDoColor = configObj.todo;

	var count = 1;

	while(stagesGR.next()){
		var isLastStage = false;
		var stageIcon = "icon-success-circle";
		var stageIconYouAreHere = "";
		var stageLabel = stagesGR.getValue(stageLabelField);
		var stageValue = stagesGR.getValue(stageValueField);
		var stageSequence = stagesGR.getValue(stageSequenceField);
		var youAreHere = "";

		//var stageClass = "completed";
		var stageChevronColor = chevronCompletedColor;
		if (stageValue == currentStage){
			foundCurrent = true;
			stageIcon = "icon-vcr-left";
			stageIconYouAreHere = "icon-vcr-right";
			youAreHere = "- You Are Here.";
			stageChevronColor = chevronCurrentColor;
		} else {
			if (foundCurrent){
				stageChevronColor = chevronToDoColor;
				stageIcon = "";
			}
		}
		if (count == numberOfStages) {
			isLastStage = true;
		}
		var stageObj = {
			'stageCount': count,
			'stageLabel': stageLabel,
			'stageValue': stageValue,
			'youAreHere': youAreHere,
			'stageChevronColor': stageChevronColor,
			'isLastStage': isLastStage,
			'stageIcon': stageIcon,
			'stageIconYouAreHere':stageIconYouAreHere
		};
		stages.push(stageObj);
		count++;
	}

	return stages;

}


function getTableColorCodes(tableName){
	var processFlowConfigurationTable = gs.getProperty('x_221138_sfpflow.ProcessFlowConfigurationTable');
	var configRecords = new GlideRecord(processFlowConfigurationTable);
	var encodedQuery = "table.name=" + tableName;
	configRecords.addEncodedQuery(encodedQuery);
	configRecords.query();
	var configObj = {};
	while(configRecords.next()){
		var configStage = configRecords.getValue('process_flow_stage');
		var stageColor = configRecords.getValue('html_color_code');
		configObj[configStage] = stageColor;
	}
	return configObj;

}


function getTableChoiceFieldColumnNameObj(tableName){
	// get all the configured choiceField
	var choiceFieldConfigTable = gs.getProperty('x_221138_sfpflow.ChoiceFieldConfigurationTable');

	var choiceFieldConfig = new GlideRecord(choiceFieldConfigTable);
	var encodedQuery = "active=true^table.name=" + tableName;
	// active=true^table=928e4728c37a5210aad0be13e40131c4
	// active=true^table.name=x_221138_sfpflow_customer_order_reference
	choiceFieldConfig.addEncodedQuery(encodedQuery);
	choiceFieldConfig.orderByDesc('sys_updated_on');
	choiceFieldConfig.setLimit(1);
	choiceFieldConfig.query();
	var choiceFieldObj = {};
	if(choiceFieldConfig.next()){
		var configTableName = choiceFieldConfig.table.name;
		//choiceFieldObj = {}; 
		//choiceFieldObj[configTableName] = {}; 
		var tableChoiceFieldColumnName = choiceFieldConfig.process_flow_field.element.toString();
		var stagesSortField = "sequence"; // default to sequence which is the sort order for sys_choice table

		// now if the table choice field is a reference field, we need the real sort field from the reference table
		var tableChoiceFieldType = choiceFieldConfig.process_flow_field.internal_type; 
		//gs.info("\ntableChoiceFieldType: " + tableChoiceFieldType);
		if(tableChoiceFieldType.toLowerCase() == 'reference'){
			stagesSortField = choiceFieldConfig.sort_field.element.toString();
			//gs.info("\nstagesSortField: " + stagesSortField);
		}
		choiceFieldObj.choiceField = tableChoiceFieldColumnName;
		choiceFieldObj.stagesSortField = stagesSortField;
		var tableDisplayFieldColumnName = getTableDisplayValueField(tableName);
		choiceFieldObj.tableDisplayFieldColumnName = tableDisplayFieldColumnName;
		//choiceFieldObj[configTableName]['choiceField'] = tableChoiceFieldColumnName;
		//choiceFieldObj[configTableName]['stagesSortField'] = stagesSortField;
	}
	/*
	var choiceFieldObj = {
		'x_412720_sfplan_fire_sale': 'fire_sale_status',
		'x_412720_sfplan_process_flow_test':'stage'
	}
	*/
	return choiceFieldObj;
	//return choiceFieldObj[tableName];
}

function DEPgetTableChoiceFieldColumnNameObj(tableName){
	// get all the configured choiceField
	var choiceFieldConfigTable = gs.getProperty('x_221138_sfpflow.ChoiceFieldConfigurationTable');

	var choiceFieldConfig = new GlideRecord(choiceFieldConfigTable);
	var encodedQuery = "active=true";
	choiceFieldConfig.addEncodedQuery(encodedQuery);
	choiceFieldConfig.query();
	var choiceFieldObj = {};
	while(choiceFieldConfig.next()){
		var configTableName = choiceFieldConfig.table.name;
		var tableChoiceFieldColumnName = choiceFieldConfig.process_flow_field.element;
		choiceFieldObj[configTableName] = tableChoiceFieldColumnName;
	}
	/*
	var choiceFieldObj = {
		'x_412720_sfplan_fire_sale': 'fire_sale_status',
		'x_412720_sfplan_process_flow_test':'stage'
	}
	*/
	return choiceFieldObj;
}


function getFieldTypeAndValuesTable(tableName, fieldName){
	var fieldTypeValuesTableObj = {};
	var sysDictionary = new GlideRecord('sys_dictionary');
	var encodedQuery = "name=" + tableName + "^element=" + fieldName;

	sysDictionary.addEncodedQuery(encodedQuery);
	sysDictionary.setLimit(1);
	sysDictionary.query();
	var valuesTable = 'sys_choice';
	if (sysDictionary.next()){
		var fieldType = sysDictionary.getValue('internal_type');
		if (fieldType.toLowerCase() == 'reference'){
			var referenceTable = sysDictionary.reference.name;
			valuesTable = referenceTable;

		}
		fieldTypeValuesTableObj.fieldType = fieldType;
		fieldTypeValuesTableObj.valuesTable = valuesTable;
	}
	return fieldTypeValuesTableObj;
}
function getTableDisplayValueField(tableName){
	var sysDictionaryEntry = new GlideRecord('sys_dictionary');
	var encodedQuery = "name=" + tableName + "^display=true";
	sysDictionaryEntry.addEncodedQuery(encodedQuery);
	sysDictionaryEntry.setLimit(1);
	sysDictionaryEntry.query();
	if(sysDictionaryEntry.next()){
		var processFlowLabelFieldColumnName = sysDictionaryEntry.getValue('element');
		return processFlowLabelFieldColumnName;
	}
	return false;

}

SAMPLE PROCESS FLOWS:

Chevrons:

Sample Process Flow: Chevrons – works off a Choice field (or string field that has choices)

Progress Bar:

Sample Process Flow: Progress Bar – works off a Reference Field for Process Flow Stages

VIDEOS: Here is a series of videos that walks through some of the parts that comprise the SpeckledFISH Custom Process Flow application.

Determining which field on a particular table is used to determine the stages of the process flow.

2. Configuring the colors for each of the process stages (completed, current, todo):

3. ServiceNow Process Flows in General and using the SpeckledFISH Custom Process Flows:

4. The Portal, Portal Pages and Widgets – How They Work Together with the UI Formatter and UI Macro to create the custom process flows.

5. The Portal Magic – Part 1 – a deeper dive into the portal widget and how it creates the process flow:

6. The Portal Magic – Part 2a & b. A code review of sorts into the Portal Widget’s Server Script. (I had to break this into 2 videos)

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.