How to Sort Planner Tasks Using Order Hint and Microsoft Graph

How to Sort Planner Tasks Using Order Hint and Microsoft Graph

It recently became possible to create a new team in Teams by taking another team as a template. This feature was long-awaited and very useful to a lot of people. There is a huge need for a similar feature for Planner plans, but it is not yet possible to copy a plan via the user interface. There is a User Voice request for the feature, created in January 2016, with status Scheduled, but the feature can not yet be found on the Office 365 roadmap. This is why the functionality has often been requested as a custom solution.

Copying an existing Planner plan programmatically is totally doable thanks to Microsoft Graph. However, there was one thing I found difficult to grasp during the implementation: sorting the tasks and buckets of the new plan in the exact same order as in the original plan. That is why I’m going to tell you in this blog post, how you can succeed at sorting those Planner tasks and buckets programmatically using the order hint property and Microsoft Graph.

Copying a Planner Plan Programmatically

To programmatically copy a Planner plan, you need to do the following things using the Microsoft Graph Planner REST API:

  • Create a new empty plan for an existing group
  • Copy the plan details from the template plan to the new plan
  • Get the buckets of the template plan, and use them to create similar buckets for the new plan
  • Fetch the tasks of the template plan, and use them to create similar tasks in the same buckets for the new plan
  • Get the task details of the template plan tasks, and update the tasks of the new plan with those details

That is the basic process. In this article, we are going to focus on copying buckets and tasks and putting them in the right order.

If you have not yet set up an application registration in Azure AD for calling Microsoft Graph (required), check this blog post for instructions.

The Planner Order Hint Property

Buckets, tasks, task assignees and subtasks are sorted based on the value of their order hint property. The order hint property value isn’t a regular integer like you might first expect. It is actually a string that is generated based on the order hint values of the previous and the next item in-between which the item is placed. The items are then sorted based on the ordinal values of these strings.

Sorting tasks in Cat Plan

The official documentation gives a pretty good explanation of how an order hint is formed. There is one thing in particular in that article that I want to stress: the values for all order hints are calculated by the service. This means that whenever you assign a value to the order hint property by combining the previous and the next order hint values, the end result — the final order hint — will be completely different once you update that item. This is why it is very important to always note the new, calculated order hint value that is returned from the service before proceeding with further sorting.

Order hint updated

Setting the Order Hint for New Items

When you first set out to copy your plan, you might think that you can just straight copy the existing order hint values of the template plan buckets and tasks. But it doesn’t work that way. The order hint property expects the inserted value to be the combination of the previous and the next order hint values. You will get an error if the order hint value doesn’t contain a whitespace and an exclamation mark.

So what do you need to do then? You need to create a completely new order hint value from scratch. If you look at the official documentation linked above, you’ll notice that to add an item as the last item on a list, you need to take the previous item order hint and then just add ‘ !’ (whitespace and an exclamation mark). When there are no items yet on the list, the previous order hint is empty, and hence the order hint value of the very first item is simply ‘ !’.

Now, when you add that item, the response will contain the actual order hint value calculated by the service. The order hint of the next item is that value followed by ‘ !’ again.

Reversed Order is the Right Order

When you are copying items, you also need to consider the order of the template items you are looping through. Order hint in itself can already be a bit difficult to grasp, but there is also another thing you need to be aware of: the buckets are displayed in the Planner UI in a reversed order.

Microsoft Graph returns plan buckets in the ascending order based on the order hint value, which is logical. However, the order is reversed in the UI: The first bucket (with the smallest order hint) appears on the right, and the last bucket (with the largest order hint) is on the left. This might have something to do with how a new item created in the bucket view will always get the smallest order hint value.

Buckets value
Cat plan buckets

Now that we are aware of this, we won’t have any problems creating those buckets based on the template plan. Even though it at first looks as if Microsoft Graph has returned the buckets in a reversed order, we just proceed with creating those buckets in that order, and they will actually end up in the desired order in the UI.

Copying Buckets

OK, let’s start copying buckets! First, we’ll get the template plan buckets and loop through them. For every bucket (JToken), we first deep clone it to copy all of its properties. Then we remove the properties we don’t need and update the planId to match the new plan.

Then we create the order hint. As you can see, we create the order hint by combining the value of the previousOrderHint variable and ” !”. During the first iteration, the previousOrderHint is empty, but once we’ve created a new bucket, it will be set to the calculated order hint value of that new bucket. During future iterations, we’ll again combine that value with a ” !” to always make the latest bucket to be sorted after the previous one.

In addition to this, we’ll be storing the bucket IDs of the template buckets and the new buckets in a dictionary object. This is for a later purpose when we need to create tasks in the correct buckets.

