Showing posts with label SPFx. Show all posts
Showing posts with label SPFx. Show all posts

Saturday, 19 March 2022

How to add SPFx webpart to Microsoft Teams tab

Create SPFx new project:

Open Node.js command prompt and run 'yo @microsoft/sharepoint'


Enable the webpart to be deployed as Teams Tab:

Go to TeamsTabWebPart.manifest.jsonàand ensure the “TeamsTab” is exist in ‘supportedHosts’.

Enable Tenant wide deployed if needed

1. In this this example, we will configure this solution as tenant wide deployment, by doing this we won’t have to deploy this solution individually to each site collection.

2. It would be globally deployed to all Site collections from app catalog.

3. However please note that if you don’t want tenant wide deployment of your SPFx solution, you can skip this step. But then to add this app to MS team, you will have to individual add this solution on  MS Team site collection.

4. For Tenant deployment: go to ./config/package-solution.json, add attribute “skipFeatureDeployment”: true like below.

Updating webpart code to be read Microsoft Teams context:

  Ø  Open ./src/webparts/TeamsTab/TeamsTabWebPart.ts for the needed edits on making our solution aware of the Microsoft Teams context, if it’s used as a tab.

  Ø  Update the render() method

   We can detect if solution is hosted by Microsoft Teams by checking the                                                      this.context.sdks.microsoftTeams property.

  public render(): void {

    let title: string = '';
    let subTitle: string = '';
    let siteTabTitle: string = '';
  
    if (this.context.sdks.microsoftTeams) {
      // We have teams context for the web part
      title = "Welcome to Teams!";
      subTitle = "Building custom enterprise tabs for your business.";
      siteTabTitle = "We are in the context of following Team: " + this.context.sdks.microsoftTeams.teamsJs.app.getContext();
    }
    else
    {
      // We are rendered in normal SharePoint context
      title = "Welcome to SharePoint!";
      subTitle = "Customize SharePoint experiences using Web Parts.";
      siteTabTitle = "We are in the context of following site: " + this.context.pageContext.web.title;
    }
  
    this.domElement.innerHTML = `
      <div class="${ styles.teamsTab }">
        <div class="${ styles.container }">
          <div class="${ styles.row }">
            <div class="${ styles.column }">
              <span class="${ styles.title }">${title}</span>
              <p class="${ styles.subTitle }">${subTitle}</p>
              <p class="${ styles.description }">${siteTabTitle}</p>
              <p class="${ styles.description }">Description property value - ${escape(this.properties.description)}</p>
              <a href="https://aka.ms/spfx" class="${ styles.button }">
                <span class="${ styles.label }">Learn more</span>
              </a>
            </div>
          </div>
        </div>
      </div>`;
  }

Updating .SCSS file to fix the styles which we used in webpart file:

  Ø  Open ./src/webparts/TeamsTab/TeamsTabWebPart.module.scss and update the below classes:

@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';

.teamsTab {
  overflow: hidden;
  padding: 1em;
  color: "[theme:bodyText, default: #323130]";
  color: var(--bodyText);
  &.teams {
    font-family: $ms-font-family-fallbacks;
  }
}

.container {
  max-width: 700px;
  margin: 0px auto;
  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}

.row {
  @include ms-Grid-row;
  @include ms-fontColor-white;
  background-color: $ms-color-themeDark;
  padding: 20px;
}

.column {
  @include ms-Grid-col;
  @include ms-lg10;
  @include ms-xl8;
  @include ms-xlPush2;
  @include ms-lgPush1;
}

.title {
   @include ms-font-l;
   @include ms-fontColor-white;
   background-color: $ms-color-themePrimary;
  }
  
.subTitle {
  @include ms-font-l;
  @include ms-fontColor-white;
 }
  
.description {
  @include ms-font-l;
  @include ms-fontColor-white;
 }

 .button {
   // Our button
   text-decoration: none;
   height: 32px;
  
   // Primary Button
   min-width: 80px;
   background-color: $ms-color-themePrimary;
   border-color: $ms-color-themePrimary;
   color: $ms-color-white;
  
   // Basic Button
   outline: transparent;
   position: relative;
   font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
   -webkit-font-smoothing: antialiased;
   font-size: $ms-font-size-m;
   font-weight: $ms-font-weight-regular;
   border-width: 0;
   text-align: center;
   cursor: pointer;
   display: inline-block;
   padding: 0 16px;
  
   .label {
     font-weight: $ms-font-weight-semibold;
     font-size: $ms-font-size-m;
     height: 32px;
     line-height: 32px;
     margin: 0 4px;
     vertical-align: top;
     display: inline-block;
     }
   }

.welcome {
  text-align: center;
}

.welcomeImage {
  width: 100%;
  max-width: 420px;
}

.links {
  a {
    text-decoration: none;
    color: "[theme:link, default:#03787c]";
    color: var(--link); // note: CSS Custom Properties support is limited to modern browsers only

    &:hover {
      text-decoration: underline;
      color: "[theme:linkHovered, default: #014446]";
      color: var(--linkHovered); // note: CSS Custom Properties support is limited to modern browsers only
    }
  }
}

Check and verify teams icon and outline:

To sync  SPFx webpart to Teams, we need to make sure our solution has color and outline png file which is required. You will find this files in teams folder in your project root directory.


Package and deploy your webpart to SharePoint:

  Ø  Execute the following commands to build bundle your solution. This executes a release build of your project by using a dynamic label as the host URL for your assets.

gulp bundle --ship

  Ø  Execute the following task to package your solution. This creates an updated teams-tab-webpart.sppkg package on the sharepoint/solution folder.

gulp package-solution --ship

  Ø  Next, you need to deploy the package that was generated to the tenant App Catalog.

  Ø  Go to your tenant's SharePoint App Catalog.

  Ø  Upload or drag and drop the teams-tab-webpart.sppkg to the App Catalog.


Sunday, 6 February 2022

Error: Type 'HTMLElement | null' is not assignable to type 'HTMLElement' ts(2322)

Add ! (exclamation mark) after a property/variable name. This feature is called “Non-null assertion operator“, basically it means that when you add the exclamation mark after a property/value, you are telling TypeScript that you are certain that value is not null or undefined.

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.