Monday 17 August 2020

Trigger Power Automate flows from Power Apps Portals

Currently the only way to update records from a Power Apps Portals is to create or update data via an Entity Form, Web Form or a companion app. The companion app incurs a certain level of technical debt in terms of development of the app and management of where its hosted, such as an Azure web app or Azure function.

Power Automate to the Rescue!

The following post outlines the steps I took to trigger a Power Automate flow from a portal web page that will update a record in CDS (common data service) without needing to submit a form or navigating away from the page. Power Automate effectively acts as the Power Apps Portals “companion app”.

Note: Unless you have been living under a rock, Power Automate is the new official name for Microsoft Flow.

Note 2: These steps would work with Power Apps Portals running on Dynamics 365 instances (which, as you know, run on CDS). If I say “CDS”, Dynamics 365 could also apply.

Power Apps Portals Editable Grid

I wanted to build a simple editable grid on a Power Apps Portals page where a portal user could update a series of records without needing to open an entity form or a web form step for each record.

I started to build a custom web template. (Sample code can be found on my Github Site.)

My example will list a series of courses and allow the portal user to update the description for each course without submitting a form. It might not be the most relevant use case, but hopefully will illustrate the process.

The first step was to retrieve the course data which I would want to edit. For this I use the Liquid FetchXML tag to retrieve the data, and of course I composed the FetchXML statement using the FetchXML Builder.

1
2
3
4
5
6
7
8
9
10
11
12
13
{% fetchxml courses %}
 
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct='true' returntotalrecordcount='true'>
    <entity name="avngr_course" >
        <attribute name="avngr_coursedescription" />
        <attribute name="avngr_coursename" />
        <attribute name="avngr_courselevel" />
        <attribute name="avngr_cost" />
        <attribute name="avngr_courseid" />
    </entity>
</fetch>
 
{% endfetchxml %}

In the web template, if data is retrieved, I want to display the data in a fairly simple list. As a bonus, since the text could be quite lengthy, I decided to hide the description in an “accordion style” so the user could open and collapse each record. This part is completely optional.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- assuming data is retrieved -->
{% if courses.results.total_record_count > 0 %}
 
    <!-- for each course retrieve, show the name as the accordion section and description in text area to be edited -->
    <!-- use the GUID as the id for the textarea control to retrieve later -->
    <!-- pass the GUID to the JavaScript function to update record -->
    {% for course in courses.results.entities %}
 
        <button class="accordion">{{course.avngr_coursename}}</button>
        <div class="panel">
            <textarea id={{course.Id}} rows="4" cols="50">{{course.avngr_coursedescription}}</textarea>
            <button onclick="updaterecord('{{course.Id}}')">Update</button>
        </div>
 
    {% endfor %}
 
{% else %}
 
    <h2>No records found.</h2>
 
{% endif %}

If you look at the code, I set the description value in a HTML textarea element, so it can be edited on a portal page.

I set the textarea HTML tag id to the GUID of the record from CDS, this gives each textarea control a unique id (which will need later).

The course name, I just set as the accordion button name.

Within the panel, I also added a button item, that will pass the GUID to a JavaScript function that I will create as part of the web template.

The result looks like the following on a Power Apps Portal page:

The problem now is how to pass the user edits to update the corresponding CDS/Dynamics 365 record. Traditionally, we could write a “companion” web application that we could call from JavaScript and pass along the updates.

I decided to try to use Power Automate.

Power Automate provides a HTTPS Request trigger that we can call from a Power Apps Portal (or other places).


Power Automate HTTP Request Trigger

This trigger requires a JSON (JavaScript Object Notation) string in order to be configured and it will accept data in a JSON string. We can begin to build out our web template further to get a sample JSON string.

We will start to build our JSON string in the JavaScript function that we will call from our “Update” button that we place beside each record.

I add JavaScript code in my web template records between <script> tags to keep everything together. See the sample code for the full example.

Since we used the actual record GUID as the textarea id and passed the id to the function, we have what we need to start building our JSON string.

NOTE: If there are more than one field to edit, we could concatenate addition id information to each HTML element to remain unique, e.g. “id=textarea1{{course.Id}}”

1
2
var courseupdate = '{ "course" : [' +
        '{ "courseid": "' + id +'", "coursedetail": "' + document.getElementById(id).value.trim() + '"} ]}';

The will produce a JSON string similar to the following (you can add an “alert” function to get your sample):

{ "course" : [{ "courseid": "42e86bd1-e704-ea11-a811-000d3af962ef", "coursedetail": "This is an introductory course for AI Builder"} ]}

Once you have your sample, you can use that as a pattern for the Power Automate Http Request trigger by clicking the “Generate from sample” button;


Generate JSON Schema from sample

Adding JSON schema

The next step is to take the data collected from the HTTP request trigger and update CDS. In Power Automate, I used the Common Data Service (current environment) Update a record connector to update the Course record in the CDS environment.


Update a record (CDS current environment)

We have the “course id” and “course description” from our HTTP request trigger in our Power Automate flow. We can use these values to directly update a record in CDS/Dynamics 365.


Update the record with values from the HTTP Request

Once we save the flow, it will create the HTTP POST URL that we will use to call from our Power Apps Portal page.


Copying the HTTP POST URL

You could use a tool like Postman to test the Power Automate flow to ensure that the JSON string will update a CDS/Dynamics 365 record.

