Skip to Content
Technical Articles
Author's profile photo Mio Yasutake

Controlling CAP actions on Fiori UI

Motivation

I’m going develop a call center app (just for demonstration purpose), where incoming queries are posted into the system. When an operator receives a new query, they set its status to “started”. When the query is resolved, they set its status to “closed”.

The key here is how to enable/disable action buttons according to the query’s status.
I have achieved this using CAP annotations and service implementations.

*Please not that this project is based on OData V2 and not intended for OData V4.For V4 actions, please refer to the capire document.

 

You can find the code at GitHub.
https://github.com/miyasuta/v2-action-control

 

Actions and Functions

Actions and Functions allow encapsulating logic for modifying or requesting data that goes beyond simple CRUD. In the OData specification document, Functions and Actions are defined as below.

Actions

Actions are operations exposed by an OData service that MAY have side effects when invoked. Actions MAY return data but MUST NOT be further composed with additional path segments. 

Functions

Functions are operations exposed by an OData service that MUST return data and MUST have no observable side effects.

Simply put, Actions can be used to modify data, while Functions are used to get specific (set of) data.

Bound Actions/Functions and Unbound Actions/Functions

There are two types of Actions and Functions: Bound Actions/Functions and Unbound Actions/Functions.
Bound Actions/Functions are associated with individual entities. Bound Actions/Functions receive key fields of the entity which they are bound to, and you can use those keys to get detail of the entity or manipulate the entity.
Unbound Actions/Functions are not associated with particular entity.
How to define Actions and Functions is described in the cap document.

 

Development

In this section, I’m going to show you how to implement bound actions and enable/disable the actions according to the query’s status.

1. Schema definition

I have defined one entity for inquires and two for value help (category & status).

namespace demo.callcenter;
using { cuid, managed, sap.common } from '@sap/cds/common';

entity Inquiries: cuid, managed{
    category: Association to Category @title: 'Category';
    inquiry: String(1000) @title: 'Inquiry';
    startedAt: DateTime @title: 'Started At'; 
    answer: String(1000) @title: 'Answer';
    status: Association to Status @title: 'Status';
    satisfaction: Integer @title: 'Satisfacton' 
        enum {
            veryUnsatisfied = 1;
            unsatisfied = 2;
            neutral = 3;
            satisfied = 4;
            verySatisfied = 5
        };
    virtual startEnabled: Boolean;
    virtual closeEnabled: Boolean;    
}

entity Category: common.CodeList {
    key code   : String(1)
}

entity Status: common.CodeList {
    key code: String(1)
}

 

The important part is below. These are virtual elements for controlling actions.
As Oliver Klemenz suggested in his comment, virtual elements don’t create fields on persistence layer, thus suit the purpose.

I’ll implement the logic for filling these fields later.

    virtual startEnabled: Boolean;
    virtual closeEnabled: Boolean;    

 

An inquiry has 3 statuses. These statuses will be used for controlling actions.

  1. New
  2. In Process
  3. Closed

 

2. Service definition

I have defined two bound actions: start and close.

using { demo.callcenter as call } from '../db/schema';

service CallCenterService {
    entity Inquiries as projection on call.Inquiries
    actions {
        @sap.applicable.path: 'startEnabled'
        action start();
        @sap.applicable.path: 'closeEnabled'
        action close(satisfaction: Integer);
    }   
}

 

The important part is the following annotation. Based on the filed specified here, the service will determine if the action should be enabled. I fond this in UI5 documentation.

@sap.applicable.path: 'startEnabled'

 

3. UI annotations

Below part is relevant to showing actions on Fiori UI.

        LineItem: [
            { $Type: 'UI.DataFieldForAction', Action: 'CallCenterService.EntityContainer/Inquiries_start', Label: 'Start',  Visible, Enabled},
            { $Type: 'UI.DataFieldForAction', Action: 'CallCenterService.EntityContainer/Inquiries_close', Label: 'Close',  Visible, Enabled},
            { Value: category_code },
            { Value: inquiry },
            { Value: status_code },
            { Value: startedAt },
            { Value: satisfaction },
            { Value: createdAt },
            { Value: modifiedAt }
        ]

 

4. Service implementation

In the service implementation, I have three methods.

The first one is for toggling the actions. As defined in the service definition, if “startEnabled” is true, then the start action will be enabled. I want the start action to be enabled only for new inquires (status: “1”). Similarly, I want the close action to be enabled only for active inquires (status: not closed(3)).

const cds = require('@sap/cds')

module.exports = function () {
    const { Inquiries } = cds.entities 

    this.after('READ', 'Inquiries', (each) => {
        //for new inquires only
        if (each.status_code === '1' ) { 
            each.startEnabled = true
        }
        //for active inquires only
        if (each.status_code !== '3') {
            each.closeEnabled = true
        }
    })      
    ...
}

 

