# Upgrade guide for Scheduler v4.0.0

Be sure to also check out the news in [Grid](#Grid/guides/upgrades/4.0.0.md).

## Dropped support for Edge 18 and older

We are not actively removing the fixes we have in place yet, but moving forward we will no longer add new fixes for
versions of Edge <= 18. If you are using an old version of Edge, we strongly encourage you to update to a new blink
based version.

Existing fixes are planned to be removed in version 5.0.

### New scheduler.lite.umd.js bundle to use with Angular

Bryntum Scheduler is delivered with a new UMD package without polyfills. This was done to avoid conflicts with any
external Promise polyfills, such as `ZoneAwarePromise` from `zone.js` for Angular applications. It fixes this runtime
error:

```shell
Zone.js has detected that ZoneAwarePromise (window|global).Promise has been overwritten.
Most likely cause is that a Promise polyfill has been loaded after Zone.js 
(Polyfilling Promise api is not necessary when zone.js is loaded. If you must load one, do so before loading zone.js.)
```

We recommend that all your Angular applications should be upgraded to use the new `scheduler.lite.umd.js` bundle. Also,
there should be no polyfills imported in `app/src/polyfills.ts` after `import 'zone.js/dist/zone';`. We have updated all
our Angular examples to use this new bundle. Make sure to update all usages of `scheduler.umd.js`
to `scheduler.lite.umd.js` within your application to avoid `Bundle included twice` runtime error.

**Old code:**

```javascript
import { Button, WidgetHelper } from 'bryntum-scheduler/scheduler.umd.js';
```

**New code:**

```javascript
import { Button, WidgetHelper } from 'bryntum-scheduler/scheduler.lite.umd.js';
```

If you wish to have IE/Edge compatibility, please follow the instructions in `app/src/polyfills.ts`.

## Project holding stores

Behind the scenes, Scheduler has been brought in line with Scheduler Pro and Gantt and now always uses a project to hold
its stores. The project also has a calculation engine, which is used to normalize dates and durations as described in
"Async date manipulation below".

For basic usage, this difference should not be noticeable. Scheduler still accepts a CrudManager, separate stores or
inline data the same way as before. But in addition, you now also have the option of assigning it a project:

```javascript
const project = new ProjectModel({
    eventStore      : { /*...*/ },
    resourceStore   : { /*...*/ },
    dependencyStore : { /*...*/ }
})

new Scheduler({
    project
})
```

The stores available on the project and affected by it are:

* `EventStore`
* `ResourceStore`
* `AssignmentStore`
* `DependencyStore`

### Advanced - Standalone stores

For advanced use cases, for example using standalone stores (not linked to a Scheduler), it is worth mentioning that the
stores are no longer directly linked to each other. They are instead linked to the project.

For example this old code:

```javascript
const resourceStore = new ResourceStore();

const eventStore = new EventStore({
    resourceStore,
    /*...*/
});

const assignments = eventStore.getAssignmentsForEvent(myEvent);
```

Now requires the stores to be part of a project:

```javascript
const resourceStore = new ResourceStore();

const eventStore = new EventStore({
    /*...*/
});

// Link the stores using a project
const project = new ProjectModel({
    resourceStore,
    eventStore
})

const assignments = eventStore.getAssignmentsForEvent(myEvent);
```

Please note that if using the stores with a Scheduler, they are automatically linked using a project.

## Always using an AssignmentStore

In addition to always having a behind the scenes project, Scheduler now also always has an `AssignmentStore`. Please
note that it still supports single assignment using `resourceId`, see the next section for more information.

The decision to always use an `AssignmentStore` was taken based on:
A) It allows us to simplify the core code. There used to be many places where single assignment was treated one way and
multi assignment another. Now there is a single code path in these places instead. B) It forces us to support multi
assignment in all features. Support for multi assignment was added after the 1.0 release and not all features supported
it earlier.

In most scenarios, this change should not be noticeable.

### Single assignment

As mentioned above, Scheduler always has an `AssignmentStore` but it still supports single assignment the old way,
using `resourceId`:

```javascript
new Scheduler({
    events    : [
        { id : 1, resourceId : 'r1', /*...*/ },
        { id : 2, resourceId : 'r1', /*...*/ }
    ],
    resources : [
        { id : 'r1' },
        { id : 'r2' }
    ]
});
```

For the code above Scheduler will automatically create an `AssignmentStore` and populate it with the required records.
Pseudo code for the result of the above:

```javascript
behindTheScenesAssignmentStore.data = [
    { eventId : 1, resourceId : 'r1' },
    { eventId : 2, resourceId : 'r1' }
];
```

For most apps using single assignment, this should happen automatically and not be noticeable.

Please note that it is not supported to mix the `resourceId` approach while simultaneously using multi assignment.

### Event selection

The Event Selection API has been adapted to better support multi assignment. Previously its functions and accessors
would either return events or assignments, depending on if an `AssignmentStore` was used or not. This has now been
normalized in such way that `selectedEvents` now always returns event records and the new `selectedAssignments` always
returns assignments.

If you relied on `selectedEvents` returning assignments, you will have to adapt your code.

Affected APIs:

* `selectedEvents`, added `selectedAssignments`
* `isEventSelected()`, added `isAssignmentSelected()`
* `selectEvent()`, added `selectAssignment()` and `select()` that accepts either or
* `deselectEvent()`, added `deselectAssignment()` and `deselect()` that accepts either or
* `selectEvents()`, added `selectAssignments`()
* `deselectEvents()`, added `deselectAssignments`()
* Added an `assignmentSelectionChange` event

## Async date manipulation

As mentioned in the "Project holding stores" section, date manipulation is now handled using a calculation engine. This
matches how it works in Scheduler Pro and Gantt, keeping our API more consistent between products. The biggest impact
this has on Scheduler is that date manipulation turns async. Consider this old code:

```javascript
// Event record must be inside a Project's Store to participate in normalization
const [event] = myScheduler.eventStore.add({
    startDate : new Date(2020, 5, 22),
    duration  : 2,
    endDate   : new Date(2020, 5, 24)
});

event.duration = 4;

console.log(event.endDate);
```

The console would show that `endDate` is now the 26th, since the calculation happens in sync. With the new approach, the
calculation is now async and thus the `endDate` is not yet calculated when it is logged to the console.

Assigning `4` to `duration` is now considered a proposed change, and all proposals are calculated when project changes
are committed. The project is committed automatically on a buffer, but can also be committed manually if you need the
result of the calculations right away. The above code would then be:

```javascript
event.duration = 4;

await event.project.commitAsync();

console.log(event.endDate);
```

When using the Scheduler UI, this change should not be noticeable. Scheduler automatically redraws events when the
calculations are finished.

### Loading and adding events

The async calculation of dates and durations also applies to when loading or adding events. Consider this old code:

```javascript
const [event] = eventStore.add({ startDate : '2020-09-10', endDate : '2020-09-14' });
console.assert(event.duration === 4); // ok
```

With the new async approach, duration is not yet calculated. Instead calculation is scheduled to happen later. To
calculate it directly, you can use the approach described in the previous section:

```javascript
const [event] = eventStore.add({ startDate : '2020-09-10', endDate : '2020-09-14' });

await eventStore.project.commitAsync();

console.assert(event.duration === 4); // ok
```

The same principles apply for loading data into the store:

```javascript
eventStore.data = [my_dataset];

// calculations are not yet done

await eventStore.project.commitAsync();

// now calculations are done, data is ready for further processing
```

To simplify this a bit, we have added two new functions that performs the commit internally, `addAsync()` and
`loadDataAsync()`. Example of their usage, `loadDataAsync()` as an alternative to `store.data = ...` first:

```javascript
await eventStore.loadDataAsync([my_dataset]);

// calculations are done, data is ready for further processing
```

And `addAsync()` would be used like this:

```javascript
await eventStore.addAsync({ startDate : '2020-09-10', endDate : '2020-09-14' });

// duration is calculated and available
```

## Changed event triggering timing

The stores that are part of the project (EventStore, ResourceStore, AssignmentStore and DependencyStore) has had the
timing of their event triggering for certain events modified. The change was made to assure async date calculations are
finished when the event is triggered. This affects the following events:

