SPFx Extensions – ListView Command Set connected to Office 365 Planner via Graph API

The last of new SharePoint Framework Extensions types (for now :)) is ListView Command Set. With it you can add custom button/command to your List View Toolbar.

I want to create simple SharePoint List for tasks. When I select one of them, I want to put it to Office 365 Planner app to specific person. Connection between SharePoint List and Office 365 Planner will be represented by Microsoft Graph API.

Let the game begin. Create new SPFx Extension named spfx-react-commcust-graph-planner with Yeoman Generator. 

yo @microsoft/sharepoint

Choose Extension (Preview) as the client-side component type and ListView Command Set (Preview) as the extension type to be created.

In additional install sp-dialog for OOTB SharePoint Modal Dialog with commands below:

npm install @microsoft/sp-dialog --save

Open project with Visual Studio Code.

code .

Firstly create new React Dialog file named PlannerDialog.tsx besides already created manifest.json & .ts file.

Import React-related things, sp-dialog and office-ui-fabric-react for Office365 looks like UI (we need only TextField, DialogFooter, PrimaryButton and Button):

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { BaseDialog, IDialogConfiguration, DialogContent } from '@microsoft/sp-dialog';
import {
  autobind,
  TextField,
  PrimaryButton,
  Button,
  DialogFooter
} from 'office-ui-fabric-react';

Then create interface for our dialog with two button functions (Close & Submit) and one property for text field named title:

interface IPlannerDialogContentProps {
  close: () => void;
  submit: (title: string) => void;
  title?: string;
}

Then we need to create React Component model for our dialog rendering. The main thing is render() method, where we create our UI. In constructor we set default text for title field which we get it from Title list field.

class PlannerDialogContent extends React.Component<IPlannerDialogContentProps, {}> {
  private _title: string;

  constructor(props) {
    super(props);

    // get default title
    this._title = props.title;
  }

  public render(): JSX.Element {
    // UI
    return <DialogContent
      title='Planner Task Details'
      subText='Check details below:'
      onDismiss={this.props.close}
      showCloseButton={true}
    >
     
      <TextField label='Title' required={ true } multiline autoAdjustHeight value={ this._title } onChanged={ this._onChanged } />
      <DialogFooter>
        <Button text='Cancel' title='Cancel' onClick={this.props.close} />
        <PrimaryButton text='OK' title='OK' onClick={() => { this.props.submit(this._title); }} />
      </DialogFooter>
    </DialogContent>;
  }

  @autobind
  private _onChanged(text: string) {
    this._title = text;
  }
}

The last thing in PlannerDialog.tsx file is PlannerDialog class extended from BaseDialog which shows React Component model created before inside a SharePoint Modal Dialog:

export default class PlannerDialog extends BaseDialog {
  public title: string;

  public render(): void {
    ReactDOM.render(<PlannerDialogContent
      close={ this._close }
      title={ this.title }
      submit={ this._submit }
    />, this.domElement);
  }

  public getConfig(): IDialogConfiguration {
    return {
      isBlocking: false
    };
  }

  // onClose event
  @autobind
  private _close(): void {
    this.title = "";

    this.close();
  }

  // onSubmit event
  @autobind
  private _submit(title: string): void {
    this.title = title;

    this.close();
  }
}

We need to update commands section in manifest.json like this below. With this we set one button named CMDAddToPlanner with specific title and icon.

"commands": {
    "CMDAddToPlanner": {
      "title": "Add To O365 Planner",
      "iconImageUrl": "icons/request.png"
    }
}

Next thing we need to do is to update our .ts file. Firstly we need to import sp-dialogPlannerDialog created before and sp-http for Graph API connection:

import { Dialog } from '@microsoft/sp-dialog';
import PlannerDialog from './PlannerDialog';

import { GraphHttpClient, GraphClientResponse, IGraphHttpClientOptions } from '@microsoft/sp-http';

Then we add planId property to CommandSet interface. So we will set planId in query string when we will call our extension. PlainId represent identifier of our plan from Office 365 Planner App in which we want to add new task.

export interface IHelloWorldCommandSetProperties {
  // planId in Planner
  planId: string;
}

Inside onRefreshCommand() method we specify that our command is visible if number of selected rows is equal to 1.

@override
public onRefreshCommand(event: IListViewCommandSetRefreshEventParameters): void {
    // show button when one item is selected
    event.visible = event.selectedRows.length === 1;
}

