Premium support for our pure JavaScript UI components


Post by sterh »

Hi, can you help me please?

I am using Bryntum Scheduler for displaying events with CRUD manager. Also, I have unassigned events in a Grid with Drag-n-drop functionality. So I can move assigned event to unassigned grid and back.

Now I try to add Undo/redo functionality and it works good except one moment: when I move assigned event to unassigned grid or unassigned event from Grid to Scheduler, it creates 2 transactions:

  • 1. first transaction is correct (it removed event in one place and add it in another, sync by CRUD with backend)

  • 2. second transaction is incorrect and looks like nothing is happening, "oldData" is the same as "newData" in "transaction.queue[0]" (only difference is that "newData" contains "$PhantomId") and this data contains only inner fields, like:

crew.categoryId:  "d1dc0cff-b8b5-4906-b709-9b2aeb6f57e8"
crew.id: "43e8f095-778e-4e84-b9a4-f07edda57354"
crew.name: "New crew"
crew.slug: "new-crew"
installation.id: "f09f71f2-17d6-498a-943f-5a069f09cbc6"
installation.name: "US"
location.id: "861809db-cd7a-437f-aa72-0239c6bbce68"
location.installationId: "f09f71f2-17d6-498a-943f-5a069f09cbc6"
location.name: "New Jersey"

The project is written on React and next.js

Scheduler config:

export const schedulerConfig = {
    crudManager,
    startDate,
    endDate,
    enableUndoRedoKeys:     true,
    eventDragSelectFeature: true,
    eventDragFeature:       {
        constrainDragToTimeline: false,
        validatorFn() {
            return true;
        },
        externalDropTargetSelector: '.b-unassignedgrid',
    },
    columns: [
        {
            type:                  'resourceInfo',
            text:                  'Installations',
            width:                 200,
        },
    ],
    project: {
        stm: {
            autoRecord: true,
        },
        autoLoad:  true,
        listeners: {
            dataReady(props) {
                if (!isSchedulerLoaded) {
                    const stm = props.source.stm;
                    const storeToRemove = stm.stores.filter(
                        (store) => !(store instanceof EventStore),
                    );
                    storeToRemove.forEach((store) => stm.removeStore(store));
                    props.source.stm.addStore(unassignedEventStore);
                    props.source.stm.enable();
                    isSchedulerLoaded = true;
                }
            },
        },
    },
    tbar: [
        {
            type:  'undoredo',
            items: { transactionsCombo: null },
        },
    ],
    listeners: {
        // We listen for the `eventDrop` event and take action if dropped on the external grid
        eventDrop({eventRecords, externalDropTarget}) {
            if (externalDropTarget) {
                eventRecords.forEach((eventRecord) => {
                    eventRecord.type = 'unassigned';
                });
                eventStore.remove(eventRecords);
                unassignedEventStore.add(eventRecords);
            }
        },
    },
};

Crud manager works perfect. It syncs all data with backend (except second transaction, because it does not change anything).

export const crudManager = new CrudManager({
    eventStore,
    resourceStore,
    stores:   [ unassignedEventStore ],
    autoLoad: true,
    autoSync: true,
    transport:        {
        load: { url: `${API_URL}/api/load` },
        sync: { url: `${API_URL}/api/sync` },
    },
    listeners: {
        loadFail: processCRUDManagerError,
        syncFail: processCRUDManagerError,
    },
});

Here I added additional store "unassignedEventStore" and it syncs the same as the EventStore:

export const unassignedEventStore = new Store({
    id:             'unassignedEvents',
    syncDataOnLoad: true,
});

For drag-n-drop I used your example, so it is very ordinary ("class Drag extends DragHelper") and works good.

I have only the problem with double transactions when I move event from EventStore to Store or in reverse. Can you give me some hint or the direction where I can look how to prevent it or what I did wrong or missed to added? On forum I found a code to remove all other stores from stm and added it, but it does not help.


Post by alex.l »

Hello sterh,

Sorry, for long answer.

