Cloning Teams and Configuring Tabs via Microsoft Graph: Configuring Tabs – The Fundamentals

Cloning Teams and Configuring Tabs via Microsoft Graph: Configuring Tabs – The Fundamentals

Last updated on October 16, 2021

This blog article is the third one in this series. In the prelude, I told you a little bit about the reasons why configuring tabs automatically is important, and in the previous blog post, I showed you how you can clone a team programmatically via Microsoft Graph. Now that we have cloned the team let’s get down to business and start configuring the tabs.

Published articles in this blog post series

Table of contents

  1. Configuring Tabs
  2. The Fundamentals
  3. Supporting Classes
  4. Afterword

Configuring tabs

At this point, we have our brand new team set up with its cloned tabs. However, none of those tabs are yet configured to show any content. There’s just the button Set up tab.

Blah, can’t we just get that to provision automatically too? Yes, we can!

I can certainly understand why the configuration isn’t automatic by default. There are just so many different tabs and ways people might want them to be configured. In this blog post series, I’ll show you how it should work in my opinion. 😉 And even if you choose to write the tab specific logic in a different way, what I am about to show to you in this blog post is so fundamental that it should be very useful to you no matter which tabs and how you eventually decide to configure them.

And yes, you heard it right. All of the tabs are a bit different from one another, so we need to write individual logic for each different type of tab. But let’s not worry about that now, and just focus on how we can build a solid foundation for all of that.

The fundamentals

In this chapter, we’ll loop through all of the template team channels and their tabs. During each iteration, we’ll fetch all the different pieces of information that we need for configuring the corresponding tab in the new team.

To keep the code sample as short and readable as possible, I’ve omitted the try-catch blocks. Do add exception handling and logging on the level that you prefer. And remember to install Newtonsoft.Json NuGet package to your project if you haven’t already.

If you cloned a team using the code sample from the previous blog post, you should now have both the templateTeamId and the newTeamId available. Make a call to the method below with those parameters.

In the method, we first check if either one of those parameters is null. We can’t really do much if they are, so it’s game over in that case. However, if both of the IDs are there, we proceed and fetch the template team channels from Microsoft Graph. And after that, we start looping through them one by one.

With the ID of the channel that is currently being processed, we get all the tabs on that channel. At this point, we also fetch the channel that shares the same name in the new team and save its ID to a variable for later use.

In the same way as we are looping through the channels, we need to loop through those template tabs. And finally, things start getting a bit more interesting.

Each of the Teams tabs has a property called teamsAppId which basically tells us what type of a tab it is — or perhaps rather, what app it belongs to. The app ID can be a GUID or a string (e.g., com.microsoft.teamspace.tab.planner). Note that the property is only available in the Microsoft Graph beta endpoint, so whenever you need to use this property, you need to use the beta version of Graph. In our case, we use the property for two things: finding the correct tab in the new team, and also, later on in this series, to steer the execution to configure our tab in the correct manner for that particular tab type.

Together with the teamsAppId and the templateTabDisplayName, we proceed to get the corresponding tab from the new team. For that, we also need the newTeamId and the newChannelId which we discovered earlier. And with that, we have the final piece of information that we require: the newTabId. That is the target for our configurations.

Only thing left to do is to form the body that contains all the configurations for the “update tab” request. More of that in the next chapter!

public async Task ConfigureTeamsTabs(string templateTeamId, string newTeamId)
{
    if (templateTeamId == null || newTeamId == null) return;

    var templateChannels = (await MicrosoftGraph.GetChannels(templateTeamId)).SelectToken("value");
    foreach (var templateChannel in templateChannels)
    {
        var templateChannelId = templateChannel.SelectToken("id").ToString();
        var newChannelId = await MicrosoftGraph.GetChannelIdByName(newTeamId, templateChannel.SelectToken("displayName").ToString());

        var templateTabs = (await MicrosoftGraph.GetChannelTabs(templateTeamId, templateChannelId)).SelectToken("value");
        foreach (var templateTab in templateTabs)
        {
            // Note that the teamsAppId property is currently only available in the beta endpoint
            var teamsAppId = templateTab.SelectToken("teamsAppId").ToString();
            var templateTabDisplayName = templateTab.SelectToken("displayName").ToString();

            var newTab = await MicrosoftGraph.GetTeamChannelTabByAppIdAndName(newTeamId, newChannelId, teamsAppId, templateTabDisplayName);

            if (newTab == null) continue;

            var newTabId = newTab.SelectToken("id").ToString();

            string body = null;

            // Now we have all the information we need to configure the tabs!            
            // TO DO: Form body for tab configurations -> next chapter!

            if (!string.IsNullOrEmpty(body)) await MicrosoftGraph.UpdateTeamChannelTab(newTeamId, newChannelId, newTabId, body);
        }
    }
}

Supporting classes

Unfortunately, those 32 lines of code above are not quite enough to make all of this work. However, I’ve tried to make things as easy as possible for you. Below, you can find two supporting classes for that piece of code.

This first class contains methods for calling Microsoft Graph. For now, we need to call Graph to get the template team channels, the tabs on the template channel, the matching channel ID in the new team (by name), and the corresponding tab in the new team (with a matching app ID and name).

using System.Net;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;

namespace MyProject
{
    public class MicrosoftGraph
    {
        public static async Task<JToken> GetChannels(string teamId)
        {
            return await HttpRequest.GetResponseBodyAsJson($"https://graph.microsoft.com/v1.0/teams/{teamId}/channels", HttpRequest.Method.Get);
        }