The last one is onExecute() method, where we want to check which command from toolbox is pressed. If is CMDAddToPlanner command, we call PlannerDialog created before where user can change title for task.
When we get response from PlannerDialog we check title property. If is not empty, we call Graph API to get bucketID for specific PlanID (PlanID is predefined in CommandSet properties as I mentioned before). To get this create HTTP GET request on “beta/plans/{planID}/buckets?$select=id”.
We need to specify to which user we want to add new task. Currently via GraphHttpClient you cannot access to “v1.0/me” for any additional information about current user (for example ID which you need it later). For that reason my userID is hardcoded. You can use predefined dropdown with userIDs and Display Names or you can use ADAL JS with implicit OAuth flow instead.
Now we have planId, bucketId, userId and task title. So we could add new task in JSON to Office 365 Planner with HTTP POST request on “v1.0/planner/tasks”.

@override
public onExecute(event: IListViewCommandSetExecuteEventParameters): void {

    switch (event.commandId) {
      // if command 'Add To O365 Planner' is clicked
      case 'CMDAddToPlanner':
        // create PlannerDialog and put Title field to it
        const dialog: PlannerDialog = new PlannerDialog();
        dialog.title = event.selectedRows[0].getValueByName('Title').toString();

        // show dialog
        dialog.show().then(() => {
          // if Title is not empty
          if (dialog.title != "") {
            // get bucketID for specific PlanID from Planner (via MS Graph)
            this.context.graphHttpClient.get("beta/plans/" + this.properties.planId + "/buckets?$select=id", GraphHttpClient.configurations.v1)
            .then((response: GraphClientResponse): Promise<any> => {
              return response.json();
            })
            .then((bucketData: any): void => {
              if (bucketData.error) {
                Dialog.alert(bucketData.error.message);
              }
              else {
                // Currently via GraphHttpClient you cannot access to 'v1.0/me' for any additional information about current user (for example ID which you need it later).
                // For that reason my userID is hardcoded below. You can use predefined dropdown with userIDs and Display Names or ADAL JS with implicit OAuth flow instead.

                //this.context.graphHttpClient.get("v1.0/me?$select=id", GraphHttpClient.configurations.v1)
                //.then((response: GraphClientResponse): Promise<any> => {
                //  return response.json();
                //})
                //.then((meData: any): void => {
                //  if (meData.error) {
                //    Dialog.alert(meData.error.message);
                //  }
                //  else {
                //    var myId = meData.value[0].id;

                    var options : IGraphHttpClientOptions = {
                      method: "POST",
                      body: JSON.stringify({
                        planId: this.properties.planId,
                        bucketId: bucketData.value[0].id,
                        title: dialog.title,
                        assignments: {
                          "[your-userID-hardcoded]": { // myId: {
                            "@odata.type": "#microsoft.graph.plannerAssignment",
                            "orderHint": " !"
                          }
                        }
                      })
                    };

                    // add task to planner
                    this.context.graphHttpClient.fetch("v1.0/planner/tasks", GraphHttpClient.configurations.v1, options)
                    .then((response: GraphClientResponse): Promise<any> => {
                      return response.json();
                    })
                    .then((taskData: any): void => {
                      if (taskData.error) {
                        Dialog.alert(taskData.error.message);
                      }
                      else {
                        Dialog.alert("Task successfully added to O365 Planner! :)");
                      }
                    });

                //  }
                //});
              }
            });
          }
        });
        break;
      default:
        throw new Error('Unknown command');
    }
}

Go to Graph API Explorer (https://developer.microsoft.com/en-us/graph/graph-explorer), sign in with Microsoft account and call HTTP GET request on

https://graph.microsoft.com/v1.0/me/

In response preview windows you could find ID which represent your UserID. Replace [your-userID-hardcoded] from .ts file with this ID.

2017-07-25_1859

 

Then go to your Office 365 Planner app (https://tasks.office.com) and create new plan named SPFx Test as shown below.

2017-07-25_1853

Go into newly created plan and take last part of URL which represent ID of your plan:

https://tasks.office.com/{user}/EN-US/Home/PlanViews/{PlanId}

Serve project with command below:

gulp serve --nobrowser

Then go to your O365 developer tenant site. Create list named Tasks with OOTB Title field.

Add new test task and append this query string to your list view URL (replaced with your PlanID and extension ID from manifest.json file):

?loadSpfx=true&debugManifestsFile=https://localhost:4321/temp/manifests.js&customActions={"[extensionID]":{"location":"ClientSideExtension.ListViewCommandSet.CommandBar","properties":{"planId":"[PlanID]"}}}

Select one row in Tasks list and click on Add To O365 Planner button.

2017-07-25_1916

In popup dialog (PlannerDialog) you can modify title and click OK button.

2017-07-25_1919

As you can see in Office 365 Planner App we have now new task created and assigned:

2017-07-25_1919_001

[ Complete code on GitHub ]

Cheers!
Gašper Rupnik

{End.}

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Powered by WordPress.com.

Up ↑

%d bloggers like this: