Showing posts with label Power Apps Portals. Show all posts
Showing posts with label Power Apps Portals. Show all posts

Nov 4, 2022

First Cheat sheet and pitfalls of liquid

In Dynamics/ Power Platforms world, we use Liquid Templates along with JavaScript in Portal which is a great combination. A cool way to work with client side along with server side. This reminded me of my firsthand web development experience a long ago, using Classic Asp. 

Anyway, this combination is strong. We learned CRUD operations using Java Script in past post. When we use Client Side and Sever Side together, there are some limitations as well. Though they work together well, you should not messed up with flow of the code by mixing. Here I am providing few useful snippets long with mistakes a first-timer would do.

1. Liquid values can be used in Java Script.

Good thing is we can set alerts and check the values during development.

{% assign isCurrent = true %} 

alert("value of variable is : {{isCurrent}}");

2. Java Script values cannot be passed to Liquid

For example below is NOT WORKING, because we try to use JavaScript value inside Liquide statements.

{% assign Val1 = 100 %} 

var Val2 = 200;

{% if  Val1 > Val2 %} 

  {% assign IsVal1Greater = true %} 

{% endif %}

3. If we cant use Java Script values in Liquid we need to retrieve server side values of the record we are working with. Hence, below script going to help in retrieving ID of the current record.