* `add`
* `remove`
* `removeAll`
* `change`
* `refresh`

This change should lead to better backward compatibility with the old Scheduler. Consider the following scenario:

```javascript
const scheduler = new Scheduler({
    events : [/*...*/],

    listeners : {
        add({ records }) {
            console.assert(records[0].endDate)
        }
    }
});

scheduler.eventStore.add({ startDate : '2020-09-09', duration : 1 });
```

Without the change, the `add` event would be triggered immediately on the call to `add()`. Since date calculations are
now async (as described in section "Async date manipulation" above), the `endDate` would not yet be available.

With the change, the event is triggered after calculations has finished, making `endDate` available in the listener.

It is still possible to catch the events at the earlier stage, in case you do not care about waiting for data to be in a
calculated state. To achieve that, listen for `addBeforeCommit`, `removeBeforeCommit` and so on instead.

## DependencyStore moved to Scheduler

Previously the `dependencies` feature held its own store, and Scheduler referenced it. Now Scheduler (or rather the
project) holds the store directly. In most cases, this should not matter. But if you were actively defining the store
for the feature you might need to change your code:

**Old code:**

```javascript
const scheduler = new Scheduler({
    features : {
        dependencies : {
            store : myDependencyStore
        }
    }
})
```

**New code:**

```javascript
const scheduler = new Scheduler({
    features : {
        dependencies : true
    },

    dependencyStore : myDependencyStore
})
```

## WidgetHelper and BryntumWidgetAdapter

Registration of Widget classes for subsequent resolution through the `type` property of config objects has been
simplified. The "Adapter" concept has been removed. Widget classes now register themselves with the `Widget` base class
which is where they can also be looked up. When creating a custom widget, implement the `static get type` property to
return the type name. And at the end simply call `MyWidgetClass.initClass()` to have the new Widget class register
itself.

**Old code:**

```javascript
class MyWidget extends Widget {

}

BryntumWidgetAdapterRegister.register('mywidget', MyWidget);
```

**New code:**

```javascript
class MyWidget extends Widget {
    static get type() {
        return 'mywidget';
    }
}

MyWidget.initClass();
```

Fewer widgets are auto imported with the removal of the Adapter which imported a base set of Widgets. This means that
when not using a bundle, application code must import the widgets it uses. It also means that custom builds may be
smaller by including only what is used.

If your code previously had:

```javascript
import 'lib/Core/adapter/widget/BryntumWidgetAdapter.js';
import WidgetHelper from 'lib/Core/helper/WidgetHelper.js';

WidgetHelper.append([
        {
            type : 'button',
            text : 'Click'
        }
    ], /* ... */
);
```

It should now instead not import the adapter, and instead directly import the button:

```javascript
import WidgetHelper from 'lib/Core/helper/WidgetHelper.js';
import 'lib/Core/widget/Button.js';

WidgetHelper.append([
    {
        type : 'button',
        text : 'Click'
    }
], /* ... */
);
```

## Recurring Events

The method by which occurrences of recurring events are collected for rendering has changed.

