Add custom controls to POS views that aren't screen layout designer-based

You can enhance the information displayed on a Dynamics 365 for Retail POS view by adding custom controls. A custom control allows you to add your own custom information to the existing POS views. Custom controls can be implemented by using the POS extension framework. Currently, you cannot place the custom control in the desired location, at runtime, POS will load it in a fixed position.

This topic applies to Dynamics 365 for Finance and Operations, and Dynamics 365 for Retail with Platform update 8, and Retail Application update 4 hotfix.

The following table lists the non-screen layout designer-based views that support custom controls.

POS views Support custom control Number of custom controls
Customer Add/Edit view Yes Multiple
Address Add/Edit view Yes Multiple
Customer details view Yes Multiple
Product details view Yes Multiple
Price check view Yes Multiple

The following table lists the screen layout designer based-views that support custom controls.

POS views Support custom control Number of custom controls
Cart view Yes 10

Create the custom control

The following example demonstrates how to add a custom control to one of the existing POS views using extensions. For example, suppose you want to show the product availability information in the product details view by adding custom data list that has four columns - Location, Inventory, Reserved, and Ordered.

A custom control is an HTML page with the custom information to be displayed. A corresponding Typescript file contains the logic for the control.

  1. Open Visual Studio 2015 in administrator mode.
  2. Open Modern POS from \RetailSDK\POS.
  3. Under the POS.Extensions project, create a new folder named ProdDetailsCustomColumnExtensions.
  4. Under ProdDetailsCustomColumnExtensions, create a new folder named ViewExtensions.
  5. Under ViewExtensions, create new folder named SimpleProductDetails.
  6. Add a new HTML file inside the SimpleProductDetails folder and name it ProductAvailabilityPanel.html.
  7. Open ProductAvailabilityPanel.html and add the following code. The code adds a POS data list control to show the product availability information and the width of the control.
    <!DOCTYPE html>
    <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="utf-8" />
        <title></title>
    </head>
    <body>
        <!-- Note: The element ID is different than the ID generated by the POS extensibility framework. This 'template' ID is not used by the POS extensibility framework. -->
        <script id="Microsoft_Pos_Extensibility_Samples_ProductAvailabilityPanel" type="text/html">
            <h2 class="marginTop8 marginBottom8" data-bind="text: title"></h2>
            <div class="width400 grow col">
                <div id="Microsot_Pos_Extensibility_Samples_ProductAvailabilityPanel_DataList" data-bind="msPosDataList: dataList"></div>
            </div>
        </script>
    </body>
    </html>
    
  8. In the SimpleProductDetails folder, add a new typescript file and name it ProductAvailabilityPanel.ts.
  9. Add the following import statements to import the relevant entities and context.
    import {
    
        SimpleProductDetailsCustomControlBase,
        ISimpleProductDetailsCustomControlState,
        ISimpleProductDetailsCustomControlContext
    
    } from "PosApi/Extend/Views/SimpleProductDetailsView";
    
    import { InventoryLookupOperationRequest, InventoryLookupOperationResponse } from "PosApi/Consume/OrgUnits";
    import { ClientEntities, ProxyEntities } from "PosApi/Entities";
    import { ArrayExtensions } from "PosApi/TypeExtensions";
    import { DataList, SelectionMode } from "PosUISdk/Controls/DataList";
    
  10. Create a new class named ProductAvailabilityPanel and extend it from SimpleProductDetailsCustomControlBase.
    export default class ProductAvailabilityPanel extends SimpleProductDetailsCustomControlBase { }
    
  11. Inside the class, declare the following variables for state and data list information.
    private static readonly TEMPLATE_ID: string = "Microsot_Pos_Extensibility_Samples_ProductAvailabilityPanel";
    public readonly orgUnitAvailabilities: ObservableArray<ProxyEntities.OrgUnitAvailability>;
    public readonly dataList: DataList<ProxyEntities.OrgUnitAvailability>;
    public readonly title: Observable<string>;
    private _state: ISimpleProductDetailsCustomControlState;
    
  12. Add a class constructor method to initialize the data list columns.
    constructor(id: string, context: ISimpleProductDetailsCustomControlContext) {
    
        super(id, context);
        this.orgUnitAvailabilities = ko.observableArray([]);
        this.title = ko.observable("Product Availability");
        this.dataList = new DataList<ProxyEntities.OrgUnitAvailability>({
            columns: [
    
                {
                    title: "Location",
                    ratio: 31,
                    collapseOrder: 4,
                    minWidth: 100,
                    computeValue: (value: ProxyEntities.OrgUnitAvailability): string => {
                        return value.OrgUnitLocation.OrgUnitName;
    
                    }
    
                },
    
                {
                    title: "Inventory",
                    ratio: 23,
                    collapseOrder: 3,
                    minWidth: 60,
                    computeValue: (value: ProxyEntities.OrgUnitAvailability): string => {
                        return ArrayExtensions.hasElements(value.ItemAvailabilities) ? value.ItemAvailabilities[0].AvailableQuantity.toString() : "0";
                    }
    
                },
    
                {
                    title: "Reserved",
                    ratio: 23,
                    collapseOrder: 1,
                    minWidth: 60,
                    computeValue: (value: ProxyEntities.OrgUnitAvailability): string => {
                        return ArrayExtensions.hasElements(value.ItemAvailabilities) ? value.ItemAvailabilities[0].PhysicalReserved.toString() : "0";
                    }
                },
    
                {
                    title: "Ordered",
                    ratio: 23,
                    collapseOrder: 2,
                    minWidth: 60,
                    computeValue: (value: ProxyEntities.OrgUnitAvailability): string => {
                        return ArrayExtensions.hasElements(value.ItemAvailabilities) ? value.ItemAvailabilities[0].OrderedSum.toString() : "0";
                    }
                }
    
            ],
    
            itemDataSource: this.orgUnitAvailabilities,
            selectionMode: SelectionMode.None
    
        });
    
    }
    
  13. Add the OnReady method to bind the HTML control.
    public onReady(element: HTMLElement): void {
    
        ko.applyBindingsToNode(element, {
            template: {
                name: ProductAvailabilityPanel.TEMPLATE_ID,
                data: this
    
            }
    
        });
    }
    
  14. Add the init method to get the product availability details so when the page loads, the data is fetched and updated in the data list.
    public init(state: ISimpleProductDetailsCustomControlState): void {
    
        this._state = state;
        let correlationId: string = this.context.logger.getNewCorrelationId();
        if(!this._state.isSelectionMode) {
        this.isVisible = true;
    
        let request: InventoryLookupOperationRequest<InventoryLookupOperationResponse> =
            new InventoryLookupOperationRequest<InventoryLookupOperationResponse>
                (this._state.product.RecordId, correlationId);
        this.context.runtime.executeAsync(request)
            .then((result: ClientEntities.ICancelableDataResult<InventoryLookupOperationResponse>) => {
    
                if (!result.canceled) {
                    this.orgUnitAvailabilities(result.data.orgUnitAvailability);
                }
    
            }).catch((reason: any) => {
                this.context.logger.logError(JSON.stringify(reason), correlationId);
    
            });
    }
    
    }
    
    The entire code example is shown below.
    import {
        SimpleProductDetailsCustomControlBase,
        ISimpleProductDetailsCustomControlState,
        ISimpleProductDetailsCustomControlContext
    } from "PosApi/Extend/Views/SimpleProductDetailsView";
    
    import { InventoryLookupOperationRequest, InventoryLookupOperationResponse } from "PosApi/Consume/OrgUnits";
    import { ClientEntities, ProxyEntities } from "PosApi/Entities";
    import { ArrayExtensions } from "PosApi/TypeExtensions";
    import { DataList, SelectionMode } from "PosUISdk/Controls/DataList";
    export default class ProductAvailabilityPanel extends SimpleProductDetailsCustomControlBase {
    
        private static readonly TEMPLATE_ID: string = "Microsot_Pos_Extensibility_Samples_ProductAvailabilityPanel";
        public readonly orgUnitAvailabilities: ObservableArray<ProxyEntities.OrgUnitAvailability>;
        public readonly dataList: DataList<ProxyEntities.OrgUnitAvailability>;
        public readonly title: Observable<string>;
        private _state: ISimpleProductDetailsCustomControlState;
    
        constructor(id: string, context: ISimpleProductDetailsCustomControlContext) {
            super(id, context);
            this.orgUnitAvailabilities = ko.observableArray([]);
            this.title = ko.observable("Product Availability");
            this.dataList = new DataList<ProxyEntities.OrgUnitAvailability>({
    
                columns: [
                    {
                        title: "Location",
                        ratio: 31,
                        collapseOrder: 4,
                        minWidth: 100,
                        computeValue: (value: ProxyEntities.OrgUnitAvailability): string => {
                            return value.OrgUnitLocation.OrgUnitName;
                        }
                    },
    
                    {
                        title: "Inventory",
                        ratio: 23,
                        collapseOrder: 3,
                        minWidth: 60,
                        computeValue: (value: ProxyEntities.OrgUnitAvailability): string => {
                            return ArrayExtensions.hasElements(value.ItemAvailabilities) ? 
                            value.ItemAvailabilities[0].AvailableQuantity.toString() : "0";
                        }
                    },
    
                    {
                        title: "Reserved",
                        ratio: 23,
                        collapseOrder: 1,
                        minWidth: 60,
                        computeValue: (value: ProxyEntities.OrgUnitAvailability): string => {
                            return ArrayExtensions.hasElements(value.ItemAvailabilities) ? 
                            value.ItemAvailabilities[0].PhysicalReserved.toString() : "0";
                        }
                    },
    
                    {
                        title: "Ordered",
                        ratio: 23,
                        collapseOrder: 2,
                        minWidth: 60,
                        computeValue: (value: ProxyEntities.OrgUnitAvailability): string => {
                            return ArrayExtensions.hasElements(value.ItemAvailabilities) ? 
                            value.ItemAvailabilities[0].OrderedSum.toString() : "0";
                        }
    
                    }
    
                ],
    
                itemDataSource: this.orgUnitAvailabilities,
                selectionMode: SelectionMode.None
            });
    
        }
    
        /**
        * Binds the control to the specified element.
        * @param {HTMLElement} element The element to which the control should be bound.
        */
    
        public onReady(element: HTMLElement): void {
            ko.applyBindingsToNode(element, {
                template: {
                    name: ProductAvailabilityPanel.TEMPLATE_ID,
                    data: this
    
                }
    
            });
    
        }
    
        /**
        * Initializes the control.
        * @param {ISimpleProductDetailsCustomControlState} state The initial state of the page used to initialize the control.
        */
    
        public init(state: ISimpleProductDetailsCustomControlState): void {
            this._state = state;
            let correlationId: string = this.context.logger.getNewCorrelationId();
            if (!this._state.isSelectionMode) {
                this.isVisible = true;
                let request: InventoryLookupOperationRequest<InventoryLookupOperationResponse> =
                    new InventoryLookupOperationRequest<InventoryLookupOperationResponse>
                        (this._state.product.RecordId, correlationId);
                this.context.runtime.executeAsync(request)
                    .then((result: ClientEntities.ICancelableDataResult<InventoryLookupOperationResponse>) => {
                        if (!result.canceled) {
                            this.orgUnitAvailabilities(result.data.orgUnitAvailability);
                        }
    
                    }).catch((reason: any) => {
                        this.context.logger.logError(JSON.stringify(reason), correlationId);
    
                    });
            }
        }
    }
    
  15. Create a new .json file and under the ProdDetailsCustomColumnExtensions folder and name it manifest.json.
  16. In the manifest.json file, add the following code.
     {
    
        "$schema": "../manifestSchema.json",
            "name": "Pos_Extensibility_Samples",
                "publisher": "Microsoft",
                    "version": "7.2.0",
                        "minimumPosVersion": "7.2.0.0",
                            "components": {
            "extend": {
                "views": {
                    "SimpleProductDetailsView": {
                        "controlsConfig": {
                            "customControls": [
                                {
    
                                    "controlName": "productAvailabilityPanel",
                                    "htmlPath": "ViewExtensions/SimpleProductDetails/ProductAvailabilityPanel.html",
                                    "modulePath": "ViewExtensions/SimpleProductDetails/ProductAvailabilityPanel"
                                }
                            ]
    
                        }
                    }
                }
            }
        }
    }
    
  17. Open the extensions.json file under the POS.Extensions project and add the ProdDetailsCustomColumnExtensions samples, so during runtime POS will include the extension.
     {
        "extensionPackages": [
            {
                "baseUrl": "SampleExtensions2"
            },
            {
                "baseUrl": "ProdDetailsCustomColumnExtensions"
            }
        ]
    }
    
  18. Open the tsconfig.json and comment out the extension package folders from the exclude list. POS uses this file to include or exclude extensions. By default, the list contains the excluded extensions list. If you want to include any extension part of the POS, then you need add the extension folder name and comment out the extension from the extension list as shown.
     "exclude": [
        "AuditEventExtensionSample",
        "B2BSample",
        "CustomerSearchWithAttributesSample",
        "FiscalRegisterSample",
        "PaymentSample",
        "PromotionsSample",
        "SalesTransactionSignatureSample",
        "SampleExtensions",
        //"SampleExtensions2",
        //"ProdDetailsCustomColumnExtensions"
    ],
    
  19. Compile and rebuild the project.

Validate the customization

  1. Press F5 and deploy the POS to test your customization.
  2. After POS launches, login to POS. Search for any product and navigate to the product details view. You should see the custom control that you added.