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