Arsalan Khattak
15 November 2024

How to connect and sync Bryntum Calendar to Microsoft Planner

Bryntum Calendar is a JavaScript calendar component that’s performant, fully customizable, and can easily be used with your framework of […]

Bryntum Calendar is a JavaScript calendar component that’s performant, fully customizable, and can easily be used with your framework of choice: React, Angular, or Vue. In this tutorial, we’ll connect and sync Bryntum Calendar to a Microsoft Planner plan by doing the following:

We’ll build a Bryntum Calendar app that syncs with a Microsoft Planner plan:

Getting started

Clone the starter GitHub repository, which is a client-side Bryntum Calendar app. The code for the Bryntum Calendar that syncs with Microsoft Planner is in the completed-calendar branch of the starter GitHub repository.

The starter repository uses Vite, a development server and JavaScript bundler. You’ll need Node.js version 18+ for Vite to work. Install the Vite dev dependency by running the following command:

npm install

Install the Bryntum Calendar component by following step 1 and step 4 of the vanilla JavaScript with npm setup guide.

Run the local dev server using the following command:

npm run dev

You’ll see a Bryntum Calendar with a single event:

Now, let’s learn how to retrieve a user’s Microsoft Planner tasks with Microsoft Graph.

Accessing Microsoft Planner data: Joining the Microsoft 365 Developer Program

If you have Microsoft 365 Developer Program access or can get it, use those credentials and proceed to the next section: Creating a Microsoft Entra app to connect to Microsoft 365. Otherwise, sign up for a Microsoft 365 subscription with Planner Plan 1 and continue with this section.

Microsoft Planner is a Microsoft 365 app. To get access to Microsoft 365 data, we’ll register a Microsoft 365 app by creating an application registration in Microsoft Entra ID, which is an identity and access management service that we’ll use to authenticate users. Microsoft Entra ID is the new name for Azure Active Directory.

We’ll add a login button to the Bryntum Calendar app so that users can sign in to the app using their Microsoft 365 account. This will allow us to access the data that users give the app permission to access. A user will sign in using OAuth, which sends an access token to our app to be stored in local storage. We’ll then use the token to make authorized requests for Microsoft Planner data using Microsoft Graph. The Microsoft Graph REST API is the single endpoint that provides access to Microsoft 365 app data.

To use Microsoft Graph, you need global administrator access to a Microsoft 365 tenant. If you don’t have global administrator access, join the Microsoft 365 Developer Program with your Microsoft account. When you join the program, the subscription setup flow for a Microsoft 365 E5 developer sandbox will automatically start if you qualify for a Microsoft 365 E5 sandbox subscription. Add the following input:

Set up your Microsoft 365 E5 sandbox

Once your subscription has been created, your subscription domain name and expiration date appear on your dashboard.

Microsoft 365 Developer dashboard page

Creating a Microsoft Entra app to connect to Microsoft 365

Let’s register a Microsoft 365 application to get access to Microsoft 365 data.

Follow these steps to create a creating an application registration in the Microsoft Entra admin center:

After registering your application, take note of the Application (client) ID and the Directory (tenant) ID; you’ll need these to set up authentication for your Bryntum Calendar web app later.

Creating a Microsoft Planner plan

Sign in to Microsoft Planner using the admin email address from your Microsoft 365 Developer Program account.

Click the Create a plan button in the center of the screen.

Select the New Blank Plan option in the New Plan dialog that opens.

Give your plan a name and click the Create button.

The plan is set to private by default. Private plans can only be viewed by members that you add. Public plans can be viewed by anyone in your organization.

The plan has four different views: Grid, Board, Charts, and Schedule.

Open the Schedule tab to see the calendar view of the data.

Create an example task by clicking on the plus icon in the top right corner of a day block. Name the task (for example, “Design app”) in the popup form and click the Add task button.

Add more details to the task by clicking on your task in the calendar. This will open the task editor. Set the Start date and the Due date, set the Progress value to “In progress”, and add a note in the Notes section.

