#8212: can't sort by tags
[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('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             newRecordCollection = new Ext.util.MixedCollection();
931
932         // fill new collection
933         Ext.each(o.records, function(record) {
934             newRecordCollection.add(record.id, record);
935         });
936
937         // assemble update & keep
938         this.store.each(function(record) {
939             var newRecord = newRecordCollection.get(record.id);
940             if (newRecord) {
941                 records.push(newRecord);
942                 recordsIds.push(newRecord.id);
943             } else if (options.removeStrategy === 'keepAll' || (options.removeStrategy === 'keepBuffered' && this.editBuffer.indexOf(record.id) >= 0)) {
944                 var copiedRecord = record.copy();
945                 copiedRecord.not_in_filter = true;
946                 records.push(copiedRecord);
947                 recordsIds.push(record.id);
948             }
949         }, this);
950
951         // assemble adds
952         newRecordCollection.each(function(record, idx) {
953             if (recordsIds.indexOf(record.id) == -1 && this.deleteQueue.indexOf(record.id) == -1) {
954                 var lastRecord = newRecordCollection.itemAt(idx-1);
955                 var lastRecordIdx = lastRecord ? recordsIds.indexOf(lastRecord.id) : -1;
956                 records.splice(lastRecordIdx+1, 0, record);
957                 recordsIds.splice(lastRecordIdx+1, 0, record.id);
958             }
959         }, this);
960
961         o.records = records;
962     },
963
964     /**
965      * perform the initial load of grid data
966      */
967     initialLoad: function() {
968         var defaultFavorite = Tine.widgets.persistentfilter.model.PersistentFilter.getDefaultFavorite(this.app.appName);
969         var favoritesPanel  = this.app.getMainScreen() && typeof this.app.getMainScreen().getWestPanel().getFavoritesPanel === 'function' && this.hasFavoritesPanel 
970             ? this.app.getMainScreen().getWestPanel().getFavoritesPanel() 
971             : null;
972         if (defaultFavorite && favoritesPanel) {
973             favoritesPanel.selectFilter(defaultFavorite);
974         } else {
975             if (! this.editDialog) {
976                 this.store.load.defer(10, this.store, [ typeof this.autoLoad == 'object' ? this.autoLoad : undefined]);
977             } else {
978                 // editDialog exists, so get the records from there.
979                 var items = this.editDialog.record.get(this.editDialogRecordProperty);
980                 if (Ext.isArray(items)) {
981                     Ext.each(items, function(item) {
982                         var record = this.recordProxy.recordReader({responseText: Ext.encode(item)});
983                         this.store.addSorted(record);
984                     }, this);
985                 }
986             }
987         }
988
989         if (this.usePagingToolbar && this.recordProxy) {
990             this.pagingToolbar.refresh.disable.defer(10, this.pagingToolbar.refresh);
991         }
992     },
993
994     /**
995      * init ext grid panel
996      * @private
997      */
998     initGrid: function() {
999         // init sel model
1000         this.selectionModel = new Tine.widgets.grid.FilterSelectionModel({
1001             store: this.store,
1002             gridPanel: this
1003         });
1004         this.selectionModel.on('selectionchange', function(sm) {
1005             //Tine.widgets.actionUpdater(sm, this.actions, this.recordClass.getMeta('containerProperty'), !this.evalGrants);
1006             this.actionUpdater.updateActions(sm);
1007
1008             this.ctxNode = this.selectionModel.getSelections();
1009             if (this.updateOnSelectionChange && this.detailsPanel) {
1010                 this.detailsPanel.onDetailsUpdate(sm);
1011             }
1012         }, this);
1013
1014         if (this.usePagingToolbar) {
1015             this.pagingToolbar = new Ext.ux.grid.PagingToolbar(Ext.apply({
1016                 pageSize: this.defaultPaging && this.defaultPaging.limit ? this.defaultPaging.limit : 50,
1017                 store: this.store,
1018                 displayInfo: true,
1019                 displayMsg: Tine.Tinebase.translation._('Displaying records {0} - {1} of {2}').replace(/records/, this.i18nRecordsName),
1020                 emptyMsg: String.format(Tine.Tinebase.translation._("No {0} to display"), this.i18nRecordsName),
1021                 displaySelectionHelper: true,
1022                 sm: this.selectionModel,
1023                 disableSelectAllPages: this.disableSelectAllPages,
1024                 nested: this.editDialog ? true : false
1025             }, this.pagingConfig));
1026             // mark next grid refresh as paging-refresh
1027             this.pagingToolbar.on('beforechange', function() {
1028                 this.grid.getView().isPagingRefresh = true;
1029             }, this);
1030         }
1031
1032         // init view
1033         var view =  new Ext.grid.GridView({
1034             getRowClass: this.getViewRowClass,
1035             autoFill: true,
1036             forceFit:true,
1037             ignoreAdd: true,
1038             emptyText: this.i18nEmptyText,
1039             onLoad: Ext.grid.GridView.prototype.onLoad.createInterceptor(function() {
1040                 if (this.grid.getView().isPagingRefresh) {
1041                     this.grid.getView().isPagingRefresh = false;
1042                     return true;
1043                 }
1044
1045                 return false;
1046             }, this)
1047         });
1048
1049         // which grid to use?
1050         var Grid = this.gridConfig.quickaddMandatory ? Ext.ux.grid.QuickaddGridPanel : (this.gridConfig.gridType || Ext.grid.GridPanel);
1051
1052         this.gridConfig.store = this.store;
1053
1054         // activate grid header menu for column selection
1055         this.gridConfig.plugins = this.gridConfig.plugins ? this.gridConfig.plugins : [];
1056         this.gridConfig.plugins.push(new Ext.ux.grid.GridViewMenuPlugin({}));
1057         this.gridConfig.enableHdMenu = false;
1058
1059         if (this.stateful) {
1060             this.gridConfig.stateful = true;
1061             this.gridConfig.stateId  = this.stateId + '-Grid';
1062         }
1063
1064         this.grid = new Grid(Ext.applyIf(this.gridConfig, {
1065             border: false,
1066             store: this.store,
1067             sm: this.selectionModel,
1068             view: view
1069         }));
1070
1071         // init various grid / sm listeners
1072         this.grid.on('keydown',     this.onKeyDown,         this);
1073         this.grid.on('rowclick',    this.onRowClick,        this);
1074         this.grid.on('rowdblclick', this.onRowDblClick,     this);
1075         this.grid.on('newentry',    this.onStoreNewEntry,   this);
1076         this.grid.on('headerclick', this.onHeaderClick,   this);
1077
1078         this.grid.on('rowcontextmenu', this.onRowContextMenu, this);
1079
1080     },
1081
1082     /**
1083      * executed after outer panel rendering process
1084      */
1085     afterRender: function() {
1086         Tine.widgets.grid.GridPanel.superclass.afterRender.apply(this, arguments);
1087         this.initialLoad();
1088     },
1089
1090     /**
1091      * trigger store load with grid related options
1092      * 
1093      * TODO rethink -> preserveCursor and preserveSelection might conflict on page breaks!
1094      * TODO don't reload details panel when selection is preserved
1095      * 
1096      * @param {Object} options
1097      */
1098     loadGridData: function(options) {
1099         var options = options || {};
1100
1101         Ext.applyIf(options, {
1102             callback:           Ext.emptyFn,
1103             scope:              this,
1104             params:             {},
1105
1106             preserveCursor:     true, 
1107             preserveSelection:  true, 
1108             preserveScroller:   true, 
1109             removeStrategy:     'default'
1110         });
1111
1112         if (options.preserveCursor && this.usePagingToolbar) {
1113             options.params.start = this.pagingToolbar.cursor;
1114         }
1115
1116         this.store.load(options);
1117     },
1118
1119     /**
1120      * get action toolbar
1121      * 
1122      * @return {Ext.Toolbar}
1123      */
1124     getActionToolbar: function() {
1125         if (! this.actionToolbar) {
1126             var additionalItems = this.getActionToolbarItems();
1127
1128             var items = [];
1129             
1130             if (this.action_addInNewWindow) {
1131                 if (this.splitAddButton) {
1132                     items.push(Ext.apply(
1133                         new Ext.SplitButton(this.action_addInNewWindow), {
1134                             scale: 'medium',
1135                             rowspan: 2,
1136                             iconAlign: 'top',
1137                             arrowAlign:'right',
1138                             menu: new Ext.menu.Menu({
1139                                 items: [],
1140                                 plugins: [{
1141                                     ptype: 'ux.itemregistry',
1142                                     key:   'Tine.widgets.grid.GridPanel.addButton'
1143                                 }]
1144                             })
1145                         })
1146                     );
1147                 } else {
1148                     items.push(Ext.apply(
1149                         new Ext.Button(this.action_addInNewWindow), {
1150                             scale: 'medium',
1151                             rowspan: 2,
1152                             iconAlign: 'top'
1153                         })
1154                     );
1155                 }
1156             }
1157             
1158             if (this.action_editInNewWindow) {
1159                 items.push(Ext.apply(
1160                     new Ext.Button(this.action_editInNewWindow), {
1161                         scale: 'medium',
1162                         rowspan: 2,
1163                         iconAlign: 'top'
1164                     })
1165                 );
1166             }
1167             
1168             if (this.action_deleteRecord) {
1169                 items.push(Ext.apply(
1170                     new Ext.Button(this.action_deleteRecord), {
1171                         scale: 'medium',
1172                         rowspan: 2,
1173                         iconAlign: 'top'
1174                     })
1175                 );
1176             }
1177             
1178             if (this.actions_print) {
1179                 items.push(Ext.apply(
1180                     new Ext.Button(this.actions_print), {
1181                         scale: 'medium',
1182                         rowspan: 2,
1183                         iconAlign: 'top'
1184                     })
1185                 );
1186             }
1187             
1188             this.actionToolbar = new Ext.Toolbar({
1189                 items: [{
1190                     xtype: 'buttongroup',
1191                     plugins: [{
1192                         ptype: 'ux.itemregistry',
1193                         key:   this.app.appName + '-GridPanel-ActionToolbar-leftbtngrp'
1194                     }],
1195                     items: items.concat(Ext.isArray(additionalItems) ? additionalItems : [])
1196                 }].concat(Ext.isArray(additionalItems) ? [] : [additionalItems])
1197             });
1198
1199             if (this.filterToolbar && typeof this.filterToolbar.getQuickFilterField == 'function') {
1200                 this.actionToolbar.add('->', this.filterToolbar.getQuickFilterField());
1201             } 
1202         }
1203
1204         return this.actionToolbar;
1205     },
1206
1207     /**
1208      * template fn for subclasses to add custom items to action toolbar
1209      * 
1210      * @return {Array/Object}
1211      */
1212     getActionToolbarItems: function() {
1213         var items = this.actionToolbarItems || [];
1214
1215         if (! Ext.isEmpty(items)) {
1216             // legacy handling! subclasses should register all actions when initializing actions
1217             this.actionUpdater.addActions(items);
1218         }
1219
1220         return items;
1221     },
1222
1223     /**
1224      * returns rows context menu
1225      * 
1226      * @param {Ext.grid.GridPanel} grid
1227      * @param {Number} row
1228      * @param {Ext.EventObject} e
1229      * @return {Ext.menu.Menu}
1230      */
1231     getContextMenu: function(grid, row, e) {
1232
1233         if (! this.contextMenu) {
1234             var items = [];
1235             
1236             if (this.action_addInNewWindow) items.push(this.action_addInNewWindow);
1237             if (this.action_editCopyInNewWindow) items.push(this.action_editCopyInNewWindow);
1238             if (this.action_editInNewWindow) items.push(this.action_editInNewWindow);
1239             if (this.action_deleteRecord) items.push(this.action_deleteRecord);
1240
1241             if (this.duplicateResolvable) {
1242                 items.push(this.action_resolveDuplicates);
1243             }
1244             
1245             if (this.action_tagsMassAttach && ! this.action_tagsMassAttach.hidden) {
1246                 items.push('-', this.action_tagsMassAttach, this.action_tagsMassDetach);
1247             }
1248
1249             // lookup additional items
1250             items = items.concat(this.getContextMenuItems());
1251
1252             // New record of another app
1253             this.newRecordMenu = new Ext.menu.Menu({
1254                 items: [],
1255                 plugins: [{
1256                     ptype: 'ux.itemregistry',
1257                     key:   this.app.appName + '-GridPanel-ContextMenu-New'
1258                 }]
1259             });
1260
1261             this.newRecordAction = new Ext.Action({
1262                 text: _('New...'),
1263                 hidden: ! this.newRecordMenu.items.length,
1264                 iconCls: this.app.getIconCls(),
1265                 scope: this,
1266                 menu: this.newRecordMenu
1267             });
1268
1269             items.push(this.newRecordAction);
1270
1271             // Add to record of another app            
1272             this.addToRecordMenu = new Ext.menu.Menu({
1273                 items: [],
1274                 plugins: [{
1275                     ptype: 'ux.itemregistry',
1276                     key:   this.app.appName + '-GridPanel-ContextMenu-Add'
1277                 }]
1278             });
1279
1280             this.addToRecordAction = new Ext.Action({
1281                 text: _('Add to...'),
1282                 hidden: ! this.addToRecordMenu.items.length,
1283                 iconCls: this.app.getIconCls(),
1284                 scope: this,
1285                 menu: this.addToRecordMenu
1286             });
1287
1288             items.push(this.addToRecordAction);
1289
1290             this.contextMenu = new Ext.menu.Menu({
1291                 items: items,
1292                 plugins: [{
1293                     ptype: 'ux.itemregistry',
1294                     key:   this.app.appName + '-GridPanel-ContextMenu'
1295                 }]
1296             });
1297         }
1298
1299         return this.contextMenu;
1300     },
1301
1302     /**
1303      * template fn for subclasses to add custom items to context menu
1304      * 
1305      * @return {Array}
1306      */
1307     getContextMenuItems: function() {
1308         var items = this.contextMenuItems || [];
1309
1310         if (! Ext.isEmpty(items)) {
1311             // legacy handling! subclasses should register all actions when initializing actions
1312             this.actionUpdater.addActions(items);
1313         }
1314
1315         return items;
1316     },
1317
1318     /**
1319      * get modlog columns
1320      * 
1321      * shouldn' be used anymore
1322      * @TODO: use applicationstarter and modelconfiguration
1323      * 
1324      * @deprecated
1325      * @return {Array}
1326      */
1327     getModlogColumns: function() {
1328         var result = [
1329             { id: 'creation_time',      header: _('Creation Time'),         dataIndex: 'creation_time',         renderer: Tine.Tinebase.common.dateRenderer,        hidden: true, sortable: true },
1330             { id: 'created_by',         header: _('Created By'),            dataIndex: 'created_by',            renderer: Tine.Tinebase.common.usernameRenderer,    hidden: true, sortable: true },
1331             { id: 'last_modified_time', header: _('Last Modified Time'),    dataIndex: 'last_modified_time',    renderer: Tine.Tinebase.common.dateRenderer,        hidden: true, sortable: true },
1332             { id: 'last_modified_by',   header: _('Last Modified By'),      dataIndex: 'last_modified_by',      renderer: Tine.Tinebase.common.usernameRenderer,    hidden: true, sortable: true }
1333         ];
1334
1335         return result;
1336     },
1337
1338     /**
1339      * get custom field columns for column model
1340      * 
1341      * @return {Array}
1342      */
1343     getCustomfieldColumns: function() {
1344         var modelName = this.recordClass.getMeta('appName') + '_Model_' + this.recordClass.getMeta('modelName'),
1345             cfConfigs = Tine.widgets.customfields.ConfigManager.getConfigs(this.app, modelName),
1346             result = [];
1347
1348         Ext.each(cfConfigs, function(cfConfig) {
1349             result.push({
1350                 id: cfConfig.id,
1351                 header: cfConfig.get('definition').label,
1352                 dataIndex: 'customfields',
1353                 renderer: Tine.widgets.customfields.Renderer.get(this.app, cfConfig),
1354                 sortable: false,
1355                 hidden: true
1356             });
1357         }, this);
1358
1359         return result;
1360     },
1361
1362     /**
1363      * get custom field filter for filter toolbar
1364      * 
1365      * @return {Array}
1366      */
1367     getCustomfieldFilters: function() {
1368         var modelName = this.recordClass.getMeta('appName') + '_Model_' + this.recordClass.getMeta('modelName'),
1369             cfConfigs = Tine.widgets.customfields.ConfigManager.getConfigs(this.app, modelName),
1370             result = [];
1371         Ext.each(cfConfigs, function(cfConfig) {
1372             result.push({filtertype: 'tinebase.customfield', app: this.app, cfConfig: cfConfig});
1373         }, this);
1374
1375         return result;
1376     },
1377
1378     /**
1379      * returns filter toolbar
1380      * @private
1381      */
1382     getFilterToolbar: function(config) {
1383         config = config || {};
1384         var plugins = [];
1385         if (! Ext.isDefined(this.hasQuickSearchFilterToolbarPlugin) || this.hasQuickSearchFilterToolbarPlugin) {
1386             this.quickSearchFilterToolbarPlugin = new Tine.widgets.grid.FilterToolbarQuickFilterPlugin();
1387             plugins.push(this.quickSearchFilterToolbarPlugin);
1388         }
1389
1390         return new Tine.widgets.grid.FilterPanel(Ext.apply(config, {
1391             app: this.app,
1392             recordClass: this.recordClass,
1393             filterModels: this.recordClass.getFilterModel().concat(this.getCustomfieldFilters()),
1394             defaultFilter: 'query',
1395             filters: this.defaultFilters || [],
1396             plugins: plugins
1397         }));
1398     },
1399
1400     /**
1401      * return store from grid
1402      * 
1403      * @return {Ext.data.Store}
1404      */
1405     getStore: function() {
1406         return this.grid.getStore();
1407     },
1408
1409     /**
1410      * return view from grid
1411      * 
1412      * @return {Ext.grid.GridView}
1413      */
1414     getView: function() {
1415         return this.grid.getView();
1416     },
1417
1418     /**
1419      * return grid
1420      * 
1421      * @return {Ext.ux.grid.QuickaddGridPanel|Ext.grid.GridPanel}
1422      */
1423     getGrid: function() {
1424         return this.grid;
1425     },
1426
1427     /**
1428      * key down handler
1429      * @private
1430      */
1431     onKeyDown: function(e){
1432         if (e.ctrlKey) {
1433             switch (e.getKey()) {
1434                 case e.A:
1435                     // select only current page
1436                     this.grid.getSelectionModel().selectAll(true);
1437                     e.preventDefault();
1438                     break;
1439                 case e.E:
1440                     if (this.action_editInNewWindow && !this.action_editInNewWindow.isDisabled()) {
1441                         this.onEditInNewWindow.call(this, {
1442                             actionType: 'edit'
1443                         });
1444                         e.preventDefault();
1445                     }
1446                     break;
1447                 case e.N:
1448                     if (this.action_addInNewWindow && !this.action_addInNewWindow.isDisabled()) {
1449                         this.onEditInNewWindow.call(this, {
1450                             actionType: 'add'
1451                         });
1452                         e.preventDefault();
1453                     }
1454                     break;
1455                 case e.F:
1456                     if (this.filterToolbar && this.hasQuickSearchFilterToolbarPlugin) {
1457                         e.preventDefault();
1458                         this.filterToolbar.getQuickFilterPlugin().quickFilter.focus();
1459                     }
1460                     break;
1461             }
1462         } else {
1463             if ([e.BACKSPACE, e.DELETE].indexOf(e.getKey()) !== -1) {
1464                 if (!this.grid.editing && !this.grid.adding && !this.action_deleteRecord.isDisabled()) {
1465                     this.onDeleteRecords.call(this);
1466                     e.preventDefault();
1467                 }
1468             }
1469         }
1470     },
1471
1472     /**
1473      * row click handler
1474      * 
1475      */
1476     onRowClick: function(grid, row, e) {
1477         /* TODO check if we need this in IE
1478         // hack to get percentage editor working
1479         var cell = Ext.get(grid.getView().getCell(row,1));
1480         var dom = cell.child('div:last');
1481         while (cell.first()) {
1482             cell = cell.first();
1483             cell.on('click', function(e){
1484                 e.stopPropagation();
1485                 grid.fireEvent('celldblclick', grid, row, 1, e);
1486             });
1487         }
1488         */
1489
1490         // fix selection of one record if shift/ctrl key is not pressed any longer
1491         if (e.button === 0 && !e.shiftKey && !e.ctrlKey) {
1492             var sm = grid.getSelectionModel();
1493
1494             if (sm.getCount() == 1 && sm.isSelected(row)) {
1495                 return;
1496             }
1497
1498             sm.clearSelections();
1499             sm.selectRow(row, false);
1500             grid.view.focusRow(row);
1501         }
1502     },
1503     
1504     /**
1505      * row doubleclick handler
1506      * 
1507      * @param {} grid
1508      * @param {} row
1509      * @param {} e
1510      */
1511     onRowDblClick: function(grid, row, e) {
1512         this.onEditInNewWindow.call(this, {actionType: 'edit'});
1513     }, 
1514
1515     /**
1516      * called on row context click
1517      * 
1518      * @param {Ext.grid.GridPanel} grid
1519      * @param {Number} row
1520      * @param {Ext.EventObject} e
1521      */
1522     onRowContextMenu: function(grid, row, e) {
1523         e.stopEvent();
1524         var selModel = grid.getSelectionModel();
1525         if (!selModel.isSelected(row)) {
1526             // disable preview update if config option is set to false
1527             this.updateOnSelectionChange = this.updateDetailsPanelOnCtxMenu;
1528             selModel.selectRow(row);
1529         }
1530
1531         this.getContextMenu(grid, row, e).showAt(e.getXY());
1532         // reset preview update
1533         this.updateOnSelectionChange = true;
1534     },
1535     
1536     /**
1537      * Opens the required EditDialog
1538      * @param {Object} actionButton the button the action was called from
1539      * @param {Tine.Tinebase.data.Record} record the record to display/edit in the dialog
1540      * @param {Array} plugins the plugins used for the edit dialog
1541      * @return {Boolean}
1542      */
1543     onEditInNewWindow: function(button, record, plugins) {
1544         if (! record) {
1545             if (button.actionType == 'edit' || button.actionType == 'copy') {
1546                 if (! this.action_editInNewWindow || this.action_editInNewWindow.isDisabled()) {
1547                     // if edit action is disabled or not available, we also don't open a new window
1548                     return false;
1549                 }
1550                 var selectedRows = this.grid.getSelectionModel().getSelections();
1551                 record = selectedRows[0];
1552             } else {
1553                 record = new this.recordClass(this.recordClass.getDefaultData(), 0);
1554             }
1555         }
1556
1557         // plugins to add to edit dialog
1558         var plugins = plugins ? plugins : [];
1559         
1560         var totalcount = this.selectionModel.getCount(),
1561             selectedRecords = [],
1562             fixedFields = (button.hasOwnProperty('fixedFields') && Ext.isObject(button.fixedFields)) ? Ext.encode(button.fixedFields) : null,
1563             editDialogClass = this.editDialogClass || Tine[this.app.appName][this.recordClass.getMeta('modelName') + 'EditDialog'];
1564         
1565         // add "multiple_edit_dialog" plugin to dialog, if required
1566         if (((totalcount > 1) && (this.multipleEdit) && (button.actionType == 'edit'))) {
1567             Ext.each(this.selectionModel.getSelections(), function(record) {
1568                 selectedRecords.push(record.data);
1569             }, this );
1570             
1571             plugins.push({
1572                 ptype: 'multiple_edit_dialog', 
1573                 selectedRecords: selectedRecords,
1574                 selectionFilter: this.selectionModel.getSelectionFilter(),
1575                 isFilterSelect: this.selectionModel.isFilterSelect,
1576                 totalRecordCount: totalcount
1577             });
1578         }
1579         
1580         var popupWindow = editDialogClass.openWindow(Ext.copyTo(
1581             this.editDialogConfig || {}, {
1582                 plugins: plugins ? Ext.encode(plugins) : null,
1583                 fixedFields: fixedFields,
1584                 record: editDialogClass.prototype.mode == 'local' ? Ext.encode(record.data) : record,
1585                 copyRecord: (button.actionType == 'copy'),
1586                 listeners: {
1587                     scope: this,
1588                     'update': ((this.selectionModel.getCount() > 1) && (this.multipleEdit)) ? this.onUpdateMultipleRecords : this.onUpdateRecord
1589                 }
1590             }, 'record,listeners,fixedFields,copyRecord,plugins')
1591         );
1592         return true;
1593     },
1594
1595     /**
1596      * is called after multiple records have been updated
1597      */
1598     onUpdateMultipleRecords: function() {
1599         this.store.reload();
1600     },
1601
1602     /**
1603      * on update after edit
1604      * 
1605      * @param {String|Tine.Tinebase.data.Record} record
1606      */
1607     onUpdateRecord: function(record, mode) {
1608         if (Ext.isString(record) && this.recordProxy) {
1609             record = this.recordProxy.recordReader({responseText: record});
1610         } else if (record && Ext.isFunction(record.copy)) {
1611             record = record.copy();
1612         }
1613
1614         Tine.log.debug('Tine.widgets.grid.GridPanel::onUpdateRecord() -> record:');
1615         Tine.log.debug(record, mode);
1616         
1617         if (record && Ext.isFunction(record.copy)) {
1618             var idx = this.getStore().indexOfId(record.id);
1619             if (idx >=0) {
1620                 var isSelected = this.getGrid().getSelectionModel().isSelected(idx);
1621                 this.getStore().removeAt(idx);
1622                 this.getStore().insert(idx, [record]);
1623
1624                 if (isSelected) {
1625                     this.getGrid().getSelectionModel().selectRow(idx, true);
1626                 }
1627             } else {
1628                 this.getStore().add([record]);
1629             }
1630             this.addToEditBuffer(record);
1631         }
1632
1633         if (mode == 'local') {
1634             this.onStoreUpdate(this.getStore(), record, Ext.data.Record.EDIT);
1635         } else {
1636             this.loadGridData({
1637                 removeStrategy: 'keepBuffered'
1638             });
1639         }
1640     },
1641
1642     
1643     /**
1644      * is called to resolve conflicts from 2 records
1645      */
1646     onResolveDuplicates: function() {
1647         // TODO: allow more than 2 records      
1648         if (this.grid.getSelectionModel().getSelections().length != 2) return;
1649         
1650         var selections = [];
1651         Ext.each(this.grid.getSelectionModel().getSelections(), function(sel) {
1652             selections.push(sel.data);
1653         });
1654         
1655         var window = Tine.widgets.dialog.DuplicateMergeDialog.getWindow({
1656             selections: Ext.encode(selections),
1657             appName: this.app.name,
1658             modelName: this.recordClass.getMeta('modelName')
1659         });
1660         
1661         window.on('contentschange', function() { this.store.reload(); }, this);
1662     },
1663     
1664     /**
1665      * add record to edit buffer
1666      * 
1667      * @param {String|Tine.Tinebase.data.Record} record
1668      */
1669     addToEditBuffer: function(record) {
1670
1671         var recordData = (Ext.isString(record)) ? Ext.decode(record) : record.data,
1672             id = recordData[this.recordClass.getMeta('idProperty')];
1673
1674         if (this.editBuffer.indexOf(id) === -1) {
1675             this.editBuffer.push(id);
1676         }
1677     },
1678
1679     /**
1680      * generic delete handler
1681      */
1682     onDeleteRecords: function(btn, e) {
1683         var sm = this.grid.getSelectionModel();
1684
1685         if (sm.isFilterSelect && ! this.filterSelectionDelete) {
1686             Ext.MessageBox.show({
1687                 title: _('Not Allowed'), 
1688                 msg: _('You are not allowed to delete all pages at once'),
1689                 buttons: Ext.Msg.OK,
1690                 icon: Ext.MessageBox.INFO
1691             });
1692
1693             return;
1694         }
1695         var records = sm.getSelections();
1696
1697         if (Tine[this.app.appName].registry.containsKey('preferences') 
1698             && Tine[this.app.appName].registry.get('preferences').containsKey('confirmDelete')
1699             && Tine[this.app.appName].registry.get('preferences').get('confirmDelete') == 0
1700         ) {
1701             // don't show confirmation question for record deletion
1702             this.deleteRecords(sm, records);
1703         } else {
1704             var recordNames = records[0].get(this.recordClass.getMeta('titleProperty'));
1705             if (records.length > 1) {
1706                 recordNames += ', ...';
1707             }
1708
1709             var i18nQuestion = this.i18nDeleteQuestion ?
1710                 this.app.i18n.n_hidden(this.i18nDeleteQuestion[0], this.i18nDeleteQuestion[1], records.length) :
1711                 String.format(Tine.Tinebase.translation.ngettext('Do you really want to delete the selected record ({0})?',
1712                     'Do you really want to delete the selected records ({0})?', records.length), recordNames);
1713             Ext.MessageBox.confirm(_('Confirm'), i18nQuestion, function(btn) {
1714                 if (btn == 'yes') {
1715                     this.deleteRecords(sm, records);
1716                 }
1717             }, this);
1718         }
1719     },
1720
1721     /**
1722      * delete records
1723      * 
1724      * @param {SelectionModel} sm
1725      * @param {Array} records
1726      */
1727     deleteRecords: function(sm, records) {
1728         // directly remove records from the store (only for non-filter-selection)
1729         if (Ext.isArray(records) && ! (sm.isFilterSelect && this.filterSelectionDelete)) {
1730             Ext.each(records, function(record) {
1731                 this.store.remove(record);
1732             });
1733             // if nested in an editDialog, just change the parent record
1734             if (this.editDialog) {
1735                 var items = [];
1736                 this.store.each(function(item) {
1737                     items.push(item.data);
1738                 });
1739                 this.editDialog.record.set(this.editDialogRecordProperty, items);
1740                 this.editDialog.fireEvent('updateDependent');
1741                 return;
1742             }
1743         }
1744
1745         if (this.recordProxy) {
1746             if (this.usePagingToolbar) {
1747                 this.pagingToolbar.refresh.disable();
1748             }
1749
1750             var i18nItems = this.app.i18n.n_hidden(this.recordClass.getMeta('recordName'), this.recordClass.getMeta('recordsName'), records.length),
1751                 recordIds = [].concat(records).map(function(v){ return v.id; });
1752
1753             if (sm.isFilterSelect && this.filterSelectionDelete) {
1754                 if (! this.deleteMask) {
1755                     this.deleteMask = new Ext.LoadMask(this.grid.getEl(), {
1756                         msg: String.format(_('Deleting {0}'), i18nItems) + _(' ... This may take a long time!')
1757                     });
1758                 }
1759                 this.deleteMask.show();
1760             }
1761
1762             this.deleteQueue = this.deleteQueue.concat(recordIds);
1763
1764             var options = {
1765                 scope: this,
1766                 success: function() {
1767                     this.refreshAfterDelete(recordIds);
1768                     this.onAfterDelete(recordIds);
1769                 },
1770                 failure: function (exception) {
1771                     this.refreshAfterDelete(recordIds);
1772                     this.loadGridData();
1773                     Tine.Tinebase.ExceptionHandler.handleRequestException(exception);
1774                 }
1775             };
1776
1777             if (sm.isFilterSelect && this.filterSelectionDelete) {
1778                 this.recordProxy.deleteRecordsByFilter(sm.getSelectionFilter(), options);
1779             } else {
1780                 this.recordProxy.deleteRecords(records, options);
1781             }
1782         }
1783     },
1784
1785     /**
1786      * refresh after delete (hide delete mask or refresh paging toolbar)
1787      */
1788     refreshAfterDelete: function(ids) {
1789         this.deleteQueue = this.deleteQueue.diff(ids);
1790
1791         if (this.deleteMask) {
1792             this.deleteMask.hide();
1793         }
1794
1795         if (this.usePagingToolbar) {
1796             this.pagingToolbar.refresh.show();
1797         }
1798     },
1799
1800     /**
1801      * do something after deletion of records
1802      * - reload the store
1803      * 
1804      * @param {Array} [ids]
1805      */
1806     onAfterDelete: function(ids) {
1807         this.editBuffer = this.editBuffer.diff(ids);
1808
1809         this.loadGridData({
1810             removeStrategy: 'keepBuffered'
1811         });
1812     }
1813 });