The Project model and data entities

Logical structure

Bryntum Scheduler Pro operates on a top-level entity called a "Project". A Project consists of several collections of other entities. Collections are created as instances of the Store class in the Bryntum Core package and individual entities are represented by Model instances.

Bryntum Scheduler Pro manages its project data using a [Scheduling Engine](scheduling engine). The engine is a separate project written in TypeScript. Scheduler Pro makes use of mixins defined in the Scheduling Engine to build actual classes that are used to store and manage the data. If you are new to the mixin concept, you can check this blog post to become familiar with it. The main idea is that you can freely combine several mixins and not be tied to a single superclass in the hierarchy (as you would be when using classic single superclass inheritance).

The "Project" itself is represented by a ProjectModel. It is a regular Model, capable of storing any project-wide configuration options.

The primary collections of the Project are:

Please refer to the documentation of mentioned classes above for detailed lists of available fields and methods.

Creating a project

A project can be created as any other regular Model instance. For example, here we create a project with a specified start date and inlined data for the internal stores :

const project = new ProjectModel({
    startDate  : '2020-01-01',

    events : [
        { id : 1, name : 'Proof-read docs', startDate : '2020-01-02', endDate : '2020-01-09' },
        { id : 2, name : 'Release docs', startDate : '2020-01-09', endDate : '2020-01-10' }
    ],

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

All project stores are created from scratch, but they will remain empty if no data was provided to them. When using a framework, the recommended approach is to use the ProjectModel component and pass it to the Calendar. To learn more, checkout the Angular, React, and Vue demos.

Working with inline data

The project provides an inlineData getter/setter that can be used to manage data from all Project stores at once. Populating the stores this way can be useful if you do not want to use the CrudManager for server communication but instead load data using Axios or similar.

Please supply assignments in the `assignmentsData` section. Scheduler Pro differs from Scheduler in that it does not support single assignment using `event.resourceId`.

Getting data

const data = scheduler.project.inlineData;

/*
Structure of inlineData:

data = {
    resourcesData : [/*...*/],
    eventsData : [/*...*/],
    dependenciesData : [/*...*/],
    assignmentsData : [/*...*/]
}
/*

// use the data in your application

Setting data

// Get data from server manually
const data = await axios.get('/project?id=12345');

// Feed it to the project
scheduler.project.inlineData = data;

See also loadInlineData

Getting changed records

You can access the changes in the current Project dataset anytime using the changes property. It returns an object with all changes:

const changes = project.changes;

console.log(changes);

> {
tasks : {
    updated : [{
        name : 'My task',
        id   : 12
    }]
},
assignments : {
    added : [{
        event      : 12,
        resource   : 7,
        units      : 100,
        $PhantomId : 'abc123'
    }]
    }
};

Working with remote data

Scheduler Pro project implements the Crud Manager API for working with remote data. There are two main methods for that on the ProjectModel class:

  • load method is used for loading data from a remote path
  • sync method is used for sending changes to a remote path.

You can use the loadUrl and syncUrl configs to specify URLs for data loading and syncing respectively.

const project = new ProjectModel({
    // load data from
    loadUrl   : 'php/load.php',
    // send data changes to
    syncUrl   : 'php/sync.php',
    // trigger loading automatically
    autoLoad  : true,
    // trigger changes sending automatically
    autoSync  : true,
});

Where autoLoad and autoSync configs in the above snippet result in triggering of data loading and syncing automatically.

For more details on the Crud Manager protocol including request and response formats please see this guide.

Custom handling of persisting project changes

If you want to manage saving yourself not involving the communication API provided by the Project, you can access the current changes in the Project anytime using the changes property.

const taskStore    = schedulerpro.project.taskStore
taskStore.getById(1).name = 'New name'

console.log(project.changes);

It returns an object:

{
     tasks : {
         modified : [
             { id : 1, name : 'New name' }
         ]
    }
}

For more general information about the Crud manager architecture please refer to this guide.

Monitoring data changes

While it is possible to listen for data changes on the projects individual stores, it is sometimes more convenient to have a centralized place to handle all data changes. By listening for the change event your code gets notified when data in any of the stores changes. Useful for example to keep an external data model up to date:

const scheduler = new SchedulerPro({
    project: {
        listeners : {
            change({ store, action, records }) {
                const { $name } = store.constructor;

                if (action === 'add') {
                    externalDataModel.add($name, records);
                }

                if (action === 'remove') {
                    externalDataModel.remove($name, records);
                }
            }
        }
    }
});

Depending on your use case, listening to the hasChanges event is also an option. This event is fired whenever a change to any store leads to the project having persistable changes. It does not supply any info on the actual changes, see "Getting changed records" above for that.

Adding custom fields to entities

You can add any number of custom fields to any entity of the project (including the project itself). To do this, simply subclass the entity and add any additional fields in the accessor for static fields property. For example, to add a company field to the ResourceModel:

class MyResourceModel extends ResourceModel {
    static get fields() {
        return [
            { name : 'company', type : 'string' }
        ]
    }
}

When creating the project, configure to use the newly defined model class using the resourceModelClass config. There are analogous configs for the other entities managed by the project.

class MyResourceModel extends ResourceModel {
    static get fields() {
        return [
            { name : 'company', type : 'string' }
        ]
    }
}

const project = new ProjectModel({
    resourceModelClass  : MyResourceModel
});

See the API docs for Model for more information on defining and mapping fields.

If you want to use a custom store type for some collection, do it in the same way - first subclass the store class, then use resourceStoreClass config or similar during the project creation.

Customizing a project store

There are multiple ways to customize a project store. The easiest one is to pass a configuration object for it within the project's configuration:

project : {
    // other config
    assignmentStore : {
        allowNoId      : false,
        writeAllFields : true,
        listeners      : {
            remove : () => { 
                // This listener runs after a record is removed from the store
            }
        }
    }
}

Another way to create a custom store with simple configurations is to create a new instance of store with your configuration. This is used when you want to reuse it in multiple places.

const customAssignmentStore = new AssignmentStore({
    allowNoId      : false,
    writeAllFields : true,
    listeners      : {
        remove : () => { 
            // This listener runs after a record is removed from the store
        }
    }
});

Next, you can pass it to the project:

project : {
    // other config
    assignmentStore : customAssignmentStore
}

You can subclass it if you are going to use it in multiple places, or if you want to place it in a separate file/module for code organization:

class CustomEventStore extends EventStore {
    static $name = 'CustomEventStore';
    static configurable = {
        allowNoId      : false,
        writeAllFields : true,
        listeners      : {
            remove : () => { 
                // This listener runs after a record is removed from the store
            }
        },
        tree : true
    };
}

Then create a new instance of it and pass it with the project config when creating a new SchedulerPro instance:

const customEventStore = new CustomEventStore();

const schedulerpro = new SchedulerPro({
    // other config
    project :{
        eventStore : customEventStore
    }
});

You can confirm it doing console.log(schedulerpro.eventStore).

Updating the project data

The Model class lets you define special properties of the class, called "fields". Defining a field will auto-create accessors to get/set its value, so the usage looks very much like a regular JS object property:

class MyModel extends Model {
    static get fields() {
        return [
            { name : 'myField', type : 'string' }
        ]
    }
}

const myModel = new MyModel({ myField : 'someValue' })

// read
const lowerCased = myModel.myField.toLowerCase()
// write
myModel.myField = 'anothervalue'

Data consistency

One important thing to understand is that the scheduling of events is performed asynchronously. So if you have assigned a new value for the startDate field of some event, the corresponding update of the endDate field will not happen immediately:

const event1        = project.eventStore.add({ id : 'event1', startDate : new Date(2020, 1, 1), duration : 1 })

event1.startDate    = new Date(2020, 2, 1)

// still `null` or previous value
event1.endDate

Instead, all data changes are batched together in a single transaction and an "auto-commit" is scheduled (using a 0ms setTimeout()). During the commit, consistent values for all fields across the project will be calculated. When the updated values for all entity fields are available, an update event on the corresponding Store collection will be triggered.

You can also trigger a commit immediately, using the project.commitAsync() method call:

const event1        = project.eventStore.add({ id : 'event1', startDate : new Date(2020, 1, 1), duration : 1 })

event1.startDate    = new Date(2020, 2, 1)

await project.commitAsync()

// contains consistent value now
event1.endDate

For convenience, there are also generated setter methods for many fields, which performs an update and triggers the commit immediately. A setter method is a regular method whose name starts with set and ends with the uppercased field name. For example, for the startDate field, the setter method will be: setStartDate.

Let's say that we want to change the start date of a event. Using async/await (wrapped with an extra function, since global async/await is still in the proposal stage) it will look like:

const updateStartDate = async () => {
    const eventStore    = project.eventStore
    const event         = eventStore.getById(1)

    await event.setStartDate(new Date(2020, 3, 25))

    ... // continue after start date update
}

Such asynchronous setters methods are explicitly marked as returning Promise in the documentation.

Propagating method

Updating data on event change

Sometime it you may want to update an event based on changes done to another event. To do this, you can subscribe to the change event, then assure that the action is correct (update). You should also ensure that State Tracking Manager is not restoring data, otherwise restoring will trigger recalculation again.

For example, if you need to set all predecessors to 100% done when some reaches gets 100%, or reset them to 0% otherwise:

eventStore.on({
    change : ({ action, record, changes }) => {
        const project = record.project;

        if (action === 'update' && changes && changes.percentDone != null && !project.getStm().isRestoring) {
            record.predecessors.forEach(event => event.percentDone = changes.percentDone.value === 100 ? 100 : 0);
        }
    }
});