The changes to the task are automatically saved. Close the task editor by clicking the close button at the top right of the task editor modal or by clicking outside of the modal.

The taskbar in the calendar will display an icon indicating that the task is in progress.

Now that you have a plan in Microsoft Planner, you can create a JavaScript web app that uses the Microsoft Graph API to get your Microsoft Planner data.

Next, let’s set up authentication in the Bryntum Calendar web app.

Setting up Microsoft 365 authentication in the JavaScript app

To get data using the Microsoft Graph REST API, your app needs to prove that you’re the owner of the app you just registered in Microsoft Entra. Your app will get an access token from Microsoft Entra and include it in each request to Microsoft Graph. After this is set up, users will be able to sign in to your app using their Microsoft 365 accounts. This means that you won’t have to implement authentication in your app or maintain users’ credentials.

The following diagram outlines how we’ll access the Microsoft Planner data from the Bryntum Calendar app. We’ll log in to our apps, using Microsoft Entra ID to authenticate the user. We’ll use the access token returned by Microsoft Entra ID to connect to Microsoft Planner via the Microsoft Graph API.

First, we’ll create the variables and functions we need for authentication and retrieving tasks from Microsoft Planner. Then, we’ll add the Microsoft Authentication Library and Microsoft Graph SDK that we need for authentication and using the Microsoft Graph API.

Create a .env.local file in the root directory of your Bryntum Calendar app and add the following environment variables to it:

VITE_MICROSOFT_PLANNER_PLAN_ID=""
VITE_MICROSOFT_ENTRA_APP_ID=""
VITE_MICROSOFT_ENTRA_TENANT_ID=""

Add your Microsoft Planner plan ID. You can find it in the URL of your plan at https://tasks.office.com. Add the Application (client) ID and the Directory (tenant) ID of your registered Microsoft Entra app. Prefix the variables with VITE_ to expose the variables to the JavaScript app. Vite only exposes environmental variables prefixed with VITE_ to your Vite-processed code. The JavaScript app runs on the client side, so the environment variables would be exposed to the user, which is fine in this case.

Install the Microsoft Authentication Library, MSAL.js, for single-page JavaScript web apps.

npm i @azure/msal-browser

Create a file called auth.js in your project’s root directory and add the following code to it:

const msalConfig = {
    auth : {
        clientId  : import.meta.env.VITE_MICROSOFT_ENTRA_APP_ID,
        authority : `https://login.microsoftonline.com/${
      import.meta.env.VITE_MICROSOFT_ENTRA_TENANT_ID
    }`,
        redirectUri : 'http://localhost:5173'
    }
};

This code configures the Microsoft Authentication Library (MSAL) for authentication with Microsoft Entra ID. The clientID is the unique ID of your Microsoft Entra app. The authority is the endpoint that MSAL will use to authenticate a user. The redirectUri is the URL that users will be redirected to after they’ve been authenticated.

Add the following imports to the auth.js file:

import {
    PublicClientApplication,
    InteractionRequiredAuthError
} from '@azure/msal-browser';

Add the following code to the bottom of the file:

// Initialize a PublicClientApplication object.
const msalInstance =
  await PublicClientApplication.createPublicClientApplication(msalConfig);
const msalRequest = { scopes : [] };
export function ensureScope(scope) {
    if (
        !msalRequest.scopes.some((s) => s.toLowerCase() === scope.toLowerCase())
    ) {
        msalRequest.scopes.push(scope);
    }
}
// Log the user in
export async function signIn() {
    const authResult = await msalInstance.loginPopup(msalRequest);
    localStorage.setItem('msalAccount', authResult.account.username);
}
export async function getToken() {
    const account = localStorage.getItem('msalAccount');
    if (!account) {
        throw new Error(
            'User info cleared from local storage. Please sign out and sign in again.'
        );
    }
    try {
        // First, attempt to get the token silently
        const silentRequest = {
            scopes  : msalRequest.scopes,
            account : msalInstance.getAccountByUsername(account)
        };
        const silentResult = await msalInstance.acquireTokenSilent(silentRequest);
        return silentResult.accessToken;
    }
    catch (silentError) {
        // If silent request fails with InteractionRequiredAuthError,
        // attempt to get the token interactively
        if (silentError instanceof InteractionRequiredAuthError) {
            const interactiveResult = await msalInstance.acquireTokenPopup(
                msalRequest
            );
            return interactiveResult.accessToken;
        }
        else {
            throw silentError;
        }
    }
}

export async function signOut() {
    const account = localStorage.getItem('msalAccount');
    if (account) {
        const logoutRequest = {
            account : msalInstance.getAccountByUsername(account)
        };
        await msalInstance.logoutPopup(logoutRequest);
        localStorage.removeItem('msalAccount');
    }
}

This code instantiates a PublicClientApplication object to use the MSAL.js library. The msalRequest variable stores the current Microsoft Authentication Library request. This variable initially contains an empty array of scopes because your app needs to include a list of scopes when it requests an access token from Microsoft Entra ID. The list of permissions granted to your app is part of the access token returned when a user logs in.

The ensureScope function checks the permissions the user has. We’ll use this function when making requests to the Microsoft Graph API to perform CRUD operations on the user’s Microsoft Planner tasks. Each operation in Microsoft Graph has its own list of scopes. The list of permissions required for each operation is available in the Microsoft Graph permissions reference.

The signIn function grants the user access and stores their access token in the browser’s local storage. The getToken function gets the user’s access token from local storage.

The signOut function uses the username stored in local storage to sign out the user.

Using Microsoft Graph to access a user’s Microsoft Planner events

We’ll use the Microsoft Graph Planner REST API to perform CRUD operations on the user’s Microsoft Planner plan tasks.

First, install the Microsoft Graph JavaScript client library:

npm i @microsoft/microsoft-graph-client

This library is a lightweight wrapper around the Microsoft Graph API.

Create a file called graph.js in your project’s root directory and add the following lines of code to it:

import { Client } from '@microsoft/microsoft-graph-client';
import { ensureScope, getToken } from './auth.js';
const authProvider = {
    getAccessToken : async() => {
        return await getToken();
    }
};
// Initialize the Graph client
const graphClient = Client.initWithMiddleware({ authProvider });
export async function getTasks() {
    ensureScope('Tasks.Read');
    return await graphClient
        .api(
      `/planner/plans/${
        import.meta.env.VITE_MICROSOFT_PLANNER_PLAN_ID
      }/tasks?$expand=details`
        )
        .select('id, title, startDateTime, dueDateTime, details, percentComplete')
        .get();
}

This code creates a Microsoft Graph SDK client instance, grantClient and passes in the authProvider to the instance as one of the ClientOptions. This passes the user’s access token to the Microsoft Graph API.

The getTasks function uses the client instance to fetch the user’s tasks from their Microsoft Planner plan. It ensures that the user has read access and then fetches the tasks using the plan ID. It fetches the data from the Microsoft Graph REST API plannerPlan endpoint. The request URL has an expand parameter to get the task details object, which contains additional information about the tasks. Each task object has a details object. You can use the select method to select the fields that you want to fetch. We only select some of the available fields for simplicity in this guide.

Adding the Microsoft Planner events to Bryntum Calendar

Let’s add a Microsoft 365 sign-in link to the Bryntum Gantt app.

Replace the contents of the <main> HTML tag in the index.html with the following:

      <div id="content" style="display: none">
        <div id="calendar"></div>
      </div>
      <div class="loader-container">
        <div class="loader"></div>
      </div>
      <a id="signin" href="#" style="display: none"> 
        <img
          src="./images/ms-symbollockup_signin_light.png"
          alt="Sign in with Microsoft"
        />
      </a>

Add the following styles to the styles.css file:

.loader-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh
}
.loader {
  display: inline-block;
  width: 50px;
  height: 50px;
  border: 3px solid black;
  border-radius: 50%;
  border-top-color: #fff;
  animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}
@-webkit-keyframes spin {
  to {transform: rotate(360deg); }
}

Initially, the app will display the sign-in link only. When a user signs in, it will display the Bryntum Calendar.

In the main.js file, add the following lines to store the “Sign in with Microsoft” link element object and loader container in variables:

const signInLink = document.getElementById('signin');
const loaderContainer = document.querySelector('.loader-container');

Now add the following function at the bottom of the file:

async function displayUI() {
    const account = localStorage.getItem('msalAccount');
    if (!account) {
        await signIn();
    }
    const content = document.getElementById('content');
    content.style = 'display: block';
    // Display calendar after sign in
    const events = await getTasks();
    const calendarEvents = [];
    const resourceID = 1;
    events.value.forEach((event) => {
        const startDateUTC = new Date(event.startDateTime);
        // Convert to local timezone
        const startDateLocal = new Date(
            startDateUTC.getTime() - startDateUTC.getTimezoneOffset() * 60000
        );
        const endDateUTC = new Date(event.dueDateTime);
        // Convert to local timezone
        const endDateLocal = new Date(
            endDateUTC.getTime() - endDateUTC.getTimezoneOffset() * 60000
        );
        calendarEvents.push({
            id              : event.id,
            name            : event.title,
            startDate       : startDateLocal,
            endDate         : endDateLocal,
            taskETag        : event['@odata.etag'].replace(/\\"/g, '"'),
            taskDetailsETag : event?.details['@odata.etag'].replace(/\\"/g, '"'),
            resourceId      : resourceID,
            description     : event.details ? event.details.description : '',
            percentComplete : event?.percentComplete
        });
    });
    calendar.events = calendarEvents;
}

Remove the example inline events data in the calendar config and add the signIn and getTasks function imports:

import { signIn } from './auth.js';
import { getTasks } from './graph.js';

The displayUI function calls the signIn function in auth.js to sign the user in to the app. Once the user is signed in, the sign-in link is hidden and the Bryntum Calendar is displayed. The getTasks function in the graph.js file gets the Microsoft Planner plan tasks. The push method then uses the retrieved tasks to create events for the Bryntum Calendar and adds them to the calendar.events store. The Bryntum EventModel represents a single event. This code populates some of the Bryntum EventModel fields with the Microsoft Planner plan tasks data.

The calendar will have a single example resource with an ID of 1 and the events will link to this resource. You can learn more about Bryntum Calendar data models in the project data guide.

Now add code to display the correct HTML elements as well as a “click” event listener to the “Sign in with Microsoft” link:

if (localStorage.getItem('msalAccount')) {
    displayUI();
    signInLink.style = 'display: none';
}
else {
    signInLink.style = 'display: block';
}

loaderContainer.style = 'display: none';

signInLink.addEventListener('click', displayUI);

Some of the Microsoft Planner plan task fields, such as the Etags, don’t have an equivalent field in the Bryntum Calendar events. The ETag is used to determine the change in the content of a resource at a given URL. There are two different ETags: taskETag and taskDetailsETag.

We need to create a custom Bryntum Calendar event model to add these fields.

Create a lib folder in the root directory and create a CustomEventModel.js file inside it. Add the following lines of code to the CustomEventModel.js file:

import { EventModel } from '@bryntum/calendar';
// Custom event model
export default class CustomEventModel extends EventModel {
    static $name = 'CustomEventModel';
    static fields = [
        { name : 'taskETag', type : 'string' },
        { name : 'taskDetailsETag', type : 'string' },
        { name : 'description', type : 'string' },
        { name : 'percentComplete', type : 'number', values : [0, 50, 100] }
    ];
}

This extends the Bryntum Calendar EventModel to include the Microsoft Planner plan task-specific fields. You can find the types of the Microsoft Planner task properties in the Microsoft Graph docs: plannerTask resource type.

Import this custom event model in the main.js file:

import CustomEventModel from './lib/CustomEventModel.js';

Set the calendar to use this event model for its event store:

    eventStore : {
        modelClass : CustomEventModel
    }

Run your dev server using npm run dev, you’ll see the sign-in link. Sign in with the same admin email address that you used to log in to Microsoft Planner:

Once you’ve signed in and given the app the necessary permissions, you’ll see your Microsoft Planner plan task in your Bryntum Calendar:

Next, we’ll sync the Microsoft Planner plan to the Bryntum Calendar by implementing CRUD functionality using Microsoft Graph. Updates to Bryntum Calendar events will update the tasks in the Microsoft Planner plan.

Syncing changes in the Bryntum Calendar to Microsoft Planner

Now that we’ve connected our calendar to the Graph API, let’s create the functions needed to implement the rest of the CRUD functionality using the Microsoft Graph JavaScript Client Library.

Creating events

In the graph.js file, add the following createTask function:

export async function createTask(
    name,
    startDate,
    endDate,
    description,
    percentComplete
) {
ensureScope('Tasks.ReadWrite');
    const task = {
        planId          : import.meta.env.VITE_MICROSOFT_PLANNER_PLAN_ID,
        title           : `${name}`,
        startDateTime   : `${startDate.toISOString()}`,
        dueDateTime     : `${endDate.toISOString()}`,
        details         : { description : description },
        percentComplete : percentComplete
    };
    return await graphClient.api('/planner/tasks').post(task);
}

This code first checks that the user has read-write access and then make a POST request to the Microsoft Graph API endpoint. The event object added to the POST request body has the planId

It then passes the event object, which is a representation of a plannerTask object, in the request body. The plannerTask object must use the ID of an existing plannerPlan object as its planID.

Updating events

In the graph.js file, add the following updateTask function:

export async function updateTask(
    id,
    name,
    startDate,
    endDate,
    taskETag,
    taskDetailsETag,
    description,
    percentComplete
) {
    ensureScope('Tasks.ReadWrite');
    const resData = {
        id              : '',
        taskETag        : '',
        taskDetailsETag : ''
    };
    // update task and task details
    const task = {};
    const taskDetails = {};
    if (name) task.title = `${name}`;
    if (startDate) task.startDateTime = `${startDate.toISOString()}`;
    if (endDate) task.dueDateTime = `${endDate.toISOString()}`;
    if (percentComplete) task.percentComplete = percentComplete;
    if (description) taskDetails.description = description;
    // update task only
    if (
        Object.keys(task).length !== 0 &&
      Object.keys(taskDetails).length === 0
    ) {
        const updateTaskRes = await graphClient
            .api(`/planner/tasks/${id}/`)
            .header('If-Match', taskETag)
            .header('prefer', 'return=representation')
            .update(task);
        resData.id = updateTaskRes.id;
        resData.taskETag = updateTaskRes['@odata.etag'];
        return resData;
    }
    // update task details only
    if (
        Object.keys(taskDetails).length !== 0 &&
      Object.keys(task).length === 0
    ) {
        const updateTaskDetailsRes = await graphClient
            .api(`/planner/tasks/${id}/details`)
            .header('If-Match', taskDetailsETag)
            .header('prefer', 'return=representation')
            .update(taskDetails);
        resData.id = updateTaskDetailsRes.id;
        resData.taskDetailsETag = updateTaskDetailsRes['@odata.etag'];
        return resData;
    }
    if (
        Object.keys(task).length !== 0 &&
      Object.keys(taskDetails).length !== 0
    ) {
        // update task and task details
        const updateTaskPromise = graphClient
            .api(`/planner/tasks/${id}/`)
            .header('If-Match', taskETag)
            .header('prefer', 'return=representation')
            .update(task);
        const updateTaskDetailsPromise = graphClient
            .api(`/planner/tasks/${id}/details`)
            .header('If-Match', taskDetailsETag)
            .header('prefer', 'return=representation')
            .update(taskDetails);
        const [updateTaskRes, updateTaskDetailsRes] = await Promise.all([
            updateTaskPromise,
            updateTaskDetailsPromise
        ]);
        resData.id = updateTaskRes.id;
        resData.taskETag = updateTaskRes['@odata.etag'];
        resData.taskDetailsETag = updateTaskDetailsRes['@odata.etag'];
        return resData;
    }
}

This function first checks that the user has read-write access, then makes a PATCH request to the Microsoft Graph API endpoint, passing in the changed data to the update method.

It then updates the event, the event details, or both based on the event data. The request has the required ‘If-Match’ header with the latest ETag value for the task. The Prefer header has a value of return=representation so that the response returns the updated task or task details object in the response body. If this header is not specified, the update methods return a 204 No Content response and empty content.

Deleting events

In the graph.js file, add the following deleteTask function:

export async function deleteTask(id, taskEtag) {
    ensureScope('Tasks.ReadWrite');
    return await graphClient
        .api(`/planner/tasks/${id}`)
        .header('If-Match', taskEtag)
        .delete();
}

This deletes the event by its id.

Listening for event data changes in Bryntum Scheduler

Next, we’ll add a data change event listener to the Bryntum Calendar so that we can update the user’s Microsoft Planner plan tasks when the user updates the Calendar events.

Add the following listener property to the calendar config in the main.js file:

  listeners: {
    dataChange: function (event) {
      if (event.store.id === "events") {
        updateMicrosoftPlanner(event);
      }
    },
  },

The event argument in the dataChange event listener callback contains the event data of the changed event. The property passes the event data to a function called updateMicrosoftPlanner that will update the user’s Microsoft Planner plan tasks.

Add the following definition for the updateMicrosoftPlanner function to the bottom of your main.js file:

async function updateMicrosoftPlanner(event) {
    if (event.action == 'update') {
        const id = event.record.id;
        if (`${id}`.startsWith('_generated')) {
            const createTaskRes = await createTask(
                event.record.name,
                event.record.startDate,
                event.record.endDate,
                event.record.description,
                event.record.percentComplete
            );
            // update id and eTags
            calendar.eventStore.applyChangeset({
                updated : [
                    // Will set proper id and eTag for added task
                    {
                        $PhantomId : id,
                        id         : createTaskRes.id,
                        taskETag   : createTaskRes['@odata.etag']
                    }
                ]
            });
            return;
        }
        if (!event.record.taskDetailsETag) return;
        const updateTaskRes = await updateTask(
            id,
            event.record.name,
            event.record.startDate,
            event.record.endDate,
            event.record.taskETag,
            event.record.taskDetailsETag,
            event.record.description,
            event.record.percentComplete
        );
        const updatedObj = {
            id : updateTaskRes.id
        };
        if (updateTaskRes.taskETag) {
            updatedObj.taskETag = updateTaskRes.taskETag;
        }
        if (updateTaskRes.taskDetailsETag) {
            updatedObj.taskDetailsETag = updateTaskRes.taskDetailsETag;
        }
        calendar.eventStore.applyChangeset({
            updated : [
                // Will set proper eTags for updated task
                updatedObj
            ]
        });
    }
    if (event.action == 'remove') {
        const recordsData = event.records.map((record) => record.data);
        recordsData.forEach((record) => {
            if (record.id.startsWith('_generated')) return;
            deleteTask(record.id, record.taskETag);
        });
    }
}

This code calls the appropriate CRUD function in the graph.js file, depending on the action that triggered the data change: “update” or “remove”. It creates a new task when an “update” action occurs and the event has an id that starts with "_generated". New records in a Bryntum Calendar are assigned a temporary UUID that starts with "_generated" and an “update” action occurs right after a “create” action occurs for a created event.

After creating a Microsoft Planner plan task, the applyChangeset method updates the calendar task store with the id and taskEtag values assigned to the task by Microsoft Planner. The $PhantomId is a phantom identifier, a unique, autogenerated client-side value used to identify a record. You can read more about phantom identifiers in the Bryntum docs.

Import the CRUD functions from the graph.js file:

import { createTask, deleteTask, updateTask } from './graph.js';

Changes in the Bryntum Calendar app will now be synced to the Microsoft Planner plan.

Customizing the event editor

Open the event editor in the Bryntum Calendar by double-clicking on an event. You’ll see that our two custom fields, description and percentComplete, are not displayed in the form. To display them, we need to customize the event editor.

Add the following features property to the calendar configuration object in main.js:

    features : {
        eventEdit : {
            items : {
                nameField : {
                    required : true
                },
                // Custom fields
                percentCompleteField : {
                    type        : 'combo',
                    label       : 'Progress',
                    name        : 'percentComplete',
                    multiSelect : false,
                    required    : true,
                    items       : [
                        {
                            value : 0,
                            text  : 'Not started'
                        },
                        {
                            value : 50,
                            text  : 'In progress'
                        },
                        { value : 100, text : 'Completed' }
                    ]
                },
                descriptionField : {
                    type  : 'textarea',
                    label : 'Notes',
                    // Name of the field in the event record to read/write data to
                    // NOTE: Make sure your EventModel has this field for this to link up correctly
                    name  : 'description'
                }
            }
        }
    },

This property sets the nameField to be required so that the user must supply a name for the event. It adds the two custom fields as form items and sets the input types, labels, and names. The name of each item should match the name of the custom field in the lib/CustomEventModel.js file.

Customizing the rendered events

We can make our Bryntum Calendar match the Microsoft Planner plan schedule view more closely by changing the month view.

Add the following modes property to the calendar to configure the month view:

    modes : {
        month : {
            // Render an icon showing progress state (editable in the event editor)
            eventRenderer : ({ eventRecord, renderData }) => {
                if (eventRecord.percentComplete === 0) {
                    renderData.eventColor = '#605e5c';
                }
                if (eventRecord.percentComplete === 50) {
                    renderData.eventColor = '#327eaa';
                    renderData.iconCls['b-fa b-fa-hourglass-half'] = 1;
                }
                if (eventRecord.percentComplete === 100) {
                    renderData.eventColor = '#107c41';
                    renderData.iconCls['b-fa b-fa-exclamation'] = 1;
                }
                return `
              <span class="b-event-name">${StringHelper.xss`${eventRecord.name}`}</span>
          `;
            }
        }
    },

The eventRenderer method alters the styling of the rendered event to indicate event progress by changing the color and adding an icon based on the percentComplete field.

Add the import for the StringHelper function that we use to perform HTML encoding that prevents XSS (Cross-Site Scripting) attacks:

import { StringHelper } from "@bryntum/calendar";

Add the following toolbar property to the calendar to add a sign-out button to the toolbar:

    tbar : {
        items : {
            deleteButton : {
                text : 'Signout',
                icon : 'b-fa b-fa-sign-out',
                onClick() {
                    signOut().then(() => {
                        // Refresh the page after sign out
                        location.reload();
                    });
                }
            }
        }
    },

Import the signOut function:

import { signOut } from './auth.js';

Run your dev server using npm run dev. Try to create, update, delete, and edit an event in the Bryntum Calendar. Changes you make will be reflected in your Microsoft Planner plan.

Next steps

This tutorial gives you a starting point for creating a Bryntum Calendar with vanilla JavaScript and syncing it to Microsoft Teams. You can sync more of the Microsoft Planner plan task fields, including assignments, priority, bucket, checklist, comments, and attachments. At present, you can only sync recurring events using the Beta version of the Microsoft Graph REST API.

You can further recreate the Microsoft Planner Schedule view by using a Bryntum Task Board to add an Unscheduled tasks to-do list next to the Bryntum Calendar. Take a look at our blog post Using multiple Bryntum components to learn how to do this.

Arsalan Khattak

Bryntum Scheduler Microsoft