Arsalan Khattak
1 November 2024

Building a Bryntum Scheduler with React and Microsoft Power Apps component framework

The Power Apps component framework (PCF) allows developers to create custom code components for Microsoft Power Apps applications using HTML, […]

The Power Apps component framework (PCF) allows developers to create custom code components for Microsoft Power Apps applications using HTML, CSS, and JavaScript.

You can connect your Power Apps applications to different data sources such as Excel, Microsoft SQL Server, and Microsoft Dataverse. Dataverse is the cloud data platform for the Microsoft Power Platform, which Power Apps is a part of.

The Power Apps component framework has various APIs to simplify development, such as the Web API that provides methods to perform database CRUD operations. The Power Apps component framework supports using React, which the Power Apps platform uses, to build components.

Bryntum Scheduler is a performant, feature-rich, and highly customizable scheduling UI component that you can use with popular JavaScript frameworks such as React, Angular, and Vue.

In this tutorial, we’ll create a Bryntum Scheduler PCF component and add it to a custom page in a Power Apps app.

We’ll do the following:

You can see the code for the completed Bryntum Scheduler Power Apps component in our GitHub repository.

Prerequisites

To follow along, you’ll need the following software installed on your machine:

You’ll also need to install the VS Code Power Platform Tools extension.

Getting access to Microsoft Power Apps

To start using Power Apps, sign in to your organizational Microsoft 365 account or create a Microsoft account using your work email address and join the Microsoft 365 Developer Program with that Microsoft account.

Then go to the Power Apps Maker Portal at make.powerapps.com, where you can begin creating your app.

Sign up for a 30-day Power Apps trial plan if you don’t already have a Power Apps license or a license through Office 365.

The trial plan provides temporary access to the following activities:

Creating an app in Power Apps

We’ll now create a Power Apps app in the Power Apps Maker Portal. Follow these steps to create an app:

We’ll create a custom Bryntum Scheduler React component to add to this custom page. But first, we’ll create Dataverse tables for the Scheduler events, resources, dependencies, and assignments.

Creating custom Dataverse tables

We’ll store our data in Dataverse tables. There are multiple benefits to using Dataverse as a data store for a Power Apps app, including:

We’ll create four Dataverse tables to store scheduler data and import example CSV data into them.

Creating a Dataverse table to store events data

Follow these steps to create a Dataverse table for events and populate it with example CSV data:

Copy and paste the following data into a bryntum-scheduler-events.csv file.

name,startDate,endDate,readOnly,timeZone,draggable,resizable,children,allDay,duration,durationUnit,exceptionDates,recurrenceRule,cls,eventColor,eventStyle,iconCls,style
Conference call,2024/07/01 09:00,2024/07/01 10:30,,,,,,,,,,,,,,,
Sprint planning,2024/07/01 11:30,2024/07/01 13:00,,,,,,,,,,,,,,,
Team meeting,2024/07/01 12:00,2024/07/01 13:30,,,,,,,,,,,,,,,

The headings in this CSV represent most of the fields for a Bryntum Scheduler event.

Now upload this CSV file. Once the CSV file is uploaded, you’ll see the following screen:

Click on the Edit table properties button. Change the following properties of the table:

Save the table properties.

We’ll use the schema name when making queries to the database table. Note that the names of tables and columns have an auto-generated prefix that makes their names unique.

The primary column is used when a table has a relationship to another table. You can use a lookup column to show data from another table. If another table has a lookup column that links to this Bryntum Scheduler Events table, you can add rows to the lookup column using a dropdown list. The dropdown list will contain the events of the linked Bryntum Scheduler Events table. Each list item will be an event name, which is the primary column of the Bryntum Scheduler Events table.

We’ll now edit each column by clicking on the column header and then clicking on the Edit column pop-up:

Make sure that the Use first row as column headers toggle at the top-right of the page is on.

Change the column properties as follows:

Display nameSchema nameData typeDefault choice
namenameSingle line of plain text
startDatestartdateDate and time
endDateenddateDate and time
readOnlyreadonlyYes/no choiceNo
timeZonetimezoneSingle line of plain text
draggabledraggableYes/no choiceYes
resizableresizableSingle line of plain text
childrenchildrenSingle line of plain text
allDayalldayYes/no choiceNo
durationdurationFloat
durationUnitdurationunitSingle line of plain text
exceptionDatesexceptiondatesSingle line of plain text
recurrenceRulerecurrenceruleSingle line of plain text
clsclsSingle line of plain text
eventColoreventcolorSingle line of plain text
eventStyleeventstyleSingle line of plain text
iconClsiconclsSingle line of plain text
stylestyleSingle line of plain text

Click the Create button at the bottom-right of the screen to create the table.

You can find your custom Dataverse table by opening the Tables menu item in the navigation on the left and then clicking the Custom button above the table of tables:

Creating a Dataverse table to store resources data

We’ll now create a table for the resources data.

Copy and paste the following data into a bryntum-scheduler-resources.csv file.

name,eventColor,readOnly
Peter,,
Kate,,
Winston,,

The headings in this CSV text represent most of the fields for a Bryntum Scheduler resource.

Select Create with Excel or .CSV file and upload this CSV file.

Click on the Edit table properties button. Change the following properties of the table:

Change the column properties as follows:

Display nameSchema nameData typeDefault choice
namenameSingle line of plain text
eventColoreventcolorSingle line of plain text
readOnlyreadonlyYes/no choiceNo

Click the Create button to create the table.

Creating a Dataverse table to store dependencies data

We’ll now create a table for the dependencies data.

Copy and paste the following data into a bryntum-scheduler-dependencies.csv file.

type,cls,lag,lagUnit,fromSide,toSide
2,,0,day,end,start

The headings in this CSV text represent most of the fields for a Bryntum Scheduler dependency.

Select Create with Excel or .CSV file and upload this CSV file.

Click the Edit table properties button and change the following properties of the table:

Change the column properties as follows:

Display nameSchema nameData type
typetypeWhole number
clsclsSingle line of plain text
laglagFloat
lagUnitlagunitSingle line of plain text
fromSidefromsideSingle line of plain text
toSidetosideSingle line of plain text

Note that you can use the choice data type to restrict the possible values of a column row when there’s a limited set of valid values. You could do this for the type, lagUnit, fromSide, and toSide columns.

Click the Create button to create the table.

Open the Bryntum Scheduler Dependencies table in a separate tab, click on the +18 more button on the right of the dependencies table, and then check the Bryntum Scheduler Dependencies column:

This ID column uses globally unique identifiers (GUIDs) and is auto-generated by Dataverse. If you open the edit column popup for this column, you’ll see that its data type is Unique identifier.

Open the edit column popup for one of the custom columns that you created. You see a new Logical name property under the Advanced options dropdown. We’ll use this property when we create and update records using the Dataverse Web API from the custom Bryntum Scheduler Power Apps component that we’ll create.

Create from and to columns for the dependencies table with relationships to the events table

We’ll now create “from” and “to” lookup columns for the dependencies table that are related to the events table. These columns will indicate which events are connected in a dependency.

Click the New column button to the right of the +17 more button in the Bryntum Scheduler Dependencies columns and data table.

Set the following values in the popup form and then click the save button:

You’ll now be able to add values to the “from” column using an auto-complete input. The displayed values are the primary column (name column) values of the columns of the linked events table. Set the “from” value for the example dependency to “Conference call”.

Create another column with the following values:

Set the “to” value for the example dependency to “Sprint planning”.

Creating a Dataverse table to store assignments data

We’ll now create a table for the assignments data. Create a table by clicking on the Start with a blank table card.

Click on the Edit table properties button. Change the following properties of the table:

The primary column will be the “New column” column that’s added by default. We won’t use this column for our scheduler. Make sure that the Required value for the “New column” is set to “optional”.

Create a column by clicking on the + New column button to the right of the column header and add the following values:

Create another column with the following values:

Click the Create button to create the table.

Create three rows with the following values in the Bryntum Scheduler Assignments columns and data table:

eventIdresourceId
Conference callPeter
Sprint planningKate
Team meetingWinston

Now that our Dataverse database tables are ready, let’s create a Bryntum Scheduler Power Apps component to add to our Power Apps page.

Create a Bryntum Scheduler Power Apps component framework component

We’ll use the Power Apps component framework to create a Bryntum Scheduler code component that we can use in our Power Apps app.

We’ll use the Microsoft Power Platform CLI to create a PCF project and push the component to the Power Apps platform.

Make sure that you’ve installed the prerequisite VS Code Power Platform Tools extension in VS Code. This extension enables you to use the Microsoft Power Platform CLI commands in a PowerShell terminal in Visual Studio Code on Windows 10, Windows 11, Linux, and macOS.

Run the following command in a new directory to create a PCF React project template:

pac pcf init --name BryntumScheduler --namespace BryntumScheduler --template field --framework react

The pac pcf init command initializes a directory with a new PCF project.

Now install the project dependencies:

npm install

Next, edit the .eslintrc.json file and add the following rule to the rules object to disable ESLint from throwing errors when using type any:

"@typescript-eslint/no-explicit-any": "off"

Install the Bryntum Scheduler packages for React. Make sure that you have access to the Bryntum npm repository.

npm install @bryntum/scheduler@latest @bryntum/scheduler-react@latest

Updating the code component’s manifest

The manifest BryntumScheduler/ControlManifest.Input.xml is an XML metadata file that describes the component. We’ll update it to accurately describe our component.

Replace the contents of BryntumScheduler/ControlManifest.Input.xml with the following code:

<?xml version="1.0" encoding="utf-8"?>
<manifest>
    <control namespace="BryntumSchedulerComponent" constructor="BryntumScheduler" version="2.0.1"
             display-name-key="BryntumSchedulerComponent"
             description-key="BryntumSchedulerComponent description"
             control-type="virtual">
        <!--external-service-usage
            node declares whether this 3rd party PCF control is using external service or not, if yes,
            this control will be considered as premium and please also add the external domain it is
            using.
            If it is not using any external service, please set the enabled="false" and DO NOT add any domain
                below. The "enabled" will be false by default.
            Example1:
            <external-service-usage enabled="true">
                <domain>www.Microsoft.com</domain>
            </external-service-usage>
            Example2:
            <external-service-usage enabled="false">
            </external-service-usage>
            -->
        <external-service-usage enabled="false">
            <!--UNCOMMENT
              TO ADD EXTERNAL DOMAINS
              <domain></domain>
              <domain></domain>
              -->
        </external-service-usage>
        <!-- property node identifies a specific, configurable piece of data that the control
            expects from CDS -->
        <property name="BryntumScheduler" display-name-key="BryntumScheduler"
                  description-key="BryntumScheduler description" of-type="SingleLine.Text" usage="bound"
                  required="false" />
        <resources>
            <code path="index.ts" order="1" />
            <platform-library name="React" version="16.8.6" />
            <css path="css/BryntumSchedulerComponent.css" order="1" />
            <css path="css/scheduler.stockholm.css" order="2" />
        </resources>
        <feature-usage>
            <uses-feature name="WebAPI" required="true" />
        </feature-usage>
    </control>
</manifest>

Here we add the CSS resources we’ll create in the next step in the <resources> tag.

In the <feature-usage> tag, we add the WebAPI feature, which provides properties and methods to perform CRUD operations on our Dataverse tables.

Adding CSS styling

Let’s create the CSS resource files that we added to the manifest.

Create a css folder in the BryntumScheduler component folder. Copy the scheduler.stockholm.css file from the node_modules/@bryntum/scheduler folder and add it to the css folder.

We need to add this CSS file to our component folder because bundling font resources is currently not supported by the Power Apps component framework. Bryntum components use icons that are based on the Font Awesome 6 Free solid font. This Bryntum Scheduler CSS theme file uses these Font Awesome font resources. We’ll use the Font Awesome CDN to get these font resources in our component.

In the css/scheduler.stockholm.css file, find the four occurrences of the following @font-face source URL:

"fonts/fa-solid-900.woff2"

Replace each of them with this CDN URL:

"https://use.fontawesome.com/releases/v6.0.0/webfonts/fa-solid-900.woff2"

Now find the four occurrences of the following @font-face source URL:

"fonts/fa-solid-900.ttf"

Replace each of them with this CDN URL:

"https://use.fontawesome.com/releases/v6.0.0/webfonts/fa-solid-900.ttf"

In the BryntumScheduler/css folder, create a BryntumSchedulerComponent.css file and add the following styles to it:

#root {
  height: 100vh;
}
.loader-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  width: 100%;
}
.loader {
  border: 16px solid #f3f3f3;
  border-radius: 50%;
  border-top: 16px solid #3498db;
  width: 80px;
  height: 80px;
  -webkit-animation: spin 2s linear infinite;
  /* Safari */
  animation: spin 2s linear infinite;
}
/* Safari */
@-webkit-keyframes spin {
  0% {
    -webkit-transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
  }
}
@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

Creating the Bryntum Scheduler component

Before we create the Bryntum Scheduler React component, first create a constants.ts file in the BryntumScheduler folder and add the following lines of code to it:

⚠️ Replace the databasePrefix value with the database prefix value in your Dataverse tables.

export const databasePrefix = 'cr8a7_'; // replace with your own prefix
export const eventsDataverseTableName = 'bryntumschedulerevents';
export const resourcesDataverseTableName = 'bryntumschedulerresources';
export const dependenciesDataverseTableName = 'bryntumschedulerdependencies';
export const assignmentsDataverseTableName = 'bryntumschedulerassignments';

We’ll show a loading spinner React component while the Dataverse data is being fetched. Create a file called LoadingSpinner.tsx in the BryntumScheduler folder and add the following lines of code to it:

import * as React from 'react';
function LoadingSpinner() {
    return (
        <div className="loader-container">
            <div className="loader" />
        </div>
    );
}
export default LoadingSpinner;

Create a BryntumSchedulerComponent.types.ts file in the BryntumScheduler folder and add the following types to it:

