SPFx – Create planner task with description body

2020-03-25

I have been working with SharePoint Framework in combination with Teams and Planner lately and I thought that I would share some of my experiences. We will be using PNP/graph as well as the MSGraphClient. The use case is that we have an SPFx webpart, that is deployed and added as a tab to a Team. From this webpart we will assign a task to a user. The task will have a description and will be added to the Team´s first instance of Tasks by Planner and Todo. So let´s get started.

First, we need to consent API permissions. We need to read basic user data and we need to work with group data:

"webApiPermissionRequests": [
      {
        "resource": "Microsoft Graph",
        "scope": "User.ReadBasic.All"
      },
      {
          "resource": "Microsoft Graph",
          "scope": "Group.Read.All"
      },
      {
          "resource": "Microsoft Graph",
          "scope": "Group.ReadWrite.All"
      }      
    ]  

Our component will need webpart context and the id of the unified group. From the starter file you will find the unified group id. As long as the webpart is hosted in a group context:

let elemProps: IDemoComponentProps = {
     context: this.context,      
     unifiedGroupId: null
 };

 if (this.context.pageContext.site.group && 
     this.context.pageContext.site.group["id"] && this.context.pageContext.site.group["id"]["_guid"]) {
     elemProps.unifiedGroupId = this.context.pageContext.site.group["id"]["_guid"];
 }

For demonstration purposes I have merged the remaining code into one file so it should be easy to recreate into your own structure. You can download the complete file from Github.

So, we start with a very simple component:

export interface IDemoComponentProps {
    context: WebPartContext;    
    unifiedGroupId: string;
}

export const DemoComponent: React.FC<IDemoComponentProps> = ({context, unifiedGroupId}) => {
    const createTask = async () => {
        const demo = new Demo(context, unifiedGroupId);
        const result = await demo.createTask(
            "admin@tenant.onmicrosoft.com", 
            "You need to fix this!", 
            "Task body description"
        );
        console.log(result);
    }

    return <button onClick={createTask}>Create Task</button>;
}

First thing that the function createTask will do is to fetch a planner id. Endpoint is:

client.api(`groups/${groupId}/planner/plans`).get();

Next we will need the guid for the user. The property is called “id” and can be found via pnp graph.

const userGuid = await this._repository.getUser(upn, "id");

public getUser = async (upn: string, property?: string) : Promise<any> => {
     const user = await graph.users.getById(upn).get();
     return property ? user[property] : user;
}

When we have the planner id and the user id we can create the task. There are some issues to be aware of with the API. First, description cannot be set when creating the task. The description property exists on the task details body. So, we will create the task, then using the new id we will fetch the task details and then do the update. Details are not always available straight away so I have added support for re-tries. Also note that different etags are used.

public createPlannerTask = async (planId: string, title: string, userId: string, 
  description?: string, duedate?: string) : Promise<ITaskAddResult> => {
      
      let assignments = {};
        assignments["assignments"] = {
            [userId]: {
                "@odata.type":"#microsoft.graph.plannerAssignment",
                "orderHint":" !"
            }
        };

        const newTask = await graph.planner.tasks.add(planId, title, assignments);
        
        if (duedate) {
            await graph.planner.tasks.getById(newTask.data.id).update({                
                dueDateTime: duedate
            }, newTask.data["@odata.etag"]);
        }
        
        if (description) {            
            const details = await this.getTaskDetails(newTask.data.id, 5);
            if (details) {                
                const etag = details["@odata.etag"];                   

                await graph.planner.tasks.getById(newTask.data.id).details.update({
                    description: description
                }, etag);            
            }
        }

        return newTask;
    }

    public getTaskDetails =  async (taskId: string, attempts: number): Promise<any> => {
        return new Promise((resolve, reject) => {
            if (attempts == 0) {
                reject();
            }

            graph.planner.tasks.getById(taskId).details.get().then(result => {                
                resolve(result);
            }).catch(error => {
                console.log(error);
                this.getTaskDetails(taskId, attempts - 1)
                    .then(resolve)
                    .catch(reject);
            });
        });
    }

And that´s it!