Crud Manager in the SchedulerPro

Introduction

This guide describes how to use the Crud Manager in Bryntum SchedulerPro. It contains only SchedulerPro specific details. For general information on Crud Manager implementation and architecture see this guide.

In the SchedulerPro, Crud Manager mixins are applied to the ProjectModel class. So each project is capable of loading and saving its data using the Crud Manager protocol. It uses the Fetch API as transport system and JSON as encoding format.

SchedulerPro stores

There are a lot of stores in the SchedulerPro. They are used for keeping resources, assignments, calendars, dependencies, timeRanges, resourceTimeRanges and events. The stores reference each others records and are joined together by a project.

Providing Crud Manager functionality to ProjectModel allows handling loading and persisting of the stores.

Project creates related stores by default, and in case you need to provide your own store instances or store configuration objects, there are corresponding configs:

StoreDescription
resourceStoreHolds a collection of resources
eventStoreHolds a collection of events
assignmentStoreHolds a collection of assignments
dependencyStoreHolds a collection of dependencies
timeRangeStoreHolds a collection of time rs
calendarManagerStoreHolds a collection of calendars
resourceTimeRangeStoreHolds a collection of resource rs

Here's how a basic configuration could look:

const project = new ProjectModel({
    autoLoad : true,
    // we want to provide a custom store for events
    eventStore,
    loadUrl : 'php/read.php',
    syncUrl : 'php/save.php'
});

You can either pass the project instance to SchedulerPro:

const schedulerpro = new SchedulerPro({
    // ... other configs
    project : project
});

or pass the configuration object directly without first creating an instance:

const schedulerpro = new SchedulerPro({
    // ... other configs
    project : {
        autoLoad : true,
        // we want to provide a custom store for events
        eventStore,
        loadUrl : 'php/read.php',
        syncUrl : 'php/save.php'
    }
});

Load inline data

You can also load inline data, which can either be loaded during the initialization:

const schedulerpro = new SchedulerPro({
    // ... other configs
    project : {
        resources : [
            { id : 1, name : 'Linda', city : 'NY' },
            { id : 2, name : 'Olivia', city : 'Paris' }
        ],

        dependencies : [
            { fromEvent : 1, toEvent : 2 }
        ],

        assignments : [
            { event : 1, resource : 1 },
            { event : 2, resource : 2 }
        ],
        events : [
            { id : 1, name : 'Proof-read docs', startDate : '2017-01-02', endDate : '2017-01-09' },
            { id : 2, name : 'Release docs', startDate : '2017-01-09', endDate : '2017-01-10' }
        ],
    }
});

In case of frameworks, we recommend using ProjectModel.

<bryntum-scheduler-pro-project-model
    #project
    [events] = "projectData.events"
    [assignments] = "projectData.assignments"
    [resources] = "projectData.resources"
></bryntum-scheduler-pro-project-model>
<bryntum-scheduler-pro
    #schedulerpro
    [project] = "project"
    [columns] = "schedulerproProps.columns"
    [columnField] = "schedulerproProps.columnField"
></bryntum-scheduler-pro>
import { DateHelper } from '@bryntum/schedulerpro';
import { BryntumSchedulerProProps, BryntumSchedulerProProjectModelProps } from '@bryntum/schedulerpro-angular';

export const projectProps: BryntumSchedulerProProjectModelProps = {
    resources : [
        { id : 1, name : 'Linda' },
        { id : 2, name : 'Olivia' }
    ],

    assignments : [
        { event : 1, resource : 1 },
        { event : 2, resource : 2 }
    ],

    events : [
        { id : 1, name : 'Proof-read docs', startDate : '2022-01-01T10:00', endDate : '2022-01-02T13:00' },
        { id : 2, name : 'Release docs', startDate : '2022-01-09T15:00', endDate : '2022-01-10T16:00' }
    ]
};

export const schedulerproProps: BryntumSchedulerProProps = {
    /* schedulerpro props here */
};

For better understanding, checkout our inline-data demos for Angular, React and Vue.

You can also load it later using loadInlineData(); It uses the same format as when creating a project with inline data:

await schedulerpro.project.loadInlineData({
        resources : [
            { id : 1, name : 'Linda', city : 'NY' },
            { id : 2, name : 'Olivia', city : 'Paris' }
        ],

        dependencies : [
            { fromEvent : 1, toEvent : 2 }
        ],

        assignments : [
            { event : 1, resource : 1 },
            { event : 2, resource : 2 }
        ],
        events : [
            { id : 1, name : 'Proof-read docs', startDate : '2017-01-02', endDate : '2017-01-09' },
            { id : 2, name : 'Release docs', startDate : '2017-01-09', endDate : '2017-01-10' }
        ],
});

