Skip to main content

Custom Workspace Tab Icons

Overview
#

If you have ever seen an Interaction record in CSM Configurable Workspace, you have seen icons in the record’s tab, based on the Interaction’s type. It’s a great way to quickly identify the type of interaction, but the ability to customize the icon or implement similar behavior for other tables isn’t clear.

Luckily it’s implemented in code we can reach and it’s pretty simple, allowing you to customize both the icon and the text for the tab. I implemented it to give different case types different icons and display text, but you can customize it to whatever you want.

Studying Interaction’s Implementation
#

The Interaction record has its own variant and as part of the variant is a data resource CSM Interaction Record Page Controller. This controller is responsible for much of the custom functionality in the interaction, but it includes the code used to set the icon dynamically based on Interaction type and whether it is active.

Here is the code snippet that sets the icon. We are passing it an icon name and a status, which is either active or inactive. It’s just that simple—just call helpers.screen.updateStatus to update the icon. There is also a title property that can be set to change the text on the tab.

Update Icon Snippet
#

54 helpers.screen.updateStatus({
55        icon: {
56            name: getIndicatorIcon({
57                type,
58                status
59            }),
60            status: status
61        }
62    });

The valid icons are all from those available in the Icon component. You’ll need to append -fill or -outline to the icon name to get the fill or outline version. The icon’s inactive and active colors are theme specific and cannot be changed.

Creating a Custom Data Resource
#

There are many ways to make use of our discovered API, but when I went to implement it for a client, I felt a data resource was the best way to go. This allows for reusable functionality and means the client doesn’t have to touch a line of code in the controller and can reuse it across any record variant. I personally did this in a custom scope, so I could reuse the code across clients easily.

Our data resource is made up of a UX Macroponent Definition [sys_ux_macroponent] (macroponents are also how our variants are implemented so you may have seen them if you’ve dug in behind the scenes of UI Builder), a UX Controller [sys_ux_controller], and a UX Client Script [sys_ux_client_script].

Setting up the Data Resource
#

Create a UX Macroponent Definition

Create a UX Macroponent Definition

Create a UX Macroponent Definition [sys_ux_macroponent]. We can first just give it a name, change Category to UI Controller, and save it.
Create a UX Controller

Create a UX Controller

Create a new UX Controller [sys_ux_controller] referencing the macroponent you just created. Leave the Controller config guidance blank.
Create a UX Client Script

Create a UX Client Script

Create a new UX Client Script [sys_ux_client_script] from the related list on the macroponent definition.

Configuring our Macroponent Definition
#

The hardest part of this is configuring the macroponent definition, this is normally configured in UI Builder rather than directly in the record. I just did a general lift and shift of the interaction implementation, and I’ll explain the pieces as we go.

Internal Event Mappings
#

The internal event mappings let Next Experience know what events to listen for and what to do when they are triggered. The two we need for this are MACROPONENT_PROPERTY_CHANGED and MACROPONENT_READY. We bind these back to our client script we created, so grab that sys_id. We are mapping events for when the page loads and when a property changes on the macroponent, allowing us to update the icon and text on the tab initially and then if any of the properties change, we can update the text and icon again.

{
    "MACROPONENT_PROPERTY_CHANGED": [
        {
            "broker": null,
            "clientScript": {
                "sysId": "<client_script_id>"
            },
            "conditional": null,
            "declarativeAction": null,
            "event": null,
            "operation": null,
            "targetId": "onMacroponentPropertyChanged",
            "type": "CLIENT_SCRIPT"
        }
    ],
    "MACROPONENT_READY": [
        {
            "broker": null,
            "clientScript": {
                "sysId": "<client_script_id>"
            },
            "conditional": null,
            "declarativeAction": null,
            "event": null,
            "operation": null,
            "targetId": "onMacroponentReady",
            "type": "CLIENT_SCRIPT"
        }
    ]
}

Properties
#

