Working with Teams Content from an SPFx Tab

This is part 2 of a 3-part article series about building a “360 degree view” mashup for Microsoft Teams using the SharePoint Framework and React. The articles are:

  1. Part 1: 360 Degree Collaboration in Microsoft Teams
    This article introduces the 360 Degree pattern for collaborative applications, and explains a the workings of a sample solution based on SharePoint Framework and React
  2. Part 2: Working with Teams Content from an SPFx Tab (this article)
    This article explains how to access Team and channel content, such as the shared calendar and conversation, from a SharePoint Framework tab in Teams
  3. Part 3: Deep linking to a SharePoint Framework tab
    This article explains how to create a deep link that opens a Team, Channel, and tab, and passes information to your SharePoint Framework tab so you can display specific information

The series is based on a sample Teams tab written in SharePoint Framework which displays a mashup of information about customer visits.

Getting from the Tab to the Team, and beyond

As you may have heard, the great new way to write custom tabs for Microsoft Teams is to use the SharePoint Framework. One big SharePoint Framework web part makes a tab, and SharePoint takes care of hosting, single sign-on, configuration, and more.

To get started, follow this tutorial. It’s a great start, but it might leave you wondering how to interact with the Team your tab is running in, like read the meeting calendar or post a message. This article will explain how you can do that and also unlock the entire Office 365 Group from your code.

FieldVisitDemo

Two APIs are better than one

Well … maybe? Anyway, you need two API’s to do the job. The first is the Teams JavaScript client SDK. The second API is the Microsoft Graph. The Teams Client SDK will give you the Team ID (which is also the O365 Group ID), the channel ID, etc. You can use this information in calls the Graph API, which is able to read and write Teams and Office 365 content.

Teams tabs run in IFrames, and a SharePoint Framework tab is no exception. IFrames are isolated, which is good for security, but sometimes they need to talk to their hosting web page – in this case Microsoft Teams. That’s the job of the Teams Client SDK.

TeamsClientSDK

The Teams Client SDK sends inter-frame messages from your web part to the hosting Teams client, be it a web page or Electron app. It relays your requests across the otherwise impermeable wall between browser frames and gives you access to the context your tab is running in. It’s also used to launch login pop-up windows, but SharePoint’s single sign-on takes care of those details, so you won’t need to worry about it. However the Teams Client SDK can’t do much to the Team itself; there’s no direct access to the conversation or other content. For that, you need the Graph API.

Here are the steps to access the Team conversation and calendar from a SharePoint Framework tab:

Step 1: Get the Teams info with the Teams JavaScript Client

You can see this in the Field visit tab web part. First, import the Teams API:

import * as microsoftTeams from '@microsoft/teams-js';

Then check to see if you have the Teams Client SDK. this.context.microsoftTeams will be null on a SharePoint page, and will point to the Teams Client SDK if you’re in Teams. This code, from the web part’s onInit() method, checks for the Teams Client SDK, and if it’s present, squirrels away the Teams info in private variables.

if (this.context.microsoftTeams &&
    this.context.microsoftTeams.getContext) {

  // Get configuration from the Teams SDK
  p = new Promise((resolve, reject) => {

    if (this.context.microsoftTeams &&
        this.context.microsoftTeams.getContext) {
      this.context.microsoftTeams.getContext(context => {

        this.teamsContext = context;
        this.groupName = context.teamName;
        this.groupId = context.groupId;
        this.channelId = context.channelId;
        resolve();

      });
    }
  });
}

Step 2: Call the Graph API

To post into the channel the tab is running in, call the Graph API using the values from the Teams context. By this time, we’re down in the Conversation Service, which has received the Team and channel ID from the service factory through constructor injection. The SharePoint Framework makes the Graph call easy – we don’t need no stinking access tokens!

this.context.msGraphClientFactory
    .getClient()
    .then((graphClient: MSGraphClient): void => {
        graphClient.api(`https://graph.microsoft.com/beta/teams/${this.teamId}/channels/${this.channelId}/chatThreads`)
        .post(message, ((err, res) => {
            resolve();
        }));
    });

Reading the calendar is about as easy. Since the Team ID is the same as the O365 Group ID, the calendar service only needs ask Graph to query the group calendar.

this.context.msGraphClientFactory
    .getClient()
    .then((graphClient: MSGraphClient): void => {
        graphClient.api(`/groups/${groupId}/calendarview?startdatetime=${startDateTime}&enddatetime=${endDateTime}`)
        .get((error, data: CalendarView, rawResponse?: any) => {
            let calendarItems: ICalendarItem[] = [];
            data.value.forEach((event) => {
                // Do something with a calendar item
            });
        });
    });

The cool thing about it is that the calendar isn’t in Teams, it’s in Exchange Online where it can easily sync with Outlook. And through the power of the Microsoft Graph, you can get from Teams to Exchange with one api call. If we had to call the Exchange API, that would be an extra DNS lookup and OAuth flow, and probably an extra call to map the Group ID to the calendar – but Graph does all that in one round trip to the service endpoint.

Did you know?

The SPFx Graph Client is based on the Microsoft Graph client library for JavaScript. This open source library allows fluent Graph access from browser or server-side JavaScript, with or without SharePoint. That means your services could easily be reused in non-SharePoint solutions.

Step 3: Ensure you have permission

In order to call the Graph API, you need an access token which proves that you have permission to do what you’re asking. The SharePoint Framework’s MSGraphClient handles the access token for you, which is a great convenience and also allows many web parts to share the same access token (much more efficient). That said, you still need to have permission or the Graph call will fail.

