Cloning Teams and Configuring Tabs via Microsoft Graph: Configuring the SharePoint and Files tabs

Cloning Teams and Configuring Tabs via Microsoft Graph: Configuring the SharePoint and Files tabs

Last updated on September 3, 2023

Our blog post series is closing to the end. In the Prelude, I covered what were the reasons for starting this blog post series in the first place, and why cloning teams and after that configuring the tabs automatically is valuable. Part 1 was all about how you can successfully clone teams via Microsoft Graph and what kind of struggles you might encounter while at it. After that, we proceeded to the blog posts about individual tab configurations.

At first, I showed you how you can automatically configure the OneNote tab to either show the default OneNote file that gets created when you clone a team, or how you can create new OneNote files for the group and display those.

After OneNote, I shared with you the configuration instructions for Planner tabs, and perhaps also got you intrigued about cloning Planner plans. And in this final blog post, I will show you how you can configure two SharePoint related tabs: the SharePoint tab itself and Files.

Published articles in this blog post series

Table of Contents

  1. SharePoint App ID
  2. Forming the body for the update request
  3. Supportive methods
  4. Provision subfolder structures for the Files tab
  5. Afterword

SharePoint App ID

If you’ve been following this blog post series until now, you already have this kind of a TeamsAppId class in your project. Now it is time to add the SharePoint ID to it, to accompany the previously added IDs. And if you are new to the series: with the ID we can figure out what kind of a tab we are dealing with, because each different type of tab requires its own individual configurations.

public class TeamsAppId
{
    public static readonly string OneNote = "0d820ecd-def2-4297-adad-78056cde7c78";
    public static readonly string Planner = "com.microsoft.teamspace.tab.planner";
    public static readonly string SharePoint = "2a527703-1f6f-4559-a332-d8a7d288cd88";
}

Forming the body for the update request

Unlike with the previous two tabs we’ve configured (OneNote and Planner), we are actually not creating any new resources here. I’m a big fan of provisioning resources to new SharePoint sites via SharePoint site templates and PnP templates, so I rather use those this time as well.

The nice thing about configuring the SharePoint tab is that you do not need to have the resources available yet when you are configuring the tab. You only need to know what URL the resource will be located at after it has been provisioned. You can be certain that the tab will display your content, whether you provision the resources before or after configuring the tab.

Here, we deduce that the tab in question is indeed SharePoint, and immediately after proceed to construct the body for our update request.

if (teamsAppId == TeamsAppId.SharePoint)
{
    body = await ConfigureSharePointTab(newTeamId, templateTab.SelectToken("configuration.contentUrl").ToString());
}

The SharePoint tab can be used for displaying both lists and pages. Luckily, the URLs are very similar for the most part, and the only things we need to figure out are the new team site URL and the relative URL of the resource that we want to display.

Example content URL for a page:

https://laurakokkarinen.sharepoint.com/sites/example/_layouts/15/teamslogon.aspx?spfx=true&dest=https%3A%2F%2Flaurakokkarinen.sharepoint.com%2Fsites%2Fexample%2F_layouts%2F15%2FpageName.aspx

Example content URL for a list:

https://laurakokkarinen.sharepoint.com/sites/example/_layouts/15/teamslogon.aspx?spfx=true&dest=https%3A%2F%2Flaurakokkarinen.sharepoint.com%2Fsites%2Fexample%2FLists%2FlistName%2FAllItems.aspx%3Fp%3D11

Constructing those URLs is perhaps not as straight forward as you might first expect because it requires us to make a extra call to Microsoft Graph. But in the end, it doesn’t result in that many lines of code.

When calling this method, we’ll pass the new team ID and the template tab content URL to it as parameters. We extract the relative URL from the template tab content URL, and combine it with the URL of the new SharePoint team site that automatically got created when we cloned the team. To be on the safe side, we fetch the team site URL from Microsoft Graph using the ID of the new team, because it is not guaranteed that the team name is always an exact match with the site name.

private static async Task<string> ConfigureSharePointTab(string teamId, string templateTabContentUrl)
{
    var contentRelativeUrl = GetSharePointTabContentRelativeUrl(templateTabContentUrl);
    if (contentRelativeUrl == null) return null;

    var webUrl = (await MicrosoftGraph.GetGroupSite(teamId)).SelectToken("webUrl").ToString();

    return $"{{ 'configuration': {{ 'contentUrl': '{webUrl}/_layouts/15/teamslogon.aspx?spfx=true&dest={WebUtility.UrlEncode($"{webUrl}/{contentRelativeUrl}")}' }} }}";
}