I was trying to reproduce this behaviour in our runnable example, and I don't see the behaviour you described.
I've edited our Scheduler example to make it work with STM. Please try the code below with examples/dragfromgrid demo and try to apply required changed to reproduce the bug you mentioned.

import '../../lib/Scheduler/widget/UndoRedo.js';
import AjaxStore from '../../lib/Core/data/AjaxStore.js';

const gridStore = new AjaxStore({
    modelClass : Task,
    readUrl    : 'data/unplanned.json',
    autoLoad   : true
});

class CustomResourceModel extends ResourceModel {
    static get $name() {
        return 'CustomResourceModel';
    }

static get fields() {
    return [
        // Do not persist `cls` field because we change its value on dragging unplanned resources to highlight the row
        { name : 'cls', persist : false }
    ];
}
}

let schedule = new Schedule({
    ref         : 'schedule',
    insertFirst : 'main',
    startDate   : new Date(2025, 11, 1, 8),
    endDate     : new Date(2025, 11, 1, 18),
    flex        : 4,
    crudManager : {
        autoLoad         : true,

    // stores : [gridStore],

    // This config enables response validation and dumping of found errors to the browser console.
    // It's meant to be used as a development stage helper only so please set it to false for production systems.
    validateResponse : true,
    eventStore       : {
        storeClass : TaskStore
    },
    resourceStore : {
        modelClass : CustomResourceModel
    },
    transport : {
        load : {
            url : 'data/data.json'
        }
    }
},

project : {
    stm: {
        autoRecord: true,
    }
},

tbar : [
    'Schedule view',
    {
        type:  'undoredo',
        items: { transactionsCombo: null },
    },
    '->',
    { type : 'viewpresetcombo' },
    {
        type        : 'button',
        toggleable  : true,
        icon        : 'b-fa-calendar',
        pressedIcon : 'b-fa-calendar-check',
        text        : 'Automatic rescheduling',
        tooltip     : 'Toggles whether to automatically reschedule overlapping tasks',
        cls         : 'reschedule-button',
        onToggle({ pressed }) {
            schedule.autoRescheduleTasks = pressed;
        }
    },
    {
        type        : 'buttonGroup',
        toggleGroup : true,
        items       : [
            {
                icon            : 'b-fa-fw b-fa-arrows-alt-v',
                pressed         : 'up.isVertical',
                tooltip         : 'Vertical mode',
                schedulerConfig : {
                    mode           : 'vertical',
                    subGridConfigs : {
                        locked : {
                            minWidth : 100,
                            flex     : null
                        }
                    }
                }
            },
            {
                icon            : 'b-fa-fw b-fa-arrows-alt-h',
                pressed         : 'up.isHorizontal',
                tooltip         : 'Horizontal mode',
                schedulerConfig : {
                    mode : 'horizontal'
                }
            }
        ],
        onAction({ source : button }) {
            const newConfig = { ...schedule.initialConfig, ...button.schedulerConfig };

            // Recreate the scheduler to switch orientation
            schedule.destroy();
            schedule = new Schedule(newConfig);

            // Provide drag helper a reference to the new instance
            drag.schedule = schedule;
        }
    }
]
});


schedule.project.stm.addStore(gridStore);

new Splitter({
    appendTo : 'main'
});

const unplannedGrid = new UnplannedGrid({
    ref         : 'unplanned',
    appendTo    : 'main',
    title       : 'Unplanned Tasks',
    collapsible : true,
    flex        : '0 0 300px',
    ui          : 'toolbar',

// Schedulers stores are contained by a project, pass it to the grid to allow it to access them
project : schedule.project,
store   : gridStore
});

const drag = new Drag({
    grid         : unplannedGrid,
    schedule,
    constrain    : false,
    outerElement : unplannedGrid.element
});

schedule.assignmentStore.on({
    // When a task is unassigned move it back to the unplanned tasks grid
    remove({ records }) {
        records.forEach(({ event }) => {
            schedule.eventStore.remove(event);
            unplannedGrid.store.add(event);
        });
    },
    thisObj : this
});

All the best,
Alex


Post Reply