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: 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

Working with the BizFx SDK: Preparing the Base Solution for Customisation

In this article, we will look at preparing the BizFx project for customisation, by first aligning the default configuration of the SDK with the configuration that was deployed with the Sitecore Commerce installation, and then reviewing how to build and deploy solution.

Fair warning: I am not an expert in Angular, however the information provided is enough for getting started and performing the bare minimum to align the BizFx SDK for custom solutions.

Creating the BizFx Development Solution

The Sitecore Commerce On Premise package contains the BizFx SDK and the speak files that will be required for the new project.

Extract the contents of the Sitecore.BizFX.SDK.*.*.*.zip into your desired folder location, e.g. C:\projects\, and copy the speak-ng-bcl-*.*.*.tgz and speak-styling-*.*.*-r*****.tgz files into the same folder as the SDK.

The BizFx SDK does come with a README.md file containing some general instructions on preparing the solution for building, however we will highlight the main aspects of these instructions and cover some addition steps for local and production deployments.

In src\assets\config.json we need to copy the values from our local BizFx installation, located by default at <web root>\<BizFx>\assets\config.json, so that when we deploy our new version the configuration isn't corrupted. You'll notice the values that need to be updated are named 'PlaceholderFor<context>'.

{
  "EnvironmentName": "HabitatAuthoring",
  "EngineUri": "PlaceholderForAuthoringUrl",
  "IdentityServerUri": "PlaceholderForIdentityServerUrl",
  "BizFxUri": "PlaceholderForBizFxUrl",
  "Language": "PlaceholderForDefaultLanguage",
  "Currency": "PlaceholderForDefaultCurrency",
  "ShopName": "PlaceholderForDefaultShopName",
  "LanguageCookieName": "selectedLanguage",
  "EnvironmentCookieName": "selectedEnvironment",
  "AutoCompleteTimeout_ms": 300
}

The other value that will need to be updated for projects will be the EnvironmentName, which is used to select the default environment in BizFx.

It is recommended that the LanguageCookieName and EnvironmentCookieName properties remain as their default value as they may only need to be changed for advanced customisations. We will not cover modifying these properties in this article.

Prerequisites for Building

Assuming node installed already, from the BizFx solution folder, open your preferred CLI tool and run the following commands:-

npm config set @speak:registry=https://sitecore.myget.org/F/sc-npm-packages/npm/
npm config set @sitecore:registry=https://sitecore.myget.org/F/sc-npm-packages/npm/
​​​​​​​npm install speak-ng-bcl-0.8.0.tgz
npm install speak-styling-0.9.0-r00078.tgz
npm install @sitecore/bizfx
npm install

Building and Deploying the BizFx Solution

For building the BizFx Angular application, the ng build command will compile into an output folder named dist, defaulting to the workspace folder. Within the dist folder, the sdk will be the equivalent of the <BizFx> website folder in the web root.

For production builds execute the ng build -prod command, which optimises the compiled solution for production deployments.

For more information about the Angular commands see https://angular.io/cli.

To deploy the BizFx solution, copy the contents of the dist/sdk into the <web root>\<BizFx> folder.

Building and Deploying via Gulp

For building and deploying the BizFx solution, I use a gulp script to wrap the angular commands. See the Source Code link at the end of the article to download the script.

If you haven't installed gulp, run the following command:-

​​​​​​​npm install gulp

Running the default gulp command will build the solution, clean out the BizFx folder in the web root and the deploy the solution to the BizFx folder.

As the gulp tasks will be performing operations on system restricted folders, make sure you run the gulp command under Administrator privileges.

Source Code: Ajsuth.BizFx.DeploymentScripts

Sitecore Experience Commerce: Enabling Disassociate, Edit, and Transfer Inventory Actions for Published Sellable Items and Variants

In this article, we will look at how we can enable the Disassociate Sellable Item from Inventory Set, Edit Sellable Item Inventory and Transfer Inventory actions when viewing sellable item and variant entity views in BizFx.

For entities that have been configured to utilise entity versioning (catalogs, categories, and sellable items), via the VersioningPolicy in the VersioningPolicySet, all actions are disabled by default when the entity has been published.

The actions that are enabled are due to those actions being registered in the EntityVersionsActionsPolicy in the VersioningPolicySet, under the AllowedActions property.

There is no need to restrict the Inventory Sets actions in the Sellable Item and Variant entity views as they can be executed from within the Inventory Manager.

Note: Inventory association and disassociation actions apply to all entity versions of the sellable items as inventory records don't have strong ties to specific entity versions as inventory is not content.

To enable the Inventory Sets actions, simply add their action names to the AllowedActions, and deploy and bootstrap.

{
    "$type": "Sitecore.Commerce.Plugin.EntityVersions.EntityVersionsActionsPolicy, Sitecore.Commerce.Plugin.EntityVersions",
    "AllowedActions": {
    "$type": "System.Collections.Generic.List`1[[System.String, mscorlib]], mscorlib",
    "$values": [
            "AddEntityVersion",
            "AddCatalog",
            "DeleteCatalog",
            "AddCategory",
            "DeleteCategory",
            "AddSellableItem",
            "DeleteSellableItem",
            "AddBundle",
            "AssociateCategoryToCategoryOrCatalog",
            "AssociateSellableItemToCatalog",
            "AssociateSellableItemToCategory",
            "DisassociateItem",
            "MakePurchasable",
            "DisassociateSellableItemFromInventorySet",
            "EditSellableItemInventory",
            "TransferInventory"
        ]
    }
}

These actions can now be performed, regardless of whether or not the entity has been published.