        public static async Task<string> GetChannelIdByName(string teamId, string channelName)
        {
            return (await HttpRequest.GetResponseBodyAsJson($"https://graph.microsoft.com/v1.0/teams/{teamId}/channels?$filter=displayName eq '{channelName}'", HttpRequest.Method.Get)).SelectToken("value[0].id").ToString();
        }

        public static async Task<JToken> GetChannelTabs(string teamId, string channelId)
        {
            return await HttpRequest.GetResponseBodyAsJson($"https://graph.microsoft.com/beta/teams/{teamId}/channels/{channelId}/tabs", HttpRequest.Method.Get);
        }

        public static async Task<JToken> GetTeamChannelTabByAppIdAndName(string teamId, string channelId, string appId, string tabName)
        {
            // For some reason the tabName needs to be URL encoded again for this operation to work even though the tab names are already been encoded once by default
            return (await HttpRequest.GetResponseBodyAsJson($"https://graph.microsoft.com/beta/teams/{teamId}/channels/{channelId}/tabs?$filter=teamsAppId eq '{appId}' and displayName eq '{WebUtility.UrlEncode(tabName)}'", HttpRequest.Method.Get)).SelectToken("value[0]");
        }

        public static async Task UpdateTeamChannelTab(string teamId, string channelId, string tabId, string body)
        {
            await HttpRequest.GetResponseBodyAsJson($"https://graph.microsoft.com/v1.0/teams/{teamId}/channels/{channelId}/tabs/{tabId}", HttpRequest.Method.Patch, body);
        }
    }
}

And this second class below is a helper class for the Microsoft Graph class for making the actual HTTP requests. We use the HttpClient object, and also have a method for making patch requests (PatchAsync does not come built-in) and for converting response content to JSON.

using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;

namespace MyProject
{
    public static class HttpRequest
    {
        // The .NET Framework System.Net.Http.HttpMethod class doesn't have Patch
        public enum Method
        {
            Get, Post, Put, Patch, Delete
        }

        public static async Task<JToken> GetResponseBodyAsJson(string url, Method method, string body = null)
        {
            var response = await GetResponse(url, method, body);
            return response != null ? JToken.Parse(await response.Content.ReadAsStringAsync()) : null;
        }

        public static async Task<HttpResponseMessage> GetResponse(string url, Method method, string body = null)
        {
            using (var httpClient = new HttpClient())
            {
                // Get the access token the way you prefer here
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", await Authorization.GetAccessToken("https://graph.microsoft.com"));

                if (method == Method.Get)
                {
                    return httpClient.GetAsync(url).Result;
                }
                if (method == Method.Delete)
                {
                    return httpClient.DeleteAsync(url).Result;
                }

                var content = new StringContent(body, Encoding.UTF8, "application/json");

                if (method == Method.Post)
                {
                    return httpClient.PostAsync(url, content).Result;
                }
                if (method == Method.Put)
                {
                    return httpClient.PutAsync(url, content).Result;
                }
                if (method == Method.Patch)
                {
                    return httpClient.PatchAsync(url, content).Result;
                }

                return null;
            }
        }

        private static async Task<HttpResponseMessage> PatchAsync(this HttpClient httpClient, string url, HttpContent body)
        {
            var request = new HttpRequestMessage(new HttpMethod("PATCH"), url)
            {
                Content = body
            };

            return await httpClient.SendAsync(request);
        }
    }
}

Afterword

And that is the foundation on which we’ll build our tab-specific configuration logic. The first tab that we’ll take a look at is OneNote!

If you are into this type of a thing and would like to be notified when the next chapter of this series is out, follow me on Twitter. I also share some other tips and tricks and even try to say something funny occasionally. You can be the judge if I am actually funny or not.

Anyway, I hope you enjoyed reading this article, and until next time!

Laura



6 thoughts on “Cloning Teams and Configuring Tabs via Microsoft Graph: Configuring Tabs – The Fundamentals”

  • Is there any way through the Graph API to re-order the existing tabs within a channel, or specify a specific position when posting a new tab so that it does not always get added to the end?

    • Hi Eric,

      Looking at the Graph documentation, tabs do have a property called “sortOrderIndex”: https://docs.microsoft.com/en-us/graph/api/teamstab-add?view=graph-rest-1.0. However, at least in the documentation, it seems to be only present in responses, so it could be that it is read-only.

      If I were you, I’d quickly check if it is also possible to specify that property in the “add tab” requests, and if the request succeeds, ensure it actually gets honored. Feel free to report back on your findings!

      Laura

  • Hi Laura, I don’t know what i am doing wrong.
    System.Net.Http.HttpClient has PatchAsync . I put breakpoint inside the method you defined and it never gets called. Anyway i changed the signature of the calling as well the method as PatchAsyncx and now it was getting called. However, it returns:

    {StatusCode: 400, ReasonPhrase: ‘Bad Request’, Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:{ Cache-Control: private request-id: 656e13f2-0f39-4fb5-96be-2eed4b5f5463 client-request-id: 656e13f2-0f39-4fb5-96be-2eed4b5f5463 …………………………………}

    • Hi Nabajyoti,

      Based on that error there’s something wrong with your HTTP request. I recommend you install Telerik Fiddler to debug the traffic and see what’s the issue.

      Laura

  • The last PatchAsync with the body in its parameter does not get called from from above. The explicit method you have defined does not get called with the code you have put inside.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.