Homelab, Linux & ABAP (~˘▾˘)~
 

[SAPUI5] Get i18n texts

To simply get access to i18n texts, I useally add this helper function to my BaseController.js

// helper for direct access to the ResourceBundle getText() function
getText : function (sTextKey, aParamter) {
    return this.getOwnerComponent().getModel("i18n").getResourceBundle().getText(sTextKey, aParamter)
}

Texts can then be read in every controller with

// i18n: objects=Amount of objects: {0}
this.getText("objects", [iLength])

[SAPUI5] Parse error message

If the error response is type json:

        oModel.callFunction("/getData", {
          method: "GET",
          urlParameters: {
            ID: myID,
          },
          success: oData => {
       
          },
          error: oError => MessageBox.error(JSON.parse(oError.responseText).error.message.value)
        });

If the error response is coming from a Gateway and has an XML body (link):

 MessageBox.error(jQuery.parseXML(oError.response.body).querySelector("message").textContent)

[SAPUI5] Call function of another controller using EventBus

https://sapui5.hana.ondemand.com/#/api/sap.ui.core.EventBus%23overview

In the receiving controller you need to subscribe your eventId and function you want to call from the second controller:

// Attaches an event handler to the event with the given identifier on the given event channel            
this.getOwnerComponent().getEventBus().subscribe("Default", "myEventId", () => {
    this._myFunctionIWantToCall();
});

The sending controller has to publish the event to trigger the function call:

// Fires an event using the specified settings and notifies all attached event handlers.
this.getOwnerComponent().getEventBus().publish("Default", "myEventId", {});

[SAPUI5] Get and set properties of a binded model and submit changes

Get:

const oModel = this.getView().getModel()
const sPath = this.getView().getBindingContext().sPath
const sID = oModel.getProperty(sPath+"/ID")

Set:

const newID = "12345"
oModel.setProperty(sPath + "/ID", newID)

When using the set property function, you can now submit your changes this way:

         // First check if there are any changes    
         if (!oModel.hasPendingChanges()) {
           MessageToast.show("Nothing to do.")
           return
        }
        
        // Now submit your changes
        oModel.submitChanges({
          success: () => MessageToast.show("Success!"),
          error: (err) => alert(err)
        })

This way is much more comfortable, than using oModel.update().

[CAP] SAPUI5 Filter using FilterOperator.Contains with oData V2

In my SAPUI5 Freesstyle frontend I created a search field above a list. In the searchfield handler I’m creating a filter with the provided query.

const sQuery = oEvent.getParameter("query");
new Filter("firstName", FilterOperator.Contains, sQuery);

Afterwards I’m binding the filter to my list to trigger the binding refresh. But when debugging the backend handler I noticed the following…

In my CAP on Read handler, the filter gets converted into a V4 compatible filter expression:

oData V4: $filter=contains(firstName,'Max')

As I’m forwarding the request to an external V2 oData API (SuccessFactors) this would not work, as for V2 the following filter syntax is needed:

oData V2: $filter=substringof('Max',firstName) eq true

As I could not find any solution to this problem, I manually passed my filter as custom property to my CAP Service and did a manual select.

Adding the custom property in the frontend in my searchfield handler:

onSearch: function (oEvent) {
			if (oEvent.getParameters().refreshButtonPressed) {
				this.onRefresh();
				return;
			}

			let oBindingInfo = this._oList.getBindingInfo("items");
			if (!oBindingInfo.parameters) oBindingInfo.parameters = {};
			if (!oBindingInfo.parameters.custom) oBindingInfo.parameters.custom = {};

			if (oEvent.getParameter("query")) {
				oBindingInfo.parameters.custom.filter = "%" + oEvent.getParameter("query") + "%";
			} else {
				oBindingInfo.parameters.custom.filter = undefined
			}
			this._oList.bindItems(oBindingInfo);
}

My CAP handler with the filter handling:

const { Object } = srv.entities
const SF_Srv = await cds.connect.to('SF')

srv.on('READ', Object, async req => {

            if (!req._queryOptions.filter) {
                // share request context with the external service 
                return SF_Srv.tx(req).run(req.query);
            } else {
                //if filter provided, build manually a select statement using a where condition
                let input = req._queryOptions.filter;
                const tx = SF_Srv.transaction(req);
                return await tx.run(
                    SELECT
                        .from(Object)
                        .where`firstName like ${input} or lastName like ${input}`)
            }
    })

As alternative you could also add the where condition directly to the query object:

const { Object } = srv.entities
const SF_Srv = await cds.connect.to('SF')

srv.on('READ', Object, async req => {

            if (req._query.filter) {
                //if filter provided, build manually a select statement using a where condition
                let { query } = req
                let input = req._queryOptions.filter

                if (!query.SELECT.where) query.SELECT["where"] = []

                query.SELECT.where.push(
                    { ref: ['firstName'] }, 'like', { val: input }, "or",
                    { ref: ['lastName'] }, 'like', { val: input }, "or",
                    { ref: ['object'] }, 'like', { val: input })
            }

            // share request context with the external service 
            return SF_Srv.tx(req).run(req.query)
    })