The second and the third methods are action implementations. For bound actions, entity’s key is passed via request parameters.

    this.on ('start', async (req)=> {
        const id = req.params[0]
        const n = await UPDATE(Inquiries).set({ 
            status_code:'2',
            startedAt: Date.now()
        }).where ({ID:id}).and({status_code:'1'})
        n > 0 || req.error (404) 
    })

    this.on('close', async (req) => {
        const id = req.params[0]
        const { satisfaction } = req.data
        const n = await UPDATE(Inquiries).set({ 
            satisfaction: satisfaction,
            status_code: '3'
        }).where ({ID:id}).and({status_code:{'<>':'3'}})
        n > 0 || req.error (404)        
    })

 

I can get the key with the following code and use it for updating the entity.

const id = req.params[0]

 

5. Actions on UI

I used Fiori Tools and OData v2 adapter for creating a list report app. How to achieve this is written in my previous blog.
All you have to do is to generate a list report app using Fiori Tools. No local annotations nor extensions are required. Here is the resulting app.

When I select a record with status “1”, both start and close buttons are enabled.

When I select a record with status “2”, only close action is enabled.

For closed record, no action is enabled.

 

Summary

Using annotation@sap.applicable.path: 'startEnabled' and a virtual field, you can control actions on Fiori UI. You can implement the logic to enable actions in the CAP service implementation.

 

References

