Our powerful JS Calendar component


Post by tomas@between.as »

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)?


Post by mats »

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?

You could listen for https://bryntum.com/products/calendar/docs/api/Core/data/mixin/StoreCRUD#event-beforeRemove on your assignmentStore and return false if number of records are larger than expected and log etc. Not sure what else to advise without knowing more. Are you using latest version?


Post by tomas@between.as »

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');
            }
          }
        });
      }
      
      

Post by mats »

I don't see anything obviously wrong, super hard to say without a way to reproduce. What's the data format of this variable?

            const allAssignments = response.data.assignments;

Post by Animal »

Can you provide a set of test data that we can use to replicate the issue?

An initial data load, then the eventChangeset and allAssignments data blocks which are being used to update that which cause the issue?


Post by tomas@between.as »

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

What do you think this can be?

{
  "company_id": "COY-590",
  "assignments": {
    "removed": [
      {
        "id": 33559
      },
      {
        "id": 27176
      },
      {
        "id": "REL-5212"
      },
      {
        "id": 25542
      },
      {
        "id": "REL-5037"
      },
      {
        "id": 33959
      },
      {
        "id": 34195
      },
      {
        "id": 29227
      },
      {
        "id": "REL-5766"
      },
      {
        "id": 33562
      },
      {
        "id": 17506
      },
      {
        "id": "REL-2726"
      },
      {
        "id": 20510
      },
      {
        "id": 33560
      },
      {
        "id": 33961
      },
      {
        "id": 33960
      },
      {
        "id": 17471
      },
      {
        "id": "REL-2709"
      },
      {
        "id": 29185
      },
      {
        "id": "REL-5728"
      },
      {
        "id": 27380
      },
      {
        "id": "REL-5254"
      },
      {
        "id": 17741
      },
      {
        "id": "REL-2912"
      },
      {
        "id": 28967
      },
      {
        "id": 18990
      },
      {
        "id": 18012
      },
      {
        "id": "REL-3080"
      },
      {
        "id": 22488
      },
      {
        "id": "REL-4424"
      },
      {
        "id": 17437
      },
      {
        "id": "REL-2689"
      },
      {
        "id": 20593
      },
      {
        "id": "REL-3890"
      },
      {
        "id": 26367
      },
      {
        "id": 29627
      },
      {
        "id": 33116
      },
      {
        "id": 17891
      },
      {
        "id": "REL-2974"
      },
      {
        "id": 27381
      },
      {
        "id": "REL-5255"
      },
      {
        "id": 29186
      },
      {
        "id": "REL-5729"
      },
      {
        "id": 17742
      },
      {
        "id": "REL-2913"
      },
      {
        "id": 28968
      },
      {
        "id": 25034
      },
      {
        "id": "REL-4930"
      },
      {
        "id": 33956
      },
      {
        "id": 31469
      },
      {
        "id": 18185
      },
      {
        "id": "REL-3196"
      },
      {
        "id": 33957
      },
      {
        "id": 17315
      },
      {
        "id": "REL-2623"
      },
      {
        "id": 19627
      },
      {
        "id": "REL-3686"
      },
      {
        "id": 30894
      },
      {
        "id": 17847
      },
      {
        "id": "REL-2930"
      },
      {
        "id": 17869
      },
      {
        "id": "REL-2952"
      },
      {
        "id": 33944
      },
      {
        "id": 17579
      },
      {
        "id": "REL-2758"
      },
      {
        "id": 18092
      },
      {
        "id": 33946
      },
      {
        "id": 18129
      },
      {
        "id": "REL-3144"
      },
      {
        "id": 18110
      },
      {
        "id": 33955
      },
      {
        "id": 27382
      },
      {
        "id": "REL-5256"
      },
      {
        "id": 17743
      },
      {
        "id": "REL-2914"
      },
      {
        "id": 29187
      },
      {
        "id": "REL-5730"
      },
      {
        "id": 17907
      },
      {
        "id": "REL-2990"
      },
      {
        "id": 31649
      },
      {
        "id": 28969
      },
      {
        "id": 31637
      },
      {
        "id": 22756
      },
      {
        "id": "REL-4518"
      },
      {
        "id": 20630
      },
      {
        "id": "REL-3927"
      },
      {
        "id": 17918
      },
      {
        "id": "REL-3001"
      },
      {
        "id": 22773
      },
      {
        "id": 18218
      },
      {
        "id": "REL-3229"
      },
      {
        "id": 17934
      },
      {
        "id": "REL-3017"
      },
      {
        "id": 20789
      },
      {
        "id": "REL-4035"
      },
      {
        "id": 17744
      },
      {
        "id": "REL-2915"
      },
      {
        "id": 27383
      },
      {
        "id": "REL-5257"
      },
      {
        "id": 29188
      },
      {
        "id": "REL-5731"
      },
      {
        "id": 25974
      },
      {
        "id": 28970
      },
      {
        "id": 24954
      },
      {
        "id": 25164
      },
      {
        "id": 27416
      },
      {
        "id": "REL-5290"
      },
      {
        "id": 33950
      },
      {
        "id": 20706
      },
      {
        "id": "REL-3990"
      },
      {
        "id": 29094
      },
      {
        "id": 33954
      },
      {
        "id": 17487
      },
      {
        "id": 17955
      },
      {
        "id": "REL-3038"
      },
      {
        "id": 18258
      },
      {
        "id": "REL-3268"
      },
      {
        "id": 33953
      },
      {
        "id": 33951
      },
      {
        "id": "REL-4049"
      },
      {
        "id": 17233
      },
      {
        "id": "REL-2591"
      },
      {
        "id": 33986
      },
      {
        "id": 27384
      },
      {
        "id": "REL-5258"
      },
      {
        "id": 33985
      },
      {
        "id": 29189
      },
      {
        "id": "REL-5732"
      },
      {
        "id": 28925
      },
      {
        "id": "REL-5420"
      },
      {
        "id": 28971
      },
      {
        "id": 33947
      },
      {
        "id": 29223
      },
      {
        "id": "REL-5762"
      },
      {
        "id": 27604
      },
      {
        "id": "REL-5429"
      },
      {
        "id": 33948
      },
      {
        "id": 33949
      },
      {
        "id": 19093
      },
      {
        "id": "REL-3461"
      },
      {
        "id": 33952
      },
      {
        "id": 19605
      },
      {
        "id": "REL-3674"
      },
      {
        "id": 22579
      },
      {
        "id": "REL-4447"
      },
      {
        "id": 28972
      },
      {
        "id": 25543
      },
      {
        "id": "REL-5038"
      },
      {
        "id": 30189
      },
      {
        "id": "REL-5897"
      },
      {
        "id": 29228
      },
      {
        "id": "REL-5767"
      },
      {
        "id": 33432
      },
      {
        "id": 33429
      },
      {
        "id": 17507
      },
      {
        "id": "REL-2727"
      },
      {
        "id": 17210
      },
      {
        "id": "REL-2572"
      },
      {
        "id": 33963
      },
      {
        "id": 18170
      },
      {
        "id": "REL-3185"
      },
      {
        "id": 17472
      },
      {
        "id": "REL-2710"
      },
      {
        "id": 17745
      },
      {
        "id": "REL-2916"
      },
      {
        "id": 27385
      },
      {
        "id": "REL-5259"
      },
      {
        "id": 29190
      },
      {
        "id": "REL-5733"
      },
      {
        "id": 28973
      },
      {
        "id": 22985
      },
      {
        "id": "REL-4621"
      },
      {
        "id": 18385
      },
      {
        "id": "REL-3307"
      },
      {
        "id": 20577
      },
      {
        "id": "REL-3875"
      },
      {
        "id": 19164
      },
      {
        "id": "REL-3529"
      },
      {
        "id": 33433
      },
      {
        "id": 33434
      },
      {
        "id": 23966
      },
      {
        "id": "REL-4758"
      },
      {
        "id": 33427
      },
      {
        "id": 33431
      },
      {
        "id": 21719
      },
      {
        "id": "REL-4400"
      },
      {
        "id": 17438
      },
      {
        "id": "REL-2690"
      },
      {
        "id": 23006
      },
      {
        "id": "REL-4639"
      },
      {
        "id": 24972
      },
      {
        "id": 17892
      },
      {
        "id": "REL-2975"
      },
      {
        "id": 27386
      },
      {
        "id": "REL-5260"
      },
      {
        "id": 17746
      },
      {
        "id": "REL-2917"
      },
      {
        "id": 29191
      },
      {
        "id": "REL-5734"
      },
      {
        "id": 28974
      },
      {
        "id": 25035
      },
      {
        "id": "REL-4931"
      },
      {
        "id": 18726
      },
      {
        "id": "REL-3372"
      },
      {
        "id": 17870
      },
      {
        "id": "REL-2953"
      },
      {
        "id": 19214
      },
      {
        "id": "REL-3579"
      },
      {
        "id": 17580
      },
      {
        "id": "REL-2759"
      },
      {
        "id": 18093
      },
      {
        "id": 19132
      },
      {
        "id": "REL-3497"
      },
      {
        "id": 27387
      },
      {
        "id": "REL-5261"
      },
      {
        "id": 17747
      },
      {
        "id": "REL-2918"
      },
      {
        "id": 33435
      },
      {
        "id": 33436
      },
      {
        "id": 29192
      },
      {
        "id": "REL-5735"
      },
      {
        "id": 17908
      },
      {
        "id": "REL-2991"
      },
      {
        "id": 28975
      },
      {
        "id": 29101
      },
      {
        "id": 19182
      },
      {
        "id": "REL-3547"
      },
      {
        "id": 22856
      },
      {
        "id": 24798
      },
      {
        "id": "REL-4866"
      },
      {
        "id": 20675
      },
      {
        "id": "REL-3959"
      },
      {
        "id": 17354
      },
      {
        "id": "REL-2661"
      },
      {
        "id": 20691
      },
      {
        "id": "REL-3975"
      },
      {
        "id": 29405
      },
      {
        "id": 19113
      },
      {
        "id": "REL-3480"
      },
      {
        "id": 22788
      },
      {
        "id": "REL-4535"
      },
      {
        "id": 23771
      },
      {
        "id": "REL-4728"
      },
      {
        "id": 18234
      },
      {
        "id": "REL-3245"
      },
      {
        "id": 29492
      },
      {
        "id": "REL-5772"
      },
      {
        "id": 17748
      },
      {
        "id": "REL-2919"
      },
      {
        "id": 29193
      },
      {
        "id": "REL-5736"
      },
      {
        "id": 31636
      },
      {
        "id": 28976
      },
      {
        "id": 27417
      },
      {
        "id": "REL-5291"
      },
      {
        "id": 19073
      },
      {
        "id": "REL-3449"
      },
      {
        "id": 31640
      },
      {
        "id": 17488
      },
      {
        "id": 17956
      },
      {
        "id": "REL-3039"
      },
      {
        "id": 27377
      },
      {
        "id": "REL-5251"
      },
      {
        "id": 18259
      },
      {
        "id": "REL-3269"
      },
      {
        "id": 22804
      },
      {
        "id": "REL-4551"
      },
      {
        "id": 22820
      },
      {
        "id": "REL-4567"
      },
      {
        "id": 17234
      },
      {
        "id": "REL-2592"
      },
      {
        "id": 29194
      },
      {
        "id": "REL-5737"
      },
      {
        "id": 29493
      },
      {
        "id": "REL-5773"
      },
      {
        "id": 29549
      },
      {
        "id": "REL-5809"
      },
      {
        "id": 29617
      },
      {
        "id": 29787
      },
      {
        "id": "REL-5856"
      },
      {
        "id": 29770
      },
      {
        "id": "REL-5850"
      },
      {
        "id": 19606
      },
      {
        "id": "REL-3675"
      }
    ]
  }
}

Post by Animal »

Can you please supply the test data that is being used, and the code with which the CrudManager is being initialized?

We need to replicate this in a test case so that we can see what is happening.


Post by tomas@between.as »

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);
    });
}
Attachments
payload_dataset.json
(599.05 KiB) Downloaded 10 times

Post by Animal »

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.

https://bryntum.com/products/scheduler/docs/#Core/mixin/Events#function-on

and then see the buffer option of the linked https://bryntum.com/products/scheduler/docs/api/Core/mixin/Events#typedef-BryntumListenerConfig


Post by tomas@between.as »

Hi Anumal!

Here is our syncData method:

        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');
            }
          }
        });
      }

Post Reply