Sitecore Experience Commerce: Implementing Multi-Step Actions in the Business Tools

In this article, we will look at multi-step actions and how we can implement them for customising the Sitecore Commerce Business Tools.

What is a Multi-Step Action?

The multi-step action is the approach to building out entity view modals to act as a kind of wizard, where inputs from each step can affect the subsequent steps.

Adding a qualification/condition to a promotion - step 1.
Adding a qualification/condition to a promotion - step 2.

A Dive into the Multi-Step Action Implementation

We will take a dive into the pieces that make up the multi-step action implementation to see just how we should be building our custom multi-step modal, using the Add Qualification modal for promotions as our working example.

This section is focused on explaining what will be found in the current platform implementation, which can be useful if looking to extend the BizFx and Commerce Engine functionality, but can be skipped if you are only after Implementing Custom Multi-Step Actions as quick as possible.

MultiStepActionPolicy

The MultiStepActionPolicy houses a single property, FirstStep, which will contain the EntityActionView that will be populated in the modal entity view.

public MultiStepActionPolicy()
{
    this.FirstStep = new EntityActionView();
}

public EntityActionView FirstStep { get; set; }

In the following code snippet, we see that the MultiStepActionPolicy is added to the EntityActionView, in which the QualificationsDetails that is assigned to the EntityView will use to populate the first step of the add qualification modal.

Line 18 highlights the Name of the entity view action, which will be resolved to its localised value from the .../Commerce Terms/BusinessTools/ViewActionNames/AddQualification, and populates modal's title.

var actionPolicy = arg.GetPolicy<ActionsPolicy>();
actionPolicy.Actions.Add(
        new EntityActionView(new List<Policy>
        {
            new MultiStepActionPolicy
            {
                FirstStep = new EntityActionView
                {
                    Name = context.GetPolicy<KnownPromotionsActionsPolicy>().SelectQualification,
                    DisplayName = "Select Qualification",
                    Description = "Selects a Qualification",
                    IsEnabled = isEnabled,
                    EntityView = context.GetPolicy<KnownPromotionsViewsPolicy>().QualificationDetails
                }
            }
        })
        {
            Name = context.GetPolicy<KnownPromotionsActionsPolicy>().AddQualification,
            DisplayName = "Add Qualification",
            Description = "Adds a Qualification",
            IsEnabled = isEnabled,
            EntityView = string.Empty,
            Icon = "add"
        });
The entity view populated in the first step, of the Add Qualification modal, is resolved from the entity view action name of the EntityActionView, assigned to the MultiStepActionPolicy's FirstStep, which in this case is the SelectQualification action.

LocalizeEntityViewBlock

In the LocalizeEntityViewBlock, we find that the SelectQualification action name is being populated with the localised term, however as mentioned above, the modal utilises the AddQualification localised term instead, therefore we don't need to be concerned with this.

if (!action.HasPolicy<MultiStepActionPolicy>())
{
    continue;
}

var firstStepAction = action.GetPolicy<MultiStepActionPolicy>().FirstStep;
await SetActionLocalizedTerms(firstStepAction, context).ConfigureAwait(false);

MultiStepActionModel

This model is quite simple. The NextStep represents the name of the action to be executed when the modal form is submitted.

public MultiStepActionModel(string nextStep)
{
    this.NextStep = nextStep;
}

public string NextStep { get; set; }

In the following extract of DoActionSelectQualificationBlock, I have substituted out most of the code with comments, explaining the original code logic, to reduce the noise and keep focus on the multi-step implementation functionality.

In line 11, we see that the original condition property has been made readonly as we don't want the user to change their mind at this stage, however we don't want to hide the property as the user still needs to have context of what was previously selected.

Updating the readonly status of submitted view properties is considered a recommended practice.

Line 15 we have a comment that tells us that this is where the next step's view properties are added to the current entity view, building out the modal form.

This also means that the Do Action Blocks acts a pseudo Get Entity View Block as the initial modal's entity view is populated via the GetEntityView() API, utilising the IGetEntityViewPipeline under the hood, while subsequent updates in the multi-step implementation are triggered as the DoUxAction() API calls the IDoActionPipeline to process requests.

Finally, line 17 has the original entity view action name, 'AddQualification', added to the MultiStepActionModel as the NextStep and applied to the commerce context.

