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