When you’re calling from the browser, which is always the case for the SharePoint Framework, you’re always calling with User Delegated permissions. That means that the application (SharePoint Framework in this case) is acting on behalf of the logged-in user, and the Graph call’s effective permission is the intersection of the app’s permissions and the user’s permissions.

DelegatedPermissions

For example, suppose you want to read an Exchange calendar. Reading a calendar requires Calendars.Read permission. If the application has Calendars.Read, and the user has read/write access to calendars 1, 3, and 5, Graph will allow read-only access to calendars 1, 3, and 5 only – the intersection of the two permissions.

Fortunately, the permission is clearly documented for each and every Graph API call. Here’s the documentation for the List events call that reads the Team calendar.

GraphPermissions

When calling from SharePoint Framework you will always be using the permission type Delegated (work or school account) – that’s because Application permissions can’t be used when calling from a web browser, and even if your external user logged in with a personal Microsoft account, they’re actually using your tenant with an external Azure AD account.

So here’s what you have to do: figure out what permissions you need and request them in the config/package-solution.json file.

{
  "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
  "solution": {
    "name": "field-visit-demo-tab-client-side-solution",
    "id": "67407b5b-e3eb-480a-97a3-195810f83702",
    "version": "1.0.1.0",
    "includeClientSideAssets": true,
    "skipFeatureDeployment": true,
    "webApiPermissionRequests": [
      {
        "resource": "Microsoft Graph",
        "scope": "Calendars.Read"
      },
      {
        "resource": "Microsoft Graph",
        "scope": "Group.ReadWrite.All"
      }
    ],
    "isDomainIsolated": false
  },
  "paths": {
    "zippedPackage": "solution/field-visit-demo-tab.sppkg"
  }
}

The webApiPermissionRequests property contains an array of permission requests, each providing a resource (Microsoft Graph) and a permission scope (Calendars.Read to allow reading the calendar, and Group.ReadWrite.All to allow posting to the Teams conversation).

When the solution is installed, an administrator needs to approve the permission requests under API Management in the new SharePoint Admin Center. All outstanding resource requests are shown; select the ones you want to approve and click “Approve or reject” and approve them.

API Management

Since (nearly) all SharePoint Framework solutions share the same web page(s), the effect of this is to grant all SharePoint Framework solutions the permission, not just your solution. Be aware that if you give a generous permission to one web part, another one could use it. The exception is that you can write isolated web parts, which avoids this, however using isolated web parts in Teams is as yet uncharted territory (for me anyway – please comment if you’ve tried it!).

Accessing SharePoint Data

Getting to the SharePoint site associated with the Team is much easier! The Team’s associated SharePoint site is hosting your web part, so the SharePoint REST API is at your beck and call. Any HTTP client will work because you’re already logged into SharePoint and have the necessary auth cookie. However it’s best to use the framework’s SPHttpClient, which handles details like request digests in POSTs.

Here’s a snippet from the document service which reads a list of documents tagged with a particular customer ID.

this.context.spHttpClient
    .fetch(`${siteUrl}/_api/lists/GetByTitle('Documents')/items?$filter=Customer eq '${customerId}'&$select=Title,FileLeafRef,FileRef,UniqueId,Modified,Author/Name,Author/Title&$expand=Author/Id&$orderby=Title`,
        SPHttpClient.configurations.v1,
        {
            method: 'GET',
            headers: { "accept": "application/json" },
            mode: 'cors',
            cache: 'default'
        })
        .then((response) => {
            if (response.ok) {
                return response.json();
            } else {
                throw (`Error ${response.status}: ${response.statusText}`);
            }
        })
        .then((o: IDocumentsResponse) => {
            let docs: IDocument[] = [];
            o.value.forEach((doc) => {
                docs.push({
                    name: doc.FileLeafRef,
                    url: doc.FileRef,
                    author: doc.Author.Title,
                    date: new Date(doc.Modified)
                });
            });
            resolve(docs);
        });
    });

As you can see, the code loops through the documents and copies the fields it needs into an object array called docs.

Also notice that the result from the API call is strongly typed – it returns an IDocumentsResponse, so if the response is successful it’s type safe. This was generated by setting a breakpoint in the Chrome debugger and copying the response JSON to the clipboard, then pasting it into http://www.json2ts.com/. The resulting Typescript was pasted into the IDocumentsResponse interface.

Did you know?

The SPHttpClient in SharePoint Framework is based on the Fetch API, a replacement for the archaic XMLHttpRequestthat dates back to the days when they were so sure that XML was the future that they named the HTTP request object after it. (What else could you ever want to access with an HTTP request, right?)

Fetch is simple and much easier to use than its predecessor, and it’s built into modern browsers with varying levels of compatibility. In addition to SPHttpClient, the SharePoint Framework provides a simple Fetch polyfill called HttpClientthat you can safely use in all SharePoint supported browsers (like the unfetching IE 11).

If you prefer a fluent API for calling the SharePoint REST API, check out the Patterns and Practices PnPJS library.

What’s next?

Please check out the rest of the series for more information on building Teams tabs with SharePoint Framework!

  1. Part 1: 360 Degree Collaboration in Microsoft Teams
  2. Part 2: Working with Teams Content from an SPFx Tab (this article)
  3. Part 3: Deep linking to a SharePoint Framework tab

The sample code is here; thanks for reading!

Leave a comment