With the knowledge that adding the MultiStepActionModel to the context effectively creates the next step in the modal, there is no limit to how many steps we can add to a multi-step modal.

public override async Task<EntityView> Run(EntityView entityView, CommercePipelineExecutionContext context)
{
    /* validate action */

    /* validate promotion */

    var selectedCondition = entityView.Properties.FirstOrDefault(p => p.Name.Equals("Condition", StringComparison.OrdinalIgnoreCase));

    /* validate condition */

    selectedCondition.IsReadOnly = true;

    /* add and/or conditional operator. hide if no qualifications have been applied to the promotion so far */

    /* add new view properties for selected condition */

    context.CommerceContext.AddModel(new MultiStepActionModel(context.GetPolicy<KnownPromotionsActionsPolicy>().AddQualification));

    return entityView;
}
DoActionSelectQualificationBlock validates the Condition selection and updates the entity view to contain the remaining view properties required for configuring the 'Cart Has [count] Items?' qualification as the second step of this multi-step action modal.

CheckForMultiStepActionBlock

From the following snippet from CheckForMultiStepActionBlock, we see that it updates the current entity views action, removes the MultiStepActionModel from the commerce context, and adds the entity view to the commerce context.

In short, this logic is more of a helper, and we could get the same result by omitting the registration of the MultiStepActionModel, updating the entity view action, and add the entity view directly in the Do Action Block instead.

var multiAction = context.CommerceContext.GetModels<MultiStepActionModel>().FirstOrDefault();
if (string.IsNullOrEmpty(multiAction?.NextStep))
{
    return Task.FromResult(arg);
}

entityView.Action = multiAction.NextStep;
context.CommerceContext.RemoveModel(multiAction);
context.CommerceContext.AddModel(entityView);

The DoUxAction API

The last piece of the puzzle comes with BizFx's handling of the response of the DoUxAction API when we submit the modal dialog.

If an entity view object is returned in the API response object, BizFx will render the modal with the updated view instead of closing the modal and refreshing the current page view.

If we were to change the modal's entity view name during any of the steps it will not be reflected in the modal's title as there is no handling for this implemented by default in BizFx.

Summary

Let's review what we have learnt about multi-step actions.

  1. Adding the MultiStepActionPolicy to an EntityActionView basically swaps out the intended action from being executed in the initial GetEntityView() request, however the modal's title will reflect the initial EntityActionView's localised name.
  2. The LocalizeEntityViewBlock application to the EntityActionView in the MultiStepActionPolicy's FirstStep is superfluous and does not impact the multi-step implementation.
  3. Do Action Blocks act as pseudo Get Entity View Block for updating the modal's entity view.
  4. When updating entity views with multi-step actions, previously input view properties should be set to readonly as a recommended practice.
  5. The usage of the MultiStepActionModel is more of a helper model in CheckForMultiStepActionBlock, rather than a dependent piece of the implementation.
  6. There is no limit to how many steps we can add to a multi-step action modal.
  7. The modal title is not updated with an updated entity view.
  8. When the Commerce Engine's DoUxAction() API returns an entity view in the response model, BizFx will render it in the current modal view.

Implementing Custom Multi-Step Actions

For implementing custom multi-step actions we will skip the details about how to build out the pre-requisites, being the initial Populate View Actions Block to create the entity view action that will trigger the modal, and the Get Entity View Block that will populate the initial entity view that is rendered in the modal, and instead focus on the Do Action Block that will allow us to add the additional steps to the modal.

The multi-step sample show the initial entity view that we will create the second step for.

Essentially, we can implement multi-step actions without using any of the MultiStep classes, simply by adding the new entity view to the commerce context during a Do Action Block, however providing a more complete sample, the following code logic would be applied to our custom Do Action Block.

  1. Validate action
  2. Validate entity (if applicable)
  3. Validate current view properties
  4. Set current view properties to read only
  5. Add new view properties to entity view for next step
  6. Update entity view action with action to perform on the next time the modal is submitted
  7. Add entity view to the commerce context
