Showing posts with label Power Automate. Show all posts
Showing posts with label Power Automate. Show all posts

Saturday, 26 March 2022

Adaptive Card error in MS Teams "We're sorry, this card couldn't be displayed"

I have created a Power Automate flow where the adaptive card will trigger the flow when the user selects for a message in Microsoft Teams.

{
    "type": "AdaptiveCard",
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.5",
    "body": [
        {
            "type": "TextBlock",
            "text": "@{triggerBody()?['cardOutputs']?['actaskTitle']}",
            "wrap": true
        }
    ],
    "actions": [
        {
            "type": "Action.OpenUrl",
            "title": "Open To Do",
            "url": "https://to-do.office.com/tasks/id/@{outputs('Add_a_to-do_(V3)')?['body/id']}/details"
        }
    ]
}

When I select the task from the message:

To fix the issue, go back and edit the flow and change the version from "1.5" to "1.4" and re-run; at this point, you should see the card displays correctly.


Once you click on 'Open To Do', it will redirect you to Microsoft To To:

Saturday, 5 March 2022

Post Adaptive Cards to Teams channel or chat and wait for a response using Power Automate

 1.       Create SharePoint List that will be used as data Input source: Invoice Request

a.       Columns: Title; Status – Both are ‘Single line of Text’ column type

    2. Create SharePoint List that will be used as source to fetch the dynamic data for reason based on department: Rejection

a.       Columns: Department(renamed Title column)

b.       Rejected Reason (Single line of text)

c.       Populate some data in Rejection list which will used in Adaptive Cards.

     3. We will use Power Automate flow that will automatically post an Adaptive Card to Teams Channel or Chat once the item is created in ‘Invoice Request’ list.

     4. Create a Flow and the set the trigger ‘When an item is created or modified’

      5. Add two Initialize Variables: VarSales and VarIT

     6. Create two Array Variables: varSalesReasons, varITReasons which will be using to append the data to Adaptive Cards.

     7. Create another two String Variables: SalesResponse, ITResponse which will using to capture the response that is submitted by approver.

     8. Add ‘Get Items’ actions to filter and fetch the rejection reasons for ‘Sales’ department.

     9. In Apply to each, Append all the items to an array while incrementing.

     10. These values appended to the varSalesReasons Array which will be used later when composing the adaptive card. Expression for both title and value:

body('Get_Sales_team_rejection_reason_from_Rejection_list')['value'][variables('varSales')]['Reason_x0020_for_x0020_Rejection']

      11. Add ‘Compose’ action to compose the adaptive card message.

 

Below is the JSON code that used to compose the adaptive card:

{

  "type": "AdaptiveCard",

  "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",

  "version": "1.2",

  "body": [

    {

      "type": "TextBlock",

      "text": "Sales Team Approval Request submitted by @{triggerOutputs()?['body/Author/DisplayName']}",

      "wrap": true,

      "id": "Request_Head",

      "size": "Medium",

      "weight": "Bolder",

      "horizontalAlignment": "Center"

    },

    {

      "type": "TextBlock",

      "text": "@{triggerOutputs()?['body/Title']}",

      "wrap": true,

      "id": "Request_Body"

    }

  ],

  "actions": [

    {

      "type": "Action.Submit",

      "title": "Approve",

      "id": "Approve",

      "style": "positive"

    },

    {

      "type": "Action.ShowCard",

      "title": "Reject",

      "card": {

        "type": "AdaptiveCard",

        "body": [

          {

            "type": "TextBlock",

            "text": "Please select a reason for rejecting the request.",

            "wrap": true,

            "id": "Rejection_Heading"

          },

          {

            "type": "Input.ChoiceSet",

            "choices": @{variables('varSalesReasons')},

            "placeholder": "Placeholder text",

            "style": "expanded",

            "value": "@{body('Get_Sales_team_rejection_reason_from_Rejection_list')['value'][0]['Reason_x0020_for_x0020_Rejection']}",

            "id": "Choices"

          }

        ],

        "actions": [

          {

            "type": "Action.Submit",

            "title": "Reject",

            "id": "Rejected",

            "style": "destructive"

          }

        ]

      },

      "id": "Reject",

      "style": "destructive"

    }

  ],

  "id": "Adaptive_Card"

}

   12. Post an adaptive card to Sales channel or chat and wait for response.

13.  AC will be posted to Sales team channel once the request is submitted in the list. And it will look like below

    14.  Approver can Approve or Reject. If the approver clicks Reject, then department specific rejection reasons will show up on the AC.

    15.   Let’s capture the AC Sales team response in varSalesResponse variable.

@{body('Post_an_Adaptive_Card__and_wait_for_a_response')['submitActionId']}

    16.   Once we receive the response, update the same in the ‘Invoice Request’ list.

 

    17.   Note: I have created below two columns(as flags) to skip the infinite trigger loop in update item action.

