Search code examples
phpextjs4shopware

Why does TreeStore.sync() not write to database?


I've been assigned a task to extent and modify a Shopware plugin. The original author isn't in the company anymore. Before that I've never dealt with Shopware and ExtJs. So I spend the last couple of days getting myself into it and I think I understood the principles and paradigm so far.

The only thing I'm having trouble with right now is the following issue:

I've got an Ext.tree.Panel which I want to save into a database using Ajax. The node is being added to the tree, I can see it appearing in the GUI. But after calling optionsTree.getStore().sync() there is nothing arriving in the database. The createProductOptionAction() in the PHP controller isn't called, but I can't figure out why. There is no error message in Browser console log, no error message in the Shopware log files. Nothing. Everything seems to work fine. But the data isn't being stored.

The original plugin had an Ext.grid.Panel to store and display data. And this works fine. But after changing to Ext.tree.Panel and modifying the code, it doesn't work anymore. From my point of view it should work tho. But it doesn't and I can't see my mistake(s).

Any help is really appreciated, I'm still a bloody beginner with ExtJs. :)

Here is what I've got so far:

app.js

Ext.define('Shopware.apps.CCBConfigurablePhotoProductsManager', {

    extend:'Enlight.app.SubApplication',
    name:'Shopware.apps.CCBConfigurablePhotoProductsManager',

    bulkLoad: true,
    loadPath:'{url controller="CCBConfigurablePhotoProductsManager" action="load"}',

    controllers:['ProductConfigurator'],
    stores:['ProductOptionsList'],
    models:['ProductOption'],
    views: ['ProductOptions', 'Window' ],

    launch: function() {
        var me = this,
            mainController = me.getController('ProductConfigurator');
        return mainController.mainWindow;
    }
});

controller/controller.js

