Technical Articles
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.
- New
- In Process
- 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.
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...
Hi Oliver,
Thank you for your suggestion. Virtual elements are indeed better for the purpose.
I've updated the post.
Another great blog post, thanks for sharing this!
Hi Pierre,
Thanks for your comment!
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
Hi Afshin,
Thanks for your comment. I'm interested in CAP and Fiori elements integration, and hope to write more blogs on this topic.
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 "
thanks and kind regards,
Michael
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
Hi Michael Belenki
Maybe there is a solution for you
Step 1: Add the following code into your schema CDS
Step 2: At the exact entity, you want to custom this logic, add the code
Step 3: At fiori cds file, annotate for your action as below
Hope useful for you!
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:
Please have a look on the second action "disable".
Best Regards
Dominik
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.
Best regrads,
Mio
a
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
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
Best regards,
Mio
Hey Mio,
this is great! Thanks for sharing this!!
Cheers
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
Hi there, ok I got it
in OData V4
The following annotation controls if a button of the corresponding action is available or not!
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?
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
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
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
Hi Mio,
Thanks for your response! It helped me to display the START button in the app.
Regards,
Shilpa 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
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
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?
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.
Hello Mio Yasutake,
I have defined bounded actions as below:
and in the actions implementation I want to raise an error that must be visibile in the MessabeBox of the calling Fiori Elements application:
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.
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.
Regards,
Mio Yasutake