a.       SalesApprovalSent – Default value is: No

b.       ITApprovalSent – Default value is: No

    18.   If the Sales team approves the request, it will then be routed to IT team for further approvals.

    19. For this add a condition to verify the outcome of the Sales team approval response(varSalesReponse).

    20.   Repeat the steps from 8 to 17 for IT approval process and replace Sales with IT.

    21.  If the Sales Approver approve the request in 14th steps then it will be routed to IT team approvals and below the AC will be posted in the Sales team channel or chat.

    22.   Capture the IT team approver response in varITResponse variable.

@{body('Post_an_Adaptive_Card__and_wait_for_a_response_3')['submitActionId']}

    23.   And then update the status in the ‘Invoice Request’ list.

    24.   Note: I have added the below expression in the ‘When an item is created or modified’ trigger conditions.

@not(equals(triggerOutputs()?['body/SalesApprovalSent'],'Yes'))

Saturday, 26 February 2022

Post a Adaptive Card on Team members channel on their Birthday

1. Create a scheduled flow and set the recurrence to daily.

2.  Add 'List Group Members' action from Office 365 Groups connector and select your Team Id.

3. Loop all the Team members using apply to each.

  • To get the birth date information Add 'Get User Profile(v2)' action and select 'User Principal Name' in the User section.
  • Add condition and write expression to format the users birth date to retrieve only month and date.        
        formatDateTime(outputs('Get_user_profile_(V2)')?['body/birthday'],'MM-dd') is equal to                             formatDateTime(utcNow(),'MM-dd')
  • If the above condition is matches, add 'Get User Photo(v2) and select UPN.
  • Add a action ''Post a adaptive card in chat or channel' and select your teams channel details.
  • Paste the JSON into 'Adaptive card' section which was designed/copied from Designer | Adaptive Cards
{
    "type": "AdaptiveCard",
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.3",
    "body": [
        {
            "type": "TextBlock",
            "text": "Happy Birthday !!!",
            "wrap": true,
            "size": "Large",
            "color": "Accent",
            "horizontalAlignment": "Center"
        },
        {
            "type": "ColumnSet",
            "columns": [
                {
                    "type": "Column",
                    "width": "stretch",
                    "items": [
                        {
                            "type": "Image",
                            "url": "data:image/png;base64,@{base64(outputs('Get_user_photo_(V2)')?['body'])}"
                        }
                    ]
                },
                {
                    "type": "Column",
                    "width": "stretch",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "@{outputs('Get_user_profile_(V2)')?['body/displayName']}",
                            "wrap": true
                        }
                    ]
                }
            ]
        },
        {
            "type": "TextBlock",
            "text": "Have a nice day !",
            "wrap": true
        },
        {
            "type": "Image",
            "url": "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExcGswdGExaWl3NTYyMzNjZ2VkN2piaHI4emVmZjlzdTV5OXdoNDd2eCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/eLp6v4ca9zajMUnFDL/giphy.gif"
        }
    ]
}

Wednesday, 9 February 2022

Generate dynamic poll using Microsoft Teams Adaptive Card

If you are looking to generate a dynamic poll in Microsoft Teams on a button click, we have default template available in Power Automat which you can see from the below screenshot.


You flow will look like below:

When you run the flow, you will be asked to enter the inputs:


And here is the poll which is posted in Teams channel:

Sunday, 6 February 2022

Run With Elevated Privileges(App-Only) in SPFx webpart using Power Automate

  • SPFx Solution didn't support elevated permission using App-Only authentication, but with MS Power Automate, it's possible to use app-only elevated permission.
  • App-Only Token and elevated permission is got in MS Flow and in SPFX solution only call the flow using HttpClient object.
  • For example, in public facing sites Contact Us form, where a user will not have direct access to view or edit other items but he/she should be able to make an entry(create item) into the list. So even if the user don't have access to the list, you would like to collect information from them.Below is the Architecture diagram from Microsoft:

  1. Here in the diagram, user does not have access to the list but to input the data an interface has been provided which is build using SPFx webpart.
  2. User makes a POST request from SPFx webpart which triggers the MS Flow.
  3. Flow(HTTP) makes a POST request to ACS to retrieve the Access token.
  4. Once the ACS token received, SharePoint REST API POST call will perform using the token and that will create the item in SharePoint list. 

Make REST API Call from MS Flow:

Create and register an App in SPO with full control permissions

    1.       Login to SPO https://domainname.sharepoint.com/sites/LiakathDev/_layouts/15/appregnew.aspx  to generate Client ID and Secret ID.

    2.       Go to https://domainname.sharepoint.com/sites/LiakathDev/_layouts/15/appinv.aspx and register the app by entering the generated Client ID via lookup. Add the below XML to provide the full permissions to app and to elevate the permission at list level.

