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