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