Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
This article explains how to add a custom control to a nonscreen layout designer-based view in Microsoft Dynamics 365 Commerce.
You can enhance the information displayed on a Microsoft Dynamics 365 Commerce point of sale (POS) view by adding custom controls. A custom control allows you to add your own custom information to the existing POS views. You can implement custom controls by using the POS extension framework.
Note
Currently, you can't place a custom control in a particular location of a POS view. At runtime, the POS loads it in a fixed position.
This article applies to Dynamics 365 Finance, and Dynamics 365 Retail with Platform update 8, and Retail Application update 4 hotfix.
The following table lists the nonscreen 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 shows how to add a custom control to one of the existing POS views by using extensions. For example, suppose you want to show the product availability information in the product details view by adding a custom data list that has four columns - Location, Inventory, Reserved, and Ordered.
A custom control is an HTML page with the custom information to display. A corresponding TypeScript file contains the logic for the control.
To create the custom control, follow these steps:
Open Visual Studio 2015 in administrator mode.
Open Modern POS from \RetailSDK\POS.
Under the POS.Extensions project, create a new folder named ProdDetailsCustomColumnExtensions.
Under ProdDetailsCustomColumnExtensions, create a new folder named ViewExtensions.
Under ViewExtensions, create new folder named SimpleProductDetails.
Add a new HTML file inside the SimpleProductDetails folder and name it ProductAvailabilityPanel.html.
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>In the SimpleProductDetails folder, add a new TypeScript file and name it ProductAvailabilityPanel.ts.
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";Create a new class named ProductAvailabilityPanel and extend it from SimpleProductDetailsCustomControlBase.
export default class ProductAvailabilityPanel extends SimpleProductDetailsCustomControlBase { }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;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 }); }Add the OnReady method to bind the HTML control.
public onReady(element: HTMLElement): void { ko.applyBindingsToNode(element, { template: { name: ProductAvailabilityPanel.TEMPLATE_ID, data: this } }); }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 in the following section.
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); }); } } }Create a new .json file under the ProdDetailsCustomColumnExtensions folder and name it manifest.json.
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" } ] } } } } } }Open the extensions.json file under the POS.Extensions project and add the ProdDetailsCustomColumnExtensions samples, so that during runtime POS includes the extension.
{ "extensionPackages": [ { "baseUrl": "SampleExtensions2" }, { "baseUrl": "ProdDetailsCustomColumnExtensions" } ] }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 as part of the POS, 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" ],Compile and rebuild the project.
Access static resources in extensions
To access static resources in extensions and load them in POS, see the following example code. The code uses context.extensionPackageInfo.baseUrl to access static resources.
import { IPostProductSaleTriggerOptions, PostProductSaleTrigger } from "PosApi/Extend/Triggers/ProductTriggers";
/**
* Example implementation of a PostProductSale trigger that triggers a beep sound.
*/
export default class BeepSoundPostProductSaleTrigger extends PostProductSaleTrigger {
/**
* Executes the trigger functionality.
* @param {IPostProductSaleTriggerOptions} options The options provided to the trigger.
*/
public execute(options: IPostProductSaleTriggerOptions): Promise<void> {
this.context.logger.logInformational("Executing BeepSoundPostProductSaleTrigger with options " + JSON.stringify(options) + ".");
// You have to provide a full path to your resource file starting from the root of POS project.
let resourcePath: string = "/Resources/audio/beep.wav";
// And the apply it to the base URL of the extension package.
let filePath: string = this.context.extensionPackageInfo.baseUrl + resourcePath;
let beeper: HTMLAudioElement = new Audio(filePath);
beeper.play();
return Promise.resolve();
}
}
Validate the customization
To validate the customization, follow these steps:
- Press F5 and deploy the POS to test your customization.
- After POS launches, sign in to POS. Search for any product and go to the product details view. You should see the custom control that you added.