<AppPermissionRequests AllowAppOnlyPolicy="true">

<AppPermissionRequest Scope="http://sharepoint/content/sitecollection/web/list   " Right="FullControl" />

</AppPermissionRequests>

    3.      Select the list from the dropdown list where you need elevated permission and click on ‘Trust It’.

    4.      Verify the registered app and Note the Tenant ID from the below link:

https://domainname.sharepoint.com/sites/LiakathDev/_layouts/15/appprincipals.aspx

Create Power Automate Flow

    1.       Create flow from blank template and select the trigger as “When a Http request is received” and add the below JSON in the ‘Request Body’:

{
    "type": "object",
    "properties": {
        "title": {
            "type": "string"
        }
    }
}

    2.       Add action ‘HTTP’ post to fetch the access token from ACS using REST API call.

Body: replace Client ID, Secret ID and Tenant ID.

Grant_type=client_credentials&client_id=<<ClientID>>@<<TenantID>>&client_secret=<<ClientSecretID>>&resource=00000003-0000-0ff1-ce00-000000000000/<<TenantName>>.sharepoint.com@<<TenantID>>

    3.       Add action “Parse JSON”  and include the below JSON in to read the access token.

{

    "type""object",

    "properties": {

        "token_type": {

            "type""string"

        },

        "access_token": {

            "type""string"

        }

    }

}

         

     4.  Make Rest API call using Power Automate Flow

a.       Add action “HTTP” post to make a REST API call.

      

    5.   Add action "Response" to send a response back based on the out received from the previous step. Include Status code in the body parameter as displayed in the screenshot below.

    6.       Save the Flow and Note the URL of the HTTP Request: HTTP Post URL

https://prod-19.centralindia.logic.azure.com:443/workflows/02f9d3f348ec4f8ebee1f70be0b608f8/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=XpvU6D81LOSEtAH3Nvl_qKeYfEWNnyvqU9v6JnsPzbs 

Call Power Automate Flow From SPFx webpart:

Create SPFx webpart

    1.       Create SPFx webpart and the template is: No framework.

    2.       Once the project is created open the VS Code using this command : Code .

    3.       Update the Webpart.ts file with the below code        

