We’ve encountered a critical issue in our application using Bryntum Calendar, where a large number of assignments were deleted simultaneously without any intentional user action. This has happened multiple times, and most recently on April 3rd, when 106 assignments were removed in a single request.
This incident resulted in all assignments within a specific week being deleted. That is, every event that had a resource assigned during that week lost its assignment. The deletion was triggered from the calendar UI, as confirmed by our backend logs, but it’s unclear how or why such a mass removal occurred.
Our understanding was that assignment edits typically occur on a per-event basis, and this kind of bulk deletion should not be possible under normal user interactions.
Questions/Concerns:
1. Under what circumstances could the Calendar trigger a bulk deletion of assignments?
2. Are there known scenarios where removing a single event or modifying a resource could cascade into multiple assignment deletions?
3. Is there a way to trace or prevent such unintended bulk actions from the frontend side (e.g., custom validation or event interception)?
Sorry to hear about this, sounds very strange, but we haven't heard about this before. Assignments are removed if you remove your resources, but it doesn't sounds like you do. Do you have a way of reproducing this somehow?
Yes, we are on the latest version. And we have not been able to reproduce this. But we have several clients that are complaining about assignments suddenly disappearing without there knowledge.
Sometimes it a large amount and some times its just a few assignments.
Is there any way for the user to bulk edit or select multiple eventes and then unassign all resources?
Just to check here is our method to listening to changes and send them to backend and also update the stores with the correct ids. Is this the best practise or should we do this some other way?
syncData(): void {
const changes = this.crudManager.changes;
// If there are no changes, log the information and exit
if (!changes || Object.keys(changes).length === 0) {
console.log('No changes to sync');
return;
}
// You can log the changes for debugging
console.log('Changes to sync:', changes);
// Prepare the payload based on the changes
const payload = {
...changes
};
// Send the changes to your backend API
this.eventService.updateEvent(payload).subscribe({
next: (response: BryntumChangeSetResponse) => {
const allEvents = response.data.events.rows;
const allAssignments = response.data.assignments;
// Transform the response into a changeset
const eventChangeset = {
added: [], // If no new records, leave this empty
updated: allEvents, // Use all events from the response
removed: [] // If no removed records, leave this empty
};
// Apply the changeset using CrudManager's applyChangeset method
this.crudManager.eventStore.applyChangeset(eventChangeset);
this.crudManager.assignmentStore.applyChangeset(allAssignments);
// Commit the changes after successful sync
if (response.data.success) {
this.crudManager.acceptChanges();
if (response.data.events.rows[0].$PhantomId != null) {
if (response.data.events.rows[0].orderId != null && response.data.events.rows[0].fromSchedule === false) {
this.openOrderModal(response.data.events.rows[0].orderId);
} if (response.data.events.rows[0].orderId != null && response.data.events.rows[0].fromSchedule === true) {
this.openWorkOrderModal(response.data.events.rows[0].id);
}
}
// Optionally, show a toast message or update UI
this.toastService.successToast('syncSuccessfull');
}
},
error: (error: any) => {
console.error('Failed to sync data', error);
// Check for network connectivity issues (504 Gateway Timeout or other network errors)
if (error.status === 504 || !navigator.onLine || error.name === 'HttpErrorResponse' && error.status === 0) {
// User is offline or experiencing network issues
this.crudManager.revertChanges();
this.toastService.errorToast('networkError');
} else if (error.error.asset_id == 'no_multi_day_work_orders') {
// Specific business rule error
this.crudManager.revertChanges();
this.toastService.errorToast('noMultiDayWorkOrders');
} else {
// Other errors
this.crudManager.revertChanges();
this.toastService.errorToast('syncFailed');
}
}
});
}
We just experienced another incident involving mass deletion. Below is the payload sent to our backend, triggered by changes from the CrudManager. This occurred in the week view, and we suspect it involves all events visible in the user’s week view.
The issue happened when the user attempted to remove one resource from an event using the event editor.
Total of 168 events
Timespan: 2025-04-28 03:00 -> 2025-06-20 16:30
Yes, we can provide the same data that the user who experienced the issue had. However, we have not been able to reproduce the issue on our side. We have, however, seen the error occur in a videomeeting with the user.
Attached are our event data. Below is our setup method for the crudManager. Let me know if you need more of our code.
// Setup CrudManager with stores and API transport
setupCrudManager(): void {
this.crudManager = new CrudManager({
resourceStore: new ResourceStore(), // Ensure these are properly initialized
eventStore: new EventStore({
modelClass: CustomEventModel, // Use CustomEventModel here
}),
writeAllFields: true,
assignmentStore: new AssignmentStore(),
autoLoad: false, // Disable automatic loading from default transport
autoSync: false, // Disable automatic syncing from default transport
// Disable default transport since we will handle load and sync manually
transport: {
load: {}, // Empty load to disable default loading
sync: {} // Empty sync to disable default syncing
},
supportShortSyncResponse: false
});
// Listen to CrudManager changes and debounce sync calls
this.crudManager.on('hasChanges', () => {
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout); // Clear previous timeout
}
this.debounceTimeout = setTimeout(() => {
this.syncData();
}, 100);
});
}
So what I see from that code is that it is not CrudManager sending a lot of remove requests since you have disabled the CrudManager from performing network operations.
It is a method called syncData, which we don't know what it is doing. It seems it sends bad data. I thought the format of the update packet posted was unusual. It's not a CrudManager packet. So how is syncData deciding what to do and creating that packet?
BTW, event handlers can dedupe frequently occurring events and coalesce multiple events into one handler call.
syncData(): void {
const changes = this.crudManager.changes;
// If there are no changes, log the information and exit
if (!changes || Object.keys(changes).length === 0) {
console.log('No changes to sync');
return;
}
// You can log the changes for debugging
console.log('Changes to sync:', changes);
// Prepare the payload based on the changes
const payload = {
...changes
};
// Send the changes to your backend API
this.eventService.updateEvent(payload).subscribe({
next: (response: BryntumChangeSetResponse) => {
const allEvents = response.data.events.rows;
const allAssignments = response.data.assignments;
// Transform the response into a changeset
const eventChangeset = {
added: [], // If no new records, leave this empty
updated: allEvents, // Use all events from the response
removed: [] // If no removed records, leave this empty
};
// Apply the changeset using CrudManager's applyChangeset method
this.crudManager.eventStore.applyChangeset(eventChangeset);
this.crudManager.assignmentStore.applyChangeset(allAssignments);
// Commit the changes after successful sync
if (response.data.success) {
this.crudManager.acceptChanges();
if (response.data.events.rows[0].$PhantomId != null) {
if (response.data.events.rows[0].orderId != null && response.data.events.rows[0].fromSchedule === false) {
this.openOrderModal(response.data.events.rows[0].orderId);
} if (response.data.events.rows[0].orderId != null && response.data.events.rows[0].fromSchedule === true) {
this.openWorkOrderModal(response.data.events.rows[0].id);
}
}
// Optionally, show a toast message or update UI
this.toastService.successToast('syncSuccessfull');
}
},
error: (error: any) => {
console.error('Failed to sync data', error);
// Check for network connectivity issues (504 Gateway Timeout or other network errors)
if (error.status === 504 || !navigator.onLine || error.name === 'HttpErrorResponse' && error.status === 0) {
// User is offline or experiencing network issues
this.crudManager.revertChanges();
this.toastService.errorToast('networkError');
} else if (error.error.asset_id == 'no_multi_day_work_orders') {
// Specific business rule error
this.crudManager.revertChanges();
this.toastService.errorToast('noMultiDayWorkOrders');
} else {
// Other errors
this.crudManager.revertChanges();
this.toastService.errorToast('syncFailed');
}
}
});
}