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