/*
 *  Copyright 2025 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.
 */

 /**
  * MicroSkill tool panel
  * @private
  */
Ext.define('Ametys.plugins.odf.pilotage.tool.MicroSkillsTreeGridPanel', {
    extend: 'Ametys.plugins.odf.tree.AbstractODFTreeGridPanel',

    messageTarget: 'content',

    mixins: {
        editContentsView: "Ametys.cms.content.EditContentsView"
    },
    
    statics: {
        /**
         * Function to render the program blocking microSkills boolean value with icon
         * @param {Object} value The data value
         * @param {Object} metaData A collection of metadata about the current cell
         * @param {Boolean} editable A boolean, true if the cell is editable, false otherwhise
         */
        renderBlockingSkillBooleanIcon: function (value, metaData, editable)
        {
            if (value != null && (Ext.isBoolean(value) ? value : value == 'true'))
            {
                // if it's not a metadata cell render
                if (Ext.Object.isEmpty(metaData))
                {
                    return '<span class="a-grid-glyph ametysicon-code-html-input-checkbox-on" title="' + "{{i18n plugin.odf-pilotage:PLUGINS_ODF_TOOL_MICROSKILLS_BLOCKING_SKILL_BOOLEAN_YES}}" + '" data-qtip=""></span>';
                }
                return '<span class="a-grid-glyph a-grid-centered-glyph ametysicon-code-html-input-checkbox-on" title="' + "{{i18n plugin.odf-pilotage:PLUGINS_ODF_TOOL_MICROSKILLS_BLOCKING_SKILL_BOOLEAN_YES}}" + '" data-qtip=""></span>';
            }
            else if (editable)
            {
                // if it's not a metadata cell render
                if (Ext.Object.isEmpty(metaData))
                {
                    return '<span class="a-grid-glyph ametysicon-code-html-input-checkbox-off" data-qtip="' + "{{i18n plugin.odf-pilotage:PLUGINS_ODF_TOOL_MICROSKILLS_BLOCKING_SKILL_BOOLEAN_TOOLTIP}}" + '"></span>';
                }
                
                return '<span class="a-grid-glyph a-grid-centered-glyph ametysicon-code-html-input-checkbox-off" data-qtip="' + "{{i18n plugin.odf-pilotage:PLUGINS_ODF_TOOL_MICROSKILLS_BLOCKING_SKILL_BOOLEAN_TOOLTIP}}" + '"></span>';
            }
        },
        
        /**
         * Function to render a boolean value with icon and not tooltip
         * @param {Object} value The data value
         * @param {Object} metaData A collection of metadata about the current cell
         * @param {Ext.data.Model} record The record
         */
        renderSkillBooleanIcon: function (value, metaData, record)
        {
            if (value != null)
            {
                let isTrue = Ext.isBoolean(value) ? value : value == 'true';
                if (isTrue)
                {
                    // if it's not a metadata cell render
                    if (Ext.Object.isEmpty(metaData))
                    {
                        return '<span class="a-grid-glyph ametysicon-check34"></span>';
                    }
                    return '<span class="a-grid-glyph a-grid-centered-glyph ametysicon-check34"></span>';
                }
                else 
                {
                    // if it's not a metadata cell render
                    if (Ext.Object.isEmpty(metaData))
                    {
                        return '<span class="a-grid-glyph ametysicon-sign-raw-cross"></span>';
                    }
                    return '<span class="a-grid-glyph a-grid-centered-glyph ametysicon-sign-raw-small-cross"></span>';
                }
            }
            else // boolean value is not set
            {
                // if it's not a metadata cell render
                if (Ext.Object.isEmpty(metaData))
                {
                    return '<span class="a-grid-glyph ametysicon-sign-question" title="' + "{{i18n plugin.core-ui:PLUGINS_CORE_UI_GRID_COLUMN_BOOLEAN_NOT_DEFINED}}" + '"></span>';
                }
                return "";
            }
        },
    },
    
    constructor: function(config)
    {
        config.serverRole = config.serverRole || "org.ametys.plugins.odfpilotage.helper.MicroSkillsTreeHelper";
        
        config.cls = Ext.Array.from(config.cls);
        config.cls.push("contentviewtreegrid");
        config.cls.push("treegrideditionfirst");
        config.cls.push("microskillstreegrid");

        config.saveBarPosition = 0;

        config.columns = this._getGridConfig([]);
        config.methodArguments = ['contentId', 'path', 'tree'];
        
        config.dockedItems = config.dockedItems || [];
        config.dockedItems.push(this._getNoSkillsResultPanel());
        config.dockedItems.push(this._getNoSkillsInProgramPanel());
        
        this.callParent(arguments);

        this.mixins.editContentsView.constructor.call(this, config);
        this.getStore().on('load', this._onLoad, this);     
    },
    
    _createEditPlugin: function(config) {
        let plugin = this.mixins.editContentsView._createEditPlugin.apply(this, arguments);
        plugin.editAfterSelect = false;
        return plugin;
    },
    
    _onLoad: function(store, records, successful, operation, eOpts)
    {
        // Checking if the new record is not about an existing record with the same contentId
        let modifiedContents = {}
        for (let modifiedRecord of this.store.getModifiedRecords()) // Maybe we should use this.getModifiedRecords() (from EditContentsView) to also include rootNode  
        {
            modifiedContents[this._getContentId(modifiedRecord)] = modifiedRecord;
        }
        for (let record of records)
        {
            this._loadRecord(record, modifiedContents);
        }
    },
    
    _loadRecord: function(record, modifiedContents) 
    {
        if (!modifiedContents || Object.keys(modifiedContents).length == 0)
        {
            return;
        }
        
        let modifiedRecord = modifiedContents[this._getContentId(record)];
        let changes;
        if (modifiedRecord)
        {
            changes = modifiedRecord.getChanges();
        }
        
        let changesKeys = changes ? Object.keys(changes) : null;
        if (changesKeys)
        {
            for (let changeFieldName of changesKeys)
            {
                if (modifiedRecord.get(changeFieldName + "_repeater") !== undefined)
                {
                    changes[changeFieldName + "_repeater"] = modifiedRecord.get(changeFieldName + "_repeater");
                }
                if (modifiedRecord.get(changeFieldName + "_content") !== undefined)
                {
                    changes[changeFieldName + "_content"] = modifiedRecord.get(changeFieldName + "_content");
                }
            }
            changesKeys = Object.keys(changes);
            
            for (let changeFieldName of changesKeys)
            {
                record.set(changeFieldName, changes[changeFieldName]);
            }
        }
        
        let me = this;
        window.setTimeout(function() {
            for (let childRecord of record.childNodes || [])
            {
                me._loadRecord(childRecord, modifiedContents);
            }
        }, 1)
    },    
    
    _getContentId: function(record)
    {
        return record.get('contentId');
    },
    
    _getToolBarConfig: function(config)
    {
        let toolbarCfg = this.callParent(arguments);
        
        let toolBarItems = toolbarCfg.items;

        // Insert the skill filter and clear filter button after the already existing elements
        toolBarItems.push({
            xtype: 'textfield',
            itemId: 'skills-search-filter-input',
            flex: 1,
            maxWidth: 300,
            minLength: 3,
            emptyText: "{{i18n PLUGINS_ODF_TOOL_MICROSKILLS_SKILL_FILTER_EMPTYTEXT}}",
            enableKeyEvents: true,
            msgTarget: 'qtip',
            listeners: {change: Ext.Function.createBuffered(this._filterSkills, 300, this)}
        });
        toolBarItems.push({
            // Clear skills filter
            itemId: 'skills-erase-filter-input',
            tooltip: "{{i18n PLUGINS_ODF_TOOL_MICROSKILLS_SKILL_FILTER_CLEAR}}",
            handler: Ext.bind (this._clearSkillsFilter, this),
            iconCls: 'a-btn-glyph ametysicon-eraser11 size-16',
            cls: 'a-btn-light'
        });
        
        let indexOfProgramFilter  = toolBarItems.findIndex((item) => item.itemId == "search-filter-input");

        // Change the emptyText of the program structure filter
        let programItemFilter = toolBarItems[indexOfProgramFilter];
        programItemFilter.emptyText = "{{i18n PLUGINS_ODF_TOOL_MICROSKILLS_PROGRAM_ITEM_FILTER_EMPTYTEXT}}";
        
        return toolbarCfg;
    },
    
    /**
     * @private
     * Filters the skills columns by input fields values.
     * @param {Ext.form.Field} field The field
     */
    _filterSkills: function (field)
    {
        Ext.suspendLayouts();
        this._filterSkillField = field;
        
        let skillValue = Ext.data.SortTypes.asNonAccentedUCString(field.getValue().toLowerCase());
        
        if (skillValue.length > 2)
        {   
            // Computes the columns to set visible
            let matchingSkills = [];
            let hasResult = false;
            
            Ext.Array.forEach(Object.values(this._skills), function(skill) {
                // If the macroskill matches the filter, add it by adding all its microSkills or itself
                if (Ext.data.SortTypes.asNonAccentedUCString(skill.title.toLowerCase()).includes(skillValue)
                    || Ext.data.SortTypes.asNonAccentedUCString(skill.code.toLowerCase()).includes(skillValue))
                {
                    hasResult = true;
                    // If the skill has microSkills, add all of them
                    if (skill.microSkills)
                    {
                        matchingSkills = matchingSkills.concat(Object.keys(skill.microSkills));
                    }
                    // If the skill does not have microSkills, add it directly
                    else
                    {
                        matchingSkills.push(skill.id);
                    }
                }
                // Else, check if some of its microSkills match the filter
                else if (skill.microSkills)
                {
                    Ext.Array.forEach(Object.values(skill.microSkills), function(microSkill) {
                        if (Ext.data.SortTypes.asNonAccentedUCString(microSkill.title.toLowerCase()).includes(skillValue)
                            || Ext.data.SortTypes.asNonAccentedUCString(microSkill.code.toLowerCase()).includes(skillValue))
                        {
                            matchingSkills.push(microSkill.id);
                            hasResult = true;
                        }
                    }, this);
                }
            }, this);
            
            // Hide the others (except the first column)
            Ext.Array.forEach(this.getColumns(), function(column, index) {
                // Do not touch the first column as it is the program structure one, we don't filter it. We can't set it to visible by default because the program structure filter could be overridden.
                if (index != 0)
                {
                    let visible = Ext.Array.contains(matchingSkills, column.stateId);
                    column.setVisible(visible);
                }
            }, this);
            
            if (!hasResult)
            {
                this._showHideNoSkillsResultPanel(true);
            }
            else
            {
                this._showHideNoSkillsResultPanel(false);
                if (this._filterField)
                {
                    this._filterField.clearInvalid();
                }
            }
        }
        else
        {
            // If the field is empty, show all columns
            Ext.Array.forEach(this.getColumns(), function(column, index) {
                                            // Do not touch the first column as it is the program structure one, we don't filter it. We can't set it to visible by default because the program structure filter could be overridden.
                                            if (index != 0)
                                            {
                                                column.setVisible(true);
                                            }
                                        }, this);
                                        
            this._showHideNoSkillsResultPanel(false);
        }
        
        Ext.resumeLayouts(true);
    },
    
    /**
     * @private
     * Get the 'no result' button configuration. This button is shown when filter matches no result.
     * @return {Object} the button configuration
     */
    _getNoSkillsResultPanel: function ()
    {
        return {
            dock: 'top',
            xtype: 'button',
            hidden: true,
            itemId: 'no-skills-result',
            ui: 'tool-hintmessage',
            text: "{{i18n PLUGINS_ODF_TOOL_MICROSKILLS_FILTER_NO_MATCH}}" + "{{i18n PLUGINS_ODF_TOOL_MICROSKILLS_FILTER_NO_MATCH_ACTION}}",
            scope: this,
            handler: this.clearSkillsSearchFilter
        };
    },
    
    /**
     * Clear the filter search if exists
     */
    clearSkillsSearchFilter: function()
    {
        if (this._filterSkillField)
        {
            this._filterSkillField.reset();
        }
        
        this._showHideNoSkillsResultPanel(false);
    },
        
    /**
     * @private
     * Show or hide the 'no result' button.
     * @param {Boolean} show true to show the button, false to hide it.
     */
    _showHideNoSkillsResultPanel: function (show)
    {
        if (this.down("button[itemId='no-skills-result']"))
        {
            this.down("button[itemId='no-skills-result']").setVisible(show);
        }
    },
    
    /**
     * @private
     * Get the 'no skills' button configuration. This button is shown when there are no skills columns.
     * @return {Object} the button configuration
     */
    _getNoSkillsInProgramPanel: function ()
    {
        return {
            dock: 'top',
            xtype: 'button',
            hidden: true,
            itemId: 'no-skills-in-program',
            ui: 'tool-hintmessage',
            text: "{{i18n PLUGINS_ODF_TOOL_MICROSKILLS_NO_SKILLS_IN_PROGRAM}}",
            scope: this,
            handler: this._refreshSkillsColumns
        };
    },
    
    /**
     * Refresh the skills columns
     */
    _refreshSkillsColumns: function()
    {
        this._showHideNoSkillsInProgramPanel(false);
        let tool = Ametys.tool.ToolsManager.getTool(this.ownerCt.ownerCt.uiTool)
        tool.showOutOfDate();
    },
    
    /**
     * @private
     * Show or hide the 'no skills in program' button.
     * @param {Boolean} show true to show the button, false to hide it.
     */
    _showHideNoSkillsInProgramPanel: function (show)
    {
        if (this.down("button[itemId='no-skills-in-program']"))
        {
            this.down("button[itemId='no-skills-in-program']").setVisible(show);
        }
    },
    
    /**
     * Clear the current filter
     * @param {Ext.Button} button The button
     */
    _clearSkillsFilter: function(button)
    {
        button.prev().reset();
    },
    
    _findRecords(contentId)
    {
        let records = [];
        
        // Using byIdMap allow to search even in filtered items
        for (let id in this.store.byIdMap)
        {
            if (this.store.byIdMap[id].get('contentId') == contentId)
            {
                records.push(this.store.byIdMap[id]);
            }
        }
        
        return records;
    },
    
    setContentRootNode: function(contentId)
    {
        Ametys.data.ServerComm.callMethod({
            role: this.getInitialConfig('serverRole'),
            methodName: "getSkillsColumns",
            parameters: [
                contentId,
            ],
            callback: {
                handler: this._setSkillsCb,
                scope: this,
            },
            errorMessage: true,
            waitMessage: false
        });
        this.callParent(arguments);
    },
    
    _setSkillsCb: function(skills)
    {
        this._skills = skills;
        
        let skillsColumns = this._toSkillsColumns(skills);
        let columns = this._getGridConfig(skillsColumns);
        
        let fields = this._getFields(skillsColumns);
        this.store.model.replaceFields(fields, fields.map(f => f.name)); // Remove the given fields before replacing them
        
        this.reconfigure(this.store, columns);
    },
    
    _getFields: function(columns)
    {
        let fields = [];
        
        for (let column of columns)
        {
            fields.push(
            {
                name: column.dataIndex,
                type: 'boolean'
            });
        }
        
        return fields;
    },
    
    _edit: function(editor, e)
    {
        this.mixins.editContentsView._edit.apply(this, arguments); // Cannot call callParent on mixins
        
        // If the root record was modified, we need to refresh the view to update the color of cells as blocking or not blocking skills
        if (e.record.get('root'))
        {
            this.view.refreshView()
        }
    },
    
    _validateEdit: function (editor, context)
    {
        let me = this;
        
        let b = this.mixins.editContentsView._validateEdit.apply(this, arguments); // Cannot call callParent on mixins
        if (!b)
        {
            return false;
        }
        
        function _getOriginalValue(record, dataIndex)
        {
            if (record.modified && record.modified[dataIndex] !== undefined)
            {
                return record.modified[dataIndex];
            }
            else
            {
                return record.get(dataIndex);
            }
        }

        if(editor.activeEditor && !editor.activeEditor.field.getValue() && !_getOriginalValue(context.record, context.column.dataIndex) && editor.editing)
        {
            editor.cancelEdit();
        }

        return true;
    },
    
    _unsaveEditionCB: function(answer, callback)
    {
        // Retrieve the status of the root record before unsaving
        let isRootModified = this.getStore().getRoot().isDirty();
        
        // Call the super unsave edition callback
        this.mixins.editContentsView._unsaveEditionCB.apply(this, arguments); // Cannot call callParent on mixins
        
        // If the root record was modified, we need to refresh the view to update the color of cells as blocking or not blocking skills
        if (isRootModified && answer == 'yes')
        {
            this.view.refreshView();
        }
    },
    
    _saveEditionWithContents: function(contents, callback)
    {
        let programRootId = this.getStore().getRoot().getData().contentId
        Ametys.plugins.odf.pilotage.helper.ODFTreeGridPanelHelper.prototype._saveEditionWithContents.call(this, contents, callback, [programRootId]);
    },

    /**
     * @private
     * continue the save edition process
     * @param {Object} response The response object
     * @param {Object} args The arguments of the sendMessage
     */
    _saveEditionCB: function(response, args)
    {
        let content = args['content'];
        let callback = args['callback'];
        
        // Object content may be outdated, just trust its id
        let contentId = content.getId();
        let records = this._findRecords(contentId);
        let notModifiedSkills = [];
        let needsRefresh = false;
        
        if (response.warnings)
        {
            let warningMsg = "{{i18n PLUGINS_ODF_TOOL_MICROSKILLS_INFO_MSG_START}}" + "<ul>";
            for (let [warningKey, warningDetails] of Object.entries(response.warnings))
            {
                if (warningKey == 'INEXISTING_SKILL')
                {
                    warningMsg += "<li>{{i18n PLUGINS_ODF_TOOL_MICROSKILLS_INFO_MSG_INEXISTING_SKILL}}</li>";
                }
                else if (warningKey == 'NOT_IN_PROGRAM')
                {
                    for (let contentWarning of warningDetails)
                    {
                        warningMsg += "<li>" + Ext.String.format("{{i18n plugin.odf-pilotage:PLUGINS_ODF_TOOL_MICROSKILLS_INFO_MSG_NOT_IN_PROGRAM}}", contentWarning) + "</li>";
                    }
                }
            }
            
            warningMsg += "</ul>";
            
            let contentTitle = records[0].data.title;
            
            window.setTimeout(function() {
                Ametys.Msg.show({
                    title: Ext.String.format("{{i18n plugin.odf-pilotage:PLUGINS_ODF_TOOL_MICROSKILLS_INFO_DIALOG_BOX_TITLE}}", contentTitle), 
                    msg: warningMsg,
                    buttons: Ext.Msg.OK,
                    icon: Ext.Msg.INFO
                });
            }, 1);
            
            notModifiedSkills = response.warnings.notModifiedSkills;
            needsRefresh = true;
        }
        
        if (response.success)
        {
            // graphically finish the save process
            for (let r of records) 
            {
                // Commit the changes
                r.commit(null, Object.keys(r.modified)); 
            }
            
            this._showHideSaveBar();

            Ext.create("Ametys.message.Message", {
                type: Ametys.message.Message.MODIFIED,
                
                targets: {
                    id: this.messageTarget,
                    parameters: { ids: [contentId] }
                }
            });
            
            Ext.create("Ametys.message.Message", {
                type: Ametys.message.Message.LOCK_CHANGED,
                
                targets: {
                    id: this.messageTarget,
                    parameters: { ids: [contentId] }
                }
            });
            
            Ext.create("Ametys.message.Message", {
                type: Ametys.message.Message.WORKFLOW_CHANGED,
                
                targets: {
                    id: this.messageTarget,
                    parameters: { ids: [contentId] }
                }
            });

            callback(true);
        }
        // If there is an error, display it
        else if (response.errorMsg)
        {
            let title = "{{i18n plugin.odf-pilotage:PLUGINS_ODF_TOOL_MICROSKILLS_SAVE_ACTION_ERROR}}";
            
            let contentTitle = records[0].data.title;
            
            // Show the error of the content
            Ametys.log.ErrorDialog.display({
                title: title,
                text: Ext.String.format("{{i18n plugin.odf-pilotage:PLUGINS_ODF_TOOL_MICROSKILLS_SAVE_ACTION_ERROR_CONTENT_LOCKED}}", contentTitle)
            });
            
            callback(false);
        }
        
        if (needsRefresh)
        {
            let tool = Ametys.tool.ToolsManager.getTool(this.ownerCt.ownerCt.uiTool)
            tool.showOutOfDate();
        }
    },
    
    /**
     * Function creating the JSON structure of skills grid
     * @param {Object} skills The list of skills and microskills
     * @private
     */
    _toSkillsColumns: function(skills)
    {
        let json = [];
        let i = 0;
        
        if (Object.entries(skills).length == 0)
        {
            this._showHideNoSkillsInProgramPanel(true);
        }
        else
        {
            for (let [key, macroSkill] of Object.entries(skills))
            {
                let cfg = {
                    stateId: macroSkill.id,
                    headerId: macroSkill.id, // FIXME workaround for https://issues.ametys.org/browse/CMS-9008 (see https://www.sencha.com/forum/showthread.php?469623-ExtJS-6-5-3-Grid-reconfigure-methods-generates-a-warning&p=1317295#post1317295)
                    text: macroSkill.title,
                    align: 'center',
                    sortable: false,
                    tooltip: macroSkill.title,
                    hidden: macroSkill.archived,
                    lockable: false,
                };
                
                if (macroSkill.microSkills)
                {
                    Ext.apply(cfg, { columns: this._toMicroSkillsColumns(macroSkill.microSkills)});
                }
                
                json[i++] = cfg;
            }
            
            this._showHideNoSkillsInProgramPanel(false);
        }
        
        return json;
    },
    
    _toMicroSkillsColumns: function(microSkills)
    {
        let json = [];
        let i = 0;
        for (let [key, microSkill] of Object.entries(microSkills))
        {
            let cfg = {
                text: microSkill.title,
                headerId: microSkill.id, // FIXME workaround for https://issues.ametys.org/browse/CMS-9008 (see https://www.sencha.com/forum/showthread.php?469623-ExtJS-6-5-3-Grid-reconfigure-methods-generates-a-warning&p=1317295#post1317295)
                stateId: microSkill.id,
                dataIndex: microSkill.id,
                align: 'center',
                sortable: false,
                tooltip: microSkill.title,
                hidden: microSkill.archived,
                lockable: false,
                editable: true,
                editor: 'checkbox',
                renderer: this._renderMicroSkill
            };
            
            json[i++] = cfg;
        }
        
        return json;
    },
    
    /**
     * Render the hourly volume column.
     * @param {Object} value The data value for the current cell.
     * @param {Object} metadata A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} cellIndex The index of the current cell
     * @param {Ext.data.Store} store The store
     * @param {Ext.view.View} view The current view
     * @private
     */
    _renderMicroSkill: function(value, metadata, record, rowIndex, cellIndex, store, view)
    {
        let headerCt = this.getHeaderContainer();
        let column = headerCt.getHeaderAtIndex(cellIndex);
        
        let result = value ? value : undefined;
        let editable = false;
        // Tooltip and style
        if (!(record.data['notEditableData'] === true)
            && !(record.data['notEditableDataIndex'] && Ext.Array.contains(record.data['notEditableDataIndex'], column.dataIndex))
            && !(record.data[column.dataIndex + '_external_status'] == 'external'))
        {
            metadata.tdCls += ' editable';
            editable = true;
        }
        
        // If the record is the one for the program (root), render it differently (it will enable the edition of blocking microSkills)
        if (record.get('root'))
        {
            return Ametys.plugins.odf.pilotage.tool.MicroSkillsTreeGridPanel.renderBlockingSkillBooleanIcon(result, metadata, editable);
        }
        else
        {
            let root = store.getRoot();
            
            // If the cell at the root row is set to true, the microSkill at this column is blocking, we need to set the class
            if (result != null && root.get(column.dataIndex))
            {
                metadata.tdCls += ' ametys-skill-blocking';
            }
            
            return Ametys.plugins.odf.pilotage.tool.MicroSkillsTreeGridPanel.renderSkillBooleanIcon(result, metadata);
        }
    },
    
    /**
     * Function creating the structure of the columns grid 
     * @param {Object} skills the skills columns
     * @private
     */
    _getGridConfig: function(skillsColumns)
    {
        let gridConfig = [{
            xtype: 'treecolumn',
            text: "{{i18n plugin.odf-pilotage:PLUGINS_ODF_TOOL_MICROSKILLS_GRID_MODEL}}",
            stateId: 'title',
            headerId: 'title', // FIXME workaround for https://issues.ametys.org/browse/CMS-9008 (see https://www.sencha.com/forum/showthread.php?469623-ExtJS-6-5-3-Grid-reconfigure-methods-generates-a-warning&p=1317295#post1317295)
            sortable: false,
            dataIndex: 'title',
            width: 400,
            locked: true,
            lockable: false
        }];
        
        for (let skillsColumn of skillsColumns)
        {
            gridConfig.push(skillsColumn);
        }
        
        return gridConfig;
    }
})