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