5d566707b991c8fc9aa700c75d1ae59a5269d121
[tine20] / tine20 / Calendar / js / AttendeeGridPanel.js
1 /*
2  * Tine 2.0
3  * 
4  * @package     Calendar
5  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
6  * @author      Cornelius Weiss <c.weiss@metaways.de>
7  * @copyright   Copyright (c) 2009-2013 Metaways Infosystems GmbH (http://www.metaways.de)
8  *
9  */
10  
11 Ext.ns('Tine.Calendar');
12
13 require('./AttendeePickerCombo');
14 require('./ResourcePickerCombo');
15
16 /**
17  * @namespace   Tine.Calendar
18  * @class       Tine.Calendar.AttendeeGridPanel
19  * @extends     Ext.grid.EditorGridPanel
20  * @author      Cornelius Weiss <c.weiss@metaways.de>
21  */
22 Tine.Calendar.AttendeeGridPanel = Ext.extend(Ext.grid.EditorGridPanel, {
23     autoExpandColumn: 'user_id',
24     clicksToEdit: 1,
25     enableHdMenu: false,
26     canonicalName: 'AttendeeGrid',
27     
28     /**
29      * @cfg defaut text for new attendee combo
30      * i18n._('Click here to invite another attender...')
31      */
32     addNewAttendeeText: 'Click here to invite another attender...',
33     
34     /**
35      * @cfg {Boolean} showGroupMemberType
36      * show user_type groupmember in type selection
37      */
38     showMemberOfType: false,
39     
40     /**
41      * @cfg {Boolean} showNamesOnly
42      * true to only show types and names in the list
43      */
44     showNamesOnly: false,
45
46     /**
47      * @cfg {Boolean} showAttendeeRole
48      * true to show roles in the list
49      */
50     showAttendeeRole: false,
51
52
53     /**
54      * @cfg {String} defaultAttendeeRole
55      * attendee role for new attendee row
56      */
57     defaultAttendeeRole: 'REQ',
58
59     /**
60      * The record currently being edited
61      * 
62      * @type Tine.Calendar.Model.Event
63      * @property record
64      */
65     record: null,
66     
67     /**
68      * id of current account
69      * 
70      * @type Number
71      * @property currentAccountId
72      */
73     currentAccountId: null,
74     
75     /**
76      * ctx menu
77      * 
78      * @type Ext.menu.Menu
79      * @property ctxMenu
80      */
81     ctxMenu: null,
82     
83     /**
84      * store to hold all attendee
85      * 
86      * @type Ext.data.Store
87      * @property attendeeStore
88      */
89     attendeeStore: null,
90     
91     /**
92      * grid panel phone hook for calling attendee
93      * 
94      * @type Tine.Phone.AddressbookGridPanelHook
95      */
96     phoneHook: null,
97     
98     stateful: true,
99     stateId: 'cal-attendeegridpanel',
100     
101     initComponent: function() {
102         this.app = this.app ? this.app : Tine.Tinebase.appMgr.get('Calendar');
103         
104         this.currentAccountId = Tine.Tinebase.registry.get('currentAccount').accountId;
105         
106         this.title = this.hasOwnProperty('title') ? this.title : this.app.i18n._('Attendee');
107         this.plugins = this.plugins || [];
108         if (! this.showNamesOnly) {
109             this.plugins.push(new Ext.ux.grid.GridViewMenuPlugin({}));
110         }
111         
112         this.store = new Ext.data.SimpleStore({
113             fields: Tine.Calendar.Model.Attender.getFieldDefinitions().concat('sort'),
114             sortInfo: {field: 'user_id', direction: 'ASC'},
115             sortData : function(f, direction){
116                 direction = direction || 'ASC';
117                 var st = this.fields.get(f).sortType;
118                 var fn = function(r1, r2){
119                     // make sure new-attendee line is on the bottom
120                     if (!r1.data.user_id) return direction == 'ASC';
121                     if (!r2.data.user_id) return direction != 'ASC';
122                     
123                     var v1 = st(r1.data[f]), v2 = st(r2.data[f]);
124                     return v1 > v2 ? 1 : (v1 < v2 ? -1 : 0);
125                 };
126                 this.data.sort(direction, fn);
127                 if(this.snapshot && this.snapshot != this.data){
128                     this.snapshot.sort(direction, fn);
129                 }
130             }
131         });
132         
133         this.on('beforeedit', this.onBeforeAttenderEdit, this);
134         this.on('afteredit', this.onAfterAttenderEdit, this);
135         this.addEvents('beforenewattendee');
136
137         this.initColumns();
138         
139         this.mon(Ext.getBody(), 'click', this.stopEditingIf, this);
140         
141         this.viewConfig = {
142             getRowClass: this.getRowClass
143         };
144         
145         Tine.Calendar.AttendeeGridPanel.superclass.initComponent.call(this);
146         
147         this.initPhoneGridPanelHook();
148     },
149     
150     initColumns: function() {
151         this.columns = [{
152             id: 'role',
153             dataIndex: 'role',
154             width: 70,
155             sortable: true,
156             hidden: !this.showAttendeeRole || this.showNamesOnly,
157             header: this.app.i18n._('Role'),
158             renderer: this.renderAttenderRole.createDelegate(this),
159             editor: {
160                 xtype: 'widget-keyfieldcombo',
161                 app:   'Calendar',
162                 keyFieldName: 'attendeeRoles',
163                 listeners: {
164                     scope: this,
165                     change: function (field, newValue) {
166                         this.setDefaultAttendeeRole(newValue);
167                     }
168                 }
169             }
170         }, {
171             id: 'displaycontainer_id',
172             dataIndex: 'displaycontainer_id',
173             width: 200,
174             sortable: false,
175             hidden: this.showNamesOnly || true,
176             header: i18n._hidden('Saved in'),
177             tooltip: this.app.i18n._('This is the calendar where the attender has saved this event in'),
178             renderer: this.renderAttenderDispContainer.createDelegate(this),
179             // disable for the moment, as updating calendarSelectWidget is not working in both directions
180             editor2: new Tine.widgets.container.SelectionComboBox({
181                 blurOnSelect: true,
182                 selectOnFocus: true,
183                 appName: 'Calendar',
184                 getValue: function() {
185                     if (this.selectedContainer) {
186                         // NOTE: the store checks if data changed. If we don't overwrite to string, 
187                         //  the check only sees [Object Object] wich of course never changes...
188                         var container_id = this.selectedContainer.id;
189                         this.selectedContainer.toString = function() {return container_id;};
190                     }
191                     return this.selectedContainer;
192                 },
193                 listeners: {
194                     scope: this,
195                     select: function(field, newValue) {
196                         // the field is already blured, due to the extra chooser window. We need to change the value per hand
197                         var selection = this.getSelectionModel().getSelectedCell();
198                         if (selection) {
199                             var row = selection[0];
200                             this.store.getAt(row).set('displaycontainer_id', newValue);
201                         }
202                     }
203                 }
204             })
205         }, {
206             id: 'user_type',
207             dataIndex: 'user_type',
208             width: 50,
209             sortable: true,
210             header: this.app.i18n._('Type'),
211             tooltip: this.app.i18n._('Click icon to change'),
212             renderer: this.renderAttenderType.createDelegate(this),
213             editor: new Ext.form.ComboBox({
214                 blurOnSelect  : true,
215                 expandOnFocus : true,
216                 listWidth     : 100,
217                 mode          : 'local',
218                 store         : [
219                     ['any',      '...'                     ],
220                     ['user',     this.app.i18n._('User')   ],
221                     ['group',    this.app.i18n._('Group')  ],
222                     ['resource', this.app.i18n._('Resource')]
223                 ].concat(this.showMemberOfType ? [['memberOf', this.app.i18n._('Member of group')  ]] : [])
224             })
225         }, {
226             id: 'user_id',
227             dataIndex: 'user_id',
228             width: 300,
229             sortable: true,
230             header: this.app.i18n._('Name'),
231             renderer: this.renderAttenderName.createDelegate(this),
232             editor: true
233         }, {
234             id: 'fbInfo',
235             dataIndex: 'fbInfo',
236             width: 20,
237             hidden: this.showNamesOnly,
238             header: '&nbsp',
239             tooltip: this.app.i18n._('Availability of Attendee'),
240             fixed: true,
241             sortable: false,
242             renderer: function(v, m, r) {
243                 // NOTE: already encoded
244                 return r.get('user_id') ? v : '';
245             }
246         }, {
247             id: 'status',
248             dataIndex: 'status',
249             width: 100,
250             sortable: true,
251             header: this.app.i18n._('Status'),
252             hidden: this.showNamesOnly,
253             renderer: this.renderAttenderStatus.createDelegate(this),
254             editor: {
255                 xtype: 'widget-keyfieldcombo',
256                 app:   'Calendar',
257                 keyFieldName: 'attendeeStatus'
258             }
259         }];
260     },
261
262     onEditComplete: function(ed, value, startValue) {
263         var _ = window.lodash,
264             attendeeData = _.get(ed, 'field.selectedRecord.data.user_id'),
265             type = _.get(ed, 'field.selectedRecord.data.user_type'),
266             fbInfo = _.get(ed, 'field.selectedRecord.data.fbInfo');
267
268         // attendeePickerCombo
269         if (attendeeData && type) {
270             if (this.showMemberOfType && 'group' == type) {
271                 var row = ed.row,
272                     col = ed.col,
273                     selectedRecord = _.get(ed, 'field.selectedRecord');
274
275                 Tine.widgets.dialog.MultiOptionsDialog.openWindow({
276                     title: this.app.i18n._('Whole Group or each Member of Group'),
277                     questionText: this.app.i18n._('Choose "Group" to filter for the whole group itself. Choose "Member of Group" to filter for each member of the group'),
278                     height: 170,
279                     scope: this,
280                     options: [
281                         {text: 'Group', name: 'sel_group'},
282                         {text: 'Member of Group', name: 'sel_memberOf'}
283                     ],
284
285                     handler: function(option) {
286                         this.startEditing(row, col);
287                         selectedRecord.set('user_type', option);
288                         selectedRecord.groupType = option;
289                         this.activeEditor.field.selectedRecord = selectedRecord;
290                         this.stopEditing();
291                     }
292                 });
293
294                 // abort normal flow
295                 value = startValue;
296             } else {
297                 value = attendeeData;
298                 ed.record.set('user_type', type.replace(/^sel_/, ''));
299                 ed.record.set('fbInfo', fbInfo);
300                 ed.record.commit();
301             }
302         }
303
304         Tine.Calendar.AttendeeGridPanel.superclass.onEditComplete.call(this, ed, value, startValue);
305     },
306
307     onAfterAttenderEdit: function(o) {
308         switch (o.field) {
309             case 'user_id' :
310                 // detect duplicate entry
311                 // TODO detect duplicate emails, too 
312                 var isDuplicate = false;
313                 this.store.each(function(attender) {
314                     if (o.record.getUserId() == attender.getUserId()
315                             && o.record.get('user_type') == attender.get('user_type')
316                             && o.record != attender) {
317                         attender.set('checked', true);
318                         var row = this.getView().getRow(this.store.indexOf(attender));
319                         Ext.fly(row).highlight();
320                         isDuplicate = true;
321                         return false;
322                     }
323                 }, this);
324                 
325                 if (isDuplicate) {
326                     o.record.reject();
327                     this.startEditing(o.row, o.column);
328                 } else if (o.value) {
329                     // set status authkey for contacts and recources so user can edit status directly
330                     // NOTE: we can't compute if the user has editgrant to the displaycontainer of an account here!
331                     //       WELL we could add the info to search attendee somehow
332                     if (   (o.record.get('user_type') == 'user' && ! o.value.account_id )
333                         || (o.record.get('user_type') == 'resource' && o.record.get('user_id') && o.record.get('user_id').container_id && o.record.get('user_id').container_id.account_grants && o.record.get('user_id').container_id.account_grants.editGrant)) {
334                         o.record.set('status_authkey', 1);
335                     }
336                     
337                     o.record.explicitlyAdded = true;
338                     o.record.set('checked', true);
339                     
340                     // Set status if the resource has a specific default status
341                     if (o.record.get('user_type') == 'resource' && o.record.get('user_id') && o.record.get('user_id').status) {
342                         o.record.set('status', o.record.get('user_id').status);
343                     }
344
345                     // resolve groupmembers
346                     if (o.record.get('user_type') == 'group' && !this.showMemberOfType) {
347                         this.resolveListMembers(o.record.get('user_id'));
348                     } else {
349                         this.addNewAttendeeRow();
350                         this.startEditing(o.row + 1, o.column);
351                     }
352                 }
353                 break;
354                 
355             case 'user_type' :
356                 this.startEditing(o.row, o.column +1);
357                 break;
358             
359             case 'container_id':
360                 // check if displaycontainer of owner got changed
361                 if (o.record == this.eventOriginator) {
362                     this.record.set('container_id', '');
363                     this.record.set('container_id', o.record.get('displaycontainer_id'));
364                 }
365                 break;
366         }
367         
368     },
369
370     resolveListMembers: function() {
371         var _ = window.lodash,
372             members = Tine.Calendar.Model.Attender.getAttendeeStore.getData(this.store),
373             fbInfoUpdate = [];
374
375         var mask = new Ext.LoadMask(this.getEl(), {msg: this.app.i18n._("Loading Groupmembers...")});
376         mask.show();
377
378         Tine.Calendar.resolveGroupMembers(members, function(attendeesData) {
379             var attendees = Tine.Calendar.Model.Attender.getAttendeeStore(attendeesData);
380
381             // remove not longer existing attendee
382             this.store.each(function(attendee) {
383                 if (! Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord(attendees, attendee)) {
384                     this.store.remove(attendee);
385                 }
386             });
387
388             // add new attendee
389             attendees.each(function(attendee) {
390                 if (! Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord(this.store, attendee)) {
391                     attendee.set('role', this.defaultAttendeeRole);
392                     this.fireEvent('beforenewattendee', this, attendee, this.record);
393                     this.store.add([attendee]);
394                     fbInfoUpdate.push(attendee.id);
395                 }
396             }, this);
397
398             this.updateFreeBusyInfo(fbInfoUpdate);
399             mask.hide();
400             this.addNewAttendeeRow();
401         }, this);
402     },
403
404     onBeforeAttenderEdit: function(o) {
405         if (o.field == 'status') {
406             // allow status setting if status authkey is present
407             o.cancel = ! o.record.get('status_authkey');
408             return;
409         }
410         
411         if (o.field == 'displaycontainer_id') {
412             if (! o.value || ! o.value.account_grants || ! o.value.account_grants.deleteGrant) {
413                 o.cancel = true;
414             }
415             return;
416         }
417         
418         // for all other fields user need editGrant
419         if (! this.record.get('editGrant')) {
420             o.cancel = true;
421             return;
422         }
423         
424         // don't allow to set anything besides quantity and role for already set attendee
425         if (o.record.get('user_id')) {
426             o.cancel = true;
427             if (o.field == 'quantity' && o.record.get('user_type') == 'resource') {
428                 o.cancel = false;
429             }
430             if (o.field == 'role') {
431                 o.cancel = false;
432             }
433             return;
434         }
435         
436         if (o.field == 'user_id') {
437             // switch editor
438             var colModel = o.grid.getColumnModel(),
439                 type = o.record.get('user_type');
440
441             type = type == 'memberOf' ? 'group' : type;
442
443             colModel.config[o.column].setEditor(new Tine.Calendar.AttendeePickerCombo({
444                 minListWidth: 370,
445                 blurOnSelect: true,
446                 eventRecord: this.record,
447                 additionalFilters: type != 'any' ? [{field: 'type', operator: 'oneof', value: [type]}] : null
448             }));
449             
450             colModel.config[o.column].editor.selectedRecord = null;
451         }
452     },
453     
454     /**
455      * give new attendee an extra cls
456      */
457     getRowClass: function(record, index) {
458         if (! record.get('user_id')) {
459             return 'x-cal-add-attendee-row';
460         }
461     },
462     
463     /**
464      * stop editing if user clicks else where
465      * 
466      * FIXME this breaks the paging in search combos, maybe we should check if search combo paging buttons are clicked, too
467      */
468     stopEditingIf: function(e) {
469         if (! e.within(this.getGridEl())) {
470             //this.stopEditing();
471         }
472     },
473     
474     // NOTE: Ext docu seems to be wrong on arguments here
475     onContextMenu: function(e, target) {
476         e.preventDefault();
477
478         var me = this,
479             row = this.getView().findRowIndex(target),
480             attender = this.store.getAt(row),
481             type = attender.get('user_type');
482
483         if (attender && ! this.disabled) {
484             // don't delete 'add' row
485             var attender = this.store.getAt(row);
486             if (! attender.get('user_id')) {
487                 return;
488             }
489             
490             // select name cell
491             if (Ext.isFunction(this.getSelectionModel().select)) {
492                 this.getSelectionModel().select(row, 3);
493             } else {
494                 // west panel attendee filter grid
495                 this.getSelectionModel().selectRow(row);
496             }
497             
498             Tine.log.debug('onContextMenu - attender:');
499             Tine.log.debug(attender);
500             
501             var items = [{
502                 text: this.app.i18n._('Remove Attender'),
503                 iconCls: 'action_delete',
504                 scope: this,
505                 disabled: ! this.record.get('editGrant') || type == 'groupmember',
506                 handler: function() {
507                     this.store.remove(attender);
508                     if (type == 'group' && !this.showMemberOfType) {
509                         this.resolveListMembers()
510                     }
511                 }
512             }, '-'];
513
514             var felamimailApp = Tine.Tinebase.appMgr.get('Felamimail');
515             if (felamimailApp && attender.get('user_type') == 'user') {
516                 Tine.log.debug('Adding email compose hook for attender');
517                 items = items.concat(new Ext.Action({
518                     text: felamimailApp.i18n._('Compose email'),
519                     iconCls: felamimailApp.getIconCls(),
520                     disabled: false,
521                     scope: this,
522                     handler: function() {
523                         var email = Tine.Felamimail.getEmailStringFromContact(new Tine.Addressbook.Model.Contact(attender.get('user_id')));
524                         var record = new Tine.Felamimail.Model.Message({
525                             subject: this.record.get('summary') + ' - ' + Tine.Calendar.Model.Event.datetimeRenderer(this.record.get('dtstart')),
526                             to: [email]
527                         }, 0);
528                         var popupWindow = Tine.Felamimail.MessageEditDialog.openWindow({
529                             record: record
530                         });
531                     }
532                 }));
533             }
534
535             if (attender.get('user_type') == 'resource') {
536                 Tine.log.debug('Adding resource hook for attender');
537                 var resourceId = attender.get('user_id').id,
538                     resource = new Tine.Calendar.Model.Resource(attender.get('user_id'), resourceId);
539
540                 items = items.concat(new Ext.Action({
541                     text: this.app.i18n._('Edit Resource'),
542                     iconCls: 'cal-resource',
543                     scope: this,
544                     handler: function() {
545                         Tine.Calendar.ResourceEditDialog.openWindow({record: resource});
546                     }
547                 }));
548
549                 var exportAction = Tine.widgets.exportAction.getExportButton(
550                     Tine.Calendar.Model.Resource, {
551                         getExportOptions: function() {
552                             var options = {
553                                 recordData: attender.get('user_id')
554                             };
555
556                             // do we have a 'real' event?
557                             if (me.record.data.dtstart) {
558                                 options.additionalRecords = {
559                                     Event: {
560                                         model: 'Calendar_Model_Event',
561                                             recordData: me.record.data
562                                     }
563                                 };
564                             }
565                             return options;
566                         }
567                     },
568                     Tine.widgets.exportAction.SCOPE_SINGLE
569                 );
570
571                 var actionUpdater = new Tine.widgets.ActionUpdater({
572                     containerProperty: Tine.Calendar.Model.Resource.getMeta('containerProperty'),
573                     evalGrants: true
574                 });
575                 actionUpdater.addAction(exportAction);
576                 actionUpdater.updateActions([resource]);
577
578                 items = items.concat(exportAction);
579             }
580
581             var plugins = [];
582             if (this.phoneHook && attender.get('user_type') == 'user') {
583                 var contact = new Tine.Addressbook.Model.Contact(attender.get('user_id'));
584                 this.phoneHook.setContactAndUpdateAction(contact);
585                 plugins = [{
586                     ptype: 'ux.itemregistry',
587                     key:   'Attendee-GridPanel-ContextMenu'
588                 }, {
589                     ptype: 'ux.itemregistry',
590                     key:   'Tinebase-MainContextMenu'
591                 }];
592             }
593             
594             Tine.log.debug(items);
595             
596             this.ctxMenu = new Ext.menu.Menu({
597                 items: items,
598                 // add phone call action via item registry
599                 plugins: plugins,
600                 listeners: {
601                     scope: this,
602                     hide: function() {
603                         this.getSelectionModel().clearSelections();
604                     }
605                 }
606             });
607             this.ctxMenu.showAt(e.getXY());
608         }
609     },
610     
611     /**
612      * init phone grid panel hook if Phone app is available
613      */
614     initPhoneGridPanelHook: function() {
615         var phoneApp = Tine.Tinebase.appMgr.get('Phone');
616         if (phoneApp) {
617             Tine.log.debug('Adding Phone call hook');
618             this.phoneHook = new Tine.Phone.AddressbookGridPanelHook({
619                 app: phoneApp,
620                 keyPrefix: 'Attendee',
621                 useActionUpdater: false
622             });
623         }
624     },
625     
626     /**
627      * loads this panel with data from given record
628      * called by edit dialog when record is loaded
629      * 
630      * @param {Tine.Calendar.Model.Event} record
631      * @param Array addAttendee
632      */
633     onRecordLoad: function(record) {
634         this.record = record;
635         this.store.removeAll();
636         var attendee = record.get('attendee');
637         Ext.each(attendee, function(attender) {
638
639             var record = new Tine.Calendar.Model.Attender(attender, attender.id);
640             this.store.addSorted(record);
641             
642             if (attender.displaycontainer_id  && this.record.get('container_id') && attender.displaycontainer_id.id == this.record.get('container_id').id) {
643                 this.eventOriginator = record;
644             }
645         }, this);
646
647         this.updateFreeBusyInfo();
648
649         if (record.get('editGrant')) {
650             this.addNewAttendeeRow();
651         }
652     },
653
654     updateFreeBusyInfo: function(force) {
655         var schedulingInfo = Ext.copyTo({}, this.record.data, 'id,dtstart,dtend,originator_tz,rrule,rrule_constraints,rrule_until,is_all_day_event,uid'),
656             encodedSchedulingInfo = Ext.encode(schedulingInfo);
657
658         if (encodedSchedulingInfo == this.encodedSchedulingInfo && !force) return;
659
660         // @TODO have load spinner?
661         this.encodedSchedulingInfo = encodedSchedulingInfo;
662
663         // clear state
664         this.store.each(function(attendee) {
665             if (Ext.isArray(force) && force.indexOf(attendee.id) < 0) return;
666
667             attendee.set('fbInfo', '...');
668             attendee.commit();
669         }, this);
670
671         Tine.Calendar.getFreeBusyInfo(
672             Tine.Calendar.Model.Attender.getAttendeeStore.getData(this.store),
673             schedulingInfo,
674             [this.record.get('uid')],
675             function(freeBusyData) {
676                 // outdated data
677                 if (encodedSchedulingInfo != this.encodedSchedulingInfo) return;
678
679                 var fbInfo = new Tine.Calendar.FreeBusyInfo(freeBusyData);
680
681                 this.store.each(function(attendee) {
682                     attendee.set('fbInfo', fbInfo.getStateOfAttendee(attendee, this.record));
683                     attendee.commit();
684                 }, this);
685
686         }, this);
687     },
688
689     // Add new attendee
690     addNewAttendeeRow: function() {
691         this.newAttendee = new Tine.Calendar.Model.Attender(Tine.Calendar.Model.Attender.getDefaultData(), 'new-' + Ext.id());
692         this.newAttendee.set('role', this.defaultAttendeeRole);
693         this.fireEvent('beforenewattendee', this, this.newAttendee, this.record);
694         this.store.add([this.newAttendee]);
695     },
696
697     setDefaultAttendeeRole:function(role) {
698         this.defaultAttendeeRole = role;
699         this.newAttendee.set('role', role);
700     },
701
702     /**
703      * Updates given record with data from this panel
704      * called by edit dialog to get data
705      * 
706      * @param {Tine.Calendar.Model.Event} record
707      */
708     onRecordUpdate: function(record) {
709         this.stopEditing(false);
710
711         this.updateFreeBusyInfo();
712         Tine.Calendar.Model.Attender.getAttendeeStore.getData(this.store, record);
713     },
714     
715     onKeyDown: function(e) {
716         switch(e.getKey()) {
717             
718             case e.DELETE: 
719                 if (this.record.get('editGrant')) {
720                     var selection = this.getSelectionModel().getSelectedCell();
721                     
722                     if (selection) {
723                         var row = selection[0];
724                         
725                         // don't delete 'add' row
726                         var attender = this.store.getAt(row);
727                         if (! attender.get('user_id')) {
728                             return;
729                         }
730
731                         this.store.removeAt(row);
732                     }
733                 }
734                 break;
735         }
736     },
737     
738     renderAttenderName: function(name, metaData, record) {
739         if (name) {
740             var type = record ? record.get('user_type') : 'user',
741                 fn = this['renderAttender' + Ext.util.Format.capitalize(type) + 'Name'];
742
743             return fn ? fn.apply(this, arguments): '';
744         }
745         
746         // add new user:
747         if (arguments[1]) {
748             arguments[1].css = 'x-form-empty-field';
749             return this.app.i18n._(this.addNewAttendeeText);
750         }
751     },
752
753     /**
754      * render attender user name
755      *
756      * @param name
757      * @returns {*}
758      */
759     renderAttenderUserName: function(name) {
760         name = name || "";
761         var result = "",
762             email = "";
763
764         if (typeof name.get == 'function' && name.get('n_fileas')) {
765             result = name.get('n_fileas');
766         } else if (name.n_fileas) {
767             result = name.n_fileas;
768         } else if (name.accountDisplayName) {
769             result = name.accountDisplayName;
770         } else if (Ext.isString(name) && ! name.match('^[0-9a-f-]{40,}$') && ! parseInt(name, 10)) {
771             // how to detect hash/string ids
772             result = name;
773         }
774
775         // add email address if available
776         // need to create a "dummy" app to call featureEnabled()
777         // TODO: should be improved
778         var tinebaseApp = new Tine.Tinebase.Application({
779             appName: 'Tinebase'
780         });
781         if (tinebaseApp.featureEnabled('featureShowAccountEmail')) {
782             if (typeof name.getPreferredEmail == 'function') {
783                 email = name.getPreferredEmail();
784             } else if (name.email) {
785                 email = name.email;
786             } else if (name.accountEmailAddress) {
787                 email = name.accountEmailAddress;
788             }
789             if (email !== '') {
790                 result += ' (' + email + ')';
791             }
792         }
793
794         if (result === '') {
795             result = Tine.Tinebase.appMgr.get('Calendar').i18n._('No Information')
796         } else {
797             result = Ext.util.Format.htmlEncode(result)
798         }
799
800         // NOTE: this fn gets also called from other scopes
801         return result;
802     },
803     
804     renderAttenderGroupmemberName: function(name) {
805         var name = Tine.Calendar.AttendeeGridPanel.prototype.renderAttenderUserName.apply(this, arguments);
806         return name + ' ' + Tine.Tinebase.appMgr.get('Calendar').i18n._('(as a group member)');
807     },
808     
809     renderAttenderGroupName: function(name) {
810         if (typeof name.getTitle == 'function') {
811             return Ext.util.Format.htmlEncode(name.getTitle());
812         }
813         if (name.name) {
814             return Ext.util.Format.htmlEncode(name.name);
815         }
816         if (Ext.isString(name)) {
817             return Ext.util.Format.htmlEncode(name);
818         }
819         return Tine.Tinebase.appMgr.get('Calendar').i18n._('No Information');
820     },
821     
822     renderAttenderMemberofName: function(name) {
823         return Tine.Calendar.AttendeeGridPanel.prototype.renderAttenderGroupName.apply(this, arguments);
824     },
825     
826     renderAttenderResourceName: function(name) {
827         if (typeof name.getTitle == 'function') {
828             return Ext.util.Format.htmlEncode(name.getTitle());
829         }
830         if (name.name) {
831             return Ext.util.Format.htmlEncode(name.name);
832         }
833         if (Ext.isString(name)) {
834             return Ext.util.Format.htmlEncode(name);
835         }
836         return Tine.Tinebase.appMgr.get('Calendar').i18n._('No Information');
837     },
838     
839     
840     renderAttenderDispContainer: function(displaycontainer_id, metadata, attender) {
841         metadata.attr = 'style = "overflow: none;"';
842         
843         if (displaycontainer_id) {
844             if (displaycontainer_id.name) {
845                 return Ext.util.Format.htmlEncode(displaycontainer_id.name).replace(/ /g,"&nbsp;");
846             } else {
847                 metadata.css = 'x-form-empty-field';
848                 return this.app.i18n._('No Information');
849             }
850         }
851     },
852     
853     renderAttenderQuantity: function(quantity, metadata, attender) {
854         return quantity > 1 ? quantity : '';
855     },
856     
857     renderAttenderRole: function(role, metadata, attender) {
858         var i18n = Tine.Tinebase.appMgr.get('Calendar').i18n,
859             renderer = Tine.widgets.grid.RendererManager.get('Calendar', 'Attender', 'role', Tine.widgets.grid.RendererManager.CATEGORY_GRIDPANEL);
860
861         if (this.record && this.record.get('editGrant')) {
862             metadata.attr = 'style = "cursor:pointer;"';
863         } else {
864             metadata.css = 'x-form-empty-field';
865         }
866         
867         return renderer(role);
868     },
869     
870     renderAttenderStatus: function(status, metadata, attender) {
871         var i18n = Tine.Tinebase.appMgr.get('Calendar').i18n,
872             renderer = Tine.widgets.grid.RendererManager.get('Calendar', 'Attender', 'status', Tine.widgets.grid.RendererManager.CATEGORY_GRIDPANEL);
873         
874         if (! attender.get('user_id')) {
875             return '';
876         }
877         
878         if (attender.get('status_authkey')) {
879             metadata.attr = 'style = "cursor:pointer;"';
880         } else {
881             metadata.css = 'x-form-empty-field';
882         }
883         
884         return renderer(status);
885     },
886     
887     renderAttenderType: function(type, metadata, attender) {
888         metadata.css = 'tine-grid-cell-no-dirty';
889         var cssClass = '',
890             qtipText =  '',
891             userId = attender.get('user_id'),
892             hasAccount = userId && ((userId.get && userId.get('account_id')) || userId.account_id);
893             
894         switch (type) {
895             case 'user':
896                 cssClass = hasAccount || ! userId ? 'renderer_typeAccountIcon' : 'renderer_typeContactIcon';
897                 qtipText = hasAccount || ! userId ? '' : Tine.Tinebase.appMgr.get('Calendar').i18n._('External Attendee');
898                 break;
899             case 'group':
900                 cssClass = 'renderer_accountGroupIcon';
901                 break;
902             default:
903                 cssClass = 'cal-attendee-type-' + type;
904                 break;
905         }
906         
907         var qtip = qtipText ? 'ext:qtip="' + Tine.Tinebase.common.doubleEncode(qtipText) + '" ': '';
908         
909         var result = '<div ' + qtip + 'style="background-position:0px;" class="' + cssClass + '">&#160</div>';
910         
911         if (! attender.get('user_id')) {
912             result = Tine.Tinebase.common.cellEditorHintRenderer(result);
913         }
914         
915         return result;
916     },
917     
918     /**
919      * disable contents not panel
920      */
921     setDisabled: function(v) {
922         if (v) {
923             // remove "add new attender" row
924             this.store.filterBy(function(r) {return ! (r.id && r.id.match(/^new-/))});
925         } else {
926             this.store.clearFilter();
927         }
928     }
929 });