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