Premium support for our pure JavaScript UI components


Post by alexandran »

In our Grid we have a column that has "persist" set to false but if that row has another of its field's empty then the cell get's stuck in a 'dirty' state i.e. yellowed

We have backend filtering on one of our fields (known as field A from now on), the same field that is related to the column with has 'persist' set to false so when filtering based on field A the store of records changes. Therefore when the filter is removed the records return to the store 'new'.

The issue is when we filter on field A and then swap this filter for one on another field (known as field B from now on) and then finally remove all filters. The records that did not match the original filter on field A will flash dirty for a second and then clean themselves once they realise they are not supposed to be persisted EXCEPT the records with no value for field B, these will remain in the dirty state. See screenshot below

Screenshot 2023-01-12 at 22.50.25.png
Screenshot 2023-01-12 at 22.50.25.png (33.2 KiB) Viewed 535 times

Post by tasnim »

I'm not sure how to reproduce the issue that you're facing. Could you please upload a video showing the steps to reproduce it? And the demo link where you can reproduce or the code that you're using to reproduce it.


Post by sulemanzaki »

Hi, I think I've been able to recreate something similar in the 'Cell Edit Demo' (https://bryntum.com/products/grid/examples/celledit/) that might help to find a solution for this problem for us.

I am running this code for the demo:

import { Widget, EventHelper, Grid, DataGenerator, DateHelper, Model } from '../../build/grid.module.js?465031';
import shared from '../_shared/shared.module.js?465031';

class MyModel extends Model {
    static get fields() {
        return [
            { name : 'score', persist : false }
        ];
    }
}

// YesNo is a custom button that toggles between Yes and No on click
class YesNo extends Widget {

static get $name() {
    return 'YesNo';
}

// Factoryable type name
static get type() {
    return 'yesno';
}

// Hook up a click listener during construction
construct(config) {
    // Need to pass config to super (Widget) to have things set up properly
    super.construct(config);

    // Handle click on the element
    EventHelper.on({
        element : this.element,
        click   : 'onClick',
        thisObj : this
    });
}

// Always valid, this getter is required by CellEdit feature
get isValid() {
    return true;
}

// Get current value
get value() {
    return Boolean(this._value);
}

// Set current value, updating style
set value(value) {
    this._value = value;

    this.syncInputFieldValue();
}

// Required by CellEdit feature to update display value on language locale change
// Translation is added to examples/_shared/locales/*
syncInputFieldValue() {
    const
        {
            element,
            value
        } = this;

    if (element) {
        element.classList[value ? 'add' : 'remove']('yes');
        element.innerText = value ? this.L('L{Object.Yes}') : this.L('L{Object.No}');
    }
}

// Html for this widget
template() {
    return `<button class="yesno"></button>`;
}

// Click handler
onClick() {
    this.value = !this.value;
}
}

// Register this widget type with its Factory
YesNo.initClass();

let newPlayerCount = 0;