import { Version } from '@microsoft/sp-core-library';
import {
  IPropertyPaneConfiguration,
  PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { IReadonlyTheme } from '@microsoft/sp-component-base';
//import { escape } from '@microsoft/sp-lodash-subset';

import styles from './SpFxWebpartWebPart.module.scss';
import * as strings from 'SpFxWebpartWebPartStrings';
import { HttpClient, IHttpClientOptions, HttpClientResponse } from '@microsoft/sp-http';

export interface ISpFxWebpartWebPartProps {
  //description: string;
  flowURL: string;
  FlowURLLabel: string;
}
//import { SPComponentLoader } from '@microsoft/sp-loader';
//interface IResult {
  //status: string;
//}

export default class SpFxWebpartWebPart extends BaseClientSideWebPart<ISpFxWebpartWebPartProps> {

  //private _isDarkTheme: boolean = false;
  //private _environmentMessage: string = '';

  public render(): void {
    this.domElement.innerHTML = `
    <div class="${ styles.spFxWebpart }">
        <div class="${ styles.container }">
          <div class="${ styles.row }">
            <div class="${ styles.column }">
                 <span class="${styles.title} ms-Grid-col ms-u-sm12 block">
                   Item creation using SPFx & Flow
                </span>
                <div class="ms-Grid-col ms-u-sm12 block"><br/></div>
                <div class="ms-TextField ms-Grid-col ms-u-sm12 block">
                  <label class="ms-Label ms-Grid-col ms-u-sm4 block">Title</label>
                  <input id="spfxflowIm" class="ms-TextField-field ms-Grid-col ms-u-sm8 block" value="" type="text" placeholder="">
                </div>
                <div class="ms-TextField ms-Grid-col ms-u-sm12 block"><br/></div>
                <div class="ms-TextField ms-Grid-col ms-u-sm6 block"></div>
                <div class="ms-TextField ms-Grid-col ms-u-sm6 block">
                <button class="${styles.button} create-Button">
                <span class="${styles.label}">Create Item</span>
                </button></div>
                <div id="status"></div>
              </div>
            </div>
          </div>
        </div>
      </div>`;
      this.setButtonsEventHandlers();
  }

  private setButtonsEventHandlers(): void {
    const webPart: SpFxWebpartWebPart = this;
    this.domElement.querySelector('button.create-Button')!.addEventListener('click', () => { webPart.createItem(); });
  }

  private createItem(): void {
    let result: HTMLElement = document.getElementById("status")!;
    let responseText: string = "";
    //result.style.color = "white";
    result.innerText = "Updating item...";
    var itemTitle : string = (<HTMLInputElement>document.getElementById("spfxflowIm")).value;              
    const postURL = this.properties.flowURL;   
    const body: string = JSON.stringify({
      'title': itemTitle,
    });   
    const requestHeaders: Headers = new Headers();       
    requestHeaders.append('Content-type', 'application/json');
    requestHeaders.append('Cache-Control', 'no-cache');   
    const httpClientOptions: IHttpClientOptions = {
      body: body,
      headers: requestHeaders
    };     
    this.context.httpClient.post(
      postURL,
      HttpClient.configurations.v1,
      httpClientOptions).then((response: HttpClientResponse) => {
        response.json().then((responseJSON: JSON) => {
          responseText = JSON.stringify(responseJSON);            
        result.innerText = (responseText=="201")?"Item updated successfully":"Error received while updating item";
      });
    }).catch ((response: any) => {
      let errMsg: string = `Error = ${response.message}`;
      result.style.color = "red";
      console.log(errMsg);
      result.innerText = errMsg;
    });
}



  protected onInit(): Promise<void> {
    return this._getEnvironmentMessage().then(message => {
      //this._environmentMessage = message;
    });
  }


  private _getEnvironmentMessage(): Promise<string> {
    if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook
      return this.context.sdks.microsoftTeams.teamsJs.app.getContext()
        .then(context => {
          let environmentMessage: string = '';
          switch (context.app.host.name) {
            case 'Office': // running in Office
              environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOffice : strings.AppOfficeEnvironment;
              break;
            case 'Outlook': // running in Outlook
              environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOutlook : strings.AppOutlookEnvironment;
              break;
            case 'Teams': // running in Teams
              environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment;
              break;
            default:
              throw new Error('Unknown host');
          }

          return environmentMessage;
        });
    }

    return Promise.resolve(this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment);
  }

  protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
    if (!currentTheme) {
      return;
    }

    //this._isDarkTheme = !!currentTheme.isInverted;
    const {
      semanticColors
    } = currentTheme;

    if (semanticColors) {
      this.domElement.style.setProperty('--bodyText', semanticColors.bodyText || null);
      this.domElement.style.setProperty('--link', semanticColors.link || null);
      this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered || null);
    }

  }

  protected get dataVersion(): Version {
    return Version.parse('1.0');
  }

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: strings.PropertyPaneDescription
          },
          groups: [
            {
              groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneTextField('flowURL', {
                  label: strings.FlowURLLabel
                })
              ]
            }
          ]
        }
      ]
    };
  }
}

    4.       Update the en-us.js file with the below code which is located under ‘loc’ folder (solutionpath > src > webparts > spFxWebpart > loc).

define([], function() {

  return {

    "PropertyPaneDescription": "Description",

    "BasicGroupName": "Group Name",

    "DescriptionFieldLabel": "Description Field",

    "AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part",

    "AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app",

    "AppLocalEnvironmentOffice": "The app is running on your local environment in office.com",

    "AppLocalEnvironmentOutlook": "The app is running on your local environment in Outlook",

    "AppSharePointEnvironment": "The app is running on SharePoint page",

    "AppTeamsTabEnvironment": "The app is running in Microsoft Teams",

    "AppOfficeEnvironment": "The app is running in office.com",

    "AppOutlookEnvironment": "The app is running in Outlook",

    "PropertyPaneDescription": "SPFx FLow",

    "BasicGroupName": "Flow Properties",

    "FlowURLLabel": "Flow URL"

  }

});

    5.       Update the mystring.d.ts file with the below code which is located under ‘loc’ colder(solutionpath > src > webparts > spFxWebpart > loc)

declare interface ISpFxWebpartWebPartStrings {

  PropertyPaneDescription: string;

  BasicGroupName: string;

  DescriptionFieldLabel: string;

  AppLocalEnvironmentSharePoint: string;

  AppLocalEnvironmentTeams: string;

  AppLocalEnvironmentOffice: string;

  AppLocalEnvironmentOutlook: string;

  AppSharePointEnvironment: string;

  AppTeamsTabEnvironment: string;

  AppOfficeEnvironment: string;

  AppOutlookEnvironment: string;

  FlowURLLabel: string;

}

 

declare module 'SpFxWebpartWebPartStrings' {

  const strings: ISpFxWebpartWebPartStrings;

  export = strings;

}

    6.       Run ‘Gulp Serve’ to test the webpart in Workbench url.

7.       Add the webpart on workbench url, edit the webpart and provide URL of the Flow noted in the above step: HTTP Post URL.