Assigned Tags

      27 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Oliver Klemenz
      Oliver Klemenz

      Nice Blog. You can also try CDS virtual elements (https://cap.cloud.sap/docs/cds/cdl#virtual-elements) to avoid empty/unused fields on persistence layer... Also good to see, that the CDS OData V2 Adapter Proxy works well for all your scenarios...

      Author's profile photo Mio Yasutake
      Mio Yasutake
      Blog Post Author

      Hi Oliver,

      Thank you for your suggestion. Virtual elements are indeed better for the purpose.

      I've updated the post.

      Author's profile photo Pierre Dominique
      Pierre Dominique

      Another great blog post, thanks for sharing this!

      Author's profile photo Mio Yasutake
      Mio Yasutake
      Blog Post Author

      Hi Pierre,

      Thanks for your comment!

      Author's profile photo Afshin Irani
      Afshin Irani

      Hi,

      Another excellent blog on CAP. Keep me them coming and one of the best in the space.

      A blog on demystifying the world CDS annotations would be great.

      Thanks

      Afshin

       

       

       

      Author's profile photo Mio Yasutake
      Mio Yasutake
      Blog Post Author

      Hi Afshin,

      Thanks for your comment. I'm interested in CAP and Fiori elements integration, and hope to write more blogs on this topic.

      Author's profile photo Michael Belenki
      Michael Belenki

      Hi Mio,

      I was looking for how to disable / enable action in OData V4 and found out that it can be done with another annotation @Core.OperationAvailable: { $value: true }. Unfortunately all my tries to use a path as $value failed. Do you have any idea, how to do it? 

      @Core.OperationAvailable: { $value: 'startEnabled' } does not work ('String' not allowed here. Expected: 'Edm.Boolean'CDS (annotations)

      @Core.OperationAvailable: { $value: startEnabled } returns an error in visual studio code "

      Path bookEnabled leads to action. The path should lead to type Edm.Boolean.CDS (annotations)"

      thanks and kind regards,

      Michael

      Author's profile photo Mio Yasutake
      Mio Yasutake
      Blog Post Author

      Hi Michael,

      I could reproduce the issue, however, I don't know how to achieve this in OData V4.

      I'd like to be informed once you have solved it.

      Regards,

      Mio

      Author's profile photo Hieu Ngo Xuan
      Hieu Ngo Xuan

      Hi Michael Belenki

      Maybe there is a solution for you

      Step 1: Add the following code into your schema CDS

      type TechnicalBooleanFlag : Boolean @(
          UI.Hidden,
          Core.Computed
      );

      Step 2: At the exact entity, you want to custom this logic, add the code

      entity MyEntity: cuid, managed {
          name                    : String(200) @mandatory;
          sort                    : String(100) @title:'Sort Name';
          // === allowButton property to custom logic
          allowButton          : TechnicalBooleanFlag not null default true;
      
      }

      Step 3: At fiori cds file, annotate for your action as below

      annotate YourService.Entity actions {
          
          
          @(
              Common.SideEffects : {
                  TargetEntities : [
                      _it,
                  ]
              },
              cds.odata.bindingparameter.name: '_it',
              Core.OperationAvailable : _it.allowButton,
              UI.FieldGroup
          )
          yourAction(
              email @title : 'Email' @FieldControl.Mandatory,
              position @title : 'Position'
              // something else
          );
      
      }

       

      Hope useful for you!

      Author's profile photo Dominik Espitalier
      Dominik Espitalier

      Hey Mio,

      thanks for this great bog! I have one question. Let's say I have a boolean field named "toBeValidated" in my entity, which I want to use for my @sap.applicable.path. And I have two actions which I want to enable/disable accordingly to this boolean field "toBeValidated". Do you know if there is some way to add a ! operator to the path like so:

      entity ValidationRules @(
              Capabilities : {
                  Insertable : false,
                  Updatable  : true,
                  Deletable  : false
              },
              restrict     : [{
                  grant : [
                      'enable',
                      'disable'
                  ],
                  to    : [
                      'superadmin',
                      'cockpitadmin'
                  ]
              }, ]
          ) as projection on ds.Validation_Customizing actions {
              @sap.applicable.path:'toBeValidated'
              action enable() returns  ValidationRules;
              @sap.applicable.path:'!toBeValidated'
              action disable() returns ValidationRules;
          };

      Please have a look on the second action "disable".

       

      Best Regards

      Dominik

      Author's profile photo Mio Yasutake
      Mio Yasutake
      Blog Post Author

      Hi Dominik,

      Thanks for your comment!

      Unfortunately, I don't know if it is possible to set an "expression" for a boolean property.

      A workaround would be to define a virtual property (let's say, "disable") and set its value in the read handler.

          this.after('READ', 'ValidationRules', (each) => {
              each.disabled = !each.toBeValidated
          }) 

      Best regrads,

      Mio

      a

      Author's profile photo Dominik Espitalier
      Dominik Espitalier

      Hey Mio,

      thanks for your answer. Thats what I did. And its working pretty good, but it would be awesome to use an expression in the datamodel 🙂

       

      Best Regards

      Dominik

      Author's profile photo Mio Yasutake
      Mio Yasutake
      Blog Post Author

      Hi Dominik Espitalier,

      You might have already found this, but you control actions dynamically using an expression with the following way. (perhaps works only for V4)

      https://cap.cloud.sap/docs/advanced/fiori#actions

      annotate TravelService.Travel with actions {
       acceptTravel @(
         Core.OperationAvailable : {
           $edmJson: { $Ne: [{ $Path: 'in/TravelStatus_code'}, 'A']}
         },
         Common.SideEffects.TargetProperties : ['in/TravelStatus_code'], ) };
      

      Best regards,

      Mio

      Author's profile photo Dominik Espitalier
      Dominik Espitalier

      Hey Mio,

      this is great! Thanks for sharing this!!

       

      Cheers

      Author's profile photo Rufat Gadirov
      Rufat Gadirov

      Hi @Mio Yasutake, Oliver Klemenz ,

      thank you for this very useful blog and suggestions.

      I am mainly working with OData V4 and as I know and tried out, @applicable path is only meant for OData V2 annotations. Thus, I´d like to know how to implement this kind of action button control for V4? Using a dynamically determined boolean property works on header level, but it seems not to work on item level - when specific line items are clicked, the button should be set either to hidden or disabled (@UI.Hidden annotation for data field action). It works when hard coded or when used with hasActiveEntity property (for drafts), but not with a custom boolean property that is set to true as default in data model.

      So, any ideas on how to solve this issue?

      Thank you in advance!

      Cheers,

      Rufat

      Author's profile photo Rufat Gadirov
      Rufat Gadirov

      Hi there, ok I got it

      in OData V4

      The following annotation controls if a button of the corresponding action is available or not!

       @Core.OperationAvailable : in.cancelOperationAvailable (bound data element as boolean flag!)
              action cancelBooking();
      data model:
       cancelOperationAvailable : common.TechnicalBooleanFlag not null default false;
      Cheers,
      Rufat
      Author's profile photo Tim Anlauf
      Tim Anlauf

      Hello Mio Yasutake,

      thanks for that helpful blog. Maybe it would be a good idea to add a some information when working  with buttons on object page with ODATA v4 (draft mode).

      I did not found a solution for the following problem / question.

      How do I disable an action button in the object page when the object is in edit / draft mode?

       

       

      Author's profile photo Navya Krishna
      Navya Krishna

      Hello Mio Yasutake,

      I have tried similar thing but the action implementation is not getting triggered. Please refer question https://answers.sap.com/questions/13578821/adding-action-in-object-page-section-capfiori-elem.html.

      What am I missing here.

      Thank you,

      Navya

      Author's profile photo Shilpa Gupta
      Shilpa Gupta

      Hi Mio,

       

      Thankyou for the wonderful blog!

      I am stuck at point 3, where we are displaying the buttons on UI. In which file we have to write annotations of step number 3.

       

      Thankyou!

      Shilpa

      Author's profile photo Mio Yasutake
      Mio Yasutake
      Blog Post Author

      Hi Shilpa Gupta,

      Thank you for your comment.

      You can write annotations in the same .cds fie for service definition, or create a separate .cds file for annotations.

      The following GitHub sample by may help.

      https://github.com/jcailan/cap-fe-samples/tree/master/srv

      Author's profile photo Shilpa Gupta
      Shilpa Gupta

      Hi Mio,

       

      Thanks for your response! It helped me to display the START button in the app.

       

      Regards,

      Shilpa Gupta

      Author's profile photo Manish Gupta
      Manish Gupta

      Hi Mio,

      Appreciate your detailed blog. I have developed a list report UI in CAPM using node.js service. I have a requirement where in I would like to have a custom button on list report page, let say 'Create' [this I could achieve using guided development]. Now on click of this button I would like to have a screen with some inputs field and input F4 help..how should I really achieve this? how can I create this custom screen? Can this be created using annotations and how can I call that screen on click of custom button created.

      I tried calling a fragment on click of Create button, the fragment is coming up but unfortunately if I do any action on fragment it's not getting captured in CAPM framework.

      Could you please guide a solution for my issue? Thanks.

      Regards

      Manish

      Author's profile photo Mio Yasutake
      Mio Yasutake
      Blog Post Author

      Hi Manish Gupta,

      Although I haven't tired that scenario myself, the following blog post may help.

      https://blogs.sap.com/2019/10/15/a-journey-of-building-an-action-dialog-on-a-list-report-with-annotations

      Author's profile photo Pawan Kalyan
      Pawan Kalyan

      Hi Mio Yasutake

      Thanks for the very nice blog. I have gone through it couple of time of times and tried a similar example.

       

      But somehow action buttons are not getting dynamically controlled. may I know what could be the reason? App seems working fine and when I debugged, this.after is getting executed on read  of the corresponding Entity and values for virtual elements are being populated.

      Still, they are not dynamically controlled .

       

      I have doubt in the below statement. What are those variables Visible and Enabled? Can you please help me understand this part?

       { $Type: 'UI.DataFieldForAction', Action: 'CallCenterService.EntityContainer/Inquiries_start', Label: 'Start',  Visible, Enabled},
      Author's profile photo Mio Yasutake
      Mio Yasutake
      Blog Post Author

      Hello Pawan Kalyan,

      Thanks for your comment.

      I have uploaded my app to GitHub. You can check the entire code here. Please note that this works for OData V2 and not for V4 (for V4, there is a different way to achieve this).

      https://github.com/miyasuta/v2-action-control

      Regarding "Visible, Enabled", I forgot why I added those elements and actually action control is working with or without them so you can ignore them.

       

      Author's profile photo Massimiliano Bucalo
      Massimiliano Bucalo

      Hello Mio Yasutake,

      I have defined bounded actions as below:

      service CatalogService {
      entity MyTasks as projection on MyTasks
              actions {
                      @(Common.IsActionCritical : true) @sap.applicable.path: 'visible' action approve() returns MyTasks;
                      @(Common.IsActionCritical : true) @sap.applicable.path: 'visible' action reject() returns MyTasks;
              }
      }

      and in the actions implementation I want to raise an error that must be visibile in the MessabeBox of the calling Fiori Elements application:

      srv.on ('approve', async (req)=> {
      ...
              if (cond) {
                 return req.error(404, 'Error message')
              } else {
                 return MyTask
              }
      
      });
      

      but the return req.error operation does not trigger any error message displayed in the Message Box of th calling Fiori application.

      Any suggestion why the error message is visible ?

       

      Regards,

      Massimiliano.

      Author's profile photo Mio Yasutake
      Mio Yasutake
      Blog Post Author

      Hi Massimiliano Bucalo,

      The reason why the error message is not visible is that the request is sent with $batch request. A $batch request won't fail even if an individual request fails.

      One possible solution is to set "useBatch" to false in manifest.json, but this affects other requests too. Another solution is to use a custom action (extension) instead of an annotation based action. Then you can handle errors in the controller extension.

            "": {
              "dataSource": "mainService",
              "preload": true,
              "settings": {
                "defaultBindingMode": "TwoWay",
                "defaultCountMode": "Inline",
                "refreshAfterChange": false,
                "metadataUrlParams": {
                  "sap-value-list": "none"
                },
                "useBatch": false
              }

      Regards,

      Mio Yasutake