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! :)
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:
sencha app build
after adding the required classes in order to display everything correctlymodel/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. expanded: true
or if autoLoad: true
. If we don't want to autoLoad, autoLoad
and expanded
must be both false.autoLoad: true
.api
configuration. Without it, it was not possible to tell appart an update, create and delete request.file view/TreeGrid.js
:
xtype: 'treecolumn'
is required.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.