resolve groupmembers for new events with preset attendee
[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         if (this.showMemberOfType) return;
372
373         var _ = window.lodash,
374             members = Tine.Calendar.Model.Attender.getAttendeeStore.getData(this.store),
375             fbInfoUpdate = [];
376
377         var mask = new Ext.LoadMask(this.getEl(), {msg: this.app.i18n._("Loading Groupmembers...")});
378         mask.show();
379
380         Tine.Calendar.resolveGroupMembers(members, function(attendeesData) {
381             var attendees = Tine.Calendar.Model.Attender.getAttendeeStore(attendeesData);
382
383             // remove not longer existing attendee
384             this.store.each(function(attendee) {
385                 if (! Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord(attendees, attendee)) {
386                     this.store.remove(attendee);
387                 }
388             });
389
390             // add new attendee
391             attendees.each(function(attendee) {
392                 if (! Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord(this.store, attendee)) {
393                     attendee.set('role', this.defaultAttendeeRole);
394                     this.fireEvent('beforenewattendee', this, attendee, this.record);
395                     this.store.add([attendee]);
396                     fbInfoUpdate.push(attendee.id);
397                 }
398             }, this);
399
400             this.updateFreeBusyInfo(fbInfoUpdate);
401             mask.hide();
402             this.addNewAttendeeRow();
403         }, this);
404     },
405
406     onBeforeAttenderEdit: function(o) {
407         if (o.field == 'status') {
408             // allow status setting if status authkey is present
409             o.cancel = ! o.record.get('status_authkey');
410             return;
411         }
412         
413         if (o.field == 'displaycontainer_id') {
414             if (! o.value || ! o.value.account_grants || ! o.value.account_grants.deleteGrant) {
415                 o.cancel = true;
416             }
417             return;
418         }
419         
420         // for all other fields user need editGrant
421         if (! this.record.get('editGrant')) {
422             o.cancel = true;
423             return;
424         }
425         
426         // don't allow to set anything besides quantity and role for already set attendee
427         if (o.record.get('user_id')) {
428             o.cancel = true;
429             if (o.field == 'quantity' && o.record.get('user_type') == 'resource') {
430                 o.cancel = false;
431             }
432             if (o.field == 'role') {
433                 o.cancel = false;
434             }
435             return;
436         }
437         
438         if (o.field == 'user_id') {
439             // switch editor
440             var colModel = o.grid.getColumnModel(),
441                 type = o.record.get('user_type');
442
443             type = type == 'memberOf' ? 'group' : type;
444
445             colModel.config[o.column].setEditor(new Tine.Calendar.AttendeePickerCombo({
446                 minListWidth: 370,
447                 blurOnSelect: true,
448                 eventRecord: this.record,
449                 additionalFilters: type != 'any' ? [{field: 'type', operator: 'oneof', value: [type]}] : null
450             }));
451             
452             colModel.config[o.column].editor.selectedRecord = null;
453         }
454     },
455     
456     /**
457      * give new attendee an extra cls
458      */
459     getRowClass: function(record, index) {
460         if (! record.get('user_id')) {
461             return 'x-cal-add-attendee-row';
462         }
463     },
464     
465     /**
466      * stop editing if user clicks else where
467      * 
468      * FIXME this breaks the paging in search combos, maybe we should check if search combo paging buttons are clicked, too
469      */
470     stopEditingIf: function(e) {
471         if (! e.within(this.getGridEl())) {
472             //this.stopEditing();
473         }
474     },
475     
476     // NOTE: Ext docu seems to be wrong on arguments here
477     onContextMenu: function(e, target) {
478         e.preventDefault();
479
480         var me = this,
481             row = this.getView().findRowIndex(target),
482             attender = this.store.getAt(row),
483             type = attender.get('user_type');
484
485         if (attender && ! this.disabled) {
486             // don't delete 'add' row
487             var attender = this.store.getAt(row);
488             if (! attender.get('user_id')) {
489                 return;
490             }
491             
492             // select name cell
493             if (Ext.isFunction(this.getSelectionModel().select)) {
494                 this.getSelectionModel().select(row, 3);
495             } else {
496                 // west panel attendee filter grid
497                 this.getSelectionModel().selectRow(row);
498             }
499             
500             Tine.log.debug('onContextMenu - attender:');
501             Tine.log.debug(attender);
502             
503             var items = [{
504                 text: this.app.i18n._('Remove Attender'),
505                 iconCls: 'action_delete',
506                 scope: this,
507                 disabled: ! this.record.get('editGrant') || type == 'groupmember',
508                 handler: function() {
509                     this.store.remove(attender);
510                     if (type == 'group' && !this.showMemberOfType) {
511                         this.resolveListMembers()
512                     }
513                 }
514             }, '-'];
515
516             var felamimailApp = Tine.Tinebase.appMgr.get('Felamimail');
517             if (felamimailApp && attender.get('user_type') == 'user') {
518                 Tine.log.debug('Adding email compose hook for attender');
519                 items = items.concat(new Ext.Action({
520                     text: felamimailApp.i18n._('Compose email'),
521                     iconCls: felamimailApp.getIconCls(),
522                     disabled: false,
523                     scope: this,
524                     handler: function() {
525                         var email = Tine.Felamimail.getEmailStringFromContact(new Tine.Addressbook.Model.Contact(attender.get('user_id')));
526                         var record = new Tine.Felamimail.Model.Message({
527                             subject: this.record.get('summary') + ' - ' + Tine.Calendar.Model.Event.datetimeRenderer(this.record.get('dtstart')),
528                             to: [email]
529                         }, 0);
530                         var popupWindow = Tine.Felamimail.MessageEditDialog.openWindow({
531                             record: record
532                         });
533                     }
534                 }));
535             }
536
537             if (attender.get('user_type') == 'resource') {
538                 Tine.log.debug('Adding resource hook for attender');
539                 var resourceId = attender.get('user_id').id,
540                     resource = new Tine.Calendar.Model.Resource(attender.get('user_id'), resourceId);
541
542                 items = items.concat(new Ext.Action({
543                     text: this.app.i18n._('Edit Resource'),
544                     iconCls: 'cal-resource',
545                     scope: this,
546                     handler: function() {
547                         Tine.Calendar.ResourceEditDialog.openWindow({record: resource});
548                     }
549                 }));
550
551                 var exportAction = Tine.widgets.exportAction.getExportButton(
552                     Tine.Calendar.Model.Resource, {
553                         getExportOptions: function() {
554                             var options = {
555                                 recordData: attender.get('user_id')
556                             };
557
558                             // do we have a 'real' event?
559                             if (me.record.data.dtstart) {
560                                 options.additionalRecords = {
561                                     Event: {
562                                         model: 'Calendar_Model_Event',
563                                             recordData: me.record.data
564                                     }
565                                 };
566                             }
567                             return options;
568                         }
569                     },
570                     Tine.widgets.exportAction.SCOPE_SINGLE
571                 );
572
573                 var actionUpdater = new Tine.widgets.ActionUpdater({
574                     containerProperty: Tine.Calendar.Model.Resource.getMeta('containerProperty'),
575                     evalGrants: true
576                 });
577                 actionUpdater.addAction(exportAction);
578                 actionUpdater.updateActions([resource]);
579
580                 items = items.concat(exportAction);
581             }
582
583             var plugins = [];
584             if (this.phoneHook && attender.get('user_type') == 'user') {
585                 var contact = new Tine.Addressbook.Model.Contact(attender.get('user_id'));
586                 this.phoneHook.setContactAndUpdateAction(contact);
587                 plugins = [{
588                     ptype: 'ux.itemregistry',
589                     key:   'Attendee-GridPanel-ContextMenu'
590                 }, {
591                     ptype: 'ux.itemregistry',
592                     key:   'Tinebase-MainContextMenu'
593                 }];
594             }
595             
596             Tine.log.debug(items);
597             
598             this.ctxMenu = new Ext.menu.Menu({
599                 items: items,
600                 // add phone call action via item registry
601                 plugins: plugins,
602                 listeners: {
603                     scope: this,
604                     hide: function() {
605                         this.getSelectionModel().clearSelections();
606                     }
607                 }
608             });
609             this.ctxMenu.showAt(e.getXY());
610         }
611     },
612     
613     /**
614      * init phone grid panel hook if Phone app is available
615      */
616     initPhoneGridPanelHook: function() {
617         var phoneApp = Tine.Tinebase.appMgr.get('Phone');
618         if (phoneApp) {
619             Tine.log.debug('Adding Phone call hook');
620             this.phoneHook = new Tine.Phone.AddressbookGridPanelHook({
621                 app: phoneApp,
622                 keyPrefix: 'Attendee',
623                 useActionUpdater: false
624             });
625         }
626     },
627     
628     /**
629      * loads this panel with data from given record
630      * called by edit dialog when record is loaded
631      * 
632      * @param {Tine.Calendar.Model.Event} record
633      * @param Array addAttendee
634      */
635     onRecordLoad: function(record) {
636         this.record = record;
637         this.store.removeAll();
638         var attendee = record.get('attendee'),
639             resolveListMembers = false;
640
641         Ext.each(attendee, function(attender) {
642             var record = new Tine.Calendar.Model.Attender(attender, attender.id);
643             this.store.addSorted(record);
644             
645             if (attender.displaycontainer_id  && this.record.get('container_id') && attender.displaycontainer_id.id == this.record.get('container_id').id) {
646                 this.eventOriginator = record;
647             }
648
649             if (String(record.get('user_type')).match(/^group/)) {
650                 resolveListMembers = true;
651             }
652         }, this);
653
654         this.updateFreeBusyInfo();
655
656         if (resolveListMembers) {
657             this.resolveListMembers();
658         }
659
660         else if (record.get('editGrant')) {
661             this.addNewAttendeeRow();
662         }
663     },
664
665     updateFreeBusyInfo: function(force) {
666         if (this.showMemberOfType) return;
667
668         var schedulingInfo = Ext.copyTo({}, this.record.data, 'id,dtstart,dtend,originator_tz,rrule,rrule_constraints,rrule_until,is_all_day_event,uid'),
669             encodedSchedulingInfo = Ext.encode(schedulingInfo);
670
671         if (encodedSchedulingInfo == this.encodedSchedulingInfo && !force) return;
672
673         // @TODO have load spinner?
674         this.encodedSchedulingInfo = encodedSchedulingInfo;
675
676         // clear state
677         this.store.each(function(attendee) {
678             if (Ext.isArray(force) && force.indexOf(attendee.id) < 0) return;
679
680             attendee.set('fbInfo', '...');
681             attendee.commit();
682         }, this);
683
684         Tine.Calendar.getFreeBusyInfo(
685             Tine.Calendar.Model.Attender.getAttendeeStore.getData(this.store),
686             schedulingInfo,
687             [this.record.get('uid')],
688             function(freeBusyData) {
689                 // outdated data
690                 if (encodedSchedulingInfo != this.encodedSchedulingInfo) return;
691
692                 var fbInfo = new Tine.Calendar.FreeBusyInfo(freeBusyData);
693
694                 this.store.each(function(attendee) {
695                     attendee.set('fbInfo', fbInfo.getStateOfAttendee(attendee, this.record));
696                     attendee.commit();
697                 }, this);
698
699         }, this);
700     },
701
702     // Add new attendee
703     addNewAttendeeRow: function() {
704         this.newAttendee = new Tine.Calendar.Model.Attender(Tine.Calendar.Model.Attender.getDefaultData(), 'new-' + Ext.id());
705         this.newAttendee.set('role', this.defaultAttendeeRole);
706         this.fireEvent('beforenewattendee', this, this.newAttendee, this.record);
707         this.store.add([this.newAttendee]);
708     },
709
710     setDefaultAttendeeRole:function(role) {
711         this.defaultAttendeeRole = role;
712         this.newAttendee.set('role', role);
713     },
714
715     /**
716      * Updates given record with data from this panel
717      * called by edit dialog to get data
718      * 
719      * @param {Tine.Calendar.Model.Event} record
720      */
721     onRecordUpdate: function(record) {
722         this.stopEditing(false);
723
724         this.updateFreeBusyInfo();
725         Tine.Calendar.Model.Attender.getAttendeeStore.getData(this.store, record);
726     },
727     
728     onKeyDown: function(e) {
729         switch(e.getKey()) {
730             
731             case e.DELETE: 
732                 if (this.record.get('editGrant')) {
733                     var selection = this.getSelectionModel().getSelectedCell();
734                     
735                     if (selection) {
736                         var row = selection[0];
737                         
738                         // don't delete 'add' row
739                         var attender = this.store.getAt(row);
740                         if (! attender.get('user_id')) {
741                             return;
742                         }
743
744                         this.store.removeAt(row);
745                     }
746                 }
747                 break;
748         }
749     },
750     
751     renderAttenderName: function(name, metaData, record) {
752         if (name) {
753             var type = record ? record.get('user_type') : 'user',
754                 fn = this['renderAttender' + Ext.util.Format.capitalize(type) + 'Name'];
755
756             return fn ? fn.apply(this, arguments): '';
757         }
758         
759         // add new user:
760         if (arguments[1]) {
761             arguments[1].css = 'x-form-empty-field';
762             return this.app.i18n._(this.addNewAttendeeText);
763         }
764     },
765
766     /**
767      * render attender user name
768      *
769      * @param name
770      * @returns {*}
771      */
772     renderAttenderUserName: function(name) {
773         name = name || "";
774         var result = "",
775             email = "";
776
777         if (typeof name.get == 'function' && name.get('n_fileas')) {
778             result = name.get('n_fileas');
779         } else if (name.n_fileas) {
780             result = name.n_fileas;
781         } else if (name.accountDisplayName) {
782             result = name.accountDisplayName;
783         } else if (Ext.isString(name) && ! name.match('^[0-9a-f-]{40,}$') && ! parseInt(name, 10)) {
784             // how to detect hash/string ids
785             result = name;
786         }
787
788         // add email address if available
789         // need to create a "dummy" app to call featureEnabled()
790         // TODO: should be improved
791         var tinebaseApp = new Tine.Tinebase.Application({
792             appName: 'Tinebase'
793         });
794         if (tinebaseApp.featureEnabled('featureShowAccountEmail')) {
795             if (typeof name.getPreferredEmail == 'function') {
796                 email = name.getPreferredEmail();
797             } else if (name.email) {
798                 email = name.email;
799             } else if (name.accountEmailAddress) {
800                 email = name.accountEmailAddress;
801             }
802             if (email !== '') {
803                 result += ' (' + email + ')';
804             }
805         }
806
807         if (result === '') {
808             result = Tine.Tinebase.appMgr.get('Calendar').i18n._('No Information')
809         } else {
810             result = Ext.util.Format.htmlEncode(result)
811         }
812
813         // NOTE: this fn gets also called from other scopes
814         return result;
815     },
816     
817     renderAttenderGroupmemberName: function(name) {
818         var name = Tine.Calendar.AttendeeGridPanel.prototype.renderAttenderUserName.apply(this, arguments);
819         return name + ' ' + Tine.Tinebase.appMgr.get('Calendar').i18n._('(as a group member)');
820     },
821     
822     renderAttenderGroupName: function(name) {
823         if (typeof name.getTitle == 'function') {
824             return Ext.util.Format.htmlEncode(name.getTitle());
825         }
826         if (name.name) {
827             return Ext.util.Format.htmlEncode(name.name);
828         }
829         if (Ext.isString(name)) {
830             return Ext.util.Format.htmlEncode(name);
831         }
832         return Tine.Tinebase.appMgr.get('Calendar').i18n._('No Information');
833     },
834     
835     renderAttenderMemberofName: function(name) {
836         return Tine.Calendar.AttendeeGridPanel.prototype.renderAttenderGroupName.apply(this, arguments);
837     },
838     
839     renderAttenderResourceName: function(name) {
840         if (typeof name.getTitle == 'function') {
841             return Ext.util.Format.htmlEncode(name.getTitle());
842         }
843         if (name.name) {
844             return Ext.util.Format.htmlEncode(name.name);
845         }
846         if (Ext.isString(name)) {
847             return Ext.util.Format.htmlEncode(name);
848         }
849         return Tine.Tinebase.appMgr.get('Calendar').i18n._('No Information');
850     },
851     
852     
853     renderAttenderDispContainer: function(displaycontainer_id, metadata, attender) {
854         metadata.attr = 'style = "overflow: none;"';
855         
856         if (displaycontainer_id) {
857             if (displaycontainer_id.name) {
858                 return Ext.util.Format.htmlEncode(displaycontainer_id.name).replace(/ /g,"&nbsp;");
859             } else {
860                 metadata.css = 'x-form-empty-field';
861                 return this.app.i18n._('No Information');
862             }
863         }
864     },
865     
866     renderAttenderQuantity: function(quantity, metadata, attender) {
867         return quantity > 1 ? quantity : '';
868     },
869     
870     renderAttenderRole: function(role, metadata, attender) {
871         var i18n = Tine.Tinebase.appMgr.get('Calendar').i18n,
872             renderer = Tine.widgets.grid.RendererManager.get('Calendar', 'Attender', 'role', Tine.widgets.grid.RendererManager.CATEGORY_GRIDPANEL);
873
874         if (this.record && this.record.get('editGrant')) {
875             metadata.attr = 'style = "cursor:pointer;"';
876         } else {
877             metadata.css = 'x-form-empty-field';
878         }
879         
880         return renderer(role);
881     },
882     
883     renderAttenderStatus: function(status, metadata, attender) {
884         var i18n = Tine.Tinebase.appMgr.get('Calendar').i18n,
885             renderer = Tine.widgets.grid.RendererManager.get('Calendar', 'Attender', 'status', Tine.widgets.grid.RendererManager.CATEGORY_GRIDPANEL);
886         
887         if (! attender.get('user_id')) {
888             return '';
889         }
890         
891         if (attender.get('status_authkey')) {
892             metadata.attr = 'style = "cursor:pointer;"';
893         } else {
894             metadata.css = 'x-form-empty-field';
895         }
896         
897         return renderer(status);
898     },
899     
900     renderAttenderType: function(type, metadata, attender) {
901         metadata.css = 'tine-grid-cell-no-dirty';
902         var cssClass = '',
903             qtipText =  '',
904             userId = attender.get('user_id'),
905             hasAccount = userId && ((userId.get && userId.get('account_id')) || userId.account_id);
906             
907         switch (type) {
908             case 'user':
909                 cssClass = hasAccount || ! userId ? 'renderer_typeAccountIcon' : 'renderer_typeContactIcon';
910                 qtipText = hasAccount || ! userId ? '' : Tine.Tinebase.appMgr.get('Calendar').i18n._('External Attendee');
911                 break;
912             case 'group':
913                 cssClass = 'renderer_accountGroupIcon';
914                 break;
915             default:
916                 cssClass = 'cal-attendee-type-' + type;
917                 break;
918         }
919         
920         var qtip = qtipText ? 'ext:qtip="' + Tine.Tinebase.common.doubleEncode(qtipText) + '" ': '';
921         
922         var result = '<div ' + qtip + 'style="background-position:0px;" class="' + cssClass + '">&#160</div>';
923         
924         if (! attender.get('user_id')) {
925             result = Tine.Tinebase.common.cellEditorHintRenderer(result);
926         }
927         
928         return result;
929     },
930     
931     /**
932      * disable contents not panel
933      */
934     setDisabled: function(v) {
935         if (v) {
936             // remove "add new attender" row
937             this.store.filterBy(function(r) {return ! (r.id && r.id.match(/^new-/))});
938         } else {
939             this.store.clearFilter();
940         }
941     }
942 });