为什么 TreeStore.sync() 不写入数据库?

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

我被分配了一项扩展和修改 Shopware 插件的任务。原作者不在公司了。在此之前,我从未处理过 Shopware 和 ExtJs。 所以我花了最后几天时间让自己投入其中,我想我到目前为止已经理解了原则和范例。

我现在唯一遇到的问题是以下问题:

我有一个 Ext.tree.Panel,我想使用 Ajax 将其保存到数据库中。节点被添加到树中,我可以看到它出现在 GUI 中。但是在调用 optionsTree.getStore().sync() 之后,数据库中没有任何内容。 PHP 控制器中的 createProductOptionAction() 没有被调用,但我不明白为什么。浏览器控制台日志中没有错误消息,Shopware 日志文件中也没有错误消息。没有什么。一切似乎都很好。但是没有存储数据。

原始插件有一个 Ext.grid.Panel 来存储和显示数据。这很好用。但是改成Ext.tree.Panel,修改代码后,就不行了。从我的角度来看,它应该可以工作。但它没有,我看不到我的错误。

非常感谢任何帮助,我仍然是 ExtJs 的初学者。 :)

这是我目前得到的:

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);
        // ...
    }

    // ...

编辑 1

我尝试按照 Saki 的建议为商店和模型添加作者。但不幸的是它仍然不起作用。 PHP 控制器中的 createProductOptionAction() 从未被调用。

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'
        }
    }
});

我想知道的是,原始插件没有实现编写器。但是当添加一个条目时,它立即出现在数据库中。

编辑 2

我向 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);
        }
    }
});

所有这些事件都被触发了。 beforesync 事件显示

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

但是,model.ProductOption 的 API 请求仍然没有被触发。它应该工作。不应该吗?也许这是 ExtJS 4.1 中的错误?或者使用 Shopware + ExtJS 的东西?

编辑 3

好吧,这真的越来越奇怪了。

我向 TreeStore 添加了一个 "write"-Listener。

    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();
            }
        });
    }

添加节点并调用 .getStore().sync() 后,write 事件被触发,他迭代 operation.records,找到一条记录(我刚添加的那条)...但是它不是 dirty,即使我在将它添加到树之前做了 productOption.setDirty()?!

非常感谢您的宝贵时间! :)

商店使用的代理必须配置 writer 同步操作才能与服务器通信。

关于您的代码的一点说明: beforesync 事件中存在错误:函数必须 return true,否则 sync() 将不会被触发。

我认为这不是唯一的问题。由于 ExtJs 通常是大量代码,所以我无法告诉您问题的原因是什么。我所能给出的只是一个带有一些解释的简单工作示例。

我遵循推荐的 MVC 布局,即每个 class 一个文件。这是完整的代码:

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'}
    ]
});

我没有详细说明服务器端代码。我刚刚复制了 Kitchensink example code。要开始创建、更新或删除请求,它必须 return 修改后的行以及 success: true

解释:

  • 我需要在添加所需的 class 后启动 sencha app build 才能正确显示所有内容
  • file model/TreeTest.js :如果我们想要将重新排序保存回服务器,则字段 index 是必需的。如果省略,则仅保存具有编辑字段的行。有必要为 leaf 字段添加 persist: false,因为服务器上不需要此数据。
  • 文件 store/TreeTest.js :

    • autoSync: true 开箱即用,在重新排序时提到了限制。
    • 树在根节点为 expanded: trueautoLoad: true 时自动加载。如果我们不想自动加载,autoLoadexpanded 必须都是 false。
    • 良好的工作商店需要root。如果缺少,我们必须手动加载商店,即使 autoLoad: true.
    • 需要添加 api 配置。没有它,就不可能告诉 appart 更新、创建和删除请求。
  • 文件view/TreeGrid.js:

    • 需要包含 xtype: 'treecolumn' 的列。
    • 删除行很简单,并且会自动同步存储。服务器端负责删除children,如果有。
    • 创建新行比较棘手,因为一旦调用 appendChild() 就会 sync() 存储(suspendAutoSync() 用于避免写入新的 child 在它被编辑之前)。此外,如果我们手动控制 leaf 属性 (.set('leaf', false)),网格只会更新。我希望 ExtJs 能够正确管理 leaf 属性 并将其视为一个错误。