const grid = new Grid({

appendTo : 'container',

features : {
    cellEdit : true,
    sort     : 'name',
    stripe   : true
},

// Show changed cells
showDirty : true,

async validateStartDateEdit({ grid, value }) {
    if (value > DateHelper.clearTime(new Date())) {
        return grid.features.cellEdit.confirm({
            title   : 'Selected date in future',
            message : 'Update field?'
        });
    }
    return true;
},

store : {
     modelClass : MyModel
},

columns : [
    { text : 'Name', field : 'name', flex : 1 },
    {
        text   : 'Birthplace',
        field  : 'city',
        width  : '8em',
        editor : { type : 'dropdown', items : DataGenerator.cities }
    },
    { text : 'Team', field : 'team', flex : 1 },
    { text : 'Score', field : 'score', editor : 'number', width : '5em' },
    {
        text             : 'Start',
        id               : 'start',
        type             : 'date',
        field            : 'start',
        width            : '9em',
        finalizeCellEdit : 'up.validateStartDateEdit'
    },
    { text : 'Finish (readonly)', type : 'date', field : 'finish', width : '9em', editor : false },
    { text : 'Time', id : 'time', type : 'time', field : 'time', width : '10em' },
    // Column using the custom widget defined above as its editor
    {
        text     : 'Custom', // `text` gets localized automatically, is added to examples/_shared/locales/*
        field    : 'done',
        editor   : 'yesno',
        width    : '5em',
        renderer : ({ value }) => value ? YesNo.L('L{Object.Yes}') : YesNo.L('L{Object.No}')
    },
    { type : 'percent', text : 'Percent', field : 'percent', flex : 1 }
],

data : DataGenerator.generateData(10),

listeners : {
    selectionChange({ selection }) {
        removeButton.disabled = !selection.length || grid.readOnly;
    }
},

tbar : [
    {
        type        : 'button',
        ref         : 'readOnlyButton',
        text        : 'Read-only',
        tooltip     : 'Toggles read-only mode on grid',
        toggleable  : true,
        icon        : 'b-fa-square',
        pressedIcon : 'b-fa-check-square',
        onToggle    : ({ pressed }) => {
            addButton.disabled = insertButton.disabled = grid.readOnly = pressed;

            removeButton.disabled = pressed || !grid.selectedRecords.length;
        }
    },
    {
        type  : 'buttongroup',
        items : [
            {
                type     : 'button',
                ref      : 'addButton',
                icon     : 'b-fa-plus-circle',
                text     : 'Add',
                tooltip  : 'Adds a new row (at bottom)',
                onAction : () => {
                    const
                        counter = ++newPlayerCount,
                        added   = grid.store.add({
                            name : `New player ${counter}`,
                            cls  : `new_player_${counter}`
                        });

                    grid.selectedRecord = added[0];
                }
            },
            {
                type     : 'button',
                ref      : 'insertButton',
                icon     : 'b-fa-plus-square',
                text     : 'Insert',
                tooltip  : 'Inserts a new row (at top)',
                onAction : () => {
                    const
                        counter = ++newPlayerCount,
                        added   = grid.store.insert(0, {
                            name : `New player ${counter}`,
                            cls  : `new_player_${counter}`
                        });

                    grid.selectedRecord = added[0];
                }
            }
        ]
    },
    {
        type     : 'button',
        ref      : 'removeButton',
        color    : 'b-red',
        icon     : 'b-fa b-fa-trash',
        text     : 'Remove',
        tooltip  : 'Removes selected record(s)',
        disabled : true,
        onAction : () => {
            const selected = grid.selectedRecords;

            if (selected && selected.length) {
                const
                    store      = grid.store,
                    nextRecord = store.getNext(selected[selected.length - 1]),
                    prevRecord = store.getPrev(selected[0]);

                store.remove(selected);
                grid.selectedRecord = nextRecord || prevRecord;
            }
        }
    }
]
});

const { addButton, removeButton, insertButton } = grid.widgetMap;

// Modify score cell of first record
grid.store.getAt(0).score = 200;

// log properties
console.log(grid.store.allRecords[0].fields[4].name);
console.log(grid.store.allRecords[0].fields[4].persist);
console.log(grid.store.allRecords[0].hasPersistableChanges);
console.log(grid.store.allRecords[0].isModified);
console.log(grid.store.allRecords[0].modificationData);
console.log(grid.store.allRecords[0].rawModifications);
console.log(grid.store.allRecords[0].persistableData);

As you can see I have set the persist property for the 'score' field to false in the model class (MyModel) and I am then using that model for the grid's store. I am then modifying the first record to change its 'score' value.
When I read the console logs I can see that:
- the field for the 'score' column has persist:false
and for the modified record
- hasPersistableChanges is false;
- modificationData and rawModifications are both null
- and persistableData does not contain the 'score' field.

However isModified returns true and the cell is marked as dirty in the grid. We are under the impression that any changes made to fields with persist set to false would not mark the cell as dirty. Is there any reason why isModified is returning true when the modificationData is null?

I would only expect the cell to be marked as dirty when persist is true for the changed field because then the modificationData is not null and the field is included in persistableData as expected.

I have attached screenshots showing the outputs of the console logs with both persist false & true for the 'score' field.

Many thanks,
Suleman

Attachments
Screenshot 2023-01-25 at 15.01.03.png
Screenshot 2023-01-25 at 15.01.03.png (75.43 KiB) Viewed 287 times
Screenshot 2023-01-25 at 14.59.46.jpeg
Screenshot 2023-01-25 at 14.59.46.jpeg (20.9 KiB) Viewed 287 times
Screenshot 2023-01-25 at 14.59.33.jpeg
Screenshot 2023-01-25 at 14.59.33.jpeg (22.57 KiB) Viewed 287 times

Post by alex.l »

Hi Suleman,

Thank you for detailed explanation and clear test case. I've reproduced that and confirmed that is a bug. I've opened a ticket to fix this here https://github.com/bryntum/support/issues/6054

All the best,
Alex


Post Reply