The contentUrl of the template tab has an encoded version of the resource URL in one of the query string parameters: dest. We first extract that parameter value from the full content URL, URL decode it, and then only take the site relative URL of the resource. E.g., if the decoded dest parameter value is https://laurakokkarinen.sharepoint.com/sites/example/Lists/listname/AllItems.aspx?p=11, the bit we end up returning is just Lists/listname/AllItems.aspx?p=11.

private static string GetSharePointTabContentRelativeUrl(string contentUrl)
{
    const string param = "&dest=";
    var index = contentUrl.IndexOf(param, StringComparison.OrdinalIgnoreCase) + param.Length;
    var dest = WebUtility.UrlDecode(contentUrl.Substring(index));

    var frags = dest.Split('/');

    return string.Join("/", frags, 5, frags.Length - 5);
}

Supportive methods

In addition to the code above, you’ll also need the following method to fetch the Office 365 group connected team site URL from MicrosoftGraph. If you’ve been following this blog post series until now and have already included the classes from the Configuring Tabs — The Fundamentals blog post, you only need to add the following method to the MicrosoftGraph helper class.

public static async Task<JToken> GetGroupSite(string groupId)
{
    return await HttpRequest.GetResponseBodyAsJson($"https://graph.microsoft.com/v1.0/groups/{groupId}/sites/root", HttpRequest.Method.Get);
}

Provision subfolder structures for the Files tab

Now that we are talking about SharePoint, there is another thing I’d like to touch upon. You remember when I mentioned using PnP templates to provision the resources we want to display in the SharePoint tab? There’s also another nifty thing we can do with them for Teams tabs while we are at it.

You’ve no doubt taken notice of the Files tab that displays all the files you’ve added to the Teams channel. We can actually provision subfolder structures for those Files tabs with PnP templates.

Because the files (and folders) are in fact stored in SharePoint, there’s nothing stopping us from provisioning folder structures with PnP templates that get displayed on Teams’ side as well. In addition to PnP templates, you can also provision the folders using CSOM or the SharePoint REST API.

We don’t need to deal with any folder or channel IDs or anything like that. The teams channels link to the correct folders simply based on their names. If we just make sure that we are provisioning the root level folders to the Documents library with the exact same names as what the channels are called, those folders will automatically get mapped to the correct channels on Teams when users go to browse the Files tabs. And under those root level folders, we can provision additional folders that get displayed in the Files tab.

Here is an example of what the Folders section of the PnP template could look like to achieve a similar view as in the image above. If you are not yet familiar with PnP templates, here are a couple of links to the provisioning console application sample and the latest schema documentation.

<pnp:Folders>	 	 
    <pnp:Folder Name="General">	 	 
        <pnp:Folder Name="Schematics"></pnp:Folder>	 	 
        <pnp:Folder Name="Material options"></pnp:Folder>	 	 
        <pnp:Folder Name="Images for the instruction manual"></pnp:Folder> 	 
    </pnp:Folder>	 	 
    <pnp:Folder Name="Announcements">
        <pnp:Folder Name="Published materials"></pnp:Folder>
    </pnp:Folder>
    <pnp:Folder Name="Backlog">	 	 
        <pnp:Folder Name="Attachments"></pnp:Folder>	 	 
    </pnp:Folder>	 	 
    <pnp:Folder Name="Meetings">	 	 
        <pnp:Folder Name="Shared with stakeholders"></pnp:Folder>	 	 
        <pnp:Folder Name="Internal"></pnp:Folder>	 	 
    </pnp:Folder>	 	 
    <pnp:Folder Name="Schedule and Budget">	 	 
        <pnp:Folder Name="Original estimates"></pnp:Folder>	 	 
    </pnp:Folder>	 	 
</pnp:Folders>
Note that the main channel folder is always called General, even if the channel name is different in Teams due to regional settings.

Here’s also an image of what the same subfolder structure looks like on SharePoint.

Afterword

Have you already been using PnP templates for provisioning resources on SharePoint sites, or are you just getting started? Do you feel like they are easy to get into it, or would you perhaps like me to write my own tutorial about them? Let me know in the comments!

This blog post is the last one in this series — at least for now. There’s still loads of different types of tabs we could configure programmatically, so who knows, maybe I’ll get back to this topic soon enough. 😉

What did you think of this blog series? Was the topic useful? Did you like reading shorter articles more often? Or do you prefer my usual style of having everything in one huge chunk of text? I write this blog for you, so it’d be good to hear your thoughts. 🙂

Thank you for sticking with me ’til the end, and until next time!

Laura



