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-dialog, PlannerDialog 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.

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

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.

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

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

Cheers!
Gašper Rupnik
{End.}

thx really clear !
Just curious, what is your way to highlight code extract like this with wordpress ? 🙂 thx
Tnx! 🙂
I use “Preformatted” text style and I copy code from Notepad++ with “NppExport/Copy HTML to clipboard” plugin
I have to voice my passion for your kindness giving support to those people that should have guidance on this important matter.
Hi there! Do you know if they make any plugins to help with Search Engine Optimization? I’m trying to get my blog to rank for some targeted keywords but I’m not seeing very good success. If you know of any please share. Kudos!
Thanks for the valuable post. I would be grateful if you can provide how to deselect list item automatically after new task added successfully.
Thanks,
Omran