/*
 *  Copyright 2023 Anyware Services
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */


/**
 * Show the values of a repeater in a dialog, and allow the inline modification of its entries
 */
Ext.define('Ametys.plugins.cms.search.SearchGridRepeaterDialog', {
    singleton: true,
    
    convert: function(entries, subcolumns, dataIndex)
    {
        let fields = Ametys.plugins.cms.search.SearchGridHelper.getFieldsFromJson(subcolumns);
        let store = Ext.create('Ext.data.Store', {
            model: this._createModel(fields)
        });

        let dataToStore = [];
        Ext.Object.each(entries, function (key, entry) {
            dataToStore.push(Ext.clone(entry.values));
        });
        store.loadData(dataToStore, false);

        // Update the parent value with the converted values
        store.each(function(record, index, size) {
        //    entries[index] = value;
            entries[index].values = record.data;
        });
    },
    
    /**
     * Open and show a dialog containing all the repeater entries from a content displayed in a content grid, 
     * or from another repeater dialog
     * @param {String} title The title of the dialog
     * @param {String} parentGridId The id of the parent grid. Can be a content grid, or a grid from another dialog
     * @param {String} recordId The id of the record containing the repeater
     * @param {String} contentId The id of the main content. Can be the same as the recordId.
     * @param {Object} subcolumns The sub columns to edit 
     * @param {Object} repeaterCfg The repeater info 'min-size', 'max-size', 'initial-size', 'add-label', 'del-label', 'header-label
     * @param {String} dataIndex The name of the repeater metadata
     * @param {String} metadataPath The path to the repeater metadata. Used to get the repeater columns definition
     * @param {String} contentGridId The id of the main grid containing the content.
     * @param {Function} callback a callback function to invoke after the dialog is closed, can be null
     * @return the dialog
     */
    showRepeaterDialog: function (title, parentGridId, recordId, contentId, subcolumns, repeaterCfg, dataIndex, metadataPath, contentGridId, callback) 
    {
        var parentRecord = null;
        var parentGrid = null;
        if (parentGridId != '' && recordId != '')
        {
            parentGrid = Ext.getCmp(parentGridId);
            if (parentGrid != '')
            {
                parentRecord = parentGrid.getStore().getById(recordId);
            }
        }

        if (parentRecord == null)
        {
            Ametys.log.ErrorDialog.display({
                title: "{{i18n UITOOL_SEARCH_ERROR_TITLE}}",
                text: "{{i18n UITOOL_SEARCH_ERROR_REPEATER}}",
                category: 'Ametys.plugins.cms.search.SearchGridRepeaterDialog'
            });
            return;
        }
        
        title = title || "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_SEARCH_REPEATER_MODAL_TITLE}}";

        return this._openDialog(title, parentRecord, contentId, subcolumns, repeaterCfg, dataIndex, metadataPath, contentGridId, callback);
    },

    /**
     * @private
     * Create and open a new dialog
     * @param {String} title The title of the dialog
     * @param {Ext.data.Model} parentRecord The record containing the repeater
     * @param {String} contentId The id of the main content.
     * @param {Objet} subcolumns The sub columns to edit 
     * @param {Object} repeaterCfg The repeater info 'min-size', 'max-size', 'initial-size', 'add-label', 'del-label', 'header-label
     * @param {String} dataIndex The name of the repeater metadata
     * @param {String} metadataPath The path to the repeater metadata. Used to get the repeater columns definition
     * @param {String} contentGridId The id of the main grid containing the content.
     * @param {Function} callback a callback function to invoke after the dialog is closed, can be null
     * @return the opened dialog
     */
    _openDialog: function (title, parentRecord, contentId, subcolumns, repeaterCfg, dataIndex, metadataPath, contentGridId, callback)
    {
        let items = this._createItems(title, parentRecord, contentId, subcolumns, repeaterCfg, dataIndex, metadataPath, contentGridId);
        let grid = items[0];
        
        var dialog = this._showDialog(
            title, 
            items, 
            'fit',
            function () {
                this._ok(dialog, parentRecord, grid, dataIndex, contentId, callback);
            },
            callback
        );
        
        dialog.setReadOnly = function() {
            grid.fireEvent('datachanged');
        }
        
        return dialog;
    },
    
    /**
     * Create the repeater grid
     */
    _createItems: function(title, parentRecord, contentId, subcolumns, repeaterCfg, dataIndex, metadataPath, contentGridId, repeaterListeners, filter, cellEditingPluginScope)
    {
        let me = this;
        
        cellEditingPluginScope = cellEditingPluginScope || me;
        repeaterListeners = repeaterListeners || {};
        
        var store = Ext.create('Ext.data.Store', {
            model: this._createModel(Ametys.plugins.cms.search.SearchGridHelper.getFieldsFromJson(subcolumns)),
            parentRecord: parentRecord,
            parentMetadataPath: metadataPath,
            data: []
        });
        
        let columns = Ametys.plugins.cms.search.SearchGridHelper.getColumnsFromJson(subcolumns, true);
        columns.map(c => { 
            c.lockable = false; 
            c.sortable = false;
            c.hideable = false;
            if (c.editor)
            {
                c.editor.contentInfo = c.editor.contentInfo || {};
                c.editor.contentInfo.contentId = contentId;
            }
        });
        columns.splice(0,0, {
            xtype: 'actioncolumn',
            menuDisabled: true,
            sortable: false,
            locked: true,
            lockable: false,
            
            width: 91,
            
            items: [ '@delete', '@add', '@up', '@down' ]
        });

        let grid = Ext.create("Ext.grid.Panel", {
            store: store,
            allowEdition: true,
            cls: ['edit-contents-grid', 'a-visible-focus', 'edit-contents-view'],
            
            columns: columns,
            
            actions: {
                'delete': {
                    iconCls: 'x-tool-delete',
                    tooltip: repeaterCfg['del-label'],
                    handler: function(view, rowIndex, colIndex, item, e, record, row) {
                        window.setTimeout(function() {
                            if (!Ext.fly(e.target).hasCls("x-item-disabled"))
                            {
                                // Confirm deletion
                                Ametys.Msg.confirm(
                                    "{{i18n plugin.core-ui:PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_DELETE}}",
                                    "{{i18n plugin.core-ui:PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_DELETE_CONFIRM}}",
                                    function (answer)
                                    {
                                        if (answer == 'yes')
                                        {
                                            // Remove the entry.
                                            store.removeAt(rowIndex);
                                            repeaterListeners['delete'] && repeaterListeners['delete'].call(this, record, view.ownerGrid);
                                        }
                                    },
                                    this
                                );
                            }
                        }, 1);
                    }
                },
                'add': {
                    iconCls: 'x-tool-plus',
                    tooltip: repeaterCfg['add-label'],
                    handler: function(view, rowIndex, colIndex, item, e, record, row) {
                        window.setTimeout(function() {
                            if (!Ext.fly(e.target).hasCls("x-item-disabled"))
                            {
                                me._addRepeaterItem(grid, store, parentRecord, dataIndex, rowIndex + 1, repeaterListeners);
                            }
                        }, 1);
                    }
                },
                'up': {
                    iconCls: 'x-tool-moveup',
                    tooltip: "{{i18n plugin.core-ui:PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_MOVE_UP}}",
                    handler: function(view, rowIndex, colIndex, item, e, record, row) {
                        window.setTimeout(function() {
                            if (!Ext.fly(e.target).hasCls("x-item-disabled"))
                            {
                                store.removeAt(rowIndex);
                                store.insert(rowIndex - 1, record);
                            }
                        }, 1);
                    } 
                },
                'down': {
                    iconCls: 'x-tool-movedown',
                    tooltip: "{{i18n plugin.core-ui:PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_MOVE_DOWN}}",
                    handler: function(view, rowIndex, colIndex, item, e, record, row) {
                        window.setTimeout(function() {
                            if (!Ext.fly(e.target).hasCls("x-item-disabled"))
                            {
                                store.removeAt(rowIndex);
                                store.insert(rowIndex + 1, record);
                            }
                        }, 1);
                    }
                }
            },

            plugins: [
                Ext.create('Ext.grid.plugin.CellEditing', {
                    clicksToEdit: 1,
                    editAfterSelect: false,

                    moveEditorOnEnter: true, 
                
                    listeners: {
                      'beforeedit': Ext.bind(cellEditingPluginScope._beforeEdit, cellEditingPluginScope, [contentId, contentGridId], true),
                      'canceledit': Ext.bind(cellEditingPluginScope._cancelEdit, cellEditingPluginScope, [contentId, contentGridId], true),
                      'edit': Ext.bind(cellEditingPluginScope._edit, cellEditingPluginScope, [contentId, contentGridId], true),
                      'validateedit': Ext.bind(cellEditingPluginScope._validateEdit, cellEditingPluginScope)
                    },
                    
                    activateCell: function(position) {
                        let args = [];
                        for (let a of arguments)
                        {
                            args.push(a);
                        }
                        
                        while (args.length < 6)
                        {
                            args.push(undefined);
                        }
                        args.push(dataIndex + "[" + (position.rowIdx + 1) + "]"); // special hidden arg
                        args.push(parentRecord); // special hidden args :D 
                        Ametys.cms.content.EditContentsView.prototype._activateCell.apply(this, args);
                    }
                })
            ],

            contentId: contentId,
            dialogTitle: title,
            metadataPath: metadataPath,
            contentGridId: contentGridId
        });
        
        let button = Ext.create("Ext.Button", {
            tooltip: repeaterCfg['add-label'],
            hidden: true,
            cls: ['x-tool-plus', 'a-single-addbutton', 'x-action-col-icon'],
            handler: function() {
                me._addRepeaterItem(grid, store, parentRecord, dataIndex, 0, repeaterListeners);
            }
        });

        function datachanged()
        {
            let repeaterDisabled = repeaterCfg.disabled || parentRecord.data['notEditableData'] === true || parentRecord.data['notEditableDataIndex'] && Ext.Array.contains(parentRecord.data['notEditableDataIndex'], metadataPath);
            
            let size = store.getCount();
            
            button.setDisabled(repeaterDisabled || (repeaterCfg['max-size'] ? size >= repeaterCfg['max-size'] : false));
            
            if (!grid.el) 
            {
                grid.on('afterlayout', datachanged, this, { single: true });
            }
            else
            {
                window.setTimeout(function() {
                    // avoid non matching columns height
                    grid.lockedGrid.updateLayout()
                    
                    store.getData().each(function(record, index, size) {
                        let buttonsDom = Ext.fly(grid.el.query("table[data-recordindex='" + index + "']")[0]).query("[role='button']");
                        
                        // delete
                        Ext.fly(buttonsDom[0]).toggleCls("x-item-disabled", repeaterDisabled || record.get('__notEditable') || (repeaterCfg['min-size'] ? size <= repeaterCfg['min-size'] : false));
                        // add
                        Ext.fly(buttonsDom[1]).toggleCls("x-item-disabled", repeaterDisabled                                || (repeaterCfg['max-size'] ? size >= repeaterCfg['max-size'] : false));
                        // up
                        Ext.fly(buttonsDom[2]).toggleCls("x-item-disabled", repeaterDisabled || record.get('__notEditable') || index == 0);
                        //down
                        Ext.fly(buttonsDom[3]).toggleCls("x-item-disabled", repeaterDisabled || record.get('__notEditable') || (index >= size - 1));
                    });
                }, 1);
            }
        }
        
        Ametys.cms.content.EditContentsView.prototype.updateChangeListeners.apply(grid, []);
        
        store.on('datachanged', datachanged);
        grid.view.lockedView.on('update', datachanged);
        
        var entries = (filter || function(x) {return x ? x.entries : x;}).call(this, parentRecord.getData()[dataIndex + "_repeater"]);
        if (entries != null)
        {
            var dataToStore = [];
            Ext.Object.each(entries, function (key, entry) {
                dataToStore.push(Ext.clone({...entry.values, __notEditable: entry.notEditable === true, 'previous-position': entry['previous-position'] || entry.position}));
            });
            store.loadData(dataToStore, false);
        };
        
        return [grid, button];
    },
    
    _addRepeaterItem: function(grid, store, parentRecord, dataIndex, index, repeaterListeners)
    {
        let r = store.insert(index, {});
        this._markAllAsModified(grid, r[0]);
        
        // Get external disable condition values from recird values
        let externalDisableConditionsValues = parentRecord.get(dataIndex + '_repeater')['__externalDisableConditionsValues'];
        if (!externalDisableConditionsValues)
        {
            // If there is no entry at store laod in the repeater, no external disable condition values will be found
            // Check for repeater default values
            let rootRecord = Ametys.plugins.cms.search.SearchGridHelper._getRootRecord(parentRecord).record;
            let repeatersDefaultValues = rootRecord.get('__repeatersDefaultExternalDisableConditionValues');
            if (repeatersDefaultValues)
            {
                let parentDataPath = Ametys.plugins.cms.search.SearchGridHelper._getParentMetadataPath(parentRecord);
                let dataPath = (parentDataPath ? parentDataPath + '/' : '') + dataIndex;
                externalDisableConditionsValues = repeatersDefaultValues[dataPath];
            }
        }
        r[0].set('__externalDisableConditionsValues', externalDisableConditionsValues);
        repeaterListeners['add'] && repeaterListeners['add'].call(this, r[0], grid);
    },

    _markAllAsModified: function(grid, record)
    {
        function _getDefaultValue(t)
        {
            if (!grid)
            {
                return null;
            }
            
            let columns = grid.getColumns().filter(c => c.dataIndex == t);
            if (columns.length == 0 
                || !columns[0].getEditor()
                || columns[0].getEditor().defaultValue === undefined)
            {
                return null;
            }
            return columns[0].getEditor().defaultValue;
        }
        
        let d  = {};
        for (let t in record.fieldsMap)
        {
            d[t] = record.data[t] !== undefined ?
                    record.data[t]  
                    : _getDefaultValue(t);
        }
        record.set(d)
    },
    
    /**
     * @protected
     * Display the dialog
     */
    _showDialog: function(title, items, layout, okHandler, callback, additionnalButtons)
    {
        var buttons = additionnalButtons || [];
        buttons.push({
            reference: 'okButton',
            text :"{{i18n plugin.cms:PLUGINS_CMS_UITOOL_SEARCH_REPEATER_MODAL_OK}}",
            handler: okHandler,
            scope: this
        }, {
            text :'{{i18n plugin.cms:PLUGINS_CMS_UITOOL_SEARCH_REPEATER_MODAL_CANCEL}}',
            handler: function () {
                dialog.close();
            },
            scope: this
        });
        
        let dialog = Ext.create("Ametys.window.DialogBox", {
            title: title,
            
            cls: 'a-searchgridrepeater',
            
            closeAction : 'destroy',
            width : Math.max(550, window.innerWidth * 0.8),
            height : Math.max(500, window.innerHeight * 0.8),
            layout: layout,
            
            items: items,
            
            defaultButton: 'okButton',
            referenceHolder: true,
            
            // Buttons
            buttons : buttons,
            
            listeners: {
                'beforeclose': function() {
                    if (this._closeForced)
                    {
                        this._closeForced = false;
                        if (callback && !dialog.doNotCallCallback)
                        {
                            callback(null);
                        }
                        return;
                    }
                    else
                    {
                        this._askToDiscard(dialog);
                        return false; // do not close yet
                    }
                },
                scope: this
            }
        });
        dialog.show();
        return dialog;
    },
    
    /**
     * @private
     * Test if there is any modification and ask if it is ok to discard them.
     * If it is ok, or no modification call the callback
     * @param {Ext.Component} dialog The dialog box
     */
    _askToDiscard: function(dialog)
    {
        let me = this;
        
        function finish()
        {
            me._closeForced = true;
            dialog.close();
        }
        
        if (this._anyModificationToDiscard(dialog)) // any modif ?
        {
            Ametys.Msg.confirm("{{i18n plugin.cms:UITOOL_CONTENTEDITIONGRID_SAVEBAR_UNSAVE_CONFIRM_LABEL}}", 
                "{{i18n plugin.cms:UITOOL_CONTENTEDITIONGRID_SAVEBAR_UNSAVE_CONFIRM_DESC}}",
                function(answer) {
                    if (answer == 'yes') {
                        finish();
                    }
                }
            );        
        }
        else
        {
            finish();
        }
    },
    
    /**
     * @protected
     * Test if there is any modification in the dialog
     * @param {Ext.Component} dialog The dialog box
     * @return {Boolean} true if any modification
     */
    _anyModificationToDiscard: function(dialog)
    {
        let grids = dialog.query("gridpanel[isEditable]");
        for (let grid of grids)
        {
            if (grid.store.getModifiedRecords().length > 0 || grid.store.getRemovedRecords().length > 0)
            {
                return true;
            }
        }
        return false;
    },

    /**
     * @private
     * Create a new entry model for the grid
     * @param {Object[]} fields The fields configuration
     * @return {String} The model name
     */
    _createModel: function(fields, modelName)
    {
        fields = fields || [
            {name: 'id', mapping: 'id'}
        ];
        
        modelName = modelName || (Ext.id() + "-Ametys.plugins.cms.search.SearchGridRepeaterDialog.ContentEntry");
        
        if (Ext.data.schema.Schema.get('default').hasEntity(modelName)) 
        {
            Ext.data.schema.Schema.get('default').getEntity(modelName).replaceFields(fields, true);
        }
        else
        {
            Ext.define(modelName, {
                extend: 'Ext.data.Model',
                schema: 'default',
                fields: fields
            });
        }

        return modelName
    },
    
    /**
     * @private
     * Callback when validating the dialog
     * @param {Ametys.window.DialogBox} dialog The current dialog
     * @param {Ext.data.Model} parentRecord The parent record
     * @param {Ext.grid.Panel} grid The current grid
     * @param {String} dataIndex The repeater metadata name
     * @param {String} contentId The main content id
     * @param {Function} callback a callback function to invoke after the dialog is closed, can be null
     */
    _ok: function (dialog, parentRecord, grid, dataIndex, contentId, callback)
    {
        var entriesValues = this._gridToValues(grid);
        if (entriesValues == null)
        {
            Ametys.log.ErrorDialog.display({
                title: "{{i18n UITOOL_CONTENTEDITIONGRID_REPEATER_VALUE_ERROR_TITLE}}",
                text: "{{i18n UITOOL_CONTENTEDITIONGRID_REPEATER_VALUE_ERROR_DESC}}",
                details: "The repeater has some invalid values",
                category: this.self.getName()
            });
            
            return;
        }
        
        var repeaterValues = parentRecord.getData()[dataIndex + "_repeater"];
        repeaterValues.entries = [];
        for (var index = 0; index < entriesValues.length; index++)
        {
            let notEditable = entriesValues[index].__notEditable;
            delete entriesValues[index].__notEditable;
            
            repeaterValues.entries[index] = {
                position: index + 1,
                values: entriesValues[index]
            };
            
            if (entriesValues[index]['previous-position'] != repeaterValues.entries[index].position)
            {
                repeaterValues.entries[index]['previous-position'] = entriesValues[index]['previous-position'];
            }
            delete entriesValues[index]['previous-position'];
            
            if (notEditable)
            {
                repeaterValues.entries[index].notEditable = true;
            }
        }

        if (callback)
        {
            callback(repeaterValues);
            dialog.doNotCallCallback = true;
        }
        
        this._closeForced = true;
        dialog.close();
    },
    
    /**
     * @private
     * Convert the data of the given grid into repeater values
     * @param {Ext.grid.Panel} grid The current grid
     * @return {Object[]} The array of data of the grid
     */
    _gridToValues: function(grid)
    {
        var entriesValues = [];
        
        var gridStore = grid.getStore();
        for (var index = 0; index < gridStore.getCount(); index++)
        {
            //var updatedValues = {};
            var record = gridStore.getAt(index);
            
            for (let column of grid.getColumns())
            {  
                if (!Ametys.plugins.cms.search.SearchGridHelper.isValidValue(column, record, undefined, grid.getStore()))
                {
                    return null;
                }
            }

            var data = record.getData();
            /*Ext.Array.each(grid.getColumns(), function (column) {
                if (data[column.dataIndex])
                {
                    updatedValues[column.dataIndex] = data[column.dataIndex];
                }
            });*/
            entriesValues.push(data);
            //entriesValues.push(updatedValues);
        }
        
        return entriesValues;
    },

    /**
     * Locks a content when entering in edition
     * @param {Ext.grid.plugin.CellEditing} editor The editor plugin
     * @param {Object} e An edit event with the following properties:
     * @param {Ext.grid.Panel} e.grid The grid
     * @param {Ext.data.Model} e.record The record being edited
     * @param {String} e.field The field name being edited
     * @param {Ext.data.Model} e.value The value for the field being edited.
     * @param {HTMLElement} e.row The grid table row
     * @param { Ext.grid.column.Column} e.column The grid {@link Ext.grid.column.Column Column} defining the column that is being edited.
     * @param {Number} e.rowIdx The row index that is being edited
     * @param {Number} e.colIdx The column index that is being edited
     * @param {boolean} e.cancel Set this to true to cancel the edit or return false from your handler.
     * @param {Object} eOpts The options object passed to Ext.util.Observable.addListener
     * @param {String} contentId The current content id
     * @param {String} contentGridId The id of the grid containing the content
     * @private
     */
    _beforeEdit: function (editor, e, eOpts, contentId, contentGridId)
    {
        let field = e.column.getEditor();
        if (e.value === undefined && field.defaultValue !== undefined)
        {
            field.switchToValue = field.defaultValue;
        }
        else
        {
            field.switchToValue = undefined;
        }
        
        var contentRecord = null;

        var contentGrid = Ext.getCmp(contentGridId);
        if (contentGrid != null)
        {
            contentRecord = contentGrid.editingPlugin._findRecords(contentId)[0];
        }
        if (contentRecord == null)
        {
            return false;
        }

        if (this._isRecordMetadataNotModifiable(contentRecord, e.grid.metadataPath || e.grid.ownerGrid.metadataPath, e.field, e.record, e.cell)) 
        {
            // Repeater widget need to be always clickable (when not empty) to be able to see values
            if (e.column.type != 'repeater' || e.value._size == 0)
            {
                return false;
            }
        }

        // Is an external attribute
        else if (e.record.data[e.field + '_external_status'] == 'external')
        {
            this.getLogger().debug("Cannot edit field " + e.field + " for parent record " + e.record.getId() + " because synchronization status is currently external");
            return false;
        }

        else if (!contentRecord.dirty)
        {
            Ametys.cms.content.ContentDAO.getContent(contentId, Ext.bind(this._beforeEditContentCB, this, [editor, contentRecord, contentGrid], 1));
        }
        
         // After the setValue, the size of the field may have changed... lets realign it
        window.setTimeout(function() {
            if (e.grid.editingPlugin && e.grid.editingPlugin.getActiveEditor())
            {
                e.grid.editingPlugin.getActiveEditor().realign(true);
            }
        }, 1)
    },
    
    _isRecordMetadataNotModifiable: function(contentRecord, metadataPath, fieldName, repeaterRecord, cell)
    {
        return (contentRecord.data.notEditableData === true 
            || contentRecord.data.notEditableDataIndex 
                && (Ext.Array.contains(contentRecord.data.notEditableDataIndex, metadataPath) 
                    || Ext.Array.contains(contentRecord.data.notEditableDataIndex, metadataPath + '/' + fieldName))
            || cell && Ext.fly(cell).hasCls('cell-disabled')
            || repeaterRecord && repeaterRecord.get('__notEditable'))
            
            === true;
    },

    /**
     * Callback for #_beforeEdit to get the content associated to the record
     * @param {Ametys.cms.content.Content} content The content being edited
     * @param {Ext.grid.plugin.CellEditing} editingPlugin The editor plugin
     * @param {Ext.data.Model} record The record being edited
     * @param {Ext.grid.Panel} contentGrid The grid with the content
     * @private
     */
    _beforeEditContentCB: function(content, editingPlugin, record, contentGrid)
    {
        if (content == null)
        {
            // An error was already shown to the user by the DAO
            record.data.notEditableData = true;
            this._doCancelEdition(editingPlugin);
        }
        
        // Check if edit action is possible 
        else if (content.getAvailableActions().indexOf(contentGrid.ownerGrid.workflowEditActionId) == -1)
        {
            record.data.notEditableData = true;
            this._doCancelEdition(editingPlugin);
            Ametys.log.ErrorDialog.display({
                title: "{{i18n UITOOL_CONTENTEDITIONGRID_LOCK_ERROR_TITLE}}",
                text: "{{i18n UITOOL_CONTENTEDITIONGRID_LOCK_ERROR_DESC}}",
                details: "The content '" + content.getId() + "' cannot be edited with workflow action '" + contentGrid.ownerGrid.workflowEditActionId + "'. Available actions are " + content.getAvailableActions(),
                category: this.self.getName()
            });
        }
    },

    /**
     * @private
     * The current edition has to be silently canceled.
     * @param {Ext.grid.plugin.CellEditing} editingPlugin The editor plugin
     */
    _doCancelEdition: function(editingPlugin)
    {
        editingPlugin.suspendEvent('canceledit');
        editingPlugin.cancelEdit();
        editingPlugin.resumeEvent('canceledit');
    },

    /**
     * When an edition ends, do not mark the field as dirty if the value did not changed
     * @param {Ext.grid.plugin.CellEditing} editor The editor plugin
     * @param {Object} e An edit event with the following properties:
     * @param {Ext.grid.Panel} e.grid - The grid
     * @param {Ext.data.Model} e.record - The record being edited
     * @param {String} e.field - The field name being edited
     * @param {Object} e.value - The value for the field being edited.
     * @param {Object} e.originalValue - The value for the field before the edit
     * @param {HTMLElement} e.row - The grid table row
     * @param { Ext.grid.column.Column} e.column - The grid {@link Ext.grid.column.Column Column} defining the column that is being edited.
     * @param {Number} e.rowIdx - The row index that is being edited
     * @param {Number} e.colIdx - The column index that is being edited
     * @param {Object} eOpts The options object passed to Ext.util.Observable.addListener
     * @param {String} contentId The current content id
     * @param {String} contentGridId The id of the grid containing the content
     * @private
     */
    _edit: function(editor, e, eOpts, contentId, contentGridId)
    {
        // We want to consider approximative equals
        if (e.record.modified !== undefined && (Ext.isEmpty(e.record.modified[e.field]) && Ext.isEmpty(e.record.data[e.field]))
            // We want to compare arrays of values
            || e.record.modified !== undefined && Ext.isArray(e.record.modified[e.field]) && Ext.isArray(e.record.data[e.field]) && Ext.Array.equals(e.record.modified[e.field], e.record.data[e.field]))
        {
            // We will reset such values
            e.record.set(e.field, e.record.modified[e.field]);
        }
        
        if (e.record.modified === undefined || e.record.modified[e.field] === undefined)
        {
            this._cancelEdit(editor, e, eOpts, contentId, contentGridId);
        }
        
        e.grid.ownerGrid.lockedGrid.getView().refresh();
        e.grid.ownerGrid.normalGrid.getView().refresh();
        
        // Reset after ending, so the next edition may not compare with old values (such as combobox RUNTIME-3972)
        e.column.getEditor().reset();        
    },

    /**
     * Unlocks a content when canceling edition (if no other modifications where running)
     * @param {Ext.grid.plugin.CellEditing} editor The editor plugin
     * @param {Object} e An edit event with the following properties:
     * @param {Ext.grid.Panel} e.grid - The grid
     * @param {Ext.data.Model} e.record - The record being edited
     * @param {String} e.field - The field name being edited
     * @param {Object} e.value - The value for the field being edited.
     * @param {HTMLElement} e.row - The grid table row
     * @param { Ext.grid.column.Column} e.column - The grid {@link Ext.grid.column.Column Column} defining the column that is being edited.
     * @param {Number} e.rowIdx - The row index that is being edited
     * @param {Number} e.colIdx - The column index that is being edited
     * @param {Object} eOpts The options object passed to Ext.util.Observable.addListener
     * @param {String} contentId The current content id
     * @param {String} contentGridId The id of the grid containing the content
     * @private
     */
    _cancelEdit: function(editor, e, eOpts, contentId, contentGridId)
    {
        e.grid.store.fireEvent('datachanged');  
    },
    
     /**
     * Cancel the change if the editing field has trigger the opening of a dialog box
     * @param {Ext.grid.plugin.CellEditing} editor The editor plugin
     * @param {Object} context The editing context
     * @private
     */
    _validateEdit: function (editor, context)
    {
        if (editor.activeEditor && editor.activeEditor.field)    
        {
            return !editor.activeEditor.field.triggerDialogBoxOpened;
        }
    },

    /**
     * Display the "content is dirty" hint at the top of the dialog
     * @param {Ext.grid.Panel} grid The grid
     */
    _showContentDirtyBar: function (grid)
    {
        grid.up('dialog').getDockedItems()[1].setVisible(true);
    }
});