public override async Task<EntityView> Run(EntityView entityView, CommercePipelineExecutionContext context)
{
    Condition.Requires(entityView).IsNotNull($"{Name}: The argument cannot be null");

    /* 1. Validate action */
    if (string.IsNullOrEmpty(entityView?.Action)
        || !entityView.Action.Equals("FirstAction", StringComparison.OrdinalIgnoreCase))
    {
        return await Task.FromResult(entityView).ConfigureAwait(false);
    }

    /* 2. Validate entity (if applicable) */
    // Not applicable

    /* Validate current view properties */
    // Not critical for sample implementation

    /* Set current view properties to read only */
    foreach (var property in entityView.Properties)
    {
        property.IsReadOnly = true;
    }

    /* Add new view properties to entity view for next step */
    entityView.Properties.Add(new ViewProperty
    {
        Name = "Step 2 Field 1"
    });

    entityView.Properties.Add(new ViewProperty
    {
        Name = "Step 2 Field 2",
        IsRequired = false
    });

    /* Update entity view action with action to perform on the next time the modal is submitted */
    entityView.Action = "SecondAction";

    /* Add entity view to the commerce context */
    context.CommerceContext.AddModel(entityView);

    return await Task.FromResult(entityView).ConfigureAwait(false);
}
Our multi-step sample entity view after the modal has been submitted, creating the second step view for completing data input.

Sitecore Experience Commerce: Configuring and Customising the BizFx Solution with Locale Support

In this article, we will look at how the locale can be changed in BizFx to support a desired locale type and the steps required to add support for locales that haven't been configured in the BizFx solution.

Working in Australia, a common customisation required is to add the 'en-AU' locale, primarily to support date/time formatting in BizFx.

A sample date using the default 'en' locale configuration.
The same sample date using the 'en-AU' locale.

Adding Support for a Locale

To add support for a locale that has not been registered to BizFx, the process is quite simple.

If you are looking to configure BizFx with any of the following locales, then this step will not be required. Default Locales: 'en' ('en-US'), 'fr-FR', 'de-DE', and 'Ja-JP'.

In the BizFx solution, open src\app\app.module.ts , import the locale and advanced formatting options from the extra dataset and register the locale via the registerLocaleData function.

The locales are already provided with the BizFx SDK, under src\locales.

/* Locales */
import localeFr from '../locales/fr';
import localeFrExtra from '../locales/extra/fr';
import localeJa from '../locales/ja';
import localeJaExtra from '../locales/extra/ja';
import localeDe from '../locales/de';
import localeDeExtra from '../locales/extra/de';
import localeEnAu from '../locales/en-AU';
import localeEnAuExtra from '../locales/extra/en-AU';

registerLocaleData(localeFr, 'fr-FR', localeFrExtra);
registerLocaleData(localeDe, 'de-DE', localeDeExtra);
registerLocaleData(localeJa, 'Ja-JP', localeJaExtra);
registerLocaleData(localeEnAu, 'en-AU', localeEnAuExtra);

