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