public static async Task<Dictionary<string, string>> CopyBuckets(string planId, string templatePlanId)
{
	var bucketIds = new Dictionary<string, string>();

	var templateBuckets = await GetPlanBuckets(templatePlanId);

	var previousOrderHint = string.Empty;

	foreach (var templateBucket in templateBuckets)
	{
		var newBucket = templateBucket.DeepClone();

		newBucket["id"].Parent.Remove();
		newBucket["@odata.etag"]?.Parent.Remove();

		newBucket["planId"] = planId;
		newBucket["orderHint"] = $"{previousOrderHint} !";

		var createdBucket = await CreatePlanBucket(JsonConvert.SerializeObject(newBucket));

		previousOrderHint = createdBucket["orderHint"].ToString();

		bucketIds.Add(templateBucket["id"].ToString(), createdBucket["id"].ToString());
	}

	return bucketIds;
}

private static async Task<IEnumerable<JToken>> GetPlanBuckets(string planId)
{
    return (await GraphRequest($"https://graph.microsoft.com/v1.0/planner/plans/{planId}/buckets")).SelectToken("value");
}

private static async Task<JToken> CreatePlanBucket(string body)
{
    return await GraphRequest("https://graph.microsoft.com/v1.0/planner/buckets", "POST", body);
}

private static async Task<JToken> GraphRequest(string url, string method = null, string body = null, string ifMatch = null)
{
    using (var httpClient = new HttpClient())
    {
        // Get the access token the way you prefer
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", GetAccessToken()); 

        HttpResponseMessage response;

        switch (method)
        {
            case "POST":
                var content = new StringContent(body, Encoding.UTF8, "application/json");
                response = await httpClient.PostAsync(url, content);
                break;
            // Custom PATCH method, httpClient doesn't have it by default
            case "PATCH":
                httpClient.DefaultRequestHeaders.Add("If-Match", ifMatch);
                // return=representation is required for the new order hint to be returned from the service
                httpClient.DefaultRequestHeaders.Add("Prefer", "return=representation");
                var request = new HttpRequestMessage
                {
                    Method = new HttpMethod("PATCH"),
                    RequestUri = new Uri(url),
                    Content = new StringContent(body, Encoding.UTF8, "application/json")
                };
                response = await httpClient.SendAsync(request);
                break;
            default:
                response = await httpClient.GetAsync(url);
                break;
        }

        return JToken.Parse(response.Content.ReadAsStringAsync().Result);
    }
}

Tasks Have Four Order Hints

The order hint for tasks is a bit more complex matter. Or should I say, order hints. Yes, you heard it right: there are actually four separate order hints for tasks.

When you query Microsoft Graph for all the plan tasks, the tasks are returned to you in the ascending order based on their order hint value. But that is not the same order in which you see the tasks in the bucket view. No, there are separate order hint properties for each of the different views: Bucket, Progress, and Assignee.

Plan views dropdown

To get the tasks to appear in the correct order in the bucket view, it is not the task order hint property we need to update. We need to update the order hint of the bucketTaskBoardFormat object of that task.

Sorting Tasks in the Bucket View

In the code example below, let’s assume we are calling the public method from within a loop where we iterate through the created tasks in a similar manner we did earlier with the buckets. We first need to get the bucket task board format object to find out its etag value, because we’ll need it when we make an update to the object.

The body of the update request is simple: you only need to include the new order hint. After making the request, we’ll return the new calculated order hint, so it may be used as the previousOrderHint value for the next iteration. An important thing to remember here is that you only need to sort the tasks in the right order in relation to the other tasks in the same bucket. You don’t need to care about the tasks in other buckets.

// Call this method when you loop through your tasks
public static async Task UpdateBucketTaskOrderHint(string taskId, string previousOrderHint)
{
    var bucketTaskFormat = await GetBucketTaskBoardFormat(taskId);

    var body = $"{{ 'orderHint': '{previousOrderHint} !' }}";

    var bucketTask = await UpdateBucketTaskBoardFormat(taskId, body, bucketTaskFormat["@odata.etag"].ToString());

    return bucketTask["orderHint"].ToString();
}

private static async Task GetBucketTaskBoardFormat(string taskId)
{
    return await GraphRequest($"https://graph.microsoft.com/v1.0/planner/tasks/{taskId}/bucketTaskBoardFormat");
}

private static async Task UpdateBucketTaskBoardFormat(string taskId, string body, string ifMatch)
{
    return await GraphRequest($"https://graph.microsoft.com/v1.0/planner/tasks/{taskId}/bucketTaskBoardFormat", "PATCH", body, ifMatch);
}

When copying a template plan, the tasks are usually not yet progressed or assigned. That’s why we are mostly just interested in getting the tasks in the correct order in the bucket view. But sure, it’d be nice if the tasks were in the right order in those other two views as well. We could update the other two order hint values in a similar manner as we just did for the bucket view. However, there is also another way, which we can use to nail down all three views at once.

An Alternative Approach: Creating Tasks in the Right Order

An alternative way to get the tasks to appear in the right order is to create them in the “correct” order in the first place. This requires you to sort the all of the template buckets and tasks returned by Microsoft Graph in a certain way before you start copying them, so they will be created in the right order.

After getting the tasks of the template plan, we proceed to get their bucket task board format objects, so we can find out their order hint values in the bucket view. We also want to find out the order of the buckets. We include both of these values in the task objects.