This is where we define the properties for our data resource, in this case, I’ve opted to define three different properties, one for the current table, one for the recordValues and one for a configuration object.

You can learn more about what properties are available and how to configure them in depth in this great article on Community.

I’ve configured ours to be mandatory and have fieldType’s that match the types of data we’re using.

[
    {
        "defaultValue": null,
        "description": null,
        "disabled": false,
        "fieldType": "json",
        "label": "setting",
        "mandatory": true,
        "name": "setting",
        "readOnly": false,
        "selectable": false,
        "typeMetadata": null,
        "valueType": "string"
    },
    {
        "defaultValue": null,
        "description": "",
        "disabled": false,
        "fieldType": null,
        "label": "recordValues",
        "mandatory": true,
        "name": "recordValues",
        "readOnly": false,
        "selectable": false,
        "typeMetadata": null,
        "valueType": null
    },
    {
        "defaultValue": null,
        "description": null,
        "disabled": false,
        "fieldType": "string",
        "label": "table",
        "mandatory": true,
        "name": "table",
        "readOnly": false,
        "selectable": false,
        "typeMetadata": null,
        "valueType": "string"
    }
]

Writing our UX Client Script
#

Here I’ve written a simple client script that updates the icon and title on the tab based on a combination of the setting, recordValues, and table properties. Essentially recordValues is an object, with each key representing a table, and the value being an object with the field values for that table and the icon that should be displayed. You could expand this to have conditional icons based on the record values, but I only required a single icon for each table.

If there are no values for the table, then we don’t update the icon or title. If there is a value, we update the title to be the display value of the field, and we update the icon to be the one defined in the setting object. If there are multiple fields, we join those that exist with a dash between them.

I also don’t touch the active state of the icon, since it wasn’t relevant to my use case, but you could add that if you wanted.

/**
 * @param {params} params
 * @param {api} params.api
 * @param {any} params.event
 * @param {any} params.imports
 */
function handler({ api, event, helpers, imports }) {
  let updateObj = {};

  var { setting, recordValues, table } = api.context.props;

  if (!setting || !setting[table]) return;

  //Access params from the setting object based on the tableName
  var { fields, icon } = setting[table];

  // Populate the icon from the setting object
  if (icon) {
    updateObj.icon = {
      name: icon,
      status: "active",
    };
  }

  // Populate the title from the setting object and record values
  if (fields && recordValues) {
    var outputArr = [];
    for (let field of fields) {
      if (recordValues[field] && recordValues[field].displayValue) {
        outputArr.push(recordValues[field].displayValue);
      }
    }

    //If there are multiple values, join them with a dash
    if (outputArr.length > 0) {
      updateObj.title = outputArr.join(" - ");
    }
  }

  //If we don't have a title or icon, don't update
  if (updateObj.title || updateObj.icon) {
    helpers.screen.updateStatus(updateObj);
  }
}

Using our Data Resource
#

To use our data resource, we just need to add it to a variant we want to use the functionality on. I’ve just duplicated the CSM Default record variant and added our data resource to it. Remember record variants are not created equal and there is a significant difference between the “old” and “new” variants, the way we are using the form controller only works on the “new” record variant.

Add the Data Resource to the Variant

Add the Data Resource to the Variant

We first need to add the data resource to the variant we want to use it on. It will be named after your UX Controller.
Configure the Settings Object

Configure the Settings Object

Next we need to configure the settings object. In this case I’ve configured three different tables; they don’t all overwrite the title.
Map the Record Values

Map the Record Values

We need to map the record values data resource as an input to ours. Specifically we want Record > Form > Fields
Map the Table Name

Map the Table Name

We need to map the table name data resource as an input to ours so we can use it in our client script.
Step 5: Profit??

Step 5: Profit??

Now that we have our data resource configured, it should be working, debugger your client script if you run into any issues.

Any changes to the settings object or the table will trigger the client script, which in turn re-evaluates and sets either the icon or title.

Thomas Walker
Author
Thomas Walker