Also read Working with inline data for more information.

Load request structure

The load request has a payload, which by default looks like:

{
    "type"      : "load",
    "requestId" : 17228564331330,
    "stores"    : [ "resources", "events", "assignments", "dependencies", ... ]
}

The stores property contains a list of all the stores the project uses. This property is optional and can be modified. You can use this information for any server-side implementation. For example when a backend should support loading of only certain project stores. In this case, one should pass the list of stores to load in that property.

To modify the stores property, you can use the beforeLoad event. This event allows you to customize a load request data before it's sent to the backend.

project.on('beforeLoad', ({ source, pack }) => {
    const updatedStores = ['dependencies', 'assignments'];
    pack.stores = updatedStores;
});

Alternatively, you can remove a store from the project, which will exclude it from the load request payload. Of course in such case the project will not serve the removed store (not load its data nor sync its changes). So your application will have to do it manually.

const project = new ProjectModel({});
project.removeCrudStore("assignments");

By using this method, the assignmentStore will not receive any data, ensuring it remains empty regardless of the backend response.

Load response structure

The backend (in the above case it's "php/read.php" script) should return a JSON similar to the one seen below:

{
    "success" : true,

    "project" : {
        "calendar"     : 10,
        "startDate"    : "2019-01-14",
        "hoursPerDay"  : 24,
        "daysPerWeek"  : 5,
        "daysPerMonth" : 20
    },

    "calendars" : {
        "rows" : [
            {
                "id"        : 10,
                "name"      : "General",
                "intervals" : [
                    {
                        "recurrentStartDate" : "on Sat",
                        "recurrentEndDate"   : "on Mon",
                        "isWorking"          : false
                    }
                ]
            }
        ]
    },

    "dependencies" : {
        "rows" : [
            {
                "id"      : 1,
                "from"    : 11,
                "to"      : 17,
                "type"    : 2,
                "lag"     : 0,
                "lagUnit" : "d"
            }
        ]
    },

    events : {
        "rows" : [
            {
                "id"          : 11,
                "name"        : "Investigate",
                "percentDone" : 50,
                "startDate"   : "2021-02-08",
                "endDate"     : "2021-02-13",
                "duration"    : 5
            },
            {
                "id"          : 12,
                "name"        : "Assign resources",
                "percentDone" : 50,
                "startDate"   : "2021-02-08",
                "endDate"     : "2021-02-20",
                "duration"    : 10
            },
            {
                "id"          : 17,
                "name"        : "Report to management",
                "percentDone" : 0,
                "startDate"   : "2021-02-20",
                "endDate"     : "2021-02-20",
                "duration"    : 0
            }
        ]
    },

    "resources" : {
        "rows" : [
            {
                "id"   : 1,
                "name" : "Mats"
            },
            {
                "id" : 2,
                "name" : "Nickolay"
            }
        ]
    },

    "assignments" : {
        "rows" : [
            {
                "id"       : 1,
                "event"    : 11,
                "resource" : 1,
                "units"    : 80
            }
        ]
    }
}

The above response sections contain corresponding stores data which are covered in the following chapters.

Project data

The project reads values of its own fields from the project section of the responses. In the above example it looks this:

{
    ...

    "project" : {
        "startDate"    : "2010-01-18",
        "calendar"     : 12,
        "hoursPerDay"  : 8,
        "daysPerWeek"  : 5,
        "daysPerMonth" : 20
    }
}

Please check ProjectModel docs for the full list of the project fields.

Events data

The project reads events from the events section of load response. The records are provided as an array of objects under the rows property. In the provided response example it looks this:

{
    ...

    "events": {
        "rows": [
            {
                "id"          : 11,
                "name"        : "Investigate",
                "percentDone" : 50,
                "startDate"   : "2021-02-08",
                "endDate"     : "2021-02-13",
                "duration"    : 5
            },
            {
                "id"          : 12,
                "name"        : "Assign resources",
                "percentDone" : 50,
                "startDate"   : "2021-02-08",
                "endDate"     : "2021-02-20",
                "duration"    : 10
            },
            {
                "id"          : 17,
                "name"        : "Report to management",
                "percentDone" : 0,
                "startDate"   : "2021-02-20",
                "endDate"     : "2021-02-20",
                "duration"    : 0
            }
        ]
    }
}

Each object in the events.rows array represents a EventModel where each object key represents an event field. See EventModel fields for the full list of event fields.

Resources data

The project reads resources from the resources section of the load response. The records are provided as an array of objects under the rows property. In the provided response example it looks this:

{
    ...

    "resources" : {
        "rows" : [
            {
                "id"   : 1,
                "name" : "Mats"
            },
            {
                "id"   : 2,
                "name" : "Nickolay"
            }
        ]
    }
}

Each object in the resources.rows array represents a ResourceModel where each object key represents a resource field. See ResourceModel fields for the full list of resource fields.

Assignments data

Assignments specify resources usage for certain events. The project reads them from the assignments section of the load response. The records are provided as an array of objects under the rows property. In the provided response example it looks this:

{
    ...

    "assignments" : {
        "rows" : [
            {
                "id"       : 1,
                "event"    : 11,
                "resource" : 1,
                "units"    : 80
            }
        ]
    }
}

Each object in the assignments.rows array represents an AssignmentModel where each object key represents an assignment field. See AssignmentModel fields for the full list of assignment fields.

Calendars data

Calendars in the SchedulerPro define working/non-working periods of time for resources or events. The project reads their data from the load response calendars section. The records are provided as an array of objects under the rows property. In the provided response example it looks this:

{
    ...

    "calendars" : {
        "rows" : [
            {
                "id"        : 10,
                "name"      : "General",
                "intervals" : [
                    {
                        "recurrentStartDate" : "on Sat",
                        "recurrentEndDate"   : "on Mon",
                        "isWorking"          : false
                    }
                ]
            }
        ]
    }
}

Each object in the calendars.rows array represents a CalendarModel where each object key represents a calendar field. See CalendarModel fields for the full list of calendar fields.

Dependencies data

Task dependencies represent links between events that describe how events should be scheduled based on each other. The project reads them from the dependencies section of the load response. The records are provided as an array of objects under the rows property. In the provided response example it looks this:

{
    ...

    "dependencies": {
        "rows" : [
            {
                "id"      : 1,
                "from"    : 11,
                "to"      : 17,
                "type"    : 2,
                "lag"     : 0,
                "lagUnit" : "d"
            }
        ]
    }
}

Each object in the dependencies.rows array represents a DependencyModel where each object key represents a dependency field. See DependencyModel fields for the full list of dependency fields.

Sync request structure

Syncing includes changes for all linked stores in a single request, with sections for added, updated and removed records per store. For changes to the EventStore and the ResourceStore a sync request might look like this:

{
    "requestId" : 124,
    "type"      : "sync",
    "revision"  : 5,

    "events"     : {
        "added" : [
            { "$PhantomId" : "_generated5", "name" : "New event" }
        ],
        "updated" : [
            { "id" : 50, "startDate" : "2022-05-02" }
        ],
        "removed" : [
            { "id" : 9001 }
        ]
    },

    "resources"      : {
        "added" : [
            { "$PhantomId" : "_generated7", "name" : "Steven", "surname" : "Grant" }
        ]
    }
}

Each added record is sent should include its phantom identifier (auto-generated client side unique value used to identify the record) (by default the $PhantomId, field name is used). Please do not persist phantom record identifiers as-is on the server. That might cause collisions on the client after data reloading. It's expected that backend assigns new identifiers to added records.

Please note that by default, only changed fields and any fields configured with alwaysWrite are sent. If you want all fields to always be sent, please see writeAllFields.

For more details on the sync request structure, please see the generic Crud Manager in depth guide.

Sync response structure

The Response to a sync request should confirm that changes were applied and optionally update the client with any additional changes made on the server.

If there are no additional changes made on the server, a short sync response such as this one is enough:

{
    "success"   : true,
    "requestId" : 124,
    "revision"  : 6
}

The success attribute is by default optional for successful calls, and if you are not using revision validation the response can be made even shorter:

{
    "requestId" : 124
}

Whenever the server makes changes to the synced data, the new values must be part of the response. For example, when saving a new record the server provides a new value for its id, and that has to be included for the client side to use the correct id. This is a valid response to the sync request above:

{
    "success"     : true,
    "requestId"   : 124,
    "revision"    : 6,

    "events" : {
        "rows" : [
            { "$PhantomId" : "_generated5", "id" : 543, "added_dt" : "2022-05-02T11:30:00" }
        ]
    },

    "resources" : {
        "rows" : [
            { "$PhantomId" : "_generated7", "id" : 12 }
        ],
        "removed" : [
            { "id" : 5 }
        ]
    }
}

For each store there are two possible sections: rows and removed.

The rows section list data changes made by the server.

If the server decides to update any other field of any record it should return an object holding a combination of the record identifier and new field values (this is shown in above snippet where server sets added_dt field value). When adding a new record the server generates an identifier for it and responds with both old phantom identifier and the new identifier. The field values will be applied to the corresponding store record on the client.

Note that this way the server can also provide new records to the client by passing them in the rows section.

The removed section contains identifiers of records removed by the server, perhaps by another user since the last load or sync call. In the above snippet, the response includes removal of a resource with id 5, initiated by the server.

For more details on the sync request structure, please see the generic Crud Manager in depth guide.

Sending extra HTTP request parameters

Extra params may be added using transport configuration:

const project = new ProjectModel({
    transport : {
        load : {
            url    : 'php/read.php',
            method : 'POST',
            params : {
                userAccess : 'granted',
                viewId     : 'full'
            }
        },
        sync : {
            url : 'php/save.php'
        }
    }
});

Or dynamically by passing into load method:

project.load({
    request : {
        params : {
            startDate : '2021-01-01'
        }
    }
})

Or by listening to the beforeLoad event:

const project = new Project({
    loadUrl : 'php/read.php',
    syncUrl : 'php/save.php',
    listeners : {
        beforeLoad({ pack }){
            pack.params.includeHidden = false;
        }
    }
});

Dealing with extra stores

Since ProjectModel implements a Crud Manager you can provide any number of additional stores using the crudStores config:

const
    store1      = new Store({ id : 'store1' }),
    store2      = new Store({ id : 'store2' }),
    store3      = new Store({ id : 'store3' }),
    crudManager = new CrudManager({
          // Register additional stores, to also handle them
          // in a batch when loading data
          crudStores : [ store1, store2, store3 ],
          loadUrl    : 'php/read.php',
          syncUrl    : 'php/save.php'
    });

Or add them programmatically using the addCrudStore method:

project.addCrudStore([ store2, store3 ]);

Triggering loading and saving

In the following example the project will start data loading automatically due to the provided autoLoad config. In this case the project schedules asynchronous loading on construction stage:

const project = new ProjectModel({
      autoLoad : true,
      loadUrl : 'php/read.php',
      syncUrl : 'php/save.php'
});

And in order to start data loading manually the project has load method. The method returns a Promise that gets resolved once data is loaded and processed by the Scheduling Engine:

// load data
try {
    await project.load();
    console.log('Data loaded and processed...');
} catch (e) {
    console.log('Data loading error');
}

To persist changes automatically, there is the autoSync option. When set to true it causes project to react on data changes made in the registered stores and schedule data syncing. For example in the following snippet the project will trigger data saving (handled by php/save.php script) as soon as any registered store record gets changed:

const project = new ProjectModel({
    autoSync : true,
    loadUrl : 'php/read.php',
    syncUrl : 'php/save.php'
});

And of course manual saving is also possible with the sync method:

try {
    await project.sync();
    console.log('Changes saved...');
} catch(e) {
    console.log('Data saving error');
}

Response format validation

SchedulerPro project will validate responses and log found issues to the browser console. This should help implementing backend integration on development stage.

Example of the validation report:

Project sync response error(s):
- "events" store "rows" section should mention added record(s) #XXX sent in the request. It should contain the added records identifiers (both phantom and "real" ones assigned by the backend).
- "events" store "rows" section should mention updated record(s) #XXX sent in the request. It should contain the updated record identifiers.
- "events" store "removed" section should mention removed record(s) #XXX sent in the request. It should contain the removed record identifiers.
Please adjust your response to look like this:
{
    "events": {
        "removed": [
            {
                "id": XXX
            },
            ...
        ],
        "rows": [
            {
                "$PhantomId": XXX,
                "id": ...
            },
            {
                "id": XXX
            },
            ...
        ]
    }
}
Note: Please consider enabling "supportShortSyncResponse" option to allow less detailed sync responses (https://bryntum.com/products/scheduler/docs/api/Scheduler/crud/AbstractCrudManagerMixin#config-supportShortSyncResponse)
Note: To disable this validation please set the "validateResponse" config to false

The validation can be disabled with validateResponse config.