[CAP] Using a Tree in SAPUI5 Freestyle app

The following Links helped me implementing the tree functionality:

https://sapui5.hana.ondemand.com/#/entity/sap.m.Tree/sample/sap.m.sample.TreeOData

https://answers.sap.com/questions/13192367/sap-cds-how-to-add-hierarchy-annotations-saphierar.html

Define the data model in data-model.cds

entity Node {
    key NodeID         : Integer;
        HierarchyLevel : Integer;
        ParentNodeID   : Integer;
        Description    : String;
        drillState     : String;
}

Create testdata in my.test-Node.csv

NodeID;HierarchyLevel;ParentNodeID;drillState;Description  
1;0;null;"expanded";"1"
2;0;null;"expanded";"2"
3;0;null;"expanded";"3"
4;1;1;"leaf";"1.1"
5;1;1;"expanded";"1.2"
6;2;5;"leaf";"1.2.1"
7;2;5;"leaf";"1.2.2"

and deploy the testdata to your local sql db

cds deploy --to sqlite:db/test.db

Service Definition in test-service.cds

using my.test as db from '../db/data-model';

service testService {
     entity Nodes as projection on db.Node;
}

add the Tree controll to your Fiori UI view Tree.view.xml

		<Tree
		    id="Tree"
		    items="{path: '/Nodes',
				    parameters : {
		                countMode: 'Inline',
                        numberOfExpandedLevels: 3, 
                        treeAnnotationProperties: { 
                                                    hierarchyLevelFor : 'HierarchyLevel', 
                                                    hierarchyNodeFor : 'NodeID', 
                                                    hierarchyParentNodeFor : 'ParentNodeID', 
                                                    hierarchyDrillStateFor : 'drillState' 
                                                    }
		            }
            }">
			<StandardTreeItem title="{Description}"/>
		</Tree>

The output should be similar to this:

[SAPUI5] Filter on Model read

this.getModel().read("Object", {
                filters: [
                    new Filter({
                        path: "firstName",
                        operator: FilterOperator.EQ,
                        value1: "Max"
                    }),
                    new Filter({
                        path: "lastName",
                        operator: FilterOperator.EQ,
                        value1: "Mustermann"
                    })
                ],
                success: oData => { }
                error: err => { }
});

[SAPUI5] Binding with filter on XML View

https://sapui5.hana.ondemand.com/sdk/#/topic/5338bd1f9afb45fb8b2af957c3530e8f.html

There are two ways to use a filter.

Option 1:

items="{
				path: '/myItems',
				parameters : {
				  $filter : 'itemName eq \'myItemName\'',
				  $orderby : 'createdAt desc'
				},
}">

Option 2:

items="{
				path: '/myItems',
				parameters : {
				  $orderby : 'createdAt desc'
				},
				filters : {                                     
				  path : 'itemName ',
				  operator : 'EQ',
				  value1 : 'myItemName'
				},
}">

[SAPUI5] Toogle Dark mode from the Shell Header

There are several different types of buttons you can add to the Shell Header: https://sapui5.hana.ondemand.com/sdk/#/api/sap.ushell.renderers.fiori2.Renderer%23methods/Summary
For my test I choose the “addHeaderEndItem” Button. Add the fowlloing logic in the Component.js file to create the button and the logic for switching the theme:

		_addHeaderButton: function () {
			const oRenderer = sap.ushell.Container.getRenderer("fiori2");
			oRenderer.addHeaderEndItem("sap.ushell.ui.shell.ShellHeadItem", {
				id: "toogleTheme",
				icon: "sap-icon://circle-task-2",
				visible: "{device>/orientation/landscape}",
				tooltip: "Switch Theme",
				press: (oEvent) => {
					const toogleButton = oEvent.getSource();
					if (toogleButton.getIcon() === "sap-icon://circle-task-2") {
						sap.ui.getCore().applyTheme("sap_fiori_3_dark");
						toogleButton.setIcon("sap-icon://circle-task");
					} else {
						sap.ui.getCore().applyTheme("sap_fiori_3");
						toogleButton.setIcon("sap-icon://circle-task-2");
					}
				}
			}, true);
		},

Afterwars you need call the method in the init() function of the component. No reload the app and you will find the new button in the top right corner. Pressing will switch the theme to dark or back to light theme.

[SAPUI5] Message Box

https://experience.sap.com/fiori-design-web/message-box/
https://sapui5.hana.ondemand.com/#/entity/sap.m.MessageBox
https://sapui5.hana.ondemand.com/#/api/sap.m.MessageBox

			onRemoveItem: function (event) {
				const sPath = event.getSource().getParent().getBindingContext().getPath();
				const oModel = this.getView().getModel();
				MessageBox.confirm(
					this.getView().getModel("i18n").getProperty("MessageBoxText"), {
						actions: [MessageBox.Action.YES, MessageBox.Action.NO],
						emphasizedAction: MessageBox.Action.YES,
						onClose: (sAnswer) => {
							if (sAnswer === MessageBox.Action.YES) oModel.remove(sPath);
						}
					}
				);
			},