import { databasePrefix } from './constants';
import { IInputs } from './generated/ManifestTypes';
export interface IBryntumSchedulerComponentProps {
  context?: ComponentFramework.Context<IInputs>;
}
type RecordItem = {
  data:
    | SchedulerEvent
    | SchedulerResource
    | SchedulerDependency
    | SchedulerAssignment;
  meta: {
    modified:
      | Partial<SchedulerEvent>
      | Partial<SchedulerResource>
      | Partial<SchedulerDependency>
      | Partial<SchedulerAssignment>;
  };
} & SchedulerEvent & SchedulerResource & SchedulerDependency & SchedulerAssignment;
export type SyncData = {
  action: 'dataset' | 'add' | 'remove' | 'update';
  records: RecordItem[];
  store: {
    id: 'events' | 'resources' | 'dependencies' | 'assignments';
  };
};
export type SchedulerEvent = {
  id: string;
  name: string;
  startDate: string;
  endDate: string;
  readOnly: number;
  timeZone: string;
  draggable: number;
  resizable: string | boolean;
  children: string;
  allDay: number;
  duration: number;
  durationUnit: string;
  exceptionDates: string;
  recurrenceRule: string;
  cls: string;
  eventColor: string;
  eventStyle: string;
  iconCls: string;
  style: string;
};
const KEY_ID = `${databasePrefix}id` as const;
const KEY_NAME = `${databasePrefix}name` as const;
const KEY_START_DATE = `${databasePrefix}startdate` as const;
const KEY_END_DATE = `${databasePrefix}enddate` as const;
const KEY_READONLY = `${databasePrefix}readonly` as const;
const KEY_TIME_ZONE = `${databasePrefix}timezone` as const;
const KEY_DRAGGABLE = `${databasePrefix}draggable` as const;
const KEY_RESIZABLE = `${databasePrefix}resizable` as const;
const KEY_CHILDREN = `${databasePrefix}children` as const;
const KEY_ALL_DAY = `${databasePrefix}allday` as const;
const KEY_DURATION = `${databasePrefix}duration` as const;
const KEY_DURATION_UNIT = `${databasePrefix}durationunit` as const;
const KEY_EXCEPTION_DATES = `${databasePrefix}exceptiondates` as const;
const KEY_RECURRENCE_RULE = `${databasePrefix}recurrencerule` as const;
const KEY_CLS = `${databasePrefix}cls` as const;
const KEY_EVENT_COLOR = `${databasePrefix}eventcolor` as const;
const KEY_EVENT_STYLE = `${databasePrefix}eventstyle` as const;
const KEY_ICON_CLS = `${databasePrefix}iconcls` as const;
const KEY_STYLE = `${databasePrefix}style` as const;
export type SchedulerEventDataverse = {
  [KEY_ID]: string;
  [KEY_NAME]: string;
  [KEY_START_DATE]: string;
  [KEY_END_DATE]: string;
  [KEY_READONLY]: boolean;
  [KEY_TIME_ZONE]: string;
  [KEY_DRAGGABLE]: boolean;
  [KEY_RESIZABLE]: string;
  [KEY_CHILDREN]: string;
  [KEY_ALL_DAY]: boolean;
  [KEY_DURATION]: number;
  [KEY_DURATION_UNIT]: string;
  [KEY_EXCEPTION_DATES]: string;
  [KEY_RECURRENCE_RULE]: string;
  [KEY_CLS]: string;
  [KEY_EVENT_COLOR]: string;
  [KEY_EVENT_STYLE]: string;
  [KEY_ICON_CLS]: string;
  [KEY_STYLE]: string;
};
export type SchedulerResource = {
  id: string;
  name: string;
  eventColor: string;
  readOnly: string;
};
export type SchedulerResourceDataverse = {
  [KEY_ID]: string;
  [KEY_NAME]: string;
  [KEY_EVENT_COLOR]: string;
  [KEY_READONLY]: boolean;
};
export type SchedulerDependency = {
  id: string;
  type: number;
  cls: string;
  lag: number;
  lagUnit: string;
  fromSide: string;
  toSide: string;
  from: string;
  to: string;
};
const KEY_TYPE = `${databasePrefix}type` as const;
const KEY_LAG = `${databasePrefix}lag` as const;
const KEY_LAG_UNIT = `${databasePrefix}lagunit` as const;
const KEY_FROM_SIDE = `${databasePrefix}fromside` as const;
const KEY_TO_SIDE = `${databasePrefix}toside` as const;
const KEY_FROM = `${databasePrefix}from@odata.bind` as const;
const KEY_TO = `${databasePrefix}to@odata.bind` as const;
export type SchedulerDependencyDataverse = {
  [KEY_ID]: string;
  [KEY_TYPE]: number;
  [KEY_CLS]: string;
  [KEY_LAG]: number;
  [KEY_LAG_UNIT]: string;
  [KEY_FROM_SIDE]: string;
  [KEY_TO_SIDE]: string;
  [KEY_FROM]: string;
  [KEY_TO]: string;
};
export type SchedulerAssignment = {
  id: string;
  eventId: string;
  resourceId: string;
};
const KEY_EVENT_ID = `${databasePrefix}eventid@odata.bind` as const;
const KEY_RESOURCE_ID = `${databasePrefix}resourceid@odata.bind` as const;
export type SchedulerAssignmentDataverse = {
  [KEY_ID]: string;
  [KEY_EVENT_ID]: string;
  [KEY_RESOURCE_ID]: string;
};

These are the TypeScript types we’ll use for the Bryntum Scheduler and Dataverse data.

We’ll now define some basic configurations for our Bryntum Scheduler. Create a file called schedulerConfig.ts in the BryntumScheduler folder and add the following lines of code to it:

import type { BryntumSchedulerProps } from '@bryntum/scheduler-react';
const schedulerConfig: Partial<BryntumSchedulerProps> = {
    stripeFeature: true,
    dependenciesFeature: true,
    columns       : [{ text : 'Name', field : 'name', width : 250 }],
    viewPreset    : 'hourAndDay',
    startDate     : new Date(2024, 6, 1, 9),
    endDate       : new Date(2024, 6, 1, 17),
    barMargin     : 10,
    selectionMode : {
        multiSelect : false
    }
};
export { schedulerConfig };

Now create the Bryntum Scheduler React component. Create a BryntumSchedulerComponent.tsx file in the BryntumScheduler folder and add the following lines of code to it:

import { BryntumScheduler as OriginalBryntumScheduler } from '@bryntum/scheduler-react';
import * as React from 'react';
import { FunctionComponent, useRef } from 'react';
import {
    IBryntumSchedulerComponentProps,
    SchedulerAssignment,
    SchedulerDependency,
    SchedulerEvent,
    SchedulerResource
} from './BryntumSchedulerComponent.types';
import LoadingSpinner from './LoadingSpinner';
import { schedulerConfig } from './schedulerConfig';
// eslint-disable-next-line
const BryntumScheduler: React.ComponentType<any> = OriginalBryntumScheduler as any;
const BryntumSchedulerComponent: FunctionComponent<IBryntumSchedulerComponentProps> = (
    props
) => {
    const [data, setData] = React.useState<{
    events: SchedulerEvent[];
    resources: SchedulerResource[];
    dependencies: SchedulerDependency[];
    assignments: SchedulerAssignment[];
  }>();
    const scheduler = useRef<OriginalBryntumScheduler>(null);
    return data ? (
        <BryntumScheduler
            ref={scheduler}
            events={data.events}
            resources={data.resources}
            dependencies={data.dependencies}
            assignments={data.assignments}
            {...schedulerConfig}
        />
    ) : (
        <LoadingSpinner />
    );
};
export default BryntumSchedulerComponent;