The final step to get the process functional is the add the code to the JavaScript function in the portal web template to submit the JSON string to Power Automate to update the record in CDS/Dynamics 365. This will use the XMLHttpRequest function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//Update record by calling HTTP request Power Automate flow
function updaterecord(id) {
             
        //build JSON string using values from display
        var courseupdate = '{ "course" : [' +
        '{ "courseid": "' + id +'", "coursedetail": "' + document.getElementById(id).value.trim() + '"} ]}';
 
        //use the alert to get JSON example for flow
        //alert(courseupdate);
 
        //build http request string using URL from Power Automate flow
        var req = new XMLHttpRequest();
        var url = "https://<<copy HTTP POST URL from Power Automate here>>";
 
        //send https request to Power Automate
        req.open("POST", url, true);
        req.setRequestHeader('Content-Type', 'application/json');
        req.send(courseupdate);
 
        alert("updated");
 
    }

Navigating to our Power Apps Portal page, we can edit some of the text displayed on the record, and then click “Update”.


Update data directly on portal page

If we look at the data in CDS (in this case, from a Model Driven App) we see that the data was updated.


Data updated in CDS Model Driven App


Gotchas

If the Portal is high transaction, high volume, then it could potentially eat up flow allocations quickly. I could see the use cases working well for very low volume portals.

Even with checking the origin, there are other potential security concerns that would need to be addressed with this method.

In order to see the updates reflected (accurately) on the portal, a user would need to navigate away or refresh the web page where they are viewing the edited data. The CDS records should be configured with change tracking enabled to ensure the most up to date data is visible on the portal.

At this point, the code does not have a listener enabled to determine of the flow was successful or not updating the data in CDS.

Another method would be to have a single update button that would build a JSON string from all the updates. However, this could exceed limits of how much data can be passed to the HTTP request.

Summary

This example showed an simple example of how Power Automate could be used with a Power Apps Portal. There are other applications beyond creating or updating data where triggering a flow could be beneficial. Power Apps Portals do have the ability to run “classic” Dynamics 365 workflows from entity forms and entity lists. The sample provided here provides an option to run processes via Power Automate.


Appendix A – Full Web Template Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{% extends 'Layout 1 Column' %}
{% block main %}
 
<!--Web Template to display a list of records, allow user to update course detail and update via Power Automate Http request-->
<!--Nick Doelman-->
<!--December 27, 2019-->
 
<!-- CSS style for accordion panel -->
<!-- code source: https://www.w3schools.com/ -->
 
<style>
/* Style the buttons that are used to open and close the accordion panel */
.accordion {
    background-color: #eee;
    color: #444;
    cursor: pointer;
    padding: 18px;
    width: 100%;
    text-align: left;
    border: none;
    outline: none;
    transition: 0.4s;
}
 
/* Add a background color to the button if it is clicked on (add the .active class with JS), and when you move the mouse over it (hover) */
.active, .accordion:hover {
    background-color: #ccc;
}
 
/* Style the accordion panel. Note: hidden by default */
.panel {
    padding: 0 18px;
    background-color: white;
    display: none;
    overflow: hidden;
}
 
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
{% fetchxml courses %}
 
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct='true' returntotalrecordcount='true'>
    <entity name="avngr_course" >
        <attribute name="avngr_coursedescription" />
        <attribute name="avngr_coursename" />
        <attribute name="avngr_courselevel" />
        <attribute name="avngr_cost" />
        <attribute name="avngr_courseid" />
    </entity>
</fetch>
 
{% endfetchxml %}
 
<!-- assuming data is retrieved -->
{% if courses.results.total_record_count > 0 %}
 
    <!-- for each course retrieve, show the name as the accordion section and description in text area to be edited -->
    <!-- use the GUID as the id for the textarea control to retrieve later -->
    <!-- pass the GUID to the JavaScript function to update record -->
    {% for course in courses.results.entities %}
 
        <button class="accordion">{{course.avngr_coursename}}</button>
        <div class="panel">
            <textarea id={{course.Id}} rows="4" cols="50">{{course.avngr_coursedescription}}</textarea>
            <button onclick="updaterecord('{{course.Id}}')">Update</button>
        </div>
 
    {% endfor %}
 
{% else %}
 
    <h2>No records found.</h2>
 
{% endif %}
 
<script>
 
    //Accordion display as per https://www.w3schools.com/
    var acc = document.getElementsByClassName("accordion");
    var i;
 
    for (i = 0; i < acc.length; i++) {
        acc[i].addEventListener("click", function() {
        /* Toggle between adding and removing the "active" class, to highlight the button that controls the panel */
        this.classList.toggle("active");
 
        /* Toggle between hiding and showing the active panel */
        var panel = this.nextElementSibling;
        if (panel.style.display === "block") {
            panel.style.display = "none";
        }
        else {
            panel.style.display = "block";
            }
        });
    }
 
    //Update record by calling HTTP request Power Automate flow
     function updaterecord(id) {
        //alert("Update record: " + id);
        //alert("Update text: " + document.getElementById(id).value.trim());
 
        //build JSON string using values from display
        var courseupdate = '{ "course" : [' +
        '{ "courseid": "' + id +'", "coursedetail": "' + document.getElementById(id).value.trim() + '"} ]}';
 
        //alert(courseupdate);
 
        //build http request string using URL from Power Automate flow
        var req = new XMLHttpRequest();
        var url = "<<HTTP POST URL HERE>>";
 
        //send https request to Power Automate
        req.open("POST", url, true);
        req.setRequestHeader('Content-Type', 'application/json');
        req.send(courseupdate);
 
        alert("updated");
 
    }
 
 
 
</script>
{% endblock %}

Hide New... button on lookup controls in model-driven apps

  The 'New ...' button is shown upon opening the lookup search dialog whenever the logged in user has at least user create privileg...