Ext.define('Shopware.apps.CCBConfigurablePhotoProductsManager.controller.ProductConfigurator', {

    extend:'Ext.app.Controller',

    refs: [
        { ref: 'productOptionsTree', selector: 'product-configurator-settings-window product-options-tree' }
    ],

    init:function () {
        var me = this;

        me.mainWindow = me.createMainWindow();
        me.addControls();
        me.callParent(arguments);

        return me.mainWindow;
    },

    addControls: function() {
        var me = this;

        me.control({
            'product-configurator-settings-window product-options-tree': {
                addProductOption: me.onAddProductOption
            }
        });
    },

    createMainWindow: function() {
        var me = this,
            window = me.getView('Window').create({
                treeStore: Ext.create('Shopware.apps.CCBConfigurablePhotoProductsManager.store.ProductOptionsList').load()
            }).show();

        return window;
    },

    onAddProductOption: function() {
        var me = this,
            optionsTree = me.getProductOptionsTree(),
            parentNode = optionsTree.getRootNode(),
            nodeCount = parentNode.childNodes.length + 1,
            productOption = Ext.create('Shopware.apps.CCBConfigurablePhotoProductsManager.model.ProductOption', {
                parent: 0,
                type: 0,
                title: Ext.String.format('{s name="group/default_name"}New Group [0]{/s}', optionsTree.getRootNode().childNodes.length + 1),
                active: true,
                leaf: false
            });
        productOption.setDirty();
        parentNode.appendChild(productOption);
        optionsTree.getStore().sync(); // Nothing arrives at DB
        optionsTree.expandAll();
    },

    // ...

view/window.js

Ext.define('Shopware.apps.CCBConfigurablePhotoProductsManager.view.Window', {

    extend:'Enlight.app.Window',

    cls:Ext.baseCSSPrefix + 'product-configurator-settings-window',
    alias:'widget.product-configurator-settings-window',

    border:false,
    autoShow:true,
    maximizable:true,
    minimizable:true,

    layout: {
        type: 'hbox',
        align: 'stretch'
    },

    width: 700,
    height: 400,

    initComponent:function () {
        var me = this;
        me.createItems();
        me.title = '{s name=window/title}Configurator Settings{/s}';
        me.callParent(arguments);
    },

    createItems: function() {
        var me = this;

        me.items = [
            me.createProductOptionsTree()
        ];
    },

    createProductOptionsTree: function() {
        var me = this;
        return Ext.create('Shopware.apps.CCBConfigurablePhotoProductsManager.view.ProductOptions', {
            store: me.treeStore,
            width: '20%',
            flex: 1
        });
    }
});

store/product_options_list.js

Ext.define('Shopware.apps.CCBConfigurablePhotoProductsManager.store.ProductOptionsList', {
    extend: 'Ext.data.TreeStore',
    pageSize: 30,
    autoLoad: false,
    remoteSort: true,
    remoteFilter: true,
    model : 'Shopware.apps.CCBConfigurablePhotoProductsManager.model.ProductOption',
    proxy:{
        type:'ajax',
        url:'{url controller="CCBConfigurablePhotoProductsManager" action="getProductOptionsList"}',
        reader:{
            type:'json',
            root:'data',
            totalProperty:'total'
        }
    }
});

model/product_option.js

Ext.define('Shopware.apps.CCBConfigurablePhotoProductsManager.model.ProductOption', {
    extend : 'Ext.data.Model',
    fields : [
        { name : 'id', type : 'int', useNull: true },
        { name : 'parent', type : 'int' },
        { name : 'title', type : 'string' },
        { name : 'active', type: 'boolean' },
        { name : 'type', type : 'int' }
    ],
    idProperty : 'id',
    proxy : {
        type : 'ajax',
        api: {
            create: '{url controller="CCBConfigurablePhotoProductsManager" action="createProductOption"}',
            update: '{url controller="CCBConfigurablePhotoProductsManager" action="updateProductOption"}',
            destroy: '{url controller="CCBConfigurablePhotoProductsManager" action="deleteProductOption"}'
        },
        reader : {
            type : 'json',
            root : 'data',
            totalProperty: 'total'
        }
    }
});

php/controller.php

<?php
use Shopware\CustomModels\CCB\ProductOption;

class Shopware_Controllers_Backend_CCBConfigurablePhotoProductsManager extends Shopware_Controllers_Backend_ExtJs
{

    public function createProductOptionAction()
    {
        // Never being called
        file_put_contents('~/test.log', "createProductOptionAction\n", FILE_APPEND);
        $this->View()->assign(
            $this->saveProductOption($this->Request()->getParams())
        );
    }

    public function getProductOptionsListAction()
    {
        // Works fine
        file_put_contents('~/test.log', "getProductOptionsListAction\n", FILE_APPEND);
        // ...
    }

    // ...

EDIT 1

I tried adding a writer for both, the store and the model, as suggested by Saki. But unfortunately it still doesn't work. The createProductOptionAction() in the PHP controller is never being called.

Ext.define('Shopware.apps.CCBConfigurablePhotoProductsManager.model.ProductOption', {
    extend : 'Ext.data.Model',
    fields : [
        { name : 'id', type : 'int', useNull: true },
        { name : 'parent', type : 'int' },
        { name : 'title', type : 'string' },
        { name : 'active', type: 'boolean' },
        { name : 'type', type : 'int' }
    ],
    idProperty : 'id',
    proxy : {
        type: 'ajax',
        api: {
            create: '{url controller="CCBConfigurablePhotoProductsManager" action="createProductOption"}',
            update: '{url controller="CCBConfigurablePhotoProductsManager" action="updateProductOption"}',
            destroy: '{url controller="CCBConfigurablePhotoProductsManager" action="deleteProductOption"}'
        },
        reader: {
            type : 'json',
            root : 'data',
            totalProperty: 'total'
        },
        writer: {
            type: 'json'
        }
    }
});

What I'm wondering tho, the original plugin had no writer implemented. But when adding an entry it immediately appeared in the database.

EDIT 2

I added several listeners to the store.ProductOptionsList:

Ext.define('Shopware.apps.CCBConfigurablePhotoProductsManager.store.ProductOptionsList', {
    extend: 'Ext.data.TreeStore',
    pageSize: 30,
    autoLoad: false,
    remoteSort: true,
    remoteFilter: true,
    model : 'Shopware.apps.CCBConfigurablePhotoProductsManager.model.ProductOption',
    root: {
        text: 'Product Options',
        id: 'productOptions',
        expanded: true
    },
    proxy:{
        type: 'ajax',
        url: '{url controller="CCBConfigurablePhotoProductsManager" action="getProductOptionsList"}',
        reader: {
            type:'json',
            root:'data',
            totalProperty:'total'
        }
    },
    listeners: {
        add: function(store, records, index, eOpts) {
            console.log("**** Add fired");
            console.log(records);
        },
        append: function(store, node, index, eOpts) {
            console.log("**** Append fired");
            console.log(node);
        },
        beforesync: function(operations) {
            console.log("**** Beforesync fired");
            console.log(operations);
        }
    }
});

All these Events are getting fired. The beforesync event shows

**** Beforesync fired
Object {create: Array[1]}
  create: Array[1]
  ...

But still, the API requests of the model.ProductOption are not getting fired. It should work. Shouldn't it? Maybe this is a bug in ExtJS 4.1? Or something with Shopware + ExtJS?

EDIT 3

Ok, this is really getting weird.

I added a "write"-Listener to the TreeStore.

    write: function(store, operation, opts){
        console.log("**** Write fired");
        console.log(operation);
        Ext.each(operation.records, function(record){
            console.log("**** ...");
            if (record.dirty) {
                console.log("**** Commiting dirty record");
                record.commit();
            }
        });
    }

After adding a Node and calling .getStore().sync(), the write-event IS fired, he iterates operation.records, finds one record (the one I just added)... but it isn't dirty, even though I do productOption.setDirty() before adding it to the Tree?!

Thanks alot for your time! :)


Solution

  • A little note on your code: There is an error in the beforesync event: the function must return true, else sync() will not get fired.

    I don't think this is the only problem. Since ExtJs is usually extensive code, I cannot tell you what is the reason of your problem. All I can give, is a simple working example with some explanations.

    I'm following the recommended MVC layout, i.e. one file for each class. Here is the complete code:

    Ext.define('Sandbox.Application', {
        name: 'Sandbox',
        extend: 'Ext.app.Application',
        controllers: [
            'Sandbox.controller.Trees'
        ]
    });
    Ext.define('Sandbox.controller.Trees', {
        extend: 'Ext.app.Controller',
        requires: ['Ext.tree.*', 'Ext.data.*', 'Ext.grid.*'],
        models: ['TreeTest'],
        stores: ['TreeTest'],
        views: ['TreeGrid'],
        init: function(){
            this.control({
                'treegrid toolbar button#addchild': {click: this.onAddChild},
                'treegrid toolbar button#removenode': {click: this.onRemoveNode}
            })
        },
        onAddChild: function(el){
            var grid = el.up('treepanel'),
            sel = grid.getSelectionModel().getSelection()[0],
            store = grid.getStore();
            store.suspendAutoSync()
            var child = sel.appendChild({task: '', user: '', leaf: true});
            sel.set('leaf', false)
            sel.expand()
            grid.getView().editingPlugin.startEdit(child);
            store.resumeAutoSync();
        },
        onRemoveNode: function(el){
            var grid = el.up('treepanel'),
            sel = grid.getSelectionModel().getSelection()[0];
            sel.remove()
        }
    });
    Ext.define('Sandbox.model.TreeTest', {
        extend: 'Ext.data.TreeModel',
        fields: [
            {name: 'id',  type: 'int'},
            {name: 'task',  type: 'string'},
            {name: 'user', type: 'string'},
            {name: 'index', type: 'int'},
            {name: 'parentId', type: 'int'},
            {name: 'leaf', type: 'boolean', persist: false}
        ]
    });
    Ext.define('Sandbox.store.TreeTest', {
        extend: 'Ext.data.TreeStore',
        model: 'Sandbox.model.TreeTest',
        proxy: {
            type: 'ajax',
            url: 'resources/treedata.php',
            api: {
                create: 'resources/treedata.php?action=create',
                read: undefined,
                update: 'resources/treedata.php?action=update',
                destroy: 'resources/treedata.php?action=destroy'
            }
        },
        autoSync: true,
        autoLoad: false,
        root: {id: 1, text: "Root Node", expanded: false}
    });
    Ext.define('Sandbox.view.TreeGrid', {
        extend: 'Ext.tree.Panel',
        alias: 'widget.treegrid',
        store: 'TreeTest',
        columns: [{
            xtype: 'treecolumn',
            text: 'Task',
            flex: 2,
            sortable: true,
            dataIndex: 'task',
            editor: {xtype: 'textfield', allowBlank: false}
        },{
            dataIndex: 'id',
            align: 'right',
            text: 'Id'
        }, {
            dataIndex: 'user',
            flex: 1,
            text: 'Utilisateur',
            editor: {xtype: 'textfield', allowBlank: false}
        }],
        plugins: [{
            ptype: 'rowediting',
            clicksToMoveEditor: 1,
            autoCancel: false
        }],
        viewConfig: {
            plugins: [{
                ptype: 'treeviewdragdrop',
                containerScroll: true
            }]
        },
        tbar:[
            {xtype: 'button', text: 'Add Child', itemId: 'addchild'},
            {xtype: 'button', text: 'Remove', itemId: 'removenode'}
        ]
    });
    

    I didn't elaborate the server side code. I just copied the Kitchensink example code. To get to work a create, update or delete request, it has to return the modified rows along with success: true.

    Explanations:

    • I needed to launch sencha app build after adding the required classes in order to display everything correctly
    • file model/TreeTest.js : the field index is required if we want a reorder to be saved back to the server. If it is ommitted, only rows with edited fields are saved back. It was necessary to add persist: false for the leaf field, because this data is not needed on the server.
    • file store/TreeTest.js :

      • autoSync: true worked out of the box, with the restriction mentionned on reordering.
      • the tree autoLoads when the root node is expanded: true or if autoLoad: true. If we don't want to autoLoad, autoLoad and expanded must be both false.
      • root is required for a good working store. If it is missing, we must load the store manually, even if autoLoad: true.
      • It was necessary to add the api configuration. Without it, it was not possible to tell appart an update, create and delete request.
    • file view/TreeGrid.js :

      • a column with xtype: 'treecolumn' is required.
      • The removal of a row is simple and syncs the store automatically. The server side is responsible for deleting children if there are.
      • The creation of a new row is trickier, because the store is sync()'d as soon as appendChild() is called (suspendAutoSync() is used to avoid to write the new child before it is edited). Also, the grid gets only updated, if we control the leaf property manually (.set('leaf', false)). I expect ExtJs to correctly manage the leaf property and consider this as a bug.