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:
- 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.
- 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.
- 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.
- 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.
- I wanted to have 2 process flow versions in the initial release:
- A chevron-like process flow that mimics the OOB process flow.
- A progress bar-like process flow.
- I wanted to use CSS instead of images to create the chevron/stage indicators.
- I wanted to allow user configuration for:
- Selecting what table to use the custom process flow (select the Application Scope, Table, Field and Sort Order for the tables).
- Selecting the HTML colors for the 3 different stage phases: Completed, Current and ToDo–custom for each table’s implementation.
- I wanted to exploit the existing UI Formatter and UI Macro combination concept while minimizing the amount of Jelly code needed.
- I wanted to use a portal page and widget to create the process flows and pull those into the form via the UI Formatter.
- I wanted to automate getting the table’s available process stages that drive the process flow.
- I wanted to automate setting the process stage colors based on:
- 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:
- 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:
- 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:
- 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.
- 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:
- Two Configuration Tables:
- Process Flow Configuration: This table allows selecting the table and its choice field then associating html color codes to the 3 states
- 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.
- Portal Items:
- A portal just for custom process flows — can have no header or footer styling
- A portal page (for each process flow type)
- Chevron process flow
- Progress Bar Process Flow
- A portal widget (for each process flow type)
- A UI Macro (sys_ui_macro):
- One for each process flow type.
- 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.
- Tables that will use a process flow formatter:
- This table needs a field on the table that is either a Choice type or a Reference Type
Summary of how this works.
- 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.
- 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.
- 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.
- 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.
- The UI macro retrieves the sys_id into a jelly variable and add it to the IFrame’s src url as a parameter
- The UI Macro retrieves the tableName into a jelly variabale and add to the url as a parameter
- The UI macro contains css styling to ensure the IFrame sits in a good location on the form:
- This css uses some negative margins to minimize the wasted space the process flow otherwise consumes.
- 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.
- The portal widget captures the tableName and sys_id of the current record and uses that to determine:
- The ToDo, Completed and Current html color codes to use for each state
- This is determined by getting all the Configuration records for the particular table
- Then compares those with the selected item and sets the chevron colors
- 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
- 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.
- if a match – that stage in current and we’ve found the current stage
- After we’ve found the current stage, naturally everything after that is ToDo
- The widget HTML doesn’t need to be modified
- The widget CSS doesn’t need to be modified
- The widget client script isn’t used
- The widget’s server script dynamically gets what it needs to work, so should not need to be edited.
- NOTE: This is where all the magic happens, so spend some time reverse engineering here.
- 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.
- The ToDo, Completed and Current html color codes to use for each state
How to Implement: You can either Download the V1.0.1 update set or create the application from scratch following the below steps.
- Determine which of the 2 pre-packaged process flow types you will use (or both):
- Chevron
- Progress Bar
- Create the Configuration Tables:
- Process Flow Configuration:
- Add to your application’s menu
- Application Access:
- Accessible from: All application scopes
- Caller access: Caller Tracking
- Can Read: true
- Can Create: true
- Can Update: true
- Allow access to this table via web services: true.
- Controls:
- Auto Number
- Prefix: PFCONFIG
- Number: 1,000
- Number of digits: 7
- Role: Whatever role you want to manage process flow configurations.
- Fields:
- Active, active, true/false, default value true
- HTML Color Code, html_color_code, string, 60 max char
- There’s a hint and Label link on this field about Section 508 Compliance
- Process Flow Stage, Choice
- ToDo, todo
- Current, current
- Completed, completed
- Application Scope (application_scope), Reference, (sys_scope)
- Table, table, reference, Table (sys_db_object)
- Reference Qual: Advanced:
- javascript:”sys_scope=” + current.getValue(‘application_scope’);
- Reference Qual: Advanced:
- Table Choice Field Configuration
- Add to your application’s menu.
- Application Access:
- Accessible from: All application scopes
- Caller access: Caller Tracking
- Can Read: true
- Can Create: true
- Can Update: true
- Allow access to this table via web services: true.
- Controls:
- Auto Number
- Prefix: TCFCONFIG
- Number: 1,000
- Number of digits: 7
- Role: Whatever role you want to manage process flow configurations.
- Fields:
- Active, active, true/false, default value true
- Process Flow Field, process_flow_field, reference, Dictionary Entry
- Reference Qual: Advanced:
- javascript:”sys_scope=” + gs.getCurrentApplicationId() + “^internal_type=choice^name=” + current.table.name;
- Reference Qual: Advanced:
- Application Scope, application_scope, reference, (sys_scope):
- Table, table, reference, Table (sys_db_object)
- Reference Qual: Advanced:
- javascript:”sys_scope=” + current.getValue(‘application_scope’);
- Reference Qual: Advanced:
- Sort Field, sort_field, reference, Dictionary Entry (sys_dictionary)
- Reference Qual: Advanced:
- javascript:”name=”+ current.process_flow_field.reference;
- Reference Qual: Advanced:
- Process Flow Configuration:
- Create the 2 properties that store the table names of these 2 tables.
- The property to store the sys_id of the Process Flow Configuration table
- Suffix: ProcessFlowConfigurationTable
- Name: Auto generated (use this in the widget server code)
- 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.
- Type: string
- Value: the scoped database table name.
- The property to store the sys_id of the Table Choice Field Configuration table
- Suffix: ChoiceFieldConfigurationTable
- Name: Auto generated (use this in the widget server code)
- 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.
- Type: string
- Value: the scoped database table name.
- The property to store the sys_id of the Process Flow Configuration table
- Create a UI Macro for each process flow type you’ll implement
- Name the Macro
- process_flow_{process_flow_type}. Examples:
- process_flow_chevrons
- process_flow_progress_bar
- process_flow_{process_flow_type}. Examples:
- Copy the code below into the XML
- Name the Macro
- Create a new Process Flow UI Formatter (sys_ui_formatter)–one for each table where a formatter will be
- 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).
- PF Formatter-Cust Order Chevrons
- PF Formatter-Cust Order Progress Bar
- Formatter: the API value from the Macro above – add “.xml” at the end
- Select the applicable table
- Type: Formatter
- 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).
- 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.
- Update the UI Macro’s iframe src url to use the applicable portal
- src=”/portalurl?id=portal_page_id$[AMP]tableName=$[tableName]$[AMP]mySysID=$[mySysID]”
- Update the UI Macro’s iframe src url to use the applicable portal
- Create the Portal page
- Add the css on the page to override some bootstrap css
- Create the portal widget
- See code below
- Adjust the system properties in 2 places
- See code below
- Update the portal page:
- Add to a 12-column-wide container on the page
- Insert the new widget in the 12-column container
- 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.
- For each table that will use the custom process flow, add a record for each stage: ToDo, Current, Completed
- 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.
- Add the formatter to the form in the desired location.
SCREENSHOTS AND CODE SNIPPETS:
UI Macros
Chevrons Process Flow:
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:
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.
Portal Pages (1 for each 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
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:
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;
}