13 thoughts on “Cloning Teams and Configuring Tabs via Microsoft Graph: Configuring the SharePoint and Files tabs”

  • Hi Uday,

    here is how I add custom SPFx webparts to teams programmatically using PowerShell (replace the single and double quotes as appropriate):
    1. Get the component Id of the SPFx package: open the manifest of the webpart and find the value of attribute “id”. Alternatively navigate to ‘$yourAppCatalogUrl/Lists/ComponentManifests’ after uploading the webpart to the app catalog, find your SPFx webpart in the list, and copy the Component ID without brackets. I will refer to this value as $componentId. This value will not differ between tenants.
    2. Go to Teams -> Apps, find your custom app, and click on ‘copy link’. Save the GUID between ‘…/apps/{GUID}?source…’ to variable $teamsAppId. This value will differ between tenants. Also create a random GUID and save it to variable $instanceId. Save the title and description of your webpart to variables $title and $description. Save your MS Teams team GUID to variable $teamGuid, and the GUID of the target Teams channel to variable $channelId. Create variables $teamSiteUrl $teamSiteRelativeUrl to save your full and relative team site URLs. The relative URL should look something like ‘/sites/BestTeamEver’.
    3. Ensure that the team site already has the list ‘Lists/HostedAppConfigs’ by making an HTTP Get call to ‘$teamSiteUrl/_layouts/15/teamshostedapp.aspx?openPropertyPane=true&teams&componentId=$componentId&forceLocale=en-us’. I don’t know how to authenticate for this URL best, but I use two SPO cookies called `FedAuth` and `rtFa` along with the call for it to work. I generate these cookies with puppeteer and pass them to my automation anyway for other purposes. I tried to use a bearer token from an App-Only connection to authentication but ran into a 401.
    4. Connect to the team site with ‘Connect-PnPOnline -ClientId/-AppId …’, and then save the bearer token for this session by calling ‘$bearer = Get-PnPAppAuthAccessToken’.
    5. Make an HTTP Post call to ‘$teamSiteUrl/_api/web/hostedapps/add’ with headers
    @{“Content-Type” = “application/json”;”Accept” = “application/json;odata=nometadata”; “Authorization”, “Bearer $bearer” }
    and payload
    @{ hostType = “Teams”; webPartDataAsJson = “{““dataVersion““:““1.0““,““description““:““$description““,““id““:““$componentId““,““properties““:{““description““:““$description““},““instanceId““:““$instanceId““,““title““:““$title““}”}
    The response should look like this {“value”: }. Save the to variable $listItemId.
    6. Now list ‘$teamSiteUrl/Lists/HostedAppConfigs’ should exist and contain at least one item. Fetch the GUID of this list with ‘Get-PnPList’, and save its GUID to $listId. The Id of the list item should have been already saved to variable $listItemId.
    7. Enable your custom app for the specific team by making an HTTP Post call to URL “https://graph.microsoft.com/v1.0/teams/$teamGuid/installedApps” with payload @{“teamsApp@odata.bind” = “https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/$teamsAppId”}. Make sure to provide a token with an appropriate permissions scope.
    8. Add the new tab by making an HTTP Post call to “https://graph.microsoft.com/v1.0/teams/$teamGuid/channels/$channelId/tabs” with the following payload
    @{
    displayName = $title;
    “teamsApp@odata.bind” = “https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/$teamsAppId”;
    configuration = @{
    entityId = [Guid]::NewGuid().ToString();
    contentUrl = “$teamSiteUrl/_layouts/15/TeamsLogon.aspx?SPFX=true&dest=$teamSiteRelativeUrl/_layouts/15/teamshostedapp.aspx%3Flist=$listId%26id=$listItemId%26webPartInstanceId=$instanceId%26openPropertyPane=true”;
    removeUrl = “$teamSiteUrl/_layouts/15/TeamsLogon.aspx?SPFX=true&dest=$teamSiteRelativeUrl/_layouts/15/teamshostedapp.aspx%3Flist=$listId%26id=$listItemId%26webPartInstanceId=$instanceId%26removeTab”;
    websiteUrl = $null
    }
    }
    Make sure to provide a token with an appropriate permissions scope.
    9. Test if MS Teams still works. No just joking :), check if the new tab is available in the channel and is working as expected.

    Hi Laura, thanks for your valuable contributions, I’ve learned a lot from them. Feel free to use and modify this guide for any purpose. I am sure there is a cleverer way to put these things up, but I haven’t found any references so far.

    • can i know the meaning of entityid ? if I manually setup a sharepoint tab, the entity id is : sharepointtab_0.5667175748704791

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.