Then the most important bit. We sort the tasks based on these values so they are all in a completely reversed order. You’ve probably noticed this also in the Planner UI: the task that is created first will appear as the last one on the list. Hence, we need to create the tasks in a reversed order.

In our case, the first item in that collection will be Task 9 from bucket 3, and the last item will be Task 1 from Bucket 1. After that, it is very similar to what we did with the buckets earlier: we just loop through the tasks and the order hint values will automatically turn out right in all three views, because we are creating the tasks in the correct, reversed order.

public static async Task CopyTasks(IReadOnlyDictionary<string, string> bucketIds, string templatePlanId, string planId)
{
    var templateTasks = await GetPlanTasks(templatePlanId);

    foreach (var task in templateTasks)
    {
        var bucketTask = await GetBucketTaskBoardFormat(task["id"].ToString());
        task.Last.AddAfterSelf(new JProperty("bucketTaskOrderHint", bucketTask["orderHint"]));

        var bucket = await GetPlanBucket(task["bucketId"].ToString());
        task.Last.AddAfterSelf(new JProperty("bucketOrderHint", bucket["orderHint"]));
    }

    templateTasks = new JArray(templateTasks.OrderBy(x => (string)x["bucketOrderHint"]).ThenByDescending(x => (string)x["bucketTaskOrderHint"], StringComparer.Ordinal));

    var previousOrderHint = string.Empty;

    foreach (var templateTask in templateTasks)
    {
        var newTask = CopyTask(templateTask);

        newTask["planId"] = planId;
        newTask["orderHint"] = $"{previousOrderHint} !";
        newTask["bucketId"] = bucketIds[templateTask["bucketId"].ToString()];

        var createdTask = await CreatePlanTask(JsonConvert.SerializeObject(newTask));

        previousOrderHint = createdTask["orderHint"].ToString();
    }
}

private static JToken CopyTask(JToken templateTask)
{
    var newTask = templateTask.DeepClone();

    newTask["id"]?.Parent.Remove();
    newTask["@odata.etag"]?.Parent.Remove();
    newTask["createdDateTime"]?.Parent.Remove();
    newTask["createdBy"]?.Parent.Remove();
    newTask["conversationThreadId"]?.Parent.Remove();
    newTask["percentComplete"]?.Parent.Remove();
    newTask["hasDescription"]?.Parent.Remove();
    newTask["referenceCount"]?.Parent.Remove();
    newTask["checklistItemCount"]?.Parent.Remove();
    newTask["activeChecklistItemCount"]?.Parent.Remove();
    newTask["assigneePriority"]?.Parent.Remove();
    newTask["previewType"]?.Parent.Remove();
    newTask["completedDateTime"]?.Parent.Remove();
    newTask["completedBy"]?.Parent.Remove();

    return newTask;
}

private static async Task GetPlanTasks(string planId)
{
    return (await GraphRequest($"https://graph.microsoft.com/v1.0/planner/plans/{planId}/tasks")).SelectToken("value");
}

private static async Task GetPlanBucket(string bucketId)
{
    return await GraphRequest($"https://graph.microsoft.com/v1.0/planner/buckets/{bucketId}");
}

private static async Task CreatePlanTask(string body)
{
    return await GraphRequest("https://graph.microsoft.com/v1.0/planner/tasks", "POST", body);
}

And now, the tasks are in the right order in all three views.

The Conclusion

To sum it up: to automatically make the tasks appear in the right order when you create them, sort the template tasks first in a reversed order before looping through them. And if you want to update the order of existing tasks in bucket view, you need to make an update to the bucketTaskBoardFormat object.

Have you played around with the order hint property before? Were you at first as confused as I or did you get what it was all about right away? Let me know in the comments below! This article is very “niche”, but I hope you still enjoyed reading it and found it useful or inspiring. If you have not yet done so, feel free to also take a look at my other articles, and if you like what you see, follow me on Twitter to get notified of future posts. Have a relaxing summer and until next time!

Laura

Congratulations, you’ve just finished reading one of my blog post classics! Please note that I’ve personally stopped answering questions left in the comments section of this article because I no longer actively work with the topic. Still, you are more than welcome to comment and ask questions as other readers also often offer their help.


13 thoughts on “How to Sort Planner Tasks Using Order Hint and Microsoft Graph”

  • Laura,

    In reference to checklist comment above. If dealing with a template as in the case of this blog. You can insert all checklists at once in order. If you have initially sorted the checklist in reverse order you can assign the order hint of the fist item as ” !” and each append the order hint with another ” !” for each item in the list. That will then order the items in the correct order.

    First item orderHint ” !”
    Second item orderHint ” ! ! ”
    Third item order Hint ” ! ! !”
    …. and so on.

    This is assuming you ordered the order hints in reverse order according to the template checklist order hints.

    • Hi Philip,

      I do not work for Microsoft so unfortunately, I can’t fix bugs in their products. I recommend that you report the bug to Microsoft via your tenant’s admin center.

      Laura

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.