We render the Bryntum Scheduler React component and pass in schedulerConfig as a prop. We’ll store the data in the data state variable and pass in the events, resources, dependencies, and assignments data properties as props. When there’s no data, the LoadingSpinner component is displayed.

To render the component, we need to add the PCF component logic to the auto-generated index.ts file. The index.ts file has a class that creates an object that implements the following methods to control the lifecycle of the PCF component:

Add the following lines of code to the init function:

Object.defineProperty(window, 'globalThis', {
    value    : window,
    writable : false
});

Replace the updateView function with the following lines of code:

public updateView(
  context: ComponentFramework.Context<IInputs>
): React.ReactElement {
  const props: IBryntumSchedulerComponentProps = { context };
  return React.createElement(BryntumSchedulerComponent, props);
}

We render our BryntumSchedulerComponent in this function, which is called when any value in the property bag has changed. The property bag includes field values, datasets, and global values like container height and width.

Import the component and the prop types along with the other imports at the top of the file:

import BryntumSchedulerComponent from './BryntumSchedulerComponent';
import { IBryntumSchedulerComponentProps } from './BryntumSchedulerComponent.types';

You can remove the imports from the HelloWorld.tsx file and delete the file, too.

Let’s create a build of the component.

npm run build

This command creates an out folder in the root directory. The folder contains the component’s manifest, CSS, and JavaScript bundle.

Now run the local browser test harness to view the component:

npm run start

A browser window or tab will automatically open to the local test harness URL: http://localhost:8181/. You’ll see the loading spinner:

You can use your browser React dev tools to change the data state variable to an empty array. If you do this, you’ll see the Bryntum Scheduler with no data:

Let’s fetch the Dataverse data and add it to our Scheduler component.

Fetching data using the Web API retrieveMultipleRecords method

We’ll use the Web API retrieveMultipleRecords method to fetch the events, resources, dependencies, and assignment records from our Dataverse tables.

Add the following lines of code to the BryntumSchedulerComponent.tsx file, below the scheduler variable declaration:

const fetchRecords = async() => {
    try {
        const eventsPromise = props?.context?.webAPI.retrieveMultipleRecords(
        `${databasePrefix}${eventsDataverseTableName}`
        );
        const resourcesPromise =
        props?.context?.webAPI.retrieveMultipleRecords(
        `${databasePrefix}${resourcesDataverseTableName}`
        );
        const dependenciesPromise:
        | Promise<ComponentFramework.WebApi.RetrieveMultipleResponse>
        | undefined = props?.context?.webAPI.retrieveMultipleRecords(
        `${databasePrefix}${dependenciesDataverseTableName}`,
        `?$select=${databasePrefix}type,${databasePrefix}lag,${databasePrefix}lagunit,${databasePrefix}fromside,${databasePrefix}toside,${databasePrefix}from,${databasePrefix}to&$expand=${databasePrefix}from($select=${databasePrefix}${eventsDataverseTableName}id),${databasePrefix}to($select=${databasePrefix}${eventsDataverseTableName}id)`
        );
        const assignmentsPromise:
        | Promise<ComponentFramework.WebApi.RetrieveMultipleResponse>
        | undefined = props?.context?.webAPI.retrieveMultipleRecords(
        `${databasePrefix}${assignmentsDataverseTableName}`,
        `?$select=${databasePrefix}eventid,${databasePrefix}resourceid&$expand=${databasePrefix}eventid($select=${databasePrefix}${eventsDataverseTableName}id),${databasePrefix}resourceid($select=${databasePrefix}${resourcesDataverseTableName}id)`
        );
        const [events, resources, dependencies, assignments] = await Promise.all([
            eventsPromise,
            resourcesPromise,
            dependenciesPromise,
            assignmentsPromise
        ]);
        if (events && resources && dependencies && assignments) {
            setData({
                events       : removeEventsDataColumnPrefixes(events.entities) as SchedulerEvent[],
                resources    : removeResourcesDataColumnPrefixes(resources.entities) as SchedulerResource[],
                dependencies : removeDependenciesDataColumnPrefixes(
                    dependencies.entities
                ) as SchedulerDependency[],
                assignments : removeAssignmentsDataColumnPrefixes(
                    assignments.entities
                ) as SchedulerAssignment[]
            });
        }
    }
    catch (e) {
        if (e instanceof Error) {
            if (e.name === 'PCFNonImplementedError') {
                console.log('PCFNonImplementedError: ', e.message);
                // You can add fallback data state for the development mode browser test harness
                // webAPI is not available in the test harness
            }
        }
        throw e;
    }
};
React.useEffect(() => {
    fetchRecords();
}, []);

When the component is mounted, we call the fetchRecords function, which calls the retrieveMultipleRecords method to fetch the events, resources, dependencies, and assignments data. The logical name of the Dataverse table is passed in as an argument to the retrieveMultipleRecords method. We access the Web API through the context object that we passed in from our index.ts file as a prop.

The removeEventsDataColumnPrefixes, removeResourcesDataColumnPrefixes, removeDependenciesDataColumnPrefixes, and removeAssignmentsDataColumnPrefixes functions transform the Dataverse data to match the format expected by the Bryntum Scheduler.

Add the following definitions for these functions at the top of the file:

function removeEventsDataColumnPrefixes(entities: any[]) {
    return entities.map((entity: ComponentFramework.WebApi.Entity) => {
        const event: Partial<SchedulerEvent> = {};
        if (entity[`${databasePrefix}${eventsDataverseTableName}id`]) {
            event.id = entity[`${databasePrefix}${eventsDataverseTableName}id`];
        }
        if (entity[`${databasePrefix}name`]) {
            event.name = entity[`${databasePrefix}name`];
        }
        if (entity[`${databasePrefix}startdate`]) {
            event.startDate = entity[`${databasePrefix}startdate`];
        }
        if (entity[`${databasePrefix}enddate`]) {
            event.endDate = entity[`${databasePrefix}enddate`];
        }
        if (entity[`${databasePrefix}readonly`]) {
            event.readOnly = entity[`${databasePrefix}readonly`];
        }
        if (entity[`${databasePrefix}timezone`]) {
            event.timeZone = entity[`${databasePrefix}timezone`];
        }
        if (entity[`${databasePrefix}draggable`]) {
            event.draggable = entity[`${databasePrefix}draggable`];
        }
        if (entity[`${databasePrefix}resizable`]) {
            event.resizable = entity[`${databasePrefix}resizable`];
        }
        if (entity[`${databasePrefix}children`]) {
            event.children = entity[`${databasePrefix}children`];
        }
        if (entity[`${databasePrefix}allday`]) {
            event.allDay = entity[`${databasePrefix}allday`];
        }
        if (entity[`${databasePrefix}duration`]) {
            event.duration = entity[`${databasePrefix}duration`];
        }
        if (entity[`${databasePrefix}durationunit`]) {
            event.durationUnit = entity[`${databasePrefix}durationunit`];
        }
        if (entity[`${databasePrefix}exceptiondates`]) {
            event.exceptionDates = entity[`${databasePrefix}exceptiondates`];
        }
        if (entity[`${databasePrefix}recurrencerule`]) {
            event.recurrenceRule = entity[`${databasePrefix}recurrencerule`];
        }
        if (entity[`${databasePrefix}cls`]) {
            event.cls = entity[`${databasePrefix}cls`];
        }
        if (entity[`${databasePrefix}eventcolor`]) {
            event.eventColor = entity[`${databasePrefix}eventcolor`];
        }
        if (entity[`${databasePrefix}eventstyle`]) {
            event.eventStyle = entity[`${databasePrefix}eventstyle`];
        }
        if (entity[`${databasePrefix}iconcls`]) {
            event.iconCls = entity[`${databasePrefix}iconcls`];
        }
        if (entity[`${databasePrefix}style`]) {
            event.style = entity[`${databasePrefix}style`];
        }
        return event;
    });
}
function removeResourcesDataColumnPrefixes(entities: any[]) {
    return entities
        .map((entity: ComponentFramework.WebApi.Entity) => {
            const resource: Partial<SchedulerResource> = {};
            if (entity[`${databasePrefix}${resourcesDataverseTableName}id`]) {
                resource.id =
              entity[`${databasePrefix}${resourcesDataverseTableName}id`];
            }
            if (entity[`${databasePrefix}name`]) {
                resource.name = entity[`${databasePrefix}name`];
            }
            if (entity[`${databasePrefix}eventcolor`]) {
                resource.eventColor = entity[`${databasePrefix}eventcolor`];
            }
            if (entity[`${databasePrefix}readonly`]) {
                resource.readOnly = entity[`${databasePrefix}readonly`];
            }
            return resource;
        });
}
function removeDependenciesDataColumnPrefixes(entities: any[]) {
    return entities
        .map((entity: ComponentFramework.WebApi.Entity) => {
            const dependency: Partial<SchedulerDependency> = {};
            if (entity[`${databasePrefix}${dependenciesDataverseTableName}id`]) {
                dependency.id =
            entity[`${databasePrefix}${dependenciesDataverseTableName}id`];
            }
            if (entity[`${databasePrefix}type`]) {
                dependency.type = entity[`${databasePrefix}type`];
            }
            if (entity[`${databasePrefix}cls`]) {
                dependency.cls = entity[`${databasePrefix}cls`];
            }
            if (entity[`${databasePrefix}lag`]) {
                dependency.lag = entity[`${databasePrefix}lag`];
            }
            if (entity[`${databasePrefix}lagunit`]) {
                dependency.lagUnit = entity[`${databasePrefix}lagunit`];
            }
            if (entity[`${databasePrefix}fromside`]) {
                dependency.fromSide = entity[`${databasePrefix}fromside`];
            }
            if (entity[`${databasePrefix}toside`]) {
                dependency.toSide = entity[`${databasePrefix}toside`];
            }
            if (entity[`${databasePrefix}from`]) {
                dependency.from = entity[`${databasePrefix}from`][
           `${databasePrefix}${eventsDataverseTableName}id`
                ];
            }
            if (entity[`${databasePrefix}to`]) {
                dependency.to = entity[`${databasePrefix}to`][
           `${databasePrefix}${eventsDataverseTableName}id`
                ];
            }
            return dependency;
        });
}
function removeAssignmentsDataColumnPrefixes(entities: any[]) {
    return entities
        .map((entity: ComponentFramework.WebApi.Entity) => {
            const assignment: Partial<SchedulerAssignment> = {};
            if (entity[`${databasePrefix}${assignmentsDataverseTableName}id`]) {
                assignment.id =
            entity[`${databasePrefix}${assignmentsDataverseTableName}id`];
            }
            if (entity[`${databasePrefix}eventid`]) {
                assignment.eventId = entity[`${databasePrefix}eventid`][
           `${databasePrefix}${eventsDataverseTableName}id`
                ];
            }
            if (entity[`${databasePrefix}resourceid`]) {
                assignment.resourceId = entity[`${databasePrefix}resourceid`][
           `${databasePrefix}${resourcesDataverseTableName}id`
                ];
            }
            return assignment;
        });
}

Add the imports for the constant values:

import {
    databasePrefix,
    eventsDataverseTableName,
    resourcesDataverseTableName,
    dependenciesDataverseTableName,
    assignmentsDataverseTableName
} from './constants';

The Web API methods currently can’t be tested in the test harness. We need to publish and host the component in a Microsoft Power Platform environment.

Creating a component solution package

You need to deploy your component to a Microsoft Dataverse environment before using it in Power Apps. The first step is to package the component into a solution that you can import. There are two ways to do this:

  1. During development, use the Microsoft Power Platform CLI push command to create a temporary solution file that pushes the component to a Microsoft Dataverse environment. The solution file consists of a bundle of all of the component elements.
  2. For build pipelines or manual deploys, create a solution for the component and import it separately into your Dataverse environment.

We’ll use the first way.

To push the Bryntum Scheduler component to your Microsoft Power Platform, do the following:

⚠️ To add custom components to a Power Apps app, you need to enable the Power Apps component framework for canvas apps feature in the environment where you want to use the component. You can do this by following the Microsoft guide to enabling the Power Apps component framework feature.

npm run build
pac pcf push --publisher-prefix dev

This command imports your Power Apps component framework project into the current Dataverse organization. The publisher prefix is the customization prefix value for the Dataverse solution publisher. You can change it.

⚠️ If the push fails due to the size of the component, try the following to increase the file size limit:

You should now be able to use the component in a Power Apps app.

Adding the Bryntum Scheduler React component to the Power Apps app

Go back to the custom Power Apps app page that you created and do the following to add the Bryntum Scheduler component to your page:

⚠️ Note that you may need to refresh the page multiple times to show the updated page with the Bryntum Scheduler.

Now let’s make the component save changes to the Bryntum Scheduler in the Dataverse tables.

Creating a syncData function to sync data changes

Add the following onDataChange property to the BryntumScheduler React component in the BryntumSchedulerComponent.tsx file:

    onDataChange={syncData}

When a data change occurs in the Bryntum Scheduler, the dataChange event will be fired and the syncData function will be called.

Define the syncData function in the BryntumSchedulerComponent component:

const syncData = async ({ store, action, records }: SyncData) => {
    const storeId = store.id;
    if (storeId === 'events') {
        if (action === 'remove') {
        }
        if (action === 'update') {
        }
    }
    if (storeId === 'resources') {
        if (action === 'add') {
        }
        if (action === 'remove') {
        }
        if (action === 'update') {
        }
    }
    if (storeId === 'dependencies') {
        if (action === 'add') {
        }
        if (action === 'remove') {
        }
        if (action === 'update') {
        }
    }
    if (storeId === 'assignments') {
        if (action === 'add') {
        }
        if (action === 'remove') {
        }
        if (action === 'update') {
        }
    }
};

We get information about the store, action, and records from the dataChange event. The store is used to determine which data store has been changed, "events", "resources", "dependencies", or "assignments". The action determines the type of data change, "add", "remove", or "update".

Now add the following type imports to the top of the BryntumSchedulerComponent.tsx file:

