Fix generic grid model and renderer for virtual fields
[tine20] / tine20 / Tinebase / js / widgets / grid / GridPanel.js
1 /*
2  * Tine 2.0
3  * 
4  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
5  * @author      Cornelius Weiss <c.weiss@metaways.de>
6  * @copyright   Copyright (c) 2007-2013 Metaways Infosystems GmbH (http://www.metaways.de)
7  */
8 Ext.ns('Tine.widgets.grid');
9
10 /**
11  * tine 2.0 app grid panel widget
12  * 
13  * @namespace   Tine.widgets.grid
14  * @class       Tine.widgets.grid.GridPanel
15  * @extends     Ext.Panel
16  * 
17  * <p>Application Grid Panel</p>
18  * <p>
19  * </p>
20  * 
21  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
22  * @author      Cornelius Weiss <c.weiss@metaways.de>
23  * 
24  * @param       {Object} config
25  * @constructor
26  * Create a new GridPanel
27  */
28 Tine.widgets.grid.GridPanel = function(config) {
29     Ext.apply(this, config);
30
31     this.gridConfig = this.gridConfig || {};
32     this.defaultSortInfo = this.defaultSortInfo || {};
33     this.defaultPaging = this.defaultPaging || {
34         start: 0,
35         limit: 50
36     };
37
38     // autogenerate stateId
39     if (this.stateful !== false && ! this.stateId) {
40         this.stateId = this.recordClass.getMeta('appName') + '-' + this.recordClass.getMeta('recordName') + '-GridPanel';
41     }
42
43     if (this.stateId && Ext.isTouchDevice) {
44         this.stateId = this.stateId + '-Touch';
45     }
46
47     Tine.widgets.grid.GridPanel.superclass.constructor.call(this);
48 };
49
50 Ext.extend(Tine.widgets.grid.GridPanel, Ext.Panel, {
51     /**
52      * @cfg {Tine.Tinebase.Application} app
53      * instance of the app object (required)
54      */
55     app: null,
56     /**
57      * @cfg {Object} gridConfig
58      * Config object for the Ext.grid.GridPanel
59      */
60     gridConfig: null,
61     /**
62      * @cfg {Ext.data.Record} recordClass
63      * record definition class  (required)
64      */
65     recordClass: null,
66     /**
67      * @cfg {Ext.data.DataProxy} recordProxy
68      */
69     recordProxy: null,
70     /**
71      * @cfg {Tine.widgets.grid.FilterToolbar} filterToolbar
72      */
73     filterToolbar: null,
74     /**
75      * @cfg {Boolean} evalGrants
76      * should grants of a grant-aware records be evaluated (defaults to true)
77      */
78     evalGrants: true,
79     /**
80      * @cfg {Boolean} filterSelectionDelete
81      * is it allowed to deleteByFilter?
82      */
83     filterSelectionDelete: false,
84     /**
85      * @cfg {Object} defaultSortInfo
86      */
87     defaultSortInfo: null,
88     /**
89      * @cfg {Object} storeRemoteSort
90      */
91     storeRemoteSort: true,
92     /**
93      * @cfg {Boolean} usePagingToolbar 
94      */
95     usePagingToolbar: true,
96     /**
97      * @cfg {Object} defaultPaging 
98      */
99     defaultPaging: null,
100     /**
101      * @cfg {Object} pagingConfig
102      * additional paging config
103      */
104     pagingConfig: null,
105     /**
106      * @cfg {Tine.widgets.grid.DetailsPanel} detailsPanel
107      * if set, it becomes rendered in region south 
108      */
109     detailsPanel: null,
110     /**
111      * @cfg {Array} i18nDeleteQuestion 
112      * spechialised strings for deleteQuestion
113      */
114     i18nDeleteQuestion: null,
115     /**
116      * @cfg {String} i18nAddRecordAction 
117      * spechialised strings for add action button
118      */
119     i18nAddActionText: null,
120     /**
121      * @cfg {String} i18nEditRecordAction 
122      * specialised strings for edit action button
123      */
124     i18nEditActionText: null,
125     /**
126      * @cfg {String} i18nMoveActionText
127      * specialised strings for move action button
128      */
129     i18nMoveActionText: null,
130     /**
131      * @cfg {Array} i18nDeleteRecordAction 
132      * specialised strings for delete action button
133      */
134     i18nDeleteActionText: null,
135     /**
136      * Tree panel referenced to this gridpanel
137      */
138     treePanel: null,
139
140     /**
141      * if this resides in a editDialog, this property holds it
142      * if it is so, the grid can't save records itsef, just update
143      * the editDialogs record property holding these records
144      * 
145      * @cfg {Tine.widgets.dialog.EditDialog} editDialog
146      */
147     editDialog: null,
148     
149     /**
150      * if this resides in an editDialog, this property defines the 
151      * property of the record of the editDialog, holding these records
152      * 
153      * @type {String} editDialogRecordProperty
154      */
155     editDialogRecordProperty: null,
156     
157     /**
158      * config passed to edit dialog to open from this grid
159      * 
160      * @cfg {Object} editDialogConfig
161      */
162     editDialogConfig: null,
163
164     /**
165      * the edit dialog class to open from this grid
166      * 
167      * @cfg {String} editDialogClass
168      */
169     editDialogClass: null,
170
171     /**
172      * @cfg {String} i18nEmptyText 
173      */
174     i18nEmptyText: null,
175
176     /**
177      * @cfg {String} newRecordIcon 
178      * icon for adding new records button
179      */
180     newRecordIcon: null,
181
182     /**
183      * @cfg {Boolean} i18nDeleteRecordAction 
184      * update details panel if context menu is shown
185      */
186     updateDetailsPanelOnCtxMenu: true,
187
188     /**
189      * @cfg {Number} autoRefreshInterval (seconds)
190      */
191     autoRefreshInterval: 300,
192
193     /**
194      * @cfg {Boolean} hasFavoritesPanel 
195      */
196     hasFavoritesPanel: true,
197     
198     /**
199      * @cfg {Boolean} hasQuickSearchFilterToolbarPlugin 
200      */
201     hasQuickSearchFilterToolbarPlugin: true,
202
203     /**
204      * disable 'select all pages' in paging toolbar
205      * @cfg {Boolean} disableSelectAllPages
206      */
207     disableSelectAllPages: false,
208
209     /**
210      * enable if records should be multiple editable
211      * @cfg {Boolean} multipleEdit
212      */
213     multipleEdit: false,
214     
215     /**
216      * set if multiple edit requires special right
217      * @type {String}  multipleEditRequiredRight
218      */
219     multipleEditRequiredRight: null,
220     
221     /**
222      * enable if selection of 2 records should allow merging
223      * @cfg {Boolean} duplicateResolvable
224      */
225     duplicateResolvable: false,
226     
227     /**
228      * @property autoRefreshTask
229      * @type Ext.util.DelayedTask
230      */
231     autoRefreshTask: null,
232
233     /**
234      * @type Boolean
235      * @property updateOnSelectionChange
236      */
237     updateOnSelectionChange: true,
238
239     /**
240      * @type Boolean
241      * @property copyEditAction
242      * 
243      * TODO activate this by default
244      */
245     copyEditAction: false,
246
247     /**
248      * @cfg {Boolean} moveAction
249      * activate moveAction
250      */
251     moveAction: true,
252
253     /**
254      * disable delete confirmation by default
255      *
256      * @type Boolean
257      * @property disableDeleteConfirmation
258      */
259     disableDeleteConfirmation: false,
260
261     /**
262      * @type Ext.Toolbar
263      * @property actionToolbar
264      */
265     actionToolbar: null,
266
267     /**
268      * @type Ext.ux.grid.PagingToolbar
269      * @property pagingToolbar
270      */
271     pagingToolbar: null,
272
273     /**
274      * @type Ext.Menu
275      * @property contextMenu
276      */
277     contextMenu: null,
278
279     /**
280      * @property lastStoreTransactionId 
281      * @type String
282      */
283     lastStoreTransactionId: null,
284
285     /**
286      * @property editBuffer  - array of ids of records edited since last explicit refresh
287      * @type Array of ids
288      */
289     editBuffer: null,
290
291     /**
292      * @property deleteQueue - array of ids of records currently being deleted
293      * @type Array of ids
294      */
295     deleteQueue: null,
296
297     /**
298      * configuration object of model from application starter
299      * @type object
300      */
301     modelConfig: null,
302     
303     /**
304      * group grid by this property
305      * 
306      * @type {String}
307      */
308     groupField: null,
309     
310     /**
311      * header template for the grouping view, if needed
312      * 
313      * @type String
314      */
315     groupTextTpl: null,
316     
317     /**
318      * @property selectionModel
319      * @type Tine.widgets.grid.FilterSelectionModel
320      */
321     selectionModel: null,
322     
323     /**
324      * add records from other applications using the split add button
325      * - activated by default
326      * 
327      * @type Boolean
328      * @property splitAddButton
329      */
330     splitAddButton: true,
331
332
333     /**
334      * do initial load (by loading default favorite) after render
335      *
336      * @type Boolean
337      */
338     initialLoadAfterRender: true,
339
340     /**
341      * add "create new record" button
342      * 
343      * @type Boolean
344      * @property addButton
345      */
346     addButton: true,
347     
348     layout: 'border',
349     border: false,
350     stateful: true,
351
352     stateIdSuffix: '',
353
354     /**
355      * Makes the grid readonly, this means, no dialogs, no actions, nothing else than selection, no dbclick
356      */
357     readOnly: false,
358
359     /**
360      * extend standard initComponent chain
361      * 
362      * @private
363      */
364     initComponent: function(){
365         // init some translations
366         this.i18nRecordName = this.i18nRecordName ? this.i18nRecordName : this.recordClass.getRecordName();
367         this.i18nRecordsName = this.i18nRecordsName ? this.i18nRecordsName : this.recordClass.getRecordsName();
368         this.i18nContainerName = this.i18nContainerName ? this.i18nContainerName : this.recordClass.getContainerName();
369         this.i18nContainersName = this.i18nContainersName ? this.i18nContainersName : this.recordClass.getContainersName();
370         
371         this.i18nEmptyText = this.i18nEmptyText ||
372             this.i18nContainersName
373             ? String.format(i18n._("There could not be found any {0}. Please try to change your filter-criteria, view-options or the {1} you search in."), this.i18nRecordsName, (this.i18nContainersName ? this.i18nContainersName : this.i18nRecordsName))
374             : String.format(i18n._("There could not be found any {0}. Please try to change your filter-criteria, view-options or change the module you search in."), this.i18nRecordsName);
375
376         this.i18nEditActionText = this.i18nEditActionText ? this.i18nEditActionText : [String.format(i18n.ngettext('Edit {0}', 'Edit {0}', 1), this.i18nRecordName), String.format(i18n.ngettext('Edit {0}', 'Edit {0}', 2), this.i18nRecordsName)];
377
378         this.editDialogConfig = this.editDialogConfig || {};
379         this.editBuffer = [];
380         this.deleteQueue = [];
381         
382         // init generic stuff
383         if (this.modelConfig) {
384             this.initGeneric();
385         }
386         
387         this.initFilterPanel();
388         
389         // init store
390         this.initStore();
391         
392         // init (ext) grid
393         this.initGrid();
394
395         // init actions
396         this.actionUpdater = new Tine.widgets.ActionUpdater({
397             evalGrants: this.evalGrants
398         });
399
400         if (!this.readOnly) {
401             this.initActions();
402         }
403
404         this.initLayout();
405
406         // for some reason IE looses split height when outer layout is layouted
407         if (Ext.isIE6 || Ext.isIE7) {
408             this.on('show', function() {
409                 if (this.layout.rendered && this.detailsPanel) {
410                     var height = this.detailsPanel.getSize().height;
411                     this.layout.south.split.setCurrentSize(height);
412                 }
413             }, this);
414         }
415
416         if (this.detailsPanel) {
417             this.on('resize', this.onContentResize, this, {buffer: 100});
418         }
419
420         if (this.listenMessageBus) {
421             this.initMessageBus();
422         }
423
424         Tine.widgets.grid.GridPanel.superclass.initComponent.call(this);
425     },
426
427     initMessageBus: function() {
428         postal.subscribe({
429             channel: "recordchange",
430             topic: [this.recordClass.getMeta('appName'), this.recordClass.getMeta('modelName'), '*'].join('.'),
431             callback: this.onRecordChanges.createDelegate(this)
432         });
433     },
434
435     /**
436      * bus notified about record changes
437      */
438     onRecordChanges: function(data, e) {
439         var existingRecord = this.store.getById(data.id);
440         if (existingRecord && e.topic.match(/\.update/)) {
441             // NOTE: local mode saves again (and again...)
442             this.onUpdateRecord(JSON.stringify(data)/*, 'local'*/);
443         } if (existingRecord && e.topic.match(/\.delete/)) {
444             this.store.remove(existingRecord);
445         } else {
446             // we can't evaluate the filters on client side to check compute if this affects us
447             // so just lets reload
448             this.loadGridData({
449                 removeStrategy: 'keepBuffered'
450             });
451         }
452     },
453
454     /**
455      * returns canonical path part
456      * @returns {string}
457      */
458     getCanonicalPathSegment: function () {
459         var pathSegment = '';
460         if (this.canonicalName) {
461             // simple segment e.g. when used in a dialog
462             pathSegment = this.canonicalName;
463         } else if (this.recordClass) {
464             // auto segment
465             pathSegment = [this.recordClass.getMeta('modelName'), 'Grid'].join(Tine.Tinebase.CanonicalPath.separator);
466         }
467
468         return pathSegment;
469     },
470
471     onContentResize: function() {
472         // make sure details panel doesn't hide grid
473         if (this.detailsPanel) {
474             var gridHeight = this.grid.getHeight(),
475                 detailsHeight = this.detailsPanel.getHeight();
476
477             if (detailsHeight/2 > gridHeight) {
478                 var newDetailsHeight = this.getHeight() *.4;
479                 this.layout.south.panel.setHeight(newDetailsHeight);
480                 this.doLayout();
481             }
482         }
483     },
484
485     /**
486      * initializes generic stuff when used with ModelConfiguration
487      */
488     initGeneric: function() {
489         if (this.modelConfig) {
490             
491             Tine.log.debug('init generic gridpanel with config:');
492             Tine.log.debug(this.modelConfig);
493
494             // TODO move to uiConfig
495             if (this.modelConfig.hasOwnProperty('multipleEdit') && (this.modelConfig.multipleEdit === true)) {
496                 this.multipleEdit = true;
497                 this.multipleEditRequiredRight = (this.modelConfig.hasOwnProperty('multipleEditRequiredRight'))
498                     ? this.modelConfig.multipleEditRequiredRight
499                     : null;
500             }
501
502             // TODO move to uiConfig
503             if (this.modelConfig.hasOwnProperty('copyEditAction') && (this.modelConfig.copyEditAction === true)) {
504                 this.copyEditAction = true;
505             }
506         }
507         
508         // init generic columnModel
509         this.initGenericColumnModel();
510     },
511     
512     /**
513      * initialises the filter panel 
514      * 
515      * @param {Object} config
516      */
517     initFilterPanel: function(config) {
518         if (! this.filterToolbar && ! this.editDialog) {
519             var filterModels = [];
520             if (this.modelConfig) {
521                 filterModels = this.getCustomfieldFilters();
522             } else if (Ext.isFunction(this.recordClass.getFilterModel)) {
523                 filterModels = this.recordClass.getFilterModel().concat(this.getCustomfieldFilters());
524             }
525             this.filterToolbar = new Tine.widgets.grid.FilterPanel(Ext.apply({}, {
526                 app: this.app,
527                 recordClass: this.recordClass,
528                 allowSaving: true,
529                 filterModels: filterModels,
530                 defaultFilter: this.recordClass.getMeta('defaultFilter') ? this.recordClass.getMeta('defaultFilter') : 'query',
531                 filters: this.defaultFilters || []
532             }, config || {}));
533             
534             this.plugins = this.plugins || [];
535             this.plugins.push(this.filterToolbar);
536         }
537     },
538
539     /**
540      * initializes the generic column model on auto bootstrap
541      */
542     initGenericColumnModel: function() {
543         if (this.modelConfig) {
544             var columns = [];
545             Ext.each(this.modelConfig.fieldKeys, function(key) {
546                 var fieldConfig = this.modelConfig.fields[key];
547                     globalI18n = (fieldConfig && fieldConfig.hasOwnProperty('useGlobalTranslation'));
548
549                 if (fieldConfig.type === 'virtual') {
550                     fieldConfig = fieldConfig.config;
551                 }
552
553                 // don't show multiple record fields
554                 if (fieldConfig.type == 'records') {
555                     return true;
556                 }
557                 
558                 // don't show parent property in dependency of an editDialog
559                 if (this.editDialog && fieldConfig.hasOwnProperty('config') && fieldConfig.config.isParent) {
560                     return true;
561                 }
562                 
563                 // don't show record field if the user doesn't have the right on the application
564                 if (fieldConfig.type == 'record' && !(fieldConfig.config && fieldConfig.config.doNotCheckModuleRight) && (! Tine.Tinebase.common.hasRight('view', fieldConfig.config.appName, fieldConfig.config.modelName.toLowerCase() + 's'))) {
565                     return true;
566                 }
567                 
568                 // If no label exists, don't use in grid
569                 if (fieldConfig.label) {
570                     var config = {
571                         id: key,
572                         dataIndex: (fieldConfig.type == 'relation') ? 'relations' : key,
573                         header: globalI18n ? i18n._(fieldConfig.label) : this.app.i18n._(fieldConfig.label),
574                         hidden: fieldConfig.hasOwnProperty('shy') ? fieldConfig.shy : false,    // defaults to false
575                         sortable: (fieldConfig.hasOwnProperty('sortable') && fieldConfig.sortable == false) ? false : true // defaults to true
576                     };
577                     
578                     if (fieldConfig.hasOwnProperty('summaryType')) {
579                         config.summaryType = fieldConfig.summaryType;
580                     }
581                     
582                     var renderer = Tine.widgets.grid.RendererManager.get(this.app.name, this.recordClass.getMeta('modelName'), key);
583                     if (renderer) {
584                         config.renderer = renderer;
585                     }
586                     columns.push(config);
587                 }
588             }, this);
589             
590             if (this.modelConfig.hasCustomFields) {
591                 columns = columns.concat(this.getCustomfieldColumns());
592             }
593             
594             columns = columns.concat(this.getCustomColumns());
595             
596             this.gridConfig.cm = new Ext.grid.ColumnModel({
597                 defaults: {
598                     resizable: true
599                 },
600                 columns: columns
601             });
602         }
603     },
604     
605     /**
606      * template method to allow adding custom columns
607      * 
608      * @return {Array}
609      */
610     getCustomColumns: function() {
611         return [];
612     },
613     
614     /**
615      * @private
616      * 
617      * NOTE: Order of items matters! Ext.Layout.Border.SplitRegion.layout() does not
618      *       fence the rendering correctly, as such it's impotant, so have the ftb
619      *       defined after all other layout items
620      */
621     initLayout: function() {
622         this.items = [{
623             region: 'center',
624             xtype: 'panel',
625             layout: 'fit',
626             border: false,
627             tbar: this.pagingToolbar,
628             items: this.grid
629         }];
630
631
632         // add detail panel
633         if (this.detailsPanel) {
634
635             // just in case it's a config only
636             this.detailsPanel = Ext.ComponentMgr.create(this.detailsPanel);
637
638             this.items.push({
639                 region: 'south',
640                 border: false,
641                 collapsible: true,
642                 collapseMode: 'mini',
643                 header: false,
644                 split: true,
645                 layout: 'fit',
646                 height: this.detailsPanel.defaultHeight ? this.detailsPanel.defaultHeight : 125,
647                 items: this.detailsPanel,
648                 canonicalName: 'DetailsPanel'
649
650             });
651             this.detailsPanel.doBind(this.grid);
652         }
653
654         // add filter toolbar
655         if (this.filterToolbar) {
656             this.items.push({
657                 region: 'north',
658                 border: false,
659                 autoScroll: true,
660                 items: this.filterToolbar,
661                 listeners: {
662                     scope: this,
663                     afterlayout: function(ct) {
664                         ct.suspendEvents();
665                         ct.setHeight(Math.min(120, this.filterToolbar.getHeight() + (ct.topToolbar ? ct.topToolbar.getHeight() : 0)));
666                         ct.getEl().child('div[class^="x-panel-body"]', true).scrollTop = 1000000;
667                         ct.ownerCt.layout.layout();
668                         ct.resumeEvents();
669                     }
670                 }
671             });
672         }
673
674     },
675
676     /**
677      * init actions with actionToolbar, contextMenu and actionUpdater
678      * 
679      * @private
680      */
681     initActions: function() {
682         this.newRecordIcon =  this.newRecordIcon!== null ? this.newRecordIcon : this.app.appName + 'IconCls';
683         if (! Ext.util.CSS.getRule('.' + this.newRecordIcon)) {
684             this.newRecordIcon = 'ApplicationIconCls';
685         }
686         
687         var services = Tine.Tinebase.registry.get('serviceMap').services;
688         
689         this.action_editInNewWindow = new Ext.Action({
690             requiredGrant: 'readGrant',
691             requiredMultipleGrant: 'editGrant',
692             requiredMultipleRight: this.multipleEditRequiredRight,
693             text: this.i18nEditActionText ? this.i18nEditActionText[0] : String.format(i18n._('Edit {0}'), this.i18nRecordName),
694             singularText: this.i18nEditActionText ? this.i18nEditActionText[0] : String.format(i18n._('Edit {0}'), this.i18nRecordName),
695             pluralText:  this.i18nEditActionText ? this.i18nEditActionText[1] : String.format(i18n.ngettext('Edit {0}', 'Edit {0}', 1), this.i18nRecordsName),
696             disabled: true,
697             translationObject: this.i18nEditActionText ? this.app.i18n : i18n,
698             actionType: 'edit',
699             handler: this.onEditInNewWindow.createDelegate(this, [{actionType: 'edit'}]),
700             iconCls: 'action_edit',
701             scope: this,
702             allowMultiple: this.multipleEdit
703         });
704
705         this.action_editCopyInNewWindow = new Ext.Action({
706             hidden: ! this.copyEditAction,
707             requiredGrant: 'readGrant',
708             text: String.format(i18n._('Copy {0}'), this.i18nRecordName),
709             disabled: true,
710             actionType: 'copy',
711             handler: this.onEditInNewWindow.createDelegate(this, [{actionType: 'copy'}]),
712             iconCls: 'action_editcopy',
713             scope: this
714         });
715
716         this.action_addInNewWindow = (this.addButton) ? new Ext.Action({
717             requiredGrant: 'addGrant',
718             actionType: 'add',
719             text: this.i18nAddActionText ? this.app.i18n._hidden(this.i18nAddActionText) : String.format(i18n._('Add {0}'), this.i18nRecordName),
720             handler: this.onEditInNewWindow.createDelegate(this, [{actionType: 'add'}]),
721             iconCls: this.newRecordIcon,
722             scope: this
723         }) : null;
724
725         this.actions_print = new Ext.Action({
726             requiredGrant: 'readGrant',
727             text: i18n._('Print Page'),
728             disabled: false,
729             handler: function() {
730                 Ext.ux.Printer.print(this.getGrid());
731             },
732             iconCls: 'action_print',
733             scope: this,
734             allowMultiple: true
735         });
736
737         this.initDeleteAction(services);
738
739         this.action_move = new Ext.Action({
740             requiredGrant: 'editGrant',
741             requiredMultipleGrant: 'editGrant',
742             requiredMultipleRight: this.multipleEditRequiredRight,
743             singularText: this.i18nMoveActionText ? this.i18nMoveActionText[0] : String.format(i18n.ngettext('Move {0}', 'Move {0}', 1), this.i18nRecordName),
744             pluralText: this.i18nMoveActionText ? this.i18nMoveActionText[1] : String.format(i18n.ngettext('Move {0}', 'Move {0}', 1), this.i18nRecordsName),
745             translationObject: this.i18nMoveActionText ? this.app.i18n : i18n,
746             text: this.i18nMoveActionText ? this.i18nMoveActionText[0] : String.format(i18n.ngettext('Move {0}', 'Move {0}', 1), this.i18nRecordName),
747             disabled: true,
748             hidden: !this.moveAction || !this.recordClass.getMeta('containerProperty'),
749             actionType: 'edit',
750             handler: this.onMoveRecords,
751             scope: this,
752             iconCls: 'action_move',
753             allowMultiple: this.multipleEdit
754         });
755
756
757         this.action_tagsMassAttach = new Tine.widgets.tags.TagsMassAttachAction({
758             hidden:         ! this.recordClass.getField('tags'),
759             selectionModel: this.grid.getSelectionModel(),
760             recordClass:    this.recordClass,
761             updateHandler:  this.loadGridData.createDelegate(this),
762             app:            this.app
763         });
764
765         this.action_tagsMassDetach = new Tine.widgets.tags.TagsMassDetachAction({
766             hidden:         ! this.recordClass.getField('tags'),
767             selectionModel: this.grid.getSelectionModel(),
768             recordClass:    this.recordClass,
769             updateHandler:  this.loadGridData.createDelegate(this),
770             app:            this.app
771         });
772
773         this.action_resolveDuplicates = new Ext.Action({
774             requiredGrant: null,
775             text: String.format(i18n._('Merge {0}'), this.i18nRecordsName),
776                 iconCls: 'action_resolveDuplicates',
777                 scope: this,
778                 handler: this.onResolveDuplicates,
779                 disabled: false,
780                 actionUpdater: function(action, grants, records) {
781                     if (records && (records.length != 2)) action.setDisabled(true);
782                     else action.setDisabled(false);
783                 }
784         });
785
786         this.initExports();
787         this.initImports();
788
789         // add actions to updater
790         this.actionUpdater.addActions([
791             this.action_addInNewWindow,
792             this.action_editInNewWindow,
793             this.action_move,
794             this.action_editCopyInNewWindow,
795             this.action_deleteRecord,
796             this.action_tagsMassAttach,
797             this.action_tagsMassDetach,
798             this.action_resolveDuplicates
799         ]);
800
801         // init actionToolbar (needed for correct fitertoolbar init atm -> fixme)
802         this.getActionToolbar();
803     },
804
805     initExports: function() {
806         if (this.actions_export !== undefined) return;
807
808         var _ = window.lodash,
809             exportFunction = this.app.name + '.export' + this.recordClass.getMeta('modelName') + 's',
810             additionalItems = [];
811
812         // create items from available export formats (depricated -> use definitions)
813         if (this.modelConfig && this.modelConfig['export']) {
814             if (this.modelConfig['export'].supportedFormats) {
815                 Ext.each(this.modelConfig['export'].supportedFormats, function (format, i) {
816                     additionalItems.unshift(new Tine.widgets.grid.ExportButton({
817                         // TODO format toUpper
818                         text: String.format(i18n._('Export as {0}'), format),
819                         format: format,
820                         iconCls: 'tinebase-action-export-' + format,
821                         exportFunction: exportFunction,
822                         order: i * 10,
823                         gridPanel: this
824                     }));
825                 }, this);
826             }
827         }
828
829         // exports from export definitions
830         this.actions_export = Tine.widgets.exportAction.getExportButton(this.recordClass, {
831             exportFunction: exportFunction,
832             gridPanel: this
833         }, Tine.widgets.exportAction.SCOPE_MULTI, additionalItems);
834
835         if (this.actions_export) {
836             this.actionUpdater.addActions([this.actions_export]);
837         }
838     },
839
840     initImports: function() {
841         if (this.actions_import !== undefined) return;
842
843         if (this.modelConfig && this.modelConfig['import']) {
844             this.actions_import = new Ext.Action({
845                 requiredGrant: 'addGrant',
846                 text: i18n._('Import items'),
847                 disabled: false,
848                 handler: this.onImport,
849                 iconCls: 'action_import',
850                 scope: this,
851                 allowMultiple: true
852             });
853
854             this.actionUpdater.addActions([this.actions_import]);
855         }
856     },
857
858     /**
859      * import inventory items
860      *
861      * @param {Button} btn
862      */
863     onImport: function(btn) {
864         var treePanel = this.treePanel || this.app.getMainScreen().getWestPanel().getContainerTreePanel(),
865             _ = window.lodash;
866
867         var container = _.get(this.modelConfig, 'import.defaultImportContainerRegistryKey', false);
868
869         if (container) {
870             container = treePanel.getDefaultContainer(container);
871         }
872
873         Tine.widgets.dialog.ImportDialog.openWindow({
874             appName: this.app.name,
875             modelName: this.recordClass.getMeta('modelName'),
876             defaultImportContainer: container,
877             listeners: {
878                 scope: this,
879                 'finish': function() {
880                     this.loadGridData({
881                         preserveCursor:     false,
882                         preserveSelection:  false,
883                         preserveScroller:   false,
884                         removeStrategy:     'default'
885                     });
886                 }
887             }
888         });
889     },
890
891     /**
892      * initializes the delete action
893      * 
894      * @param {Object} services the rpc service map from the registry
895      */
896     initDeleteAction: function(services) {
897         // note: unprecise plural form here, but this is hard to change
898         this.action_deleteRecord = new Ext.Action({
899             requiredGrant: 'deleteGrant',
900             allowMultiple: true,
901             singularText: this.i18nDeleteActionText ? this.i18nDeleteActionText[0] : String.format(i18n.ngettext('Delete {0}', 'Delete {0}', 1), this.i18nRecordName),
902             pluralText: this.i18nDeleteActionText ? this.i18nDeleteActionText[1] : String.format(i18n.ngettext('Delete {0}', 'Delete {0}', 1), this.i18nRecordsName),
903             translationObject: this.i18nDeleteActionText ? this.app.i18n : i18n,
904             text: this.i18nDeleteActionText ? this.i18nDeleteActionText[0] : String.format(i18n.ngettext('Delete {0}', 'Delete {0}', 1), this.i18nRecordName),
905             handler: this.onDeleteRecords,
906             disabled: true,
907             iconCls: 'action_delete',
908             scope: this
909         });
910         // if nested in a editDialog (dependent record), the service won't exist
911         if (! this.editDialog) {
912             this.disableDeleteActionCheckServiceMap(services);
913         }
914     },
915     
916     /**
917      * disable delete action if no delete method was found in serviceMap
918      * 
919      * @param {Object} services the rpc service map from the registry
920      * 
921      * TODO this should be configurable as not all grids use remote delete
922      */
923     disableDeleteActionCheckServiceMap: function(services) {
924         if (services) {
925             var serviceKey = this.app.name + '.delete' + this.recordClass.getMeta('modelName') + 's';
926             if (! services.hasOwnProperty(serviceKey)) {
927                 this.action_deleteRecord.setDisabled(1);
928                 this.action_deleteRecord.initialConfig.actionUpdater = function(action) {
929                     Tine.log.debug("disable delete action because no delete method was found in serviceMap");
930                     action.setDisabled(1);
931                 }
932             }
933         }
934     },
935
936     /**
937      * init store
938      * @private
939      */
940     initStore: function() {
941         if (this.store) {
942             // store is already initialized
943             return;
944         }
945
946         if (this.recordProxy) {
947             var storeClass = this.groupField ? Ext.data.GroupingStore : Ext.data.Store;
948             this.store = new storeClass({
949                 fields: this.recordClass,
950                 proxy: this.recordProxy,
951                 reader: this.recordProxy.getReader(),
952                 remoteSort: this.storeRemoteSort,
953                 sortInfo: this.defaultSortInfo,
954                 groupField: 'month',
955                 listeners: {
956                     scope: this,
957                     'add': this.onStoreAdd,
958                     'remove': this.onStoreRemove,
959                     'update': this.onStoreUpdate,
960                     'beforeload': this.onStoreBeforeload,
961                     'load': this.onStoreLoad,
962                     'beforeloadrecords': this.onStoreBeforeLoadRecords,
963                     'loadexception': this.onStoreLoadException
964                 }
965             });
966         } else {
967             this.store = new Tine.Tinebase.data.RecordStore({
968                 recordClass: this.recordClass
969             });
970         }
971
972         // init autoRefresh
973         this.autoRefreshTask = new Ext.util.DelayedTask(this.loadGridData, this, [{
974             removeStrategy: 'keepBuffered',
975             autoRefresh: true
976         }]);
977     },
978
979     /**
980      * returns view row class
981      */
982     getViewRowClass: function(record, index, rowParams, store) {
983         var noLongerInFilter = record.not_in_filter;
984
985         var className = '';
986         if (noLongerInFilter) {
987             className += 'tine-grid-row-nolongerinfilter';
988         }
989         return className;
990     },    
991
992     /**
993      * new entry event -> add new record to store
994      * 
995      * @param {Object} recordData
996      * @return {Boolean}
997      */
998     onStoreNewEntry: function(recordData) {
999         var initialData = null;
1000         if (Ext.isFunction(this.recordClass.getDefaultData)) {
1001             initialData = Ext.apply(this.recordClass.getDefaultData(), recordData);
1002         } else {
1003             initialData = recordData;
1004         }
1005         var record = new this.recordClass(initialData);
1006         this.store.insert(0 , [record]);
1007
1008         if (this.usePagingToolbar) {
1009             this.pagingToolbar.refresh.disable();
1010         }
1011         this.recordProxy.saveRecord(record, {
1012             scope: this,
1013             success: function(newRecord) {
1014                 this.store.suspendEvents();
1015                 this.store.remove(record);
1016                 this.store.insert(0 , [newRecord]);
1017                 this.store.resumeEvents();
1018
1019                 this.addToEditBuffer(newRecord);
1020
1021                 this.loadGridData({
1022                     removeStrategy: 'keepBuffered'
1023                 });
1024             }
1025         });
1026
1027         return true;
1028     },
1029
1030     /**
1031      * header is clicked
1032      * 
1033      * @param {Object} grid
1034      * @param {Number} colIdx
1035      * @param {Event} e
1036      * @return {Boolean}
1037      */
1038     onHeaderClick: function(grid, colIdx, e) {
1039
1040         Ext.apply(this.store.lastOptions, {
1041             preserveCursor:     true,
1042             preserveSelection:  true, 
1043             preserveScroller:   true, 
1044             removeStrategy:     'default'
1045         });
1046     },
1047
1048     /**
1049      * called when Records have been added to the Store
1050      */
1051     onStoreAdd: function(store, records, index) {
1052         this.store.totalLength += records.length;
1053         if (this.pagingToolbar) {
1054             this.pagingToolbar.updateInfo();
1055         }
1056     },
1057     
1058     /**
1059      * called when a Record has been removed from the Store
1060      */
1061     onStoreRemove: function(store, record, index) {
1062         this.store.totalLength--;
1063         if (this.pagingToolbar) {
1064             this.pagingToolbar.updateInfo();
1065         }
1066     },
1067     
1068     /**
1069      * called when the store gets updated, e.g. from editgrid
1070      * 
1071      * @param {Ext.data.store} store
1072      * @param {Tine.Tinebase.data.Record} record
1073      * @param {String} operation
1074      */
1075     onStoreUpdate: function(store, record, operation) {
1076         switch (operation) {
1077             case Ext.data.Record.EDIT:
1078                 this.addToEditBuffer(record);
1079                 
1080                 if (this.usePagingToolbar) {
1081                     this.pagingToolbar.refresh.disable();
1082                 }
1083                 // don't save these records. Add them to the parents' record store
1084                 if (this.editDialog) {
1085                     var items = [];
1086                     store.each(function(item) {
1087                         items.push(item.data);
1088                     });
1089                     
1090                     this.editDialog.record.set(this.editDialogRecordProperty, items);
1091                     this.editDialog.fireEvent('updateDependent');
1092                 } else if (this.recordProxy) {
1093                     this.recordProxy.saveRecord(record, {
1094                         scope: this,
1095                         success: function(updatedRecord) {
1096                             store.commitChanges();
1097     
1098                             // update record in store to prevent concurrency problems
1099                             record.data = updatedRecord.data;
1100     
1101                             this.loadGridData({
1102                                 removeStrategy: 'keepBuffered'
1103                             });
1104                         }
1105                     });
1106                     break;
1107                 }
1108             case Ext.data.Record.COMMIT:
1109                 //nothing to do, as we need to reload the store anyway.
1110                 break;
1111         }
1112     },
1113
1114     /**
1115      * called before store queries for data
1116      */
1117     onStoreBeforeload: function(store, options) {
1118
1119         // define a transaction
1120         this.lastStoreTransactionId = options.transactionId = Ext.id();
1121
1122         options.params = options.params || {};
1123         // always start with an empty filter set!
1124         // this is important for paging and sort header!
1125         options.params.filter = [];
1126
1127         if (! options.removeStrategy || options.removeStrategy !== 'keepBuffered') {
1128             this.editBuffer = [];
1129         }
1130
1131 //        options.preserveSelection = options.hasOwnProperty('preserveSelection') ? options.preserveSelection : true;
1132 //        options.preserveScroller = options.hasOwnProperty('preserveScroller') ? options.preserveScroller : true;
1133
1134         // fix nasty paging tb
1135         Ext.applyIf(options.params, this.defaultPaging);
1136     },
1137
1138     /**
1139      * called after a new set of Records has been loaded
1140      * 
1141      * @param  {Ext.data.Store} this.store
1142      * @param  {Array}          loaded records
1143      * @param  {Array}          load options
1144      * @return {Void}
1145      */
1146     onStoreLoad: function(store, records, options) {
1147         // we always focus the first row so that keynav starts in the grid
1148         // this resets scroller ;-( -> need a better solution
1149 //        if (this.store.getCount() > 0) {
1150 //            this.grid.getView().focusRow(0);
1151 //        }
1152
1153         // restore selection
1154         if (Ext.isArray(options.preserveSelection)) {
1155             Ext.each(options.preserveSelection, function(record) {
1156                 var row = this.store.indexOfId(record.id);
1157                 if (row >= 0) {
1158                     this.grid.getSelectionModel().selectRow(row, true);
1159                 }
1160             }, this);
1161         }
1162
1163         // restore scroller
1164         if (Ext.isNumber(options.preserveScroller)) {
1165             this.grid.getView().scroller.dom.scrollTop = options.preserveScroller;
1166         }
1167
1168         // reset autoRefresh
1169         if (window.isMainWindow && this.autoRefreshInterval) {
1170             this.autoRefreshTask.delay(this.autoRefreshInterval * 1000);
1171         }
1172     },
1173
1174     /**
1175      * on store load exception
1176      * 
1177      * @param {Tine.Tinebase.data.RecordProxy} proxy
1178      * @param {String} type
1179      * @param {Object} error
1180      * @param {Object} options
1181      */
1182     onStoreLoadException: function(proxy, type, error, options) {
1183
1184         // reset autoRefresh
1185         if (window.isMainWindow && this.autoRefreshInterval) {
1186             this.autoRefreshTask.delay(this.autoRefreshInterval * 5000);
1187         }
1188
1189         if (this.usePagingToolbar && this.pagingToolbar.refresh) {
1190             this.pagingToolbar.refresh.enable();
1191         }
1192
1193         if (! options.autoRefresh) {
1194             proxy.handleRequestException(error);
1195         } else {
1196             Tine.log.debug('Tine.widgets.grid.GridPanel::onStoreLoadException -> auto refresh failed.');
1197         }
1198     },
1199
1200     /**
1201      * onStoreBeforeLoadRecords
1202      * 
1203      * @param {Object} o
1204      * @param {Object} options
1205      * @param {Boolean} success
1206      * @param {Ext.data.Store} store
1207      */
1208     onStoreBeforeLoadRecords: function(o, options, success, store) {
1209
1210         if (this.lastStoreTransactionId && options.transactionId && this.lastStoreTransactionId !== options.transactionId) {
1211             Tine.log.debug('onStoreBeforeLoadRecords - cancelling old transaction request.');
1212             return false;
1213         }
1214
1215         // save selection -> will be applied onLoad
1216         if (options.preserveSelection) {
1217             options.preserveSelection = this.grid.getSelectionModel().getSelections();
1218         }
1219
1220         // save scroller -> will be applied onLoad
1221         if (options.preserveScroller && this.grid.getView().scroller && this.grid.getView().scroller.dom) options.preserveScroller = this.grid.getView().scroller.dom.scrollTop;
1222
1223         // apply removeStrategy
1224         if (! options.removeStrategy || options.removeStrategy === 'default') {
1225             return true;
1226         }
1227
1228         var records = [],
1229             recordsIds = [],
1230             recordToLoadCollection = new Ext.util.MixedCollection();
1231
1232         // fill new collection
1233         Ext.each(o.records, function(record) {
1234             recordToLoadCollection.add(record.id, record);
1235         });
1236
1237         // assemble update & keep
1238         this.store.each(function(currentRecord) {
1239             var recordToLoad = recordToLoadCollection.get(currentRecord.id);
1240             if (recordToLoad) {
1241                 // we replace records that are the same, because otherwise this would not work for local changes
1242                 if (recordToLoad.isObsoletedBy(currentRecord)) {
1243                     records.push(currentRecord);
1244                     recordsIds.push(currentRecord.id);
1245                 } else {
1246                     records.push(recordToLoad);
1247                     recordsIds.push(recordToLoad.id);
1248                 }
1249             } else if (options.removeStrategy === 'keepAll' || (options.removeStrategy === 'keepBuffered' && this.editBuffer.indexOf(currentRecord.id) >= 0)) {
1250                 var copiedRecord = currentRecord.copy();
1251                 copiedRecord.not_in_filter = true;
1252                 records.push(copiedRecord);
1253                 recordsIds.push(currentRecord.id);
1254             }
1255         }, this);
1256         
1257         // assemble adds
1258         recordToLoadCollection.each(function(record, idx) {
1259             if (recordsIds.indexOf(record.id) == -1 && this.deleteQueue.indexOf(record.id) == -1) {
1260                 var lastRecord = recordToLoadCollection.itemAt(idx-1);
1261                 var lastRecordIdx = lastRecord ? recordsIds.indexOf(lastRecord.id) : -1;
1262                 records.splice(lastRecordIdx+1, 0, record);
1263                 recordsIds.splice(lastRecordIdx+1, 0, record.id);
1264             }
1265         }, this);
1266
1267         o.records = records;
1268         
1269         // hide current records from store.loadRecords()
1270         // @see 0008210: email grid: set flag does not work sometimes
1271         this.store.clearData();
1272     },
1273
1274     /**
1275      * perform the initial load of grid data
1276      */
1277     initialLoad: function() {
1278         var defaultFavorite = Tine.widgets.persistentfilter.model.PersistentFilter.getDefaultFavorite(
1279                 this.app.appName, this.recordClass.prototype.modelName
1280             ),
1281             favoritesPanel  = this.app.getMainScreen()
1282             && typeof this.app.getMainScreen().getWestPanel === 'function'
1283             && typeof this.app.getMainScreen().getWestPanel().getFavoritesPanel === 'function'
1284             && this.hasFavoritesPanel
1285                 ? this.app.getMainScreen().getWestPanel().getFavoritesPanel()
1286                 : null;
1287
1288         if (defaultFavorite && favoritesPanel) {
1289             favoritesPanel.selectFilter(defaultFavorite);
1290         } else {
1291             if (! this.editDialog) {
1292                 this.store.load.defer(10, this.store, [ typeof this.autoLoad == 'object' ? this.autoLoad : undefined]);
1293             } else {
1294                 // editDialog exists, so get the records from there.
1295                 var items = this.editDialog.record.get(this.editDialogRecordProperty);
1296                 if (Ext.isArray(items)) {
1297                     Ext.each(items, function(item) {
1298                         var record = this.recordProxy.recordReader({responseText: Ext.encode(item)});
1299                         this.store.addSorted(record);
1300                     }, this);
1301                 }
1302             }
1303         }
1304
1305         if (this.usePagingToolbar && this.recordProxy) {
1306             this.pagingToolbar.refresh.disable.defer(10, this.pagingToolbar.refresh);
1307         }
1308     },
1309
1310     /**
1311      * init ext grid panel
1312      * @private
1313      */
1314     initGrid: function() {
1315         var preferences = Tine.Tinebase.registry.get('preferences');
1316
1317         if (preferences) {
1318             this.gridConfig = Ext.applyIf(this.gridConfig || {}, {
1319                 stripeRows: preferences.get('gridStripeRows') ? preferences.get('gridStripeRows') : false,
1320                 loadMask: preferences.get('gridLoadMask') ? preferences.get('gridLoadMask') : false
1321             });
1322             
1323             // added paging number of result read from settings
1324             if (preferences.get('pageSize') != null) {
1325                 this.defaultPaging = {
1326                     start: 0,
1327                     limit: parseInt(preferences.get('pageSize'), 10)
1328                 };
1329             }
1330         }
1331
1332         // generic empty text
1333         this.i18nEmptyText = i18n.gettext('No data to display');
1334         
1335         // init sel model
1336         this.selectionModel = new Tine.widgets.grid.FilterSelectionModel({
1337             store: this.store,
1338             gridPanel: this
1339         });
1340         this.selectionModel.on('selectionchange', function(sm) {
1341             this.actionUpdater.updateActions(sm);
1342
1343             this.ctxNode = this.selectionModel.getSelections();
1344             if (this.updateOnSelectionChange && this.detailsPanel) {
1345                 this.detailsPanel.onDetailsUpdate(sm);
1346             }
1347         }, this);
1348
1349         if (this.usePagingToolbar) {
1350             this.pagingToolbar = new Ext.ux.grid.PagingToolbar(Ext.apply({
1351                 pageSize: this.defaultPaging && this.defaultPaging.limit ? this.defaultPaging.limit : 50,
1352                 store: this.store,
1353                 displayInfo: true,
1354                 displayMsg: i18n._('Displaying records {0} - {1} of {2}').replace(/records/, this.i18nRecordsName),
1355                 emptyMsg: String.format(i18n._("No {0} to display"), this.i18nRecordsName),
1356                 displaySelectionHelper: true,
1357                 sm: this.selectionModel,
1358                 disableSelectAllPages: this.disableSelectAllPages,
1359                 nested: this.editDialog ? true : false
1360             }, this.pagingConfig));
1361             // mark next grid refresh as paging-refresh
1362             this.pagingToolbar.on('beforechange', function() {
1363                 this.grid.getView().isPagingRefresh = true;
1364             }, this);
1365         }
1366
1367         // which grid to use?
1368         var Grid = this.gridConfig.quickaddMandatory ? Ext.ux.grid.QuickaddGridPanel : (this.gridConfig.gridType || Ext.grid.GridPanel);
1369
1370         this.gridConfig.store = this.store;
1371
1372         // activate grid header menu for column selection
1373         this.gridConfig.plugins = this.gridConfig.plugins ? this.gridConfig.plugins : [];
1374         this.gridConfig.plugins.push(new Ext.ux.grid.GridViewMenuPlugin({}));
1375         this.gridConfig.enableHdMenu = false;
1376
1377         if (this.stateful) {
1378             this.gridConfig.stateful = true;
1379             this.gridConfig.stateId  = this.stateId + '-Grid' + this.stateIdSuffix;
1380         }
1381
1382         this.grid = new Grid(Ext.applyIf(this.gridConfig, {
1383             border: false,
1384             store: this.store,
1385             sm: this.selectionModel,
1386             view: this.createView()
1387         }));
1388
1389         // init various grid / sm listeners
1390         this.grid.on('keydown',     this.onKeyDown,         this);
1391         this.grid.on('rowclick',    this.onRowClick,        this);
1392         this.grid.on('rowdblclick', this.onRowDblClick,     this);
1393         this.grid.on('newentry',    this.onStoreNewEntry,   this);
1394         this.grid.on('headerclick', this.onHeaderClick,   this);
1395
1396         this.grid.on('rowcontextmenu', this.onRowContextMenu, this);
1397
1398     },
1399
1400     /**
1401      * creates and returns the view for the grid
1402      * 
1403      * @return {Ext.grid.GridView}
1404      */
1405     createView: function() {
1406         // init view
1407         
1408         if (this.groupField && ! this.groupTextTpl) {
1409             this.groupTextTpl = '{text} ({[values.rs.length]} {[values.rs.length > 1 ? "' + i18n._("Records") + '" : "' + i18n._("Record") + '"]})';
1410         }
1411         
1412         var viewClass = this.groupField ? Ext.grid.GroupingView : Ext.grid.GridView;
1413         var view =  new viewClass({
1414             getRowClass: this.getViewRowClass,
1415             autoFill: true,
1416             forceFit:true,
1417             ignoreAdd: true,
1418             emptyText: this.i18nEmptyText,
1419             groupTextTpl: this.groupTextTpl,
1420             onLoad: Ext.grid.GridView.prototype.onLoad.createInterceptor(function() {
1421                 if (this.grid.getView().isPagingRefresh) {
1422                     this.grid.getView().isPagingRefresh = false;
1423                     return true;
1424                 }
1425
1426                 return false;
1427             }, this)
1428         });
1429         
1430         return view;
1431     },
1432     
1433     /**
1434      * executed after outer panel rendering process
1435      */
1436     afterRender: function() {
1437         Tine.widgets.grid.GridPanel.superclass.afterRender.apply(this, arguments);
1438         if (this.initialLoadAfterRender) {
1439             this.initialLoad();
1440         }
1441     },
1442
1443     /**
1444      * trigger store load with grid related options
1445      * 
1446      * TODO rethink -> preserveCursor and preserveSelection might conflict on page breaks!
1447      * TODO don't reload details panel when selection is preserved
1448      * 
1449      * @param {Object} options
1450      */
1451     loadGridData: function(options) {
1452         var options = options || {};
1453
1454         Ext.applyIf(options, {
1455             callback:           Ext.emptyFn,
1456             scope:              this,
1457             params:             {},
1458
1459             preserveCursor:     true, 
1460             preserveSelection:  true, 
1461             preserveScroller:   true, 
1462             removeStrategy:     'default'
1463         });
1464
1465         if (options.preserveCursor && this.usePagingToolbar) {
1466             options.params.start = this.pagingToolbar.cursor;
1467         }
1468
1469         this.store.load(options);
1470     },
1471
1472     /**
1473      * get action toolbar
1474      * 
1475      * @return {Ext.Toolbar}
1476      */
1477     getActionToolbar: function() {
1478         if (! this.actionToolbar) {
1479             var additionalItems = this.getActionToolbarItems();
1480
1481             var items = [];
1482             
1483             if (this.action_addInNewWindow) {
1484                 if (this.splitAddButton) {
1485                     items.push(Ext.apply(
1486                         new Ext.SplitButton(this.action_addInNewWindow), {
1487                             scale: 'medium',
1488                             rowspan: 2,
1489                             iconAlign: 'top',
1490                             arrowAlign:'right',
1491                             menu: new Ext.menu.Menu({
1492                                 items: [],
1493                                 plugins: [{
1494                                     ptype: 'ux.itemregistry',
1495                                     key:   'Tine.widgets.grid.GridPanel.addButton'
1496                                 }]
1497                             })
1498                         })
1499                     );
1500                 } else {
1501                     items.push(Ext.apply(
1502                         new Ext.Button(this.action_addInNewWindow), {
1503                             scale: 'medium',
1504                             rowspan: 2,
1505                             iconAlign: 'top'
1506                         })
1507                     );
1508                 }
1509             }
1510             
1511             if (this.action_editInNewWindow) {
1512                 items.push(Ext.apply(
1513                     new Ext.Button(this.action_editInNewWindow), {
1514                         scale: 'medium',
1515                         rowspan: 2,
1516                         iconAlign: 'top'
1517                     })
1518                 );
1519             }
1520             
1521             if (this.action_deleteRecord) {
1522                 items.push(Ext.apply(
1523                     new Ext.Button(this.action_deleteRecord), {
1524                         scale: 'medium',
1525                         rowspan: 2,
1526                         iconAlign: 'top'
1527                     })
1528                 );
1529             }
1530             
1531             if (this.actions_print) {
1532                 items.push(Ext.apply(
1533                     new (this.actions_print.initialConfig && this.actions_print.initialConfig.menu ? Ext.SplitButton : Ext.Button) (this.actions_print), {
1534                         scale: 'medium',
1535                         rowspan: 2,
1536                         iconAlign: 'top'
1537                     })
1538                 );
1539             }
1540
1541             var importExportButtons = [];
1542
1543             if (this.actions_export) {
1544                 importExportButtons.push(Ext.apply(new Ext.Button(this.actions_export), {
1545                     scale: 'small',
1546                     rowspan: 1,
1547                     iconAlign: 'left'
1548                 }));
1549             }
1550             if (this.actions_import) {
1551                 importExportButtons.push(Ext.apply(new Ext.Button(this.actions_import), {
1552                     scale: 'small',
1553                     rowspan: 1,
1554                     iconAlign: 'left'
1555                 }));
1556             }
1557
1558             if (importExportButtons.length > 0) {
1559                 items.push({
1560                     xtype: 'buttongroup',
1561                     columns: 1,
1562                     frame: false,
1563                     items: importExportButtons
1564                 });
1565             }
1566
1567             this.actionToolbar = new Ext.Toolbar({
1568                 canonicalName: [this.recordClass.getMeta('modelName'), 'ActionToolbar'].join(Tine.Tinebase.CanonicalPath.separator),
1569                 items: [{
1570                     xtype: 'buttongroup',
1571                     layout: 'toolbar',
1572                     buttonAlign: 'left',
1573                     enableOverflow: true,
1574                     plugins: [{
1575                         ptype: 'ux.itemregistry',
1576                         key:   this.app.appName + '-' + this.recordClass.prototype.modelName + '-GridPanel-ActionToolbar-leftbtngrp'
1577                     }],
1578                     items: items.concat(Ext.isArray(additionalItems) ? additionalItems : [])
1579                 }].concat(Ext.isArray(additionalItems) ? [] : [additionalItems])
1580             });
1581
1582             this.actionToolbar.on('resize', this.onActionToolbarResize, this, {buffer: 250});
1583             this.actionToolbar.on('show', this.onActionToolbarResize, this);
1584
1585             if (this.filterToolbar && typeof this.filterToolbar.getQuickFilterField == 'function') {
1586                 this.actionToolbar.add('->', this.filterToolbar.getQuickFilterField());
1587             } 
1588         }
1589
1590         return this.actionToolbar;
1591     },
1592
1593     onActionToolbarResize: function(tb) {
1594         if (! tb.rendered) return;
1595         var actionGrp = tb.items.get(0),
1596             availableWidth = tb.getBox()['width'] - 5,
1597             maxNeededWidth = Ext.layout.ToolbarLayout.prototype.triggerWidth + 10;
1598
1599         tb.items.each(function(c, idx) {
1600             if (idx > 0 && !c.isFill) {
1601                 availableWidth -= c.getPositionEl().dom.parentNode.offsetWidth;
1602             }
1603         }, this);
1604
1605         actionGrp.items.each(function(c) {
1606             maxNeededWidth += Ext.layout.ToolbarLayout.prototype.getItemWidth(c);
1607         });
1608
1609         actionGrp.setWidth(Math.min(availableWidth, maxNeededWidth));
1610
1611     },
1612
1613     /**
1614      * template fn for subclasses to add custom items to action toolbar
1615      * 
1616      * @return {Array/Object}
1617      */
1618     getActionToolbarItems: function() {
1619         var items = this.actionToolbarItems || [];
1620
1621         if (! Ext.isEmpty(items)) {
1622             // legacy handling! subclasses should register all actions when initializing actions
1623             this.actionUpdater.addActions(items);
1624         }
1625
1626         return items;
1627     },
1628
1629     /**
1630      * returns rows context menu
1631      * 
1632      * @param {Ext.grid.GridPanel} grid
1633      * @param {Number} row
1634      * @param {Ext.EventObject} e
1635      * @return {Ext.menu.Menu}
1636      */
1637     getContextMenu: function(grid, row, e) {
1638
1639         if (! this.contextMenu) {
1640             var items = [];
1641             
1642             if (this.action_addInNewWindow) items.push(this.action_addInNewWindow);
1643             if (this.action_editInNewWindow) items.push(this.action_editInNewWindow);
1644             if (this.action_editCopyInNewWindow) items.push(this.action_editCopyInNewWindow);
1645             if (this.action_move) items.push(this.action_move);
1646             if (this.action_deleteRecord) items.push(this.action_deleteRecord);
1647
1648             if (this.duplicateResolvable) {
1649                 items.push(this.action_resolveDuplicates);
1650             }
1651
1652             if (this.actions_export) {
1653                 items.push('-', this.actions_export);
1654             }
1655
1656             if (this.action_tagsMassAttach && ! this.action_tagsMassAttach.hidden) {
1657                 items.push('-', this.action_tagsMassAttach, this.action_tagsMassDetach);
1658             }
1659
1660             // lookup additional items
1661             items = items.concat(this.getContextMenuItems());
1662
1663             // New record of another app
1664             this.newRecordMenu = new Ext.menu.Menu({
1665                 items: [],
1666                 plugins: [{
1667                     ptype: 'ux.itemregistry',
1668                     key:   this.app.appName + '-' + this.recordClass.prototype.modelName + '-GridPanel-ContextMenu-New'
1669                 }]
1670             });
1671
1672             this.newRecordAction = new Ext.Action({
1673                 text: i18n._('New...'),
1674                 hidden: ! this.newRecordMenu.items.length,
1675                 iconCls: this.app.getIconCls(),
1676                 scope: this,
1677                 menu: this.newRecordMenu
1678             });
1679
1680             items.push(this.newRecordAction);
1681
1682             // Add to record of another app            
1683             this.addToRecordMenu = new Ext.menu.Menu({
1684                 items: [],
1685                 plugins: [{
1686                     ptype: 'ux.itemregistry',
1687                     key:   this.app.appName + '-' + this.recordClass.prototype.modelName + '-GridPanel-ContextMenu-Add'
1688                 }]
1689             });
1690
1691             this.addToRecordAction = new Ext.Action({
1692                 text: i18n._('Add to...'),
1693                 hidden: ! this.addToRecordMenu.items.length,
1694                 iconCls: this.app.getIconCls(),
1695                 scope: this,
1696                 menu: this.addToRecordMenu
1697             });
1698
1699             items.push(this.addToRecordAction);
1700
1701             this.contextMenu = new Ext.menu.Menu({
1702                 items: items,
1703                 plugins: [{
1704                     ptype: 'ux.itemregistry',
1705                     key:   this.app.appName + '-' + this.recordClass.prototype.modelName + '-GridPanel-ContextMenu'
1706                 }, {
1707                     ptype: 'ux.itemregistry',
1708                     key:   'Tinebase-MainContextMenu'
1709                 }]
1710             });
1711         }
1712
1713         return this.contextMenu;
1714     },
1715
1716     /**
1717      * template fn for subclasses to add custom items to context menu
1718      * 
1719      * @return {Array}
1720      */
1721     getContextMenuItems: function() {
1722         var items = this.contextMenuItems || [];
1723
1724         if (! Ext.isEmpty(items)) {
1725             // legacy handling! subclasses should register all actions when initializing actions
1726             this.actionUpdater.addActions(items);
1727         }
1728
1729         return items;
1730     },
1731
1732     /**
1733      * get modlog columns
1734      * 
1735      * shouldn' be used anymore
1736      * @TODO: use applicationstarter and modelconfiguration
1737      * 
1738      * @deprecated
1739      * @return {Array}
1740      */
1741     getModlogColumns: function() {
1742         var result = [
1743             { id: 'creation_time',      header: i18n._('Creation Time'),         dataIndex: 'creation_time',         renderer: Tine.Tinebase.common.dateRenderer,        hidden: true, sortable: true },
1744             { id: 'created_by',         header: i18n._('Created By'),            dataIndex: 'created_by',            renderer: Tine.Tinebase.common.usernameRenderer,    hidden: true, sortable: true },
1745             { id: 'last_modified_time', header: i18n._('Last Modified Time'),    dataIndex: 'last_modified_time',    renderer: Tine.Tinebase.common.dateRenderer,        hidden: true, sortable: true },
1746             { id: 'last_modified_by',   header: i18n._('Last Modified By'),      dataIndex: 'last_modified_by',      renderer: Tine.Tinebase.common.usernameRenderer,    hidden: true, sortable: true }
1747         ];
1748
1749         return result;
1750     },
1751
1752     /**
1753      * get custom field columns for column model
1754      * 
1755      * @return {Array}
1756      */
1757     getCustomfieldColumns: function() {
1758         var modelName = this.recordClass.getMeta('appName') + '_Model_' + this.recordClass.getMeta('modelName'),
1759             cfConfigs = Tine.widgets.customfields.ConfigManager.getConfigs(this.app, modelName),
1760             result = [];
1761
1762         Ext.each(cfConfigs, function(cfConfig) {
1763             result.push({
1764                 id: cfConfig.id,
1765                 header: cfConfig.get('definition').label,
1766                 dataIndex: 'customfields',
1767                 renderer: Tine.widgets.customfields.Renderer.get(this.app, cfConfig),
1768                 sortable: false,
1769                 hidden: true
1770             });
1771         }, this);
1772
1773         return result;
1774     },
1775
1776     /**
1777      * get custom field filter for filter toolbar
1778      * 
1779      * @return {Array}
1780      */
1781     getCustomfieldFilters: function() {
1782         var modelName = this.recordClass.getMeta('appName') + '_Model_' + this.recordClass.getMeta('modelName'),
1783             cfConfigs = Tine.widgets.customfields.ConfigManager.getConfigs(this.app, modelName),
1784             result = [];
1785         Ext.each(cfConfigs, function(cfConfig) {
1786             var cfDefinition = cfConfig.get('definition');
1787             switch (cfDefinition.type) {
1788                 case 'record':
1789                     result.push({
1790                         filtertype: 'foreignrecord',
1791                         label: cfDefinition.label,
1792                         app: this.app,
1793                         ownRecordClass: this.recordClass,
1794                         foreignRecordClass: eval(cfDefinition.recordConfig.value.records),
1795                         linkType: 'foreignId',
1796                         ownField: 'customfield:' + cfConfig.id
1797                     });
1798                     break;
1799                 case 'keyfield':
1800                     // @TODO implement me
1801                     break;
1802                 default:
1803                     result.push({filtertype: 'tinebase.customfield', app: this.app, cfConfig: cfConfig});
1804                     break;
1805             }
1806         }, this);
1807
1808         return result;
1809     },
1810
1811     /**
1812      * returns filter toolbar
1813      * @private
1814      * @deprecated
1815      * 
1816      * TODO this seems to be legacy code that is only used in some apps (Calendar, Felamimail, ...)
1817      *   -> should be removed
1818      *   -> we use initFilterPanel() now
1819      */
1820     getFilterToolbar: function(config) {
1821         config = config || {};
1822         return new Tine.widgets.grid.FilterPanel(Ext.apply(config, {
1823             app: this.app,
1824             recordClass: this.recordClass,
1825             filterModels: this.recordClass.getFilterModel().concat(this.getCustomfieldFilters()),
1826             defaultFilter: 'query',
1827             filters: this.defaultFilters || []
1828         }));
1829     },
1830
1831     /**
1832      * return store from grid
1833      * 
1834      * @return {Ext.data.Store}
1835      */
1836     getStore: function() {
1837         return this.grid.getStore();
1838     },
1839
1840     /**
1841      * return view from grid
1842      * 
1843      * @return {Ext.grid.GridView}
1844      */
1845     getView: function() {
1846         return this.grid.getView();
1847     },
1848
1849     /**
1850      * return grid
1851      * 
1852      * @return {Ext.ux.grid.QuickaddGridPanel|Ext.grid.GridPanel}
1853      */
1854     getGrid: function() {
1855         return this.grid;
1856     },
1857
1858     /**
1859      * key down handler
1860      * @private
1861      */
1862     onKeyDown: function(e){
1863         if (e.ctrlKey) {
1864             switch (e.getKey()) {
1865                 case e.A:
1866                     // select only current page
1867                     this.grid.getSelectionModel().selectAll(true);
1868                     e.preventDefault();
1869                     break;
1870                 case e.E:
1871                     if (this.action_editInNewWindow && !this.action_editInNewWindow.isDisabled()) {
1872                         this.onEditInNewWindow.call(this, {
1873                             actionType: 'edit'
1874                         });
1875                         e.preventDefault();
1876                     }
1877                     break;
1878                 case e.N:
1879                     if (this.action_addInNewWindow && !this.action_addInNewWindow.isDisabled()) {
1880                         this.onEditInNewWindow.call(this, {
1881                             actionType: 'add'
1882                         });
1883                         e.preventDefault();
1884                     }
1885                     break;
1886                 case e.F:
1887                     if (this.filterToolbar && this.hasQuickSearchFilterToolbarPlugin) {
1888                         e.preventDefault();
1889                         this.filterToolbar.getQuickFilterPlugin().quickFilter.focus();
1890                     }
1891                     break;
1892             }
1893         } else {
1894             if ([e.BACKSPACE, e.DELETE].indexOf(e.getKey()) !== -1) {
1895                 if (!this.grid.editing && !this.grid.adding && !this.action_deleteRecord.isDisabled()) {
1896                     this.onDeleteRecords.call(this);
1897                     e.preventDefault();
1898                 }
1899             }
1900         }
1901     },
1902
1903     /**
1904      * row click handler
1905      * 
1906      */
1907     onRowClick: function(grid, row, e) {
1908         /* TODO check if we need this in IE
1909         // hack to get percentage editor working
1910         var cell = Ext.get(grid.getView().getCell(row,1));
1911         var dom = cell.child('div:last');
1912         while (cell.first()) {
1913             cell = cell.first();
1914             cell.on('click', function(e){
1915                 e.stopPropagation();
1916                 grid.fireEvent('celldblclick', grid, row, 1, e);
1917             });
1918         }
1919         */
1920
1921         // fix selection of one record if shift/ctrl key is not pressed any longer
1922         if (e.button === 0 && !e.shiftKey && !e.ctrlKey && ! Ext.isTouchDevice) {
1923             var sm = grid.getSelectionModel();
1924
1925             if (sm.getCount() == 1 && sm.isSelected(row)) {
1926                 return;
1927             }
1928
1929             sm.clearSelections();
1930             sm.selectRow(row, false);
1931             grid.view.focusRow(row);
1932         }
1933     },
1934     
1935     /**
1936      * row doubleclick handler
1937      * 
1938      * @param {} grid
1939      * @param {} row
1940      * @param {} e
1941      */
1942     onRowDblClick: function(grid, row, e) {
1943         this.onEditInNewWindow.call(this, {actionType: 'edit'});
1944     }, 
1945
1946     /**
1947      * called on row context click
1948      * 
1949      * @param {Ext.grid.GridPanel} grid
1950      * @param {Number} row
1951      * @param {Ext.EventObject} e
1952      */
1953     onRowContextMenu: function(grid, row, e) {
1954         e.stopEvent();
1955         var selModel = grid.getSelectionModel();
1956         if (!selModel.isSelected(row)) {
1957             // disable preview update if config option is set to false
1958             this.updateOnSelectionChange = this.updateDetailsPanelOnCtxMenu;
1959             selModel.selectRow(row);
1960         }
1961
1962         var contextMenu = this.getContextMenu(grid, row, e);
1963
1964         if (contextMenu) {
1965             contextMenu.showAt(e.getXY());
1966         }
1967
1968         // reset preview update
1969         this.updateOnSelectionChange = true;
1970     },
1971     
1972     /**
1973      * Opens the required EditDialog
1974      * @param {Object} actionButton the button the action was called from
1975      * @param {Tine.Tinebase.data.Record} record the record to display/edit in the dialog
1976      * @param {Array} plugins the plugins used for the edit dialog
1977      * @param {Object} additionalConfig plain Object, which will be applied to the edit dialog on initComponent
1978      * @return {Boolean}
1979      */
1980     onEditInNewWindow: function(button, record, plugins, additionalConfig) {
1981         if (! record) {
1982             if (button.actionType == 'edit' || button.actionType == 'copy') {
1983                 if (! this.action_editInNewWindow || this.action_editInNewWindow.isDisabled()) {
1984                     // if edit action is disabled or not available, we also don't open a new window
1985                     return false;
1986                 }
1987                 var selectedRows = this.grid.getSelectionModel().getSelections();
1988                 record = selectedRows[0];
1989             } else {
1990                 record = this.createNewRecord();
1991             }
1992         }
1993
1994         // plugins to add to edit dialog
1995         var plugins = plugins ? plugins : [];
1996         
1997         var totalcount = this.selectionModel.getCount(),
1998             selectedRecords = [],
1999             fixedFields = (button.hasOwnProperty('fixedFields') && Ext.isObject(button.fixedFields)) ? Ext.encode(button.fixedFields) : null,
2000             editDialogClass = this.editDialogClass || Tine[this.app.appName][this.recordClass.getMeta('modelName') + 'EditDialog'],
2001             additionalConfig = additionalConfig ? additionalConfig : {};
2002         
2003         // add "multiple_edit_dialog" plugin to dialog, if required
2004         if (((totalcount > 1) && (this.multipleEdit) && (button.actionType == 'edit'))) {
2005             Ext.each(this.selectionModel.getSelections(), function(record) {
2006                 selectedRecords.push(record.data);
2007             }, this );
2008             
2009             plugins.push({
2010                 ptype: 'multiple_edit_dialog', 
2011                 selectedRecords: selectedRecords,
2012                 selectionFilter: this.selectionModel.getSelectionFilter(),
2013                 isFilterSelect: this.selectionModel.isFilterSelect,
2014                 totalRecordCount: totalcount
2015             });
2016         }
2017
2018         Tine.log.debug('GridPanel::onEditInNewWindow');
2019         Tine.log.debug(record);
2020         
2021         var popupWindow = editDialogClass.openWindow(Ext.copyTo(
2022             this.editDialogConfig || {}, {
2023                 plugins: plugins ? Ext.encode(plugins) : null,
2024                 fixedFields: fixedFields,
2025                 additionalConfig: Ext.encode(additionalConfig),
2026                 record: editDialogClass.prototype.mode == 'local' ? Ext.encode(record.data) : record,
2027                 recordId: record.getId(),
2028                 copyRecord: (button.actionType == 'copy'),
2029                 listeners: {
2030                     scope: this,
2031                     'update': ((this.selectionModel.getCount() > 1) && (this.multipleEdit)) ? this.onUpdateMultipleRecords : this.onUpdateRecord
2032                 }
2033             }, 'record,recordId,listeners,fixedFields,copyRecord,plugins,additionalConfig')
2034         );
2035         return true;
2036     },
2037
2038     /**
2039      * create new record
2040      *
2041      * @returns {Tine.Tinebase.data.Record}
2042      */
2043     createNewRecord: function() {
2044         return new this.recordClass(this.recordClass.getDefaultData(), 0);
2045     },
2046
2047     /**
2048      * is called after multiple records have been updated
2049      */
2050     onUpdateMultipleRecords: function() {
2051         this.store.reload();
2052     },
2053
2054     /**
2055      * on update after edit
2056      * 
2057      * @param {String|Tine.Tinebase.data.Record} record
2058      * @param {String} mode
2059      */
2060     onUpdateRecord: function(record, mode) {
2061         if (! this.rendered) {
2062             return;
2063         }
2064
2065         if (! mode && ! this.recordProxy) {
2066             // proxy-less = local if not defined otherwise
2067             mode = 'local';
2068         }
2069         
2070         if (Ext.isString(record)) {
2071             record = this.recordProxy
2072                 ? this.recordProxy.recordReader({responseText: record})
2073                 : Tine.Tinebase.data.Record.setFromJson(record, this.recordClass);
2074
2075         } else if (record && Ext.isFunction(record.copy)) {
2076             record = record.copy();
2077         }
2078
2079         if (record.id === 0) {
2080             // we need to set a id != 0 to make identity handling in stores possible
2081             // TODO add config for this behaviour?
2082             record.id = 'new-' + Ext.id();
2083             record.setId(record.id);
2084         }
2085
2086         Tine.log.debug('Tine.widgets.grid.GridPanel::onUpdateRecord() -> record:');
2087         Tine.log.debug(record, mode);
2088
2089         if (record && Ext.isFunction(record.copy)) {
2090             var idx = this.getStore().indexOfId(record.id);
2091             if (idx >= 0) {
2092                 // only run do this in local mode as we reload the store in remote mode
2093                 // NOTE: this would otherwise delete the record if a record proxy exists!
2094                 if (mode == 'local') {
2095                     var isSelected = this.getGrid().getSelectionModel().isSelected(idx);
2096                     this.getStore().removeAt(idx);
2097                     this.getStore().insert(idx, [record]);
2098                     if (isSelected) {
2099                         this.getGrid().getSelectionModel().selectRow(idx, true);
2100                     }
2101                 }
2102             } else {
2103                 this.getStore().add([record]);
2104             }
2105             this.addToEditBuffer(record);
2106         }
2107
2108         if (mode == 'local') {
2109             this.onStoreUpdate(this.getStore(), record, Ext.data.Record.EDIT);
2110         } else {
2111             this.loadGridData({
2112                 removeStrategy: 'keepBuffered'
2113             });
2114         }
2115     },
2116
2117     onMoveRecords: function() {
2118         var containerSelectDialog = new Tine.widgets.container.SelectionDialog({
2119             recordClass: this.recordClass
2120         });
2121         containerSelectDialog.on('select', function(dlg, node) {
2122             var sm = this.grid.getSelectionModel(),
2123                 records = sm.getSelections(),
2124                 recordIds = [].concat(records).map(function(v){ return v.id; }),
2125                 filter = sm.getSelectionFilter(),
2126                 containerId = node.attributes.id,
2127                 i18nItems = this.app.i18n.n_hidden(this.recordClass.getMeta('recordName'), this.recordClass.getMeta('recordsName'), records.length);
2128
2129             if (! this.moveMask) {
2130                 this.moveMask = new Ext.LoadMask(this.grid.getEl(), {
2131                     msg: String.format(i18n._('Moving {0}'), i18nItems)
2132                 });
2133             }
2134             this.moveMask.show();
2135
2136             // move records to folder
2137             Ext.Ajax.request({
2138                 params: {
2139                     method: 'Tinebase_Container.moveRecordsToContainer',
2140                     targetContainerId: containerId,
2141                     filterData: filter,
2142                     model: this.recordClass.getMeta('modelName'),
2143                     applicationName: this.recordClass.getMeta('appName')
2144                 },
2145                 scope: this,
2146                 success: function() {
2147                     this.refreshAfterEdit(recordIds);
2148                     this.onAfterEdit(recordIds);
2149                 },
2150                 failure: function (exception) {
2151                     this.refreshAfterEdit(recordIds);
2152                     this.loadGridData();
2153                     Tine.Tinebase.ExceptionHandler.handleRequestException(exception);
2154                 }
2155             });
2156         }, this);
2157     },
2158
2159     /**
2160      * is called to resolve conflicts from 2 records
2161      */
2162     onResolveDuplicates: function() {
2163         // TODO: allow more than 2 records      
2164         if (this.grid.getSelectionModel().getSelections().length != 2) return;
2165         
2166         var selections = [];
2167         Ext.each(this.grid.getSelectionModel().getSelections(), function(sel) {
2168             selections.push(sel.data);
2169         });
2170         
2171         var window = Tine.widgets.dialog.DuplicateMergeDialog.getWindow({
2172             selections: Ext.encode(selections),
2173             appName: this.app.name,
2174             modelName: this.recordClass.getMeta('modelName')
2175         });
2176         
2177         window.on('contentschange', function() { this.store.reload(); }, this);
2178     },
2179     
2180     /**
2181      * add record to edit buffer
2182      * 
2183      * @param {String|Tine.Tinebase.data.Record} record
2184      */
2185     addToEditBuffer: function(record) {
2186
2187         var recordData = (Ext.isString(record)) ? Ext.decode(record) : record.data,
2188             id = recordData[this.recordClass.getMeta('idProperty')];
2189
2190         if (this.editBuffer.indexOf(id) === -1) {
2191             this.editBuffer.push(id);
2192         }
2193     },
2194
2195     /**
2196      * generic delete handler
2197      */
2198     onDeleteRecords: function(btn, e) {
2199         var sm = this.grid.getSelectionModel();
2200
2201         if (sm.isFilterSelect && ! this.filterSelectionDelete) {
2202             Ext.MessageBox.show({
2203                 title: i18n._('Not Allowed'),
2204                 msg: i18n._('You are not allowed to delete all pages at once'),
2205                 buttons: Ext.Msg.OK,
2206                 icon: Ext.MessageBox.INFO
2207             });
2208
2209             return;
2210         }
2211         var records = sm.getSelections();
2212
2213         if (this.disableDeleteConfirmation || (Tine[this.app.appName].registry.get('preferences')
2214             && Tine[this.app.appName].registry.get('preferences').get('confirmDelete') !== null
2215             && Tine[this.app.appName].registry.get('preferences').get('confirmDelete') == 0)
2216         ) {
2217             // don't show confirmation question for record deletion
2218             this.deleteRecords(sm, records);
2219         } else {
2220             var recordNames = records[0].getTitle();
2221             if (records.length > 1) {
2222                 recordNames += ', ...';
2223             }
2224
2225             var i18nQuestion = this.i18nDeleteQuestion ?
2226                 this.app.i18n.n_hidden(this.i18nDeleteQuestion[0], this.i18nDeleteQuestion[1], records.length) :
2227                 String.format(i18n.ngettext('Do you really want to delete the selected record ({0})?',
2228                     'Do you really want to delete the selected records ({0})?', records.length), recordNames);
2229             Ext.MessageBox.confirm(i18n._('Confirm'), i18nQuestion, function(btn) {
2230                 if (btn == 'yes') {
2231                     this.deleteRecords(sm, records);
2232                 }
2233             }, this);
2234         }
2235     },
2236
2237     /**
2238      * delete records
2239      * 
2240      * @param {SelectionModel} sm
2241      * @param {Array} records
2242      */
2243     deleteRecords: function(sm, records) {
2244         // directly remove records from the store (only for non-filter-selection)
2245         if (Ext.isArray(records) && ! (sm.isFilterSelect && this.filterSelectionDelete)) {
2246             Ext.each(records, function(record) {
2247                 this.store.remove(record);
2248             });
2249             // if nested in an editDialog, just change the parent record
2250             if (this.editDialog) {
2251                 var items = [];
2252                 this.store.each(function(item) {
2253                     items.push(item.data);
2254                 });
2255                 this.editDialog.record.set(this.editDialogRecordProperty, items);
2256                 this.editDialog.fireEvent('updateDependent');
2257                 return;
2258             }
2259         }
2260
2261         if (this.recordProxy) {
2262             if (this.usePagingToolbar) {
2263                 this.pagingToolbar.refresh.disable();
2264             }
2265
2266             var i18nItems = this.app.i18n.n_hidden(this.recordClass.getMeta('recordName'), this.recordClass.getMeta('recordsName'), records.length),
2267                 recordIds = [].concat(records).map(function(v){ return v.id; });
2268
2269             if (sm.isFilterSelect && this.filterSelectionDelete) {
2270                 if (! this.deleteMask) {
2271                     this.deleteMask = new Ext.LoadMask(this.grid.getEl(), {
2272                         msg: String.format(i18n._('Deleting {0}'), i18nItems) + ' ' + i18n._('... This may take a long time!')
2273                     });
2274                 }
2275                 this.deleteMask.show();
2276             }
2277
2278             this.deleteQueue = this.deleteQueue.concat(recordIds);
2279
2280             var options = {
2281                 scope: this,
2282                 success: function() {
2283                     this.refreshAfterDelete(recordIds);
2284                     this.onAfterDelete(recordIds);
2285                 },
2286                 failure: function (exception) {
2287                     this.refreshAfterDelete(recordIds);
2288                     this.loadGridData();
2289                     Tine.Tinebase.ExceptionHandler.handleRequestException(exception);
2290                 }
2291             };
2292
2293             if (sm.isFilterSelect && this.filterSelectionDelete) {
2294                 this.recordProxy.deleteRecordsByFilter(sm.getSelectionFilter(), options);
2295             } else {
2296                 this.recordProxy.deleteRecords(records, options);
2297             }
2298         }
2299     },
2300
2301     /**
2302      * refresh after delete (hide delete mask or refresh paging toolbar)
2303      */
2304     refreshAfterDelete: function(ids) {
2305         this.deleteQueue = this.deleteQueue.diff(ids);
2306
2307         if (this.deleteMask) {
2308             this.deleteMask.hide();
2309         }
2310
2311         if (this.usePagingToolbar) {
2312             this.pagingToolbar.refresh.show();
2313         }
2314     },
2315
2316     /**
2317      * do something after deletion of records
2318      * - reload the store
2319      * 
2320      * @param {Array} [ids]
2321      */
2322     onAfterDelete: function(ids) {
2323         this.editBuffer = this.editBuffer.diff(ids);
2324
2325         this.loadGridData({
2326             removeStrategy: 'keepBuffered'
2327         });
2328     },
2329
2330     /**
2331      * refresh after edit/move
2332      */
2333     refreshAfterEdit: function(ids) {
2334         this.editBuffer = this.editBuffer.diff(ids);
2335
2336         if (this.moveMask) {
2337             this.moveMask.hide();
2338         }
2339         if (this.editMask) {
2340             this.editMask.hide();
2341         }
2342
2343         if (this.usePagingToolbar) {
2344             this.pagingToolbar.refresh.show();
2345         }
2346     },
2347
2348     /**
2349      * do something after edit of records
2350      *
2351      * @param {Array} [ids]
2352      */
2353     onAfterEdit: function(ids) {
2354         this.editBuffer = this.editBuffer.diff(ids);
2355
2356         this.loadGridData({
2357             removeStrategy: 'keepBuffered'
2358         });
2359     }
2360 });