Actually, poral pages have Session Id as a part of URL which can be obtained to retrieve current record Id which is stored in Advanced Form Session (adx_webformsession) entity. (** Please don't forget to set permission to this entity through Table Permissions entity entry **)

In fact, this is going to be one of the most used scripts. 

   {% assign SessionId = request.params['sessionid'] %}

   {% fetchxml currId %}
   <fetch version="1.0" output-format="xml-platform" mapping="logical">
    <entity name="adx_webformsession">
        <attribute name="adx_primaryrecordid"></attribute>
        <filter type="and">
           <condition attribute="adx_webformsessionid" operator="eq" value="{{ SessionId }}" />
        </filter>
    </entity>
   </fetch>
   {% endfetchxml %}

   {% if currId.results.entities.size == 0 %}
   {% else %}
      {% assign Id = currId.results.entities[0].adx_primaryrecordid %}
   {% endif %}

4. Don't mix Liquid and JavaScript in control flow

In fact, Below is NOT WORKING

  if (X > Y) {

     {% assign myValue = false %}
 
  }

5. Below is the generic way of reading the results of a Fetchxml query which is also going to be used frequently.

   {% fetchxml office %}
    <fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false" no-lock="false">
      <entity name="so_office">
        <attribute name="so_officenumber"></attribute>
        <filter type="and">
        <condition attribute="so_officeadmin" operator="eq" value="{{ user.id }}"></condition>
        </filter>
      </entity>
    </fetch>
    {% endfetchxml %}
 
    {% if office.results.entities.size == 0 %}
        {% assign officeNumber = 0 %}
    {% else %}
        {% for result in office.results.entities %}
            {% assign officeNumber = result.so_officenumber %}
        {% endfor %}
    {% endif %}

Sep 23, 2022

Azure AD B2C to handle login for Portal

Recently I realized that Azure AD B2C is already playing a big role in Portal user access. So I jumped into it and wanted to learn fundamentals. I managed configure Azure AD B2C as the method of login, new registrations etc. Here I am documenting the steps.

1. Register the Portal in Azure AD B2C

Though there is a new App registration link, I started with legacy link.


Please find below the configuration details. Reply URL is needed later (i.e. A)

Once save, you will get Application ID (i.e. B)


2. Configure Sign in Policy / Criteria for Identity Provider

Go to User flows to start this and select Sign up and sign in option in resulting window.


Here, it is essential to give Email sign up as the type of method/ Identity Provider


Now we need to set user attributes and claims. There are more combinations to play around, but what I need is to just to use First Name, Surname along with Email to use to match the users, though collecting few more attributes in registration. Hence, below is my setting.


Other important thing is selecting tfp for claim representing user flow.


Now, you are ready to save and Run user flow and save the issuer link. (i.e. C) which is visible once you click the resulting hyperlink.


3. Configure the Portal

Now go to Portal management > Site Settings to enter below entries as the final step of the exercise.

Entry 1: Use Issuer Url


> Entry 2: Use redirect URL


> Entry 3: User Application ID


> Entry 4: Use Name, Surname and Email for mapping. (value: emailaddress1=http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress,firstname=http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname,lastname=http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname)


> Entry 5 (Optional): If same fields are to be mapped during sign in add this entry. (value: emailaddress1=http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress,firstname=http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname,lastname=http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname)


> Entry 6: Add this entry to make sure is new Contact registers, it allows to check the Contact entry to map the email


> Entry 7


Now browse to the Portal and click Sign in. You will get new login page from Azure AD B2C! Most importantly its going to handle all the user managements for you.


If you click, you will notice all the other fields we selected in attributes and claim section would appear.

Sep 2, 2022

Adding CRUD operations to a List in a Portal

Here I am discussing one of the main customizations you may need when developing a Power Apps Portal. That is adding CRUD (Create, Read, Update and Delete) operation on items in a list. 

There are two pre-requisites to proceed with this exercise;

1. A list should have configured already.

2. Below 2 entries should have added to the Site Settings entity that gives permission to nominated entity (in my case so_office) to be accessed through API.


All my JS codes will go in Options tab of the list.


Consider below JS, where I have embedded some extra lines to a standard code of doing something for each item in the list.

Block 1 - Change some fonts styles through CSS

Block 2 - Give different color for the lines where No of Clients are greater than 100

Block 3 - Call CRUD functions by introducing 4 new buttons.

$(document).ready(function (){
$(".entitylist.entity-grid").on("loaded", function () {
$(this).children(".view-grid").find("tr").each(function (){
// do something with each row

// Block 1
$(this).css("font-family","Sans-serif");
$(this).css("font-size","10px");

// Block 2
if ($(this).find('[data-attribute="so_noofclient"]').attr("data-value")) {
    var noOfclients = $(this).find('[data-attribute="so_noofclient"]').attr("data-value");   
    if (noOfclients >100) {
    $(this).css("background-color", "#E49C9C");
    }
    else {
    $(this).css("background-color", "#F6F5F5");
    }
}

// Block 3
   if ($(this).closest('tr').attr("data-id") != null)
   {
     var recId = $(this).closest('tr').attr("data-id");   
     var nameCol = $(this).find('[data-attribute="so_name"]');
         nameCol.append('&nbsp;&nbsp;<input type="button" value="C" onclick="CreateOffice();" style="color:orange; border-style: none;" />');
	 nameCol.append('&nbsp;&nbsp;<input type="button" value="R" onclick="RetrieveOffice(\''+ recId +'\');" style="color:orange; border-style: none;" />');
	 nameCol.append('&nbsp;&nbsp;<input type="button" value="U" onclick="UpdateOffice(\''+ recId +'\');" style="color:orange; border-style: none;" />');
	 nameCol.append('&nbsp;&nbsp;<input type="button" value="D" onclick="DeleteOffice(\''+ recId +'\');" style="color:orange; border-style: none;" />');
   }

});
});
}); 

Above steps resulted below. You may notice four buttons to trigger the operations.


Now we have below four functions those do the respective operations.

function CreateOffice(RecId)
{
    alert("Creating Office..");
    webapi.safeAjax({
        type: "POST",
        url: "/_api/so_offices",
        contentType: "application/json",
        data: JSON.stringify({
        "so_name": "Created Office"
        }),
        success: function (res, status, xhr) {
        console.log("entityID: "+ xhr.getResponseHeader("entityid"));
        document.location.reload(true);
        }
    });
}

function RetrieveOffice(RecId)
{
    alert("Retrieving.." + RecId);
    webapi.safeAjax({
	   type: "GET",
	   //url: "/_api/so_offices?$select=so_type,so_revenue",
           url: "/_api/so_offices("+ RecId +")?$select=so_type,so_revenue", 
		contentType: "application/json",
		success: function (res) {
			console.log(res);
            //alert(res.value[0].so_revenue);
            alert("Type :" + res.so_revenue);
		}
	});
}

function UpdateOffice(RecId)
{
    alert("Updating Office..XX" + RecId);
    webapi.safeAjax({
        type: "PATCH",
        url: "/_api/so_offices("+ RecId +")",
        contentType: "application/json",
        data: JSON.stringify({
        "so_name": "Office Updated"
        }),
        success: function (res) {
        console.log(res);
        document.location.reload(true);
        }
    });
}

function DeleteOffice(RecId)
{
    alert("Deleting Office.." + RecId);
    webapi.safeAjax({
        type: "DELETE",
        url: "/_api/so_offices("+ RecId +")",
        contentType: "application/json",
        success: function (res) {
        console.log(res);
        document.location.reload(true);
        }
    });
}

None of the above will work unless we add below Wrapper AJAX function. So added this as well.

(function(webapi, $){
		function safeAjax(ajaxOptions) {
			var deferredAjax = $.Deferred();
	
			shell.getTokenDeferred().done(function (token) {
				// add headers for AJAX
				if (!ajaxOptions.headers) {
					$.extend(ajaxOptions, {
						headers: {
							"__RequestVerificationToken": token
						}
					}); 
				} else {
					ajaxOptions.headers["__RequestVerificationToken"] = token;
				}
				$.ajax(ajaxOptions)
					.done(function(data, textStatus, jqXHR) {
						validateLoginSession(data, textStatus, jqXHR, deferredAjax.resolve);
					}).fail(deferredAjax.reject); //AJAX
			}).fail(function () {
				deferredAjax.rejectWith(this, arguments); // on token failure pass the token AJAX and args
			});
	
			return deferredAjax.promise();	
		}
		webapi.safeAjax = safeAjax;
	})(window.webapi = window.webapi || {}, jQuery)

Hope this code snippet helps!