April 9, 2020

Cloning Planner Tab without /clone REST endpoint

Series

This is series of blog articles showing how to clone Teams Channels and Tabs without using the Clone REST endpoint or direct REST queries. Except we use the Microsoft.Graph (3.1.0). First post discusses things in general, details about different tab types are separated to individual articles.

  1. Cloning Teams Channels and Tabs without /clone REST endpoint
  2. Cloning Planner Tab without /clone REST endpoint <<YOU ARE HERE>>
  3. Cloning OneNote Tab without /clone REST endpoint
  4. Cloning Web Tab without /clone REST endpoint (TBD)
  5. Copying Teams Files tab content using MoveCopyUtil

Solution

In this piece of code, we first determine current source tab is of type Planner. Then comes the tricky part: you cannot use Application permissions to create the Planner Plan itself, but you must use Delegated permissions, so basically what you need is a dedicated user account, and use a GraphServiceClient that uses UsernamePasswordProvider as IAuthenticationProvider for the Graph API calls. Yes, you really gotta put username and password of that user in Key Vault and fetch those here.

Now, after we have successfully created the Planner Plan, we will do a small trick and do not create the contentUrl property for the new tab manually, but take the ContentUrl from the source Tab and just replace the old Planner Id with the Id of the newly created Planner. Lovely. Just note that the WebsiteUrl needs to be done separately, otherwise the “Go to website” link on top right of the Planner tab is broken and will be stuck loading and give error “Error loading user settings. Please try again. If this continues please contact customer support.” after few minutes.

For creating the new Tab, there’s not much to say. These are the parameters you need to define based on a bit of trial and error. As Graph API evolves and improves, this is the part that will most probably need updating, but at least with Microsoft.Graph v. 3.1.0 it works – which is already old version as 3.2.0 was released few hours ago.

Finally, note that you may end up getting ServiceException although tab creation succeeds, thus the scary Exception swallowing.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// Planner
if (sourceTab.TeamsApp.Id.Equals(TeamsAppId.Planner))
{
    logger?.LogInformation($"Creating Planner. Channel: {sourceChannel.DisplayName}. {newGroup.Id}");

    var newPlannerPlan = await retry.ExecuteAsync(async () =>
    {
        return await Authentication.GetGraphApiClientClientUsernamePasswordProvider(keyvault).Planner.Plans.Request()
            .WithUsernamePassword(keyvault.GetSecret("teamsClone--SPOUser"), new System.Net.NetworkCredential("", keyvault.GetSecret("teamsClone--SPOPassword")).SecurePassword)
            .AddAsync(new PlannerPlan()
            {
                Owner = newGroup.Id,
                Title = $"{TextTools.RemoveSpecialCharactersForNotebook(requestData.title)} Planner"
            });
    });

    try
    {
        string contentUrl = sourceTab.Configuration.ContentUrl.Replace(sourceTab.Configuration.EntityId, newPlannerPlan.Id);
        string websiteUrl = $"https://tasks.office.com/YOURTENANT.onmicrosoft.com/Home/Planner#/plantaskboard?groupId={newTeamId}&planId={newPlannerPlan.Id}";

        logger?.LogInformation($"Adding Planner tab. Channel: {sourceChannel.DisplayName}, ContentUrl: {contentUrl}. {newGroup.Id}");

        await retry.ExecuteAsync(async () =>
        {
            await graphClient.Teams[newTeamId].Channels[channelId].Tabs.Request().AddAsync(new TeamsTab()
            {
                ODataType = null,
                DisplayName = sourceTab.DisplayName,
                TeamsApp = sourceTab.TeamsApp,
                Configuration = new TeamsTabConfiguration()
                {
                    EntityId = newPlannerPlan.Id,
                    ContentUrl = contentUrl,
                    RemoveUrl = contentUrl,
                    WebsiteUrl = webSiteUrl
                },
                AdditionalData = new Dictionary<string, object>()
    {
        {
            "teamsApp@odata.bind", $"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/{sourceTab.TeamsApp.Id}"
        }
    }
            });
        });
    }
    catch (ServiceException ex)
    {
        HandleTabCreationException(ex, logger, sourceTab);
    }
}

No comments:

Post a Comment