Once we have registered the locale, if we were to configure the locale at this point we would find that there is some messaging in that is not rendered quite right. This is because there are also a handful of text overrides that are stored in the internationalisation folder of BizFx ('src\assets\i18n\') that also need to be added to override the default messaging in BizFx.

The LanguageSelector display text has not been configured.

Copy an existing locale file fom the internationalisation folder and update the messaging accordingly.

{
    "ValidationErrors": {
        "IsRequired": "{{DisplayName}} is required.",
        "IncorrectDecimalValue": "{{DisplayName}} has an incorrect decimal value.",
        "IncorrectDateValue": "{{DisplayName}} has an incorrect date value."
    },
    "ActionExecuted": "{{DisplayName}} executed.",
    "Back": "Back",
    "Logout": "Log out",
    "LanguageSelector": "Language displayed",
    "NoSearchResults": "No results matching your search were found.",
    "Searching": "Searching..."
}
The LanguageSelector display text when configured for 'en-AU'.

Configuring the Locale for BizFx

Now that the locale has been registered to the BizFx solution, the next step is to update the BizFx configuration to use it.

Open src\assets\config.json and set the Language to the newly registered locale.

{
  "EnvironmentName": "HabitatAuthoring",
  "EngineUri": "https://commerceauthoring.XC92.local",
  "IdentityServerUri": "https://XP0.IdentityServer",
  "BizFxUri": "https://bizfx.XC92.local",
  "Language": "en-AU",
  "Currency": "USD",
  "ShopName": "CommerceEngineDefaultStorefront",
  "LanguageCookieName": "selectedLanguage",
  "EnvironmentCookieName": "selectedEnvironment",
  "AutoCompleteTimeout_ms": 300
}

Deploy the BizFx solution as per your preferred deployment method and the locale will now be utilised throughout the BizFx tooling.

Source Code

The source code for this example of adding the 'en-AU' locale and other BizFx customisations can be found at Ajsuth.BizFx.

Sitecore Experience Commerce: Resolving Site-Catalog Association Lock

In this article, we look at how to fix the "'<catalog>' is already associated to another site." error message that can occur when attempting to save the catalog association to a site, although no other sites have an association to this catalog at the time.

Note: This solution is only intended for local development and not recommended for production instances.

Background

Over in Sitecore Commerce, the category and sellable item entities contain a ParentCatalogList property that houses the Sitecore ids of the associated catalog entities. For catalog entities however, the ParentCatalogList is being repurposed for the site association Sitecore id and acts as a lock, preventing the catalog from being associated to other sites.

If you happen to have deleted a site that had a existing catalog association, the catalog commerce entity will not be updated to release the lock. Similar scenarios may also be seen if the commerce shared database is restored to an environment that has conflicting sitecore ids with existing sites, as often can be seen during development across teams that do not sync their Sitecore and Commerce data.

Solution

Removing the lock can be achieved by executing the following script against the commerce shared database.

Update the database name and catalog friendly id values, in the highlighted lines, as appropriate.

USE SitecoreCommerce9_SharedEnvironments
GO

DECLARE @catalogEntityId VARCHAR(MAX) = 'Habitat_Master';
DECLARE @uniqueId VARCHAR(MAX);
DECLARE @entity VARCHAR(MAX);
DECLARE @modifiedEntity VARCHAR(MAX);

SELECT @entity = Entity,
       @uniqueId = Entities.UniqueId
FROM   sitecore_commerce_storage.CatalogEntities AS Entities
       INNER JOIN sitecore_commerce_storage.CatalogEntity AS Entity
               ON Entities.UniqueId = Entity.UniqueId
WHERE  Id = 'Entity-Catalog-' + @catalogEntityId

UPDATE sitecore_commerce_storage.CatalogEntity
SET    Entity = JSON_MODIFY(@entity, '$.ParentCatalogList', '')
WHERE  UniqueId = @uniqueId

Now that we have released the lock, we can associate the catalog to our site, however we may still encounter issues navigating through the catalog structure in the content editor and attempting to publish the site may throw null reference exceptions.

Error message when trying to navigate to category under catalog in the Content Editor.

Site Publish Error Message
Job started: Publish to 'web'|#Exception: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.NullReferenceException: Object reference not set to an instance of an object.
at Sitecore.Commerce.Engine.Connect.DataProvider.ReadOnlyCatalogDataProvider.GetChildIDs(ItemDefinition parentItem, CallContext context)
...

These issues are caused by the catalog's Template Overrides, which are configured to templates that don't exist in this Sitecore instance.

Updating the Template Overrides to valid templates for the site will resolve these issues and you should be up and running again.

Sitecore Experience Commerce: Associating Inventory from Sellable Item and Variant Pages

In this article, we introduce a small custom plugin for Sitecore Commerce Business Tools that enables the inventory association for sellable items and variants directly from the Merchandising Manager pages.

Establishing inventory information associations is an action hosted within the Inventory Manager. Prior to the introduction of inventory indexes in XC 9.3, locating an existing inventory information record for large catalogs via the Inventory Manager can be quite a tedious task with the pagination controls being the only form of search.

The custom plugin adds the Associate Sellable Item to an inventory set action to the Inventory Sets entity view for the Sellable Item and Variant merchanding pages.

A transitional step to select the Inventory Set is added to the modal view, which in the Inventory Manager is driven by the inventory set being viewed.

The final step locks the Inventory Set and Sellable Item fields, as they have already been identified, and if the sellable item already had been associated to the selected inventory set then this would behave like an edit view, keeping the UX at the forefront of this implementation.

Managing existing inventory information records can also be more achieved via the Sellable Item and Variant pages in the Inventory Sets entity view actions, however prior to XC 9.3 a business was required to create a new entity version to enable these controls, unless the EntityVersionsActionsPolicy had been updated to allow these actions to bypass the entity version (as they should). This process was also documented in Enabling Disassociate, Edit, and Transfer Inventory Actions for Published Sellable Items and Variants.

Source Code: Extended Sitecore Commerce Inventory project repository

Sitecore Identity Server: Increasing the Token Lifetime for Local Development

In this article, we will review how to change the authentication token timeout values that force us to log back in to Sitecore or request a new token from Postman. If you are like me, generally working with Sitecore/Sitecore Commerce 10+ hours per day, 6 days a week, it can seem like you are kicked out every 5 minutes. Personally, I set these timeouts to a week (604800 seconds).

Changing the timeouts are not recommended for production instances.

Changing the Timeouts in Sitecore Identity Server

Sitecore Identity Server was first introduced with Sitecore Commerce 9.0.0 and with the release of Sitecore 9.1, Sitecore Identity Server was added to Sitecore authentication process.

Updating the Token Lifetimes in 9.0.X

  1. Open <Sitecore Identity Server root>\wwwroot\appsettings.json.
  2. Under AppSettings.Clients, update the CommerceBusinessTools and Postman API clients for BizFx and Postman applications respectively :-
    1. Update the AccessTokenLifetimeInSeconds and IdentityTokenLifetimeInSeconds from the default 3600 (seconds) to the desired timespan, in seconds.
    2. Save the configuration.
  3. Restart the Sitecore Identity Server so that the updated configuration is consumed on startup.

Updating the Token Lifetimes in 9.3

  1. Open <Sitecore Identity Server root>\Config\production\Sitecore.Commerce.IdentityServer.Host.xml.
  2. Under /Settings/Sitecore/IdentityServer/Clients, update the CommerceClient and PostmanClient for BizFx and Postman applications respectively:-
    1. Update the AccessTokenLifetimeInSeconds and IdentityTokenLifetimeInSeconds from the default 3600 (seconds) to the desired timespan, in seconds.
    2. Save the configuration.
  3. Restart the Sitecore Identity Server so that the updated configuration is consumed on startup.

Sitecore: Using a Dedicated Custom Include Folder for Actual Custom Configuration Files

In this article, we will look at creating our own custom include folder in Sitecore to avoid the ugliness of the folder/file structure that comes with a Sitecore installation and all of its modules.

Include folder contents from default installation of Sitecore with Sitecore XC 9.3

Previously, I covered Managing Commerce Configuration to Align with Helix Principles, however this approach was overly complex and not really developer friendly. Since then I have concluded that the simpler approach would be leave the "custom" folder for the Sitecore integrations and create a dedicated custom folder outside of the "custom" for solution specific custom configurations.

Added benefits to this approach include:

  • When performing an upgrade, a new or updated configuration will not override your custom configurations based on load order.
  • You won't need to revisit the Layers.config file as potentially required with my previously documented approach.
  • For local development, you can comfortablely delete the dedicated custom folder and redeploy, avoiding any stale custom configurations from your current build.

Configuring Layers.config

All that is required for this approach is to add the new custom folder to Layers.config and ensure the custom solution configuration files are placed in the custom folder accordingly.

...
<layer name="Custom" includeFolder="/App_Config/Include/">
  <loadOrder>
   <add path="Foundation" type="Folder" />
   <add path="Feature" type="Folder" />
   <add path="Project" type="Folder" />
  </loadOrder>
</layer>
<layer name="Custom_Include" includeFolder="/App_Config/Custom_Include/">
  <loadOrder>
    <add path="Foundation" type="Folder" />
    <add path="Feature" type="Folder" />
    <add path="Project" type="Folder" />
  </loadOrder>
</layer>
...

Add the custom folder after the "Custom" layer entry and include the "Foundation", "Feature", and "Project" folders to manage configuration files using Helix principles.

Custom Include folder contents segregated from the Include folder of the default installation of Sitecore with Sitecore XC 9.3

Summary

No longer will you need to fight for configuration priority over the platform's standard configuration files or soil your solution with 'zzz' prefixed patch files.