import {
    SyncData,
    SchedulerEventDataverse,
    SchedulerResourceDataverse,
    SchedulerDependencyDataverse,
    SchedulerAssignmentDataverse
} from './BryntumSchedulerComponent.types';

Creating Dataverse data using the Web API createRecord method

Add the following code to the if statement in the syncData function where storeId === "resources" and action === "add" are:

const resourcesIds = data?.resources.map((obj) => obj.id);
for (let i = 0; i < records.length; i++) {
    const resourceExists =
    resourcesIds && resourcesIds.includes(records[i].data.id);
    if (resourceExists) return;
    try {
        const newResource = records[i] as SchedulerResource;
        const insertData: Partial<SchedulerResourceDataverse> = {};
        if (newResource?.name) {
            insertData[`${databasePrefix}name`] = newResource.name;
        }
        if (newResource?.eventColor) {
            insertData[`${databasePrefix}eventcolor`] =
        newResource.eventColor;
        }
        if (newResource?.readOnly) {
            insertData[`${databasePrefix}readonly`] = Boolean(
                newResource.readOnly
            );
        }
        return props?.context?.webAPI
            .createRecord(
        `${databasePrefix}${resourcesDataverseTableName}`,
        insertData
            )
            .then((res) => {
                if (scheduler?.current?.instance) {
                    scheduler.current.instance.resourceStore.applyChangeset({
                        updated : [
                            // Will set proper id for added resource
                            {
                                $PhantomId : newResource.id,
                                id         : res.id
                            }
                        ]
                    });
                }
            });
    }
    catch (error) {
        console.error(error);
    }
}

We create an insertData object that’s passed into the Web API createRecord method as an argument. We use the insertData object to format the resource data for insertion into the resources Dataverse table. The query uses the Dataverse columns’ logical names, which are lowercase and have prefixes.

Once the resource has been successfully inserted into the database, we update the id of the resource in the Scheduler data store to use the Dataverse id instead of the auto-generated client-side $PhantomId.

⚠️ You should not persist phantom record identifiers as-is on the server. Doing this may cause id collisions on the client after data reloading. The backend should assign a new id to added records.

Add the following code to the if statement in the syncData function where storeId === "dependencies" and action === "add" are:

for (let i = 0; i < records.length; i++) {
    try {
        const newDependency = records[i] as SchedulerDependency;
        const insertData: Partial<SchedulerDependencyDataverse> = {};
        if (newDependency?.type) {
            insertData[`${databasePrefix}type`] = Number(newDependency.type);
        }
        if (newDependency?.cls) {
            insertData[`${databasePrefix}cls`] = newDependency.cls;
        }
        if (newDependency?.lag) {
            insertData[`${databasePrefix}lag`] = Number(newDependency.lag);
        }
        if (newDependency?.lagUnit) {
            insertData[`${databasePrefix}lagunit`] = newDependency.lagUnit;
        }
        if (newDependency?.fromSide) {
            insertData[`${databasePrefix}fromside`] = newDependency.fromSide;
        }
        if (newDependency?.toSide) {
            insertData[`${databasePrefix}toside`] = newDependency.toSide;
        }
        if (newDependency?.from) {
            insertData[
          `${databasePrefix}from@odata.bind`
            ] = `/${databasePrefix}${eventsDataverseTableName}es(${newDependency.from})`;
        }
        if (newDependency?.to) {
            insertData[
          `${databasePrefix}to@odata.bind`
            ] = `/${databasePrefix}${eventsDataverseTableName}es(${newDependency.to})`;
        }
        return props?.context?.webAPI
            .createRecord(
          `${databasePrefix}${dependenciesDataverseTableName}`,
          insertData
            )
            .then((res) => {
                if (scheduler?.current?.instance) {
                    scheduler.current.instance.dependencyStore.applyChangeset({
                        updated : [
                            // Will set proper id for added dependency
                            {
                                $PhantomId : newDependency.id,
                                id         : res.id
                            }
                        ]
                    });
                }
            });
    }
    catch (error) {
        console.error(error);
    }
}

This code follows the same logic as the code to add a new resource.

Add the following code to the if statement in the syncData function where storeId === "assignments" and action === "add" are:

for (let i = 0; i < records.length; i++) {
    const newAssignment = records[i] as SchedulerAssignment;
    // add assignment for new event
    if (newAssignment.eventId.startsWith('_generated')) {
        newAssignmentResourceIdRef.current[newAssignment.id] = [
            newAssignment.eventId,
            newAssignment.resourceId
        ];
        // add assignment for existing event
    }
    else {
        try {
            const insertData: Partial<SchedulerAssignmentDataverse> = {};
            if (newAssignment?.eventId) {
                insertData[
            `${databasePrefix}eventid@odata.bind`
                ] = `/${databasePrefix}${eventsDataverseTableName}es(${newAssignment.eventId})`;
            }
            if (newAssignment?.resourceId) {
                insertData[
            `${databasePrefix}resourceid@odata.bind`
                ] = `/${databasePrefix}${resourcesDataverseTableName}es(${newAssignment.resourceId})`;
            }
            return props?.context?.webAPI
                .createRecord(
            `${databasePrefix}${assignmentsDataverseTableName}`,
            insertData
                )
                .then((res) => {
                    if (scheduler?.current?.instance) {
                        scheduler.current.instance.assignmentStore.applyChangeset({
                            updated : [
                                // Will set proper id for added assignment
                                {
                                    $PhantomId : newAssignment.id,
                                    id         : res.id
                                }
                            ]
                        });
                    }
                });
        }
        catch (error) {
            console.error(error);
        }
    }
}

Add the newAssignmentResourceIdRef React ref declaration at the top of the component:

  const newAssignmentResourceIdRef = useRef<NewAssignments>({});

Add the NewAssignments type to the top of the file below the imports.

type NewAssignments = {
  // key is assignment phantomId, value is [eventId, resourceId]
  [key: string]: string[];
};

If the new assignment has an eventId that starts with “_generated“, it’s an assignment created for an event that’s been created. Creating a new event also triggers a new assignment to be created. The event has a phantom ID, which is an auto-generated client-side ID. An ID has not been assigned to the event by Dataverse yet.

We save the new assignment data in the newAssignmentResourceIdRef ref so that we can create the assignment once a Dataverse ID has been assigned to the event.

If the assignment is for an event that already exists in the Dataverse database, we create a new assignment using the createRecord Web API method.

Deleting Dataverse data using the Web API deleteRecord method

Add the following code to the if statement where storeId === "events" and action === "remove" are:

records.forEach((record) => {
    if (record.data.id.startsWith('_generated')) return;
    try {
        return props?.context?.webAPI
            .deleteRecord(
          `${databasePrefix}${eventsDataverseTableName}`,
          record.data.id
            )
            .then((res) => {
                console.log('deleteRecord: ', { res });
            });
    }
    catch (error) {
        console.error(error);
    }
});

We call the Web API deleteRecord method and pass in the event id as an argument.

Add the following code to the if statement where storeId === "resources" and action === "remove" are:

records.forEach((record) => {
    if (record.data.id.startsWith('_generated')) return;
    try {
        return props?.context?.webAPI
            .deleteRecord(
          `${databasePrefix}${resourcesDataverseTableName}`,
          record.data.id
            )
            .then((res) => {
                console.log('deleteRecord: ', { res });
            });
    }
    catch (error) {
        console.error(error);
    }
});