Prior to 4.0.0, after any [EventStore](#Scheduler/data/EventStore) or [EventModel](#Scheduler/model/EventModel)
mutation (load, filter, record change etc), the [EventStore](#Scheduler/data/EventStore) was scanned for instances of
events with a [recurrenceRule](#Scheduler/model/EventModel#field-recurrenceRule). For each one found, a sequence of
occurrences which fell within the Scheduler's current TimeAxis date range was generated and these were inserted into
the [EventStore](#Scheduler/data/EventStore).

This could result in difficulty in managing the store content since "ephemeral" occurrences were mixed in with concrete
event definitions and [EventStore](#Scheduler/data/EventStore) would fire [add](#Scheduler/data/EventStore#event-add)
events for these ephemeral occurrences which apps had to have code to ignore.

From 4.0.0, there is no `RecurringEvents` feature anymore (its base class `RecurringTimeSpans` is also removed as no
longer needed). There is an [enableRecurringEvents](#Scheduler/view/Scheduler#config-enableRecurringEvents) boolean
config on the Scheduler instead. Occurrences of recurring events are provided on a "just in time" basis by a new
EventStore API which must now be used when interrogating an EventStore.

**Old code:**

```javascript
// Define a new Model extending TimeSpan class with RecurringTimeSpan mixin which adds recurrence support
class MyTimeRange extends RecurringTimeSpan(TimeSpan) {}

// Define a new store extending the Store class with RecurringTimeSpansMixin mixin to add recurrence support to the store.
// This store will contain time ranges.
class MyTimeRangeStore extends RecurringTimeSpansMixin(Store) {
    static get defaultConfig() {
        return {
            // use our new MyTimeRange model
            modelClass : MyTimeRange
        };
    }
}

// instantiate store for time ranges using our new classes
const timeRangeStore = new MyTimeRangeStore();

new Scheduler({
    //...
    features : {
        // enable recurrence support for events
        recurringEvents    : true,
        // we use time ranges feature
        timeRanges         : {
            store : timeRangeStore
        },
        // and bind recurringTimeSpans to the store timeRanges use
        // which makes time ranges supporting recurrence
        recurringTimeSpans : {
            store : timeRangeStore
        }
    }
})
```

**New code:**

```javascript
// Define a new Model extending TimeSpan class with RecurringTimeSpan mixin which adds recurrence support
class MyTimeRange extends RecurringTimeSpan(TimeSpan) {}

// Define a new store extending the Store class with RecurringTimeSpansMixin mixin to add recurrence support to the store.
// This store will contain time ranges.
class MyTimeRangeStore extends RecurringTimeSpansMixin(Store) {
    static get defaultConfig() {
        return {
            // use our new MyTimeRange model
            modelClass : MyTimeRange
        };
    }
}

// instantiate store for time ranges using our new classes
const timeRangeStore = new MyTimeRangeStore();

// recurringEvents and recurringTimeSpans feature definitions are no longer needed - so we removed them
new Scheduler({
    // ...
    // enable recurrence support for events
    enableRecurringEvents : true,
    // time ranges feature
    timeRanges : {
        store : timeRangeStore
    }
})
```

[EventStore.getEvents](#Scheduler/data/EventStore#function-getEvents) is a multipurpose event gathering method which can
be asked to return events which match a set of criteria including a date range and a resource. By default, if the
requested date range contains occurrences of a recurring event, those occurrences are returned in the result array.

```javascript
myEventStore.getEvents({
    resourceRecord : myResourceRecord,
    startDate      : myScheduler.startDate,
    endDate        : myScheduler.endDate
});
```

Occurrences are *not* present in the store's data collection.

To directly access occurrences of a recurring event which *intersect* a date range, use:

```javascript
recurringEvent.getOccurrencesForDateRange(startDate, endDate);
```

The `endDate` argument is optional if the occurrence for one date is required. This method always returns an array. Note
that it may be empty if no occurrences intersect the date range.

### Convert an occurrence to an exception

To programmatically convert an occurrence to be a single exception to its owner's sequence use:

```javascript
myOccurrence.beginBatch();
myOccurrence.startDate = DateHelper.add(myOccurrence.startDate, 1, 'day');
myOccurrence.name = 'Postponed to next day';
myOccurrence.recurrence = null; // This means it does NOT become a new recurring base event.
myOccurrence.endBatch();
```

Or as a single call to `set()`:

```javascript
myOccurrence.set({
    startDate  : DateHelper.add(myOccurrence.startDate, 1, 'day'),
    name       : 'Postponed to next day',
    recurrence : null // This means it does NOT become a new recurring base event.
});
```

That will cause that event to be inserted into the store as a concrete event definition, firing
an [add](#Scheduler/data/EventStore#event-add) event as would be expected, and will add
an [exceptionDates](#Scheduler/model/mixin/RecurringTimeSpan#field-exceptionDates) to its owning recurring event.

When syncing this change back to the server,
the [exceptionDates](#Scheduler/model/mixin/RecurringTimeSpan#field-exceptionDates) array for the modified recurring
event now contains the exception dates correctly serialized into string form using the `dateFormat` defined
for [the field](#Scheduler/model/mixin/RecurringTimeSpan#field-exceptionDates). The system-supplied default value for
this is `'YYYY-MM-DDTHH:mm:ssZ'`

### Convert an occurrence to a new recurring event sequence

To programmatically convert an occurrence to be the start of a new recurring sequence, use:

```javascript
myOccurrence.beginBatch();
myOccurrence.startDate = DateHelper.set(myOccurrence.startDate, 'hour', 14);
myOccurrence.name = 'Moved to 2pm from here on';
myOccurrence.endBatch();
```

That will cause that event to be inserted into the store as a concrete *recurring* event definition, firing
an [add](#Scheduler/data/EventStore#event-add)
event as would be expected, and will terminate the previous recurring owner of that occurrence on the day before the new
event.

## Customizing the event editor feature

Customizing the event editor is even simpler now. No multiple configs to change all the provided fields in the
system-supplied event editor popup.

The `items` config of the `Container` class is now object-based with the property name being the `ref` of the resulting
child item, and the value being its config object. An `items` value from configuration is *merged* into any `items`
object that a class defines for itself. Child widget order is preserved by default, but adding a `weight` property to a
widget's config means that lower values (lighter) are to the top while higher values (heavier) are at the bottom.

These new capabilities mean that you can specify one `items` config for the `EventEdit` feature to change or add any
field in the editor.

So to change an aspect of a provided field use:

```javascript
const scheduler = new Scheduler({
    features : {
        eventEdit : {
            // Merged with the provided items
            // So we can override any config of any provided field.
            items : {
                resourceField : {
                    label : 'Machine'
                }
            }
        }
    }
});
```

To *add* to the provided field set use:

```javascript
const scheduler = new Scheduler({
    features : {
        eventEdit : {
            items : {
                owner : {
                    weight : -100, // Will sort above system-supplied fields which are weight 100 to 800
                    type   : 'usercombo',
                    name   : 'owner',
                    label  : 'Owner'
                },
                agreement : {
                    weight : 1000, // Will sort below system-supplied fields which are weight 100 to 800
                    type   : 'checkbox',
                    name   : 'agreement',
                    label  : 'Agree to terms'
                }
            }
        }
    }
});
```

## Context menu

### Base class

`TimeSpanRecordContextMenuBase` class was deprecated. Common functionality was moved to `ContextMenuBase` class, and
functionality related to TimeAxis was moved to `TimeSpanMenuBase` class.

### HeaderContextMenu feature for TimeAxis

`HeaderContextMenu` feature was replaced by `TimeAxisHeaderMenu`. `TimeAxisHeaderMenu` extends `HeaderMenu` to provide
TimeAxis specific header menu items. `extraItems` config was turned into `items` which is a named object:

**Old code:**

```javascript
const scheduler = new Scheduler({
    features : {
        headerContextMenu : {
            extraItems : [
                {
                    text : 'Extra',
                    onItem() { /*...*/ }
                }
            ],

            processItems({ items }) {
                items.push({
                    text : 'Cool action',
                    onItem() { /*...*/ }
                })
            }
        }
    }
});
```

**New code:**

```javascript
const scheduler = new Scheduler({
    features : {
        timeAxisHeaderMenu : {
            items : {
                myExtraItem : {
                    text : 'Extra',
                    onItem() { /*...*/ }
                }
            },

            processItems({ items }) {
                items.coolItem = {
                    text : 'Cool action',
                    onItem() { /*...*/ }
                }
            }
        }
    }
});
```

### Disable menu for locked grid

To disable header/cell context menu for locked grid, but leave it for normal grid please follow this way:

**Old code:**

```javascript
const scheduler = new Scheduler({
    features : {
        contextMenu : {
            processHeaderItems : ({ column }) => column instanceof TimeAxisColumn,
            processCellItems   : () => false
        }
    }
});
```

**New code:**

```javascript
const scheduler = new Scheduler({
    features : {
        headerMenu : false, // it doesn't turn off context menu for time axis column header now
        cellMenu   : false
    }
});
```

### There is no "Context" in naming

We got rid of "Context" in `EventContextMenu` and `ScheduleContextMenu` features to follow the same naming standards in
all products. So now they are called `EventMenu` and `ScheduleMenu` correspondingly.

### Context menu events

Some of Scheduler's events provided by `EventContextMenu` and `ScheduleContextMenu` features were replaced
by `EventMenu` and
`ScheduleMenu` features correspondingly. Here is the list of changes:

- `eventContextMenuBeforeShow` -> `eventMenuBeforeShow`;
- `eventContextMenuShow` -> `eventMenuShow`;
- `eventContextMenuItem` -> `eventMenuItem`;
- `scheduleContextMenuBeforeShow` -> `scheduleMenuBeforeShow`;
- `scheduleContextMenuShow` -> `scheduleMenuShow`;
- `scheduleContextMenuItem` -> `scheduleMenuItem`;

For example:

**Old code:**

```javascript
const scheduler = new Scheduler({
    listeners : {
        eventContextMenuBeforeShow    : () => { /*...*/ },
        eventContextMenuShow          : () => { /*...*/ },
        eventContextMenuItem          : () => { /*...*/ },
        scheduleContextMenuBeforeShow : () => { /*...*/ },
        scheduleContextMenuShow       : () => { /*...*/ },
        scheduleContextMenuItem       : () => { /*...*/ }
    }
});
```

**New code:**

```javascript
const scheduler = new Scheduler({
    listeners : {
        eventMenuBeforeShow    : () => { /*...*/ },
        eventMenuShow          : () => { /*...*/ },
        eventMenuItem          : () => { /*...*/ },
        scheduleMenuBeforeShow : () => { /*...*/ },
        scheduleMenuShow       : () => { /*...*/ },
        scheduleMenuItem       : () => { /*...*/ }
    }
});
```

## Event element id changes

The `id` attribute on event elements was removed. It used a cryptic generated value similar to `id="scheduler-57-r2-x"`,
which is no longer needed internally. If you need to retrieve the element for a specific event we recommend using
`scheduler.getElementFromEventRecord()`:

```javascript
const element = scheduler.getElementFromEventRecord(eventStore.first);
```

Alternatively, elements has event id and assignment id in their dataset, allowing you to use them as CSS selectors:

```javascript
const element = scheduler.element.querySelector('[data-event-id="someId"]');
```

Following this simplification, the `getResourceRecordFromDomId()` and `getEventRecordFromDomId()` functions has been
removed. To lookup an event record from a DOM element, please use
[resolveEventRecord](#Scheduler/view/mixin/SchedulerDom#function-resolveEventRecord), and to lookup the resource from a
specific DOM element, please
use [resolveResourceRecord](#Scheduler/view/mixin/SchedulerDom#function-resolveResourceRecord).

## Model Field Inheritance

Fields defined in a derived class Model that coincide by name with a field declared in a super class will now only
override those field config properties specified by the derived class. This allows derived classes to adjust a field
definition in one way, say to change the default value and still inherit other field properties, such as a convert
function.

For example:

```javascript
    class ModelOne extends Model {
    static get fields() {
        return [
            { name : 'barcode', convert : v => String(v) }
        ];
    }
}

class ModelTwo extends ModelOne {
    static get fields() {
        return [
            { name : 'barcode', defaultValue : 'ABC123' }
        ];
    }
}
```

In previous releases, `ModelTwo` would have had to redefine the `convert` config for the `barcode` field.

## Renamed CSS themes

The `Default`, `Light` and `Dark` themes were renamed to `Classic`, `Classic-Light` and `Classic-Dark`. This change
highlights the fact that they are variations of the same theme, and that it is not the default theme (Stockholm is our
default theme since version 2.0).

If you are using one of these themes, you will have to adjust your css import to match the new name.

**Old code:**

```html
<link rel="stylesheet" href="build/grid.dark.css" id="bryntum-theme">
```

**New code:**

```html
<link rel="stylesheet" href="build/grid.classic-dark.css" id="bryntum-theme">
```

If you have theme specific selectors in your code/CSS you have to adjust those also:

**Old code:**

```css
.b-theme-dark {
/*...*/
}
```

**New code:**

```css
.b-theme-classic-dark {
/*...*/
}
```


<p class="last-modified">Last modified on 2023-10-26 8:19:20</p>