Premium support for our pure JavaScript UI components


Post by ogardnerffdc »

Summary

When record.set() is called on a grouper field while the store has events suspended via beginBatch() / endBatch(), the affected records are not re-positioned under their new group header on endBatch(). They stay under their original group header until the next unbatched mutation on that field fires a change event, at which point all deferred re-grouping happens at once.

The same pattern works correctly for filtering and sorting — both of those are reapplied directly from Store.onUpdateRecord(), not via a change-event listener, so suspending events doesn't affect them.

I'd like to know whether this is intentional ("if you suspend events, you own the side-effects") or a bug worth fixing in endBatch().

Investigation

StoreGroup.onDataChanged (in data/mixin/StoreGroup.js) is a listener on the store's own change event:

construct(config) {
    super.construct(config);
    this.ion({
        change: 'onDataChanged',
        thisObj: this,
    });
}

onDataChanged({ changes, action }) {
    if (this.isGrouped && (
        !changes && resortActions[action] ||
        changes && this.groupers.some(g => g.rootFieldName in changes))) {
        this.sort();
    }
}

Store.beginBatch() calls suspendEvents(), so each record.set() inside the batch fires onModelChange but the change event is suppressed. endBatch() then fires only a refresh event with action: 'batch', which does not satisfy either branch in onDataChanged. Result: the deferred re-group is silently lost.

By contrast, the filter and sort branches in Store.onUpdateRecord() are called directly from onModelChange (above the if (!silent) check), so they survive the suspension:

onModelChange(record, toSet, wasSet, silent, fromRelationUpdate) {
    ...
    me.onUpdateRecord(record, wasSet);   // calls filter()/sort()
    if (!silent) {
        ...
        me.trigger('change', { action: 'update', ... });   // suppressed during batch
    }
}

It feels asymmetric that a batch wipes out grouping but not sorting/filtering.

Minimal repro

import { Grid } from '@bryntum/grid-thin';

const grid = new Grid({
    appendTo : document.body,
    features : { group: 'city' },
    columns  : [
        { text: 'Name', field: 'name', flex: 1 },
        { text: 'City', field: 'city', flex: 1 },
    ],
    data : [
        { id: 1, name: 'Alice',   city: 'London' },
        { id: 2, name: 'Bob',     city: 'London' },
        { id: 3, name: 'Charlie', city: 'Paris'  },
    ],
});

const { store }   = grid;
const [alice, bob] = [store.getById(1), store.getById(2)];

// Pre: alice and bob both sit under the London group header.
console.log(alice.instanceMeta(store).groupParent.meta.groupRowFor); // 'London'

// --- without the batch: row gets moved into the Paris group on the next render.
// alice.set('city', 'Paris');

// --- with the batch: row stays under the London header until something else
//     mutates a grouper field outside the batch.
store.beginBatch();
alice.set('city', 'Paris');
bob.set('city', 'Paris');
store.endBatch();

console.log(alice.instanceMeta(store).groupParent.meta.groupRowFor); // still 'London'
console.log(bob.instanceMeta(store).groupParent.meta.groupRowFor);   // still 'London'

If you then run any unbatched record.set('city', ...) on a single record, the deferred re-group fires once and everything including alice and bob ends up under the Paris header.

Workaround we're shipping

Where we have a batched bulk-edit operation, we follow endBatch() with an explicit store.group() if the touched field matches an active grouper:

store.beginBatch();
for (const record of records) {
    record.set(fieldName, value);
}
store.endBatch();

if (
    store.isGrouped &&
    store.groupers.some(g => g.field === fieldName)
) {
    store.group();   // catch up on the deferred re-group
}

This is fine for our use case but it means every consumer that batches has to remember to do this — which is precisely the kind of thing endBatch() already handles for sort/filter.

Questions

  1. Is the current behaviour intentional, or worth filing as a defect?

  2. If intentional, would you consider documenting it on beginBatch() / endBatch() so consumers know they need to re-group manually after batched grouper-field mutations? The asymmetry vs sort/filter is the confusing part.

Thanks!

Ollie

Certinia (formerly FinancialForce)


Post by tasnim »

That looks like a bug to me! Thanks for your detailed report. We'll investigate it. Here is the ticket to track progress https://github.com/bryntum/support/issues/13106

Best regards,
Tasnim

How to ask for help? Please read our Support Policy


Post Reply