Add the following code to the if statement where storeId === "dependencies" and action === "remove" are:

records.forEach((record) => {
    if (record.data.id.startsWith('_generated')) return;
    try {
        return props?.context?.webAPI
            .deleteRecord(
          `${databasePrefix}${dependenciesDataverseTableName}`,
          record.data.id
            )
            .then((res) => {
                console.log('deleteRecord: ', { res });
            });
    }
    catch (error) {
        console.error(error);
    }
});

Add the following code to the if statement where storeId === "assignments" and action === "remove" are:

records.forEach((record) => {
    if (record.data.id.startsWith('_generated')) return;
    try {
        return props?.context?.webAPI
            .deleteRecord(
          `${databasePrefix}${assignmentsDataverseTableName}`,
          record.data.id
            )
            .then((res) => {
                console.log('deleteRecord: ', { res });
            });
    }
    catch (error) {
        console.error(error);
    }
});

Updating Dataverse data using the Web API updateRecord method

Add the following code to the if statement where storeId === "events" and action === "update" are:

for (let i = 0; i < records.length; i++) {
    // create event
    if (records[i].data.id.startsWith('_generated')) {
        if (disableCreate.current === true) return;
        try {
            const newEvent = records[i] as SchedulerEvent;
            const newEventPhantomId = records[i].data.id;
            const insertData: Partial<SchedulerEventDataverse> = {};
            if (newEvent?.name) {
                insertData[`${databasePrefix}name`] = newEvent.name;
            }
            if (newEvent?.startDate) {
                insertData[`${databasePrefix}startdate`] = newEvent.startDate;
            }
            if (newEvent?.endDate) {
                insertData[`${databasePrefix}enddate`] = newEvent.endDate;
            }
            if (newEvent?.readOnly) {
                insertData[`${databasePrefix}readonly`] = Boolean(
                    newEvent.readOnly
                );
            }
            if (newEvent?.timeZone) {
                insertData[`${databasePrefix}timezone`] = newEvent.timeZone;
            }
            if (newEvent?.draggable) {
                insertData[`${databasePrefix}draggable`] = Boolean(
                    newEvent.draggable
                );
            }
            if (newEvent?.resizable) {
                insertData[
            `${databasePrefix}resizable`
                ] = `${newEvent.resizable}`;
            }
            if (newEvent?.children) {
                insertData[`${databasePrefix}children`] = newEvent.children;
            }
            if (newEvent?.allDay) {
                insertData[`${databasePrefix}allday`] = Boolean(
                    newEvent.allDay
                );
            }
            if (newEvent?.duration) {
                insertData[`${databasePrefix}duration`] = Number(
                    newEvent.duration
                );
            }
            if (newEvent?.durationUnit) {
                insertData[`${databasePrefix}durationunit`] =
            newEvent.durationUnit;
            }
            if (newEvent?.exceptionDates) {
                insertData[`${databasePrefix}exceptiondates`] = JSON.stringify(
                    newEvent.exceptionDates
                );
            }
            if (newEvent?.recurrenceRule) {
                insertData[`${databasePrefix}recurrencerule`] =
            newEvent.recurrenceRule;
            }
            if (newEvent?.cls) {
                insertData[`${databasePrefix}cls`] = newEvent.cls;
            }
            if (newEvent?.eventColor) {
                insertData[`${databasePrefix}eventcolor`] = newEvent.eventColor;
            }
            if (newEvent?.eventStyle) {
                insertData[`${databasePrefix}eventstyle`] = newEvent.eventStyle;
            }
            if (newEvent?.iconCls) {
                insertData[`${databasePrefix}iconcls`] = newEvent.iconCls;
            }
            if (newEvent?.style) {
                insertData[`${databasePrefix}style`] = newEvent.style;
            }
            const createEventRecordRes =
          await props?.context?.webAPI.createRecord(
            `${databasePrefix}${eventsDataverseTableName}`,
            insertData
          );
            if (scheduler?.current?.instance && createEventRecordRes) {
                scheduler.current.instance.eventStore.applyChangeset({
                    updated : [
                        // Will set proper id for added event
                        {
                            $PhantomId : newEventPhantomId,
                            id         : createEventRecordRes.id
                        }
                    ]
                });
            }
            // create assignment for new event
            let newAssignmentData: string[] = [];
            for (const [key, value] of Object.entries(
                newAssignmentResourceIdRef.current
            )) {
                if (value[0] === newEventPhantomId) {
                    newAssignmentData = [key, ...value];
                    const insertAssignmentsData: Partial<SchedulerAssignmentDataverse> = {};
                    if (newAssignmentData.length !== 0 && createEventRecordRes) {
                        insertAssignmentsData[
                      `${databasePrefix}eventid@odata.bind`
                        ] = `/${databasePrefix}${eventsDataverseTableName}es(${createEventRecordRes?.id})`;
                        const resourceId = newAssignmentData[2];
                        insertAssignmentsData[
                      `${databasePrefix}resourceid@odata.bind`
                        ] = `/${databasePrefix}${resourcesDataverseTableName}es(${resourceId})`;
                        insertAssignmentsData[
                      `${databasePrefix}eventid@odata.bind`
                        ] = `/${databasePrefix}${eventsDataverseTableName}es(${createEventRecordRes.id})`;
                        const createAssignmentRecordRes =
                      await props?.context?.webAPI.createRecord(
                        `${databasePrefix}${assignmentsDataverseTableName}`,
                        insertAssignmentsData
                      );
                        if (scheduler?.current?.instance && createAssignmentRecordRes) {
                            scheduler.current.instance.assignmentStore.applyChangeset({
                                updated : [
                                    // Will set proper id for added assignment
                                    {
                                        $PhantomId : newAssignmentData[0],
                                        id         : createAssignmentRecordRes.id
                                    }
                                ]
                            });
                            const newObj = {
                                ...newAssignmentResourceIdRef.current
                            };
                            delete newObj[newAssignmentData[0]];
                            newAssignmentResourceIdRef.current = newObj;
                        }
                    }
                }
            }
        }
        catch (error) {
            console.error(error);
        }
        // update event
    }
    else {
        try {
            if (records[i].data.id.startsWith('_generated')) continue;
            const event = records[i];
            const updateData: Partial<SchedulerEventDataverse> = {};
            if (event?.name) {
                updateData[`${databasePrefix}name`] = event.name;
            }
            if (event?.startDate) {
                updateData[`${databasePrefix}startdate`] = event.startDate;
            }
            if (event?.endDate) {
                updateData[`${databasePrefix}enddate`] = event.endDate;
            }
            if (event?.readOnly) {
                updateData[`${databasePrefix}readonly`] = Boolean(
                    event.readOnly
                );
            }
            if (event?.timeZone) {
                updateData[`${databasePrefix}timezone`] = event.timeZone;
            }
            if (event?.draggable) {
                updateData[`${databasePrefix}draggable`] = Boolean(
                    event.draggable
                );
            }
            if (event?.resizable) {
                updateData[`${databasePrefix}resizable`] = `${event.resizable}`;
            }
            if (event?.children) {
                updateData[`${databasePrefix}children`] = event.children;
            }
            if (event?.allDay) {
                updateData[`${databasePrefix}allday`] = Boolean(event.allDay);
            }
            if (event?.duration) {
                updateData[`${databasePrefix}duration`] = Number(
                    event.duration
                );
            }
            if (event?.durationUnit) {
                updateData[`${databasePrefix}durationunit`] =
            event.durationUnit;
            }
            if (event?.exceptionDates) {
                updateData[`${databasePrefix}exceptiondates`] = JSON.stringify(
                    event.exceptionDates
                );
            }
            if (event?.recurrenceRule) {
                updateData[`${databasePrefix}recurrencerule`] =
            event.recurrenceRule;
            }
            if (event?.cls) {
                updateData[`${databasePrefix}cls`] = event.cls;
            }
            if (event?.eventColor) {
                updateData[`${databasePrefix}eventcolor`] = event.eventColor;
            }
            if (event?.eventStyle) {
                updateData[`${databasePrefix}eventstyle`] = event.eventStyle;
            }
            if (event?.iconCls) {
                updateData[`${databasePrefix}iconcls`] = event.iconCls;
            }
            if (event?.style) {
                updateData[`${databasePrefix}style`] = event.style;
            }
            return props?.context?.webAPI
                .updateRecord(
            `${databasePrefix}${eventsDataverseTableName}`,
            event.id,
            updateData
                )
                .then((res) => {
                    console.log('webAPI.updateRecord for events response: ', res);
                });
        }
        catch (error) {
            console.error(error);
        }
    }
}

