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