Now add the following disableCreate React ref at the top of the component:

  const disableCreate = React.useRef(false);

Add onBeforeDragCreate and onAfterDragCreate event listeners to the BryntumScheduler component:

  onBeforeDragCreate={onBeforeDragCreate}
  onAfterDragCreate={onAfterDragCreate}

Add the functions called when these events occur inside the BryntumScheduler component:

  function onBeforeDragCreate() {
    disableCreate.current = true;
  }
  function onAfterDragCreate() {
    disableCreate.current = false;
  }

This sets the disableCreate ref variable to true during a drag-create event.

We create an event when a newly created event is first updated. To do this, we use the disableCreate flag variable. We determine if an event is a newly created one by checking if it has a phantom id, where its id starts with "_generated".

We need to disable the creating an event when the event is created using the Bryntum Scheduler EventDragCreate feature. This feature allows the user to create an event by clicking and dragging in the Bryntum Scheduler timeline. We disable creating the event in this case because an "add" event followed by an "update" event occurs. We prevent this update from creating an event so that an event is only created when a user clicks the “SAVE” button in the event editor popup menu.

If the event already exists, we update the event.

Now add the following code to the if statement where storeId === "resources" and action === "update":

for (let i = 0; i < records.length; i++) {
    try {
        if (records[i].data.id.startsWith('_generated')) return;
        const resource = records[i];
        const updateData: Partial<SchedulerResourceDataverse> = {};
        if (resource?.name) {
            updateData[`${databasePrefix}name`] = resource.name;
        }
        if (resource?.eventColor) {
            updateData[`${databasePrefix}eventcolor`] = resource.eventColor;
        }
        if (resource?.readOnly) {
            updateData[`${databasePrefix}readonly`] = Boolean(
                resource.readOnly
            );
        }
        return props?.context?.webAPI
            .updateRecord(
          `${databasePrefix}${resourcesDataverseTableName}`,
          resource.id,
          updateData
            )
            .then((res) => {
                console.log(
                    'webAPI.updateRecord for resources response: ',
                    res
                );
            });
    }
    catch (error) {
        console.error(error);
    }
}

Add the following code to the if statement where storeId === "dependencies" and action === "update":

for (let i = 0; i < records.length; i++) {
    try {
        if (records[i].data.id.startsWith('_generated')) return;
        const dependency = records[i];
        const updateData: Partial<SchedulerDependencyDataverse> = {};
        if (dependency?.type) {
            updateData[`${databasePrefix}type`] = Number(dependency.type);
        }
        if (dependency?.cls) {
            updateData[`${databasePrefix}cls`] = dependency.cls;
        }
        if (dependency?.lag) {
            updateData[`${databasePrefix}lag`] = Number(dependency.lag);
        }
        if (dependency?.lagUnit) {
            updateData[`${databasePrefix}lagunit`] = dependency.lagUnit;
        }
        if (dependency?.fromSide) {
            updateData[`${databasePrefix}fromside`] = dependency.fromSide;
        }
        if (dependency?.toSide) {
            updateData[`${databasePrefix}toside`] = dependency.toSide;
        }
        if (dependency?.from) {
            updateData[
          `${databasePrefix}from@odata.bind`
            ] = `/${databasePrefix}${eventsDataverseTableName}es(${dependency.from})`;
        }
        if (dependency?.to) {
            updateData[
          `${databasePrefix}to@odata.bind`
            ] = `/${databasePrefix}${eventsDataverseTableName}es(${dependency.to})`;
        }
        return props?.context?.webAPI
            .updateRecord(
          `${databasePrefix}${dependenciesDataverseTableName}`,
          dependency.id,
          updateData
            )
            .then((res) => {
                console.log(
                    'webAPI.updateRecord for dependencies response: ',
                    res
                );
            });
    }
    catch (error) {
        console.error(error);
    }
}

For the last CRUD operation, add the following code to the if statement where storeId === "assignments" and action === "update" are:

for (let i = 0; i < records.length; i++) {
    try {
        if (records[i].data.id.startsWith('_generated')) return;
        const assignment = records[i];
        const updateData: Partial<SchedulerAssignmentDataverse> = {};
        if (assignment?.eventId) {
            updateData[
          `${databasePrefix}eventid@odata.bind`
            ] = `/${databasePrefix}${eventsDataverseTableName}es(${assignment.eventId})`;
        }
        if (assignment?.resourceId) {
            updateData[
          `${databasePrefix}resourceid@odata.bind`
            ] = `/${databasePrefix}${resourcesDataverseTableName}es(${assignment.resourceId})`;
        }
        return props?.context?.webAPI
            .updateRecord(
          `${databasePrefix}${assignmentsDataverseTableName}`,
          assignment.id,
          updateData
            )
            .then((res) => {
                console.log(
                    'webAPI.updateRecord for assignments response: ',
                    res
                );
            });
    }
    catch (error) {
        console.error(error);
    }
}

Publish the app:

You now have a Bryntum Scheduler Power Apps component with CRUD functionality.

:warning: If your component does not update, this may be because Power Apps caches the component for the page, even after updating the component. You may need to do the following to see the changes:

  1. Publish the app.
  2. Create a new page.
  3. Publish the app again.

Next steps

Now that you know how to create a Bryntum Scheduler Power Apps component, you can create other Power Apps components using any Bryntum components. You can also use multiple Bryntum components together.

Creating a Bryntum Scheduler Power Apps component is similar to creating a custom Salesforce component. To learn more, read our blog post on using Bryntum Scheduler Pro as a Salesforce Lightning Web Component.

Arsalan Khattak

Bryntum Scheduler Microsoft