0009542: load event relations on demand
[tine20] / tine20 / Calendar / js / EventEditDialog.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-2012 Metaways Infosystems GmbH (http://www.metaways.de)
8  *
9  */
10  
11 Ext.ns('Tine.Calendar');
12
13 /**
14  * @namespace Tine.Calendar
15  * @class Tine.Calendar.EventEditDialog
16  * @extends Tine.widgets.dialog.EditDialog
17  * Calendar Edit Dialog <br>
18  * 
19  * @author      Cornelius Weiss <c.weiss@metaways.de>
20  */
21 Tine.Calendar.EventEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
22     /**
23      * @cfg {Number} containerId initial container id
24      */
25     containerId: -1,
26     
27     labelAlign: 'side',
28     windowNamePrefix: 'EventEditWindow_',
29     appName: 'Calendar',
30     recordClass: Tine.Calendar.Model.Event,
31     recordProxy: Tine.Calendar.backend,
32     showContainerSelector: false,
33     tbarItems: [{xtype: 'widget-activitiesaddbutton'}],
34     
35     mode: 'local',
36     
37     // note: we need up use new action updater here or generally in the widget!
38     evalGrants: false,
39     
40     onResize: function() {
41         Tine.Calendar.EventEditDialog.superclass.onResize.apply(this, arguments);
42         this.setTabHeight.defer(100, this);
43     },
44     
45     /**
46      * returns dialog
47      * 
48      * NOTE: when this method gets called, all initalisation is done.
49      * @return {Object} components this.itmes definition
50      */
51     getFormItems: function() {
52         return {
53             xtype: 'tabpanel',
54             border: false,
55             plugins: [{
56                 ptype : 'ux.tabpanelkeyplugin'
57             }],
58             defaults: {
59                 hideMode: 'offsets'
60             },
61             plain:true,
62             activeTab: 0,
63             border: false,
64             items:[{
65                 title: this.app.i18n.n_('Event', 'Events', 1),
66                 border: false,
67                 frame: true,
68                 layout: 'border',
69                 items: [{
70                     region: 'center',
71                     layout: 'hfit',
72                     border: false,
73                     items: [{
74                         layout: 'hbox',
75                         items: [{
76                             margins: '5',
77                             width: 100,
78                             xtype: 'label',
79                             text: this.app.i18n._('Summary')
80                         }, {
81                             flex: 1,
82                             xtype:'textfield',
83                             name: 'summary',
84                             listeners: {render: function(field){field.focus(false, 250);}},
85                             allowBlank: false,
86                             requiredGrant: 'editGrant',
87                             maxLength: 255
88                         }]
89                     }, {
90                         layout: 'hbox',
91                         items: [{
92                             margins: '5',
93                             width: 100,
94                             xtype: 'label',
95                             text: this.app.i18n._('View')
96                         }, Ext.apply(this.perspectiveCombo, {
97                             flex: 1
98                         })]
99                     }, {
100                         layout: 'hbox',
101                         height: 115,
102                         layoutConfig: {
103                             align : 'stretch',
104                             pack  : 'start'
105                         },
106                         items: [{
107                             flex: 1,
108                             xtype: 'fieldset',
109                             layout: 'hfit',
110                             margins: '0 5 0 0',
111                             title: this.app.i18n._('Details'),
112                             items: [{
113                                 xtype: 'columnform',
114                                 labelAlign: 'side',
115                                 labelWidth: 100,
116                                 formDefaults: {
117                                     xtype:'textfield',
118                                     anchor: '100%',
119                                     labelSeparator: '',
120                                     columnWidth: .7
121                                 },
122                                 items: [[{
123                                     columnWidth: 1,
124                                     fieldLabel: this.app.i18n._('Location'),
125                                     name: 'location',
126                                     requiredGrant: 'editGrant',
127                                     maxLength: 255
128                                 }], [{
129                                     xtype: 'datetimefield',
130                                     fieldLabel: this.app.i18n._('Start Time'),
131                                     listeners: {scope: this, change: this.onDtStartChange},
132                                     name: 'dtstart',
133                                     requiredGrant: 'editGrant'
134                                 }, {
135                                     columnWidth: .19,
136                                     xtype: 'checkbox',
137                                     hideLabel: true,
138                                     boxLabel: this.app.i18n._('whole day'),
139                                     listeners: {scope: this, check: this.onAllDayChange},
140                                     name: 'is_all_day_event',
141                                     requiredGrant: 'editGrant'
142                                 }], [{
143                                     xtype: 'datetimefield',
144                                     fieldLabel: this.app.i18n._('End Time'),
145                                     listeners: {scope: this, change: this.onDtEndChange},
146                                     name: 'dtend',
147                                     requiredGrant: 'editGrant'
148                                 }, {
149                                     columnWidth: .3,
150                                     xtype: 'combo',
151                                     hideLabel: true,
152                                     readOnly: true,
153                                     hideTrigger: true,
154                                     disabled: true,
155                                     name: 'originator_tz',
156                                     requiredGrant: 'editGrant'
157                                 }], [ this.containerSelectCombo = new Tine.widgets.container.selectionComboBox({
158                                     columnWidth: 1,
159                                     id: this.app.appName + 'EditDialogContainerSelector' + Ext.id(),
160                                     fieldLabel: _('Saved in'),
161                                     ref: '../../../../../../../../containerSelect',
162                                     //width: 300,
163                                     //listWidth: 300,
164                                     name: this.recordClass.getMeta('containerProperty'),
165                                     recordClass: this.recordClass,
166                                     containerName: this.app.i18n.n_hidden(this.recordClass.getMeta('containerName'), this.recordClass.getMeta('containersName'), 1),
167                                     containersName: this.app.i18n._hidden(this.recordClass.getMeta('containersName')),
168                                     appName: this.app.appName,
169                                     requiredGrant: this.record.data.id ? ['editGrant'] : ['addGrant'],
170                                     disabled: true
171                                 }), Ext.apply(this.perspectiveCombo.getAttendeeContainerField(), {
172                                     columnWidth: 1
173                                 })]]
174                             }]
175                         }, {
176                             width: 130,
177                             xtype: 'fieldset',
178                             title: this.app.i18n._('Status'),
179                             items: [{
180                                 xtype: 'checkbox',
181                                 hideLabel: true,
182                                 boxLabel: this.app.i18n._('non-blocking'),
183                                 name: 'transp',
184                                 requiredGrant: 'editGrant',
185                                 getValue: function() {
186                                     var bool = Ext.form.Checkbox.prototype.getValue.call(this);
187                                     return bool ? 'TRANSPARENT' : 'OPAQUE';
188                                 },
189                                 setValue: function(value) {
190                                     var bool = (value == 'TRANSPARENT' || value === true);
191                                     return Ext.form.Checkbox.prototype.setValue.call(this, bool);
192                                 }
193                             }, Ext.apply(this.perspectiveCombo.getAttendeeTranspField(), {
194                                 hideLabel: true
195                             }), {
196                                 xtype: 'checkbox',
197                                 hideLabel: true,
198                                 boxLabel: this.app.i18n._('Tentative'),
199                                 name: 'status',
200                                 requiredGrant: 'editGrant',
201                                 getValue: function() {
202                                     var bool = Ext.form.Checkbox.prototype.getValue.call(this);
203                                     return bool ? 'TENTATIVE' : 'CONFIRMED';
204                                 },
205                                 setValue: function(value) {
206                                     var bool = (value == 'TENTATIVE' || value === true);
207                                     return Ext.form.Checkbox.prototype.setValue.call(this, bool);
208                                 }
209                             }, {
210                                 xtype: 'checkbox',
211                                 hideLabel: true,
212                                 boxLabel: this.app.i18n._('Private'),
213                                 name: 'class',
214                                 requiredGrant: 'editGrant',
215                                 getValue: function() {
216                                     var bool = Ext.form.Checkbox.prototype.getValue.call(this);
217                                     return bool ? 'PRIVATE' : 'PUBLIC';
218                                 },
219                                 setValue: function(value) {
220                                     var bool = (value == 'PRIVATE' || value === true);
221                                     return Ext.form.Checkbox.prototype.setValue.call(this, bool);
222                                 }
223                             }, Ext.apply(this.perspectiveCombo.getAttendeeStatusField(), {
224                                 width: 115,
225                                 hideLabel: true
226                             })]
227                         }]
228                     }, {
229                         xtype: 'tabpanel',
230                         deferredRender: false,
231                         activeTab: 0,
232                         border: false,
233                         height: 235,
234                         form: true,
235                         items: [
236                             this.attendeeGridPanel,
237                             this.rrulePanel,
238                             this.alarmPanel
239                         ]
240                     }]
241                 }, {
242                     // activities and tags
243                     region: 'east',
244                     layout: 'accordion',
245                     animate: true,
246                     width: 200,
247                     split: true,
248                     collapsible: true,
249                     collapseMode: 'mini',
250                     header: false,
251                     margins: '0 5 0 5',
252                     border: true,
253                     items: [
254                         new Ext.Panel({
255                             // @todo generalise!
256                             title: this.app.i18n._('Description'),
257                             iconCls: 'descriptionIcon',
258                             layout: 'form',
259                             labelAlign: 'top',
260                             border: false,
261                             items: [{
262                                 style: 'margin-top: -4px; border 0px;',
263                                 labelSeparator: '',
264                                 xtype:'textarea',
265                                 name: 'description',
266                                 hideLabel: true,
267                                 grow: false,
268                                 preventScrollbars:false,
269                                 anchor:'100% 100%',
270                                 emptyText: this.app.i18n._('Enter description'),
271                                 requiredGrant: 'editGrant'                           
272                             }]
273                         }),
274                         new Tine.widgets.activities.ActivitiesPanel({
275                             app: 'Calendar',
276                             showAddNoteForm: false,
277                             border: false,
278                             bodyStyle: 'border:1px solid #B5B8C8;'
279                         }),
280                         new Tine.widgets.tags.TagPanel({
281                             app: 'Calendar',
282                             border: false,
283                             bodyStyle: 'border:1px solid #B5B8C8;'
284                         })
285                     ]
286                 }]
287             }, new Tine.widgets.activities.ActivitiesTabPanel({
288                 app: this.appName,
289                 record_id: (this.record) ? this.record.id : '',
290                 record_model: this.appName + '_Model_' + this.recordClass.getMeta('modelName')
291             })]
292         };
293     },
294     
295     initComponent: function() {
296         var organizerCombo;
297         this.attendeeGridPanel = new Tine.Calendar.AttendeeGridPanel({
298             bbar: [{
299                 xtype: 'label',
300                 html: Tine.Tinebase.appMgr.get('Calendar').i18n._('Organizer') + "&nbsp;"
301             }, organizerCombo = Tine.widgets.form.RecordPickerManager.get('Addressbook', 'Contact', {
302                 width: 300,
303                 name: 'organizer',
304                 userOnly: true,
305                 getValue: function() {
306                     var id = Tine.Addressbook.SearchCombo.prototype.getValue.apply(this, arguments),
307                         record = this.store.getById(id);
308                         
309                     return record ? record.data : id;
310                 }
311             })]
312         });
313         
314         // auto location
315         this.attendeeGridPanel.on('afteredit', function(o) {
316             if (o.field == 'user_id'
317                 && o.record.get('user_type') == 'resource'
318                 && o.record.get('user_id')
319                 && o.record.get('user_id').is_location
320             ) {
321                 this.getForm().findField('location').setValue(
322                     this.attendeeGridPanel.renderAttenderResourceName(o.record.get('user_id'))
323                 );
324             }
325         }, this);
326         
327         this.on('render', function() {this.getForm().add(organizerCombo);}, this);
328         
329         this.rrulePanel = new Tine.Calendar.RrulePanel({});
330         this.alarmPanel = new Tine.widgets.dialog.AlarmPanel({});
331         this.attendeeStore = this.attendeeGridPanel.getStore();
332         
333         // a combo with all attendee + origin/organizer
334         this.perspectiveCombo = new Tine.Calendar.PerspectiveCombo({
335             editDialog: this
336         });
337         
338         Tine.Calendar.EventEditDialog.superclass.initComponent.call(this);
339         
340         this.addAttendee();
341     },
342
343     /**
344      * if this addRelations is set, iterate and create attendee
345      */
346     addAttendee: function() {
347         var attendee = this.record.get('attendee');
348         var attendee = Ext.isArray(attendee) ? attendee : [];
349         
350         if (Ext.isArray(this.plugins)) {
351             for (var index = 0; index < this.plugins.length; index++) {
352                 if (this.plugins[index].hasOwnProperty('addRelations')) {
353
354                     var config = this.plugins[index].hasOwnProperty('relationConfig') ? this.plugins[index].relationConfig : {};
355                     
356                     for (var index2 = 0; index2 < this.plugins[index].addRelations.length; index2++) {
357                         var item = this.plugins[index].addRelations[index2];
358                         var attender = Ext.apply({
359                             user_type: 'user',
360                             role: 'REQ',
361                             quantity: 1,
362                             status: 'NEEDS-ACTION',
363                             user_id: item
364                         }, config);
365                         
366                         attendee.push(attender);
367                     }
368                 }
369             }
370         }
371         
372         this.record.set('attendee', attendee);
373     },
374     
375     /**
376      * checks if form data is valid
377      * 
378      * @return {Boolean}
379      */
380     isValid: function() {
381         var isValid = this.validateDtStart() && this.validateDtEnd();
382         
383         if (! this.rrulePanel.isValid()) {
384             isValid = false;
385             
386             this.rrulePanel.ownerCt.setActiveTab(this.rrulePanel);
387         }
388         
389         return isValid && Tine.Calendar.EventEditDialog.superclass.isValid.apply(this, arguments);
390     },
391      
392     onAllDayChange: function(checkbox, isChecked) {
393         var dtStartField = this.getForm().findField('dtstart');
394         var dtEndField = this.getForm().findField('dtend');
395         dtStartField.setDisabled(isChecked, 'time');
396         dtEndField.setDisabled(isChecked, 'time');
397         
398         if (isChecked) {
399             dtStartField.clearTime();
400             var dtend = dtEndField.getValue();
401             if (Ext.isDate(dtend) && dtend.format('H:i:s') != '23:59:59') {
402                 dtEndField.setValue(dtend.clearTime(true).add(Date.HOUR, 24).add(Date.SECOND, -1));
403             }
404             
405         } else {
406             dtStartField.undo();
407             dtEndField.undo();
408         }
409     },
410     
411     onDtEndChange: function(dtEndField, newValue, oldValue) {
412         this.validateDtEnd();
413     },
414     
415     /**
416      * on dt start change
417      * 
418      * @param {} dtStartField
419      * @param {} newValue
420      * @param {} oldValue
421      */
422     onDtStartChange: function(dtStartField, newValue, oldValue) {
423         if (this.validateDtStart() == false) {
424             return false;
425         }
426         
427         if (Ext.isDate(newValue) && Ext.isDate(oldValue)) {
428             var dtEndField = this.getForm().findField('dtend'),
429                 dtEnd = dtEndField.getValue();
430                 
431             if (Ext.isDate(dtEnd)) {
432                 var duration = dtEnd.getTime() - oldValue.getTime(),
433                     newDtEnd = newValue.add(Date.MILLI, duration);
434                 dtEndField.setValue(newDtEnd);
435                 this.validateDtEnd();
436             }
437         }
438     },
439     
440     /**
441      * copy record
442      * 
443      * TODO change attender status?
444      */
445     doCopyRecord: function() {
446         Tine.Calendar.EventEditDialog.superclass.doCopyRecord.call(this);
447         
448         // remove attender ids
449         Ext.each(this.record.data.attendee, function(attender) {
450             delete attender.id;
451         }, this);
452         
453         // Calendar is the only app with record based grants -> user gets edit grant for all fields when copying
454         this.record.set('editGrant', true);
455         
456         Tine.log.debug('Tine.Calendar.EventEditDialog::doCopyRecord() -> record:');
457         Tine.log.debug(this.record);
458     },
459     
460     /**
461      * is called after all subpanels have been loaded
462      */
463     onAfterRecordLoad: function() {
464         Tine.Calendar.EventEditDialog.superclass.onAfterRecordLoad.call(this);
465
466         // disable relations panel for non persistent exceptions till we have the baseEventId
467         if (this.record.isRecurInstance()) {
468             this.relationsPanel.setDisabled(true);
469         }
470         this.attendeeGridPanel.onRecordLoad(this.record);
471         this.rrulePanel.onRecordLoad(this.record);
472         this.alarmPanel.onRecordLoad(this.record);
473         
474         // apply grants
475         if (! this.record.get('editGrant')) {
476             this.getForm().items.each(function(f){
477                 if(f.isFormField && f.requiredGrant !== undefined){
478                     f.setDisabled(! this.record.get(f.requiredGrant));
479                 }
480             }, this);
481         }
482         
483         this.perspectiveCombo.loadPerspective();
484         // disable container selection combo if user has no right to edit
485         this.containerSelect.setDisabled.defer(20, this.containerSelect, [(! this.record.get('editGrant'))]);
486         
487         // disable time selectors if this is a whole day event
488         if (this.record.get('is_all_day_event')) {
489             this.onAllDayChange(null, true);
490         }
491     },
492     
493     onRecordUpdate: function() {
494         Tine.Calendar.EventEditDialog.superclass.onRecordUpdate.apply(this, arguments);
495         this.attendeeGridPanel.onRecordUpdate(this.record);
496         this.rrulePanel.onRecordUpdate(this.record);
497         this.alarmPanel.onRecordUpdate(this.record);
498         this.perspectiveCombo.updatePerspective();
499     },
500
501     setTabHeight: function() {
502         var eventTab = this.items.first().items.first();
503         var centerPanel = eventTab.items.first();
504         var tabPanel = centerPanel.items.last();
505         tabPanel.setHeight(centerPanel.getEl().getBottom() - tabPanel.getEl().getTop());
506     },
507     
508     validateDtEnd: function() {
509         var dtStart = this.getForm().findField('dtstart').getValue();
510         
511         var dtEndField = this.getForm().findField('dtend');
512         var dtEnd = dtEndField.getValue();
513         
514         var prefs = this.app.getRegistry().get('preferences'),
515             endTime = Date.parseDate(prefs.get('daysviewendtime'), 'H:i');
516       
517         // Update to the selected day
518         endTime.setDate(dtEnd.getDate());
519         endTime.setMonth(dtEnd.getMonth());
520         endTime.setYear(dtEnd.getYear() + 1900);
521
522         if (! Ext.isDate(dtEnd)) {
523             dtEndField.markInvalid(this.app.i18n._('End date is not valid'));
524             return false;
525         } else if (Ext.isDate(dtStart) && dtEnd.getTime() - dtStart.getTime() <= 0) {
526             dtEndField.markInvalid(this.app.i18n._('End date must be after start date'));
527             return false;
528         } else if  (! Tine.Tinebase.configManager.get('daysviewallowallevents', 'Calendar') && this.getForm().findField('is_all_day_event').checked === false && !! Tine.Tinebase.configManager.get('daysviewcroptime', 'Calendar') && dtEnd > endTime) {
529             dtEndField.markInvalid(this.app.i18n._('End date is not allowed to be be higher than the configured time range.'));
530             return false;
531         } else {
532             dtEndField.clearInvalid();
533             return true;
534         }
535     },
536     
537     validateDtStart: function() {
538         var dtStartField = this.getForm().findField('dtstart');
539         var dtStart = dtStartField.getValue();
540         
541         var prefs = this.app.getRegistry().get('preferences'),
542             startTime = Date.parseDate(prefs.get('daysviewstarttime'), 'H:i');
543       
544         // Update to the selected day
545         startTime.setDate(dtStart.getDate());
546         startTime.setMonth(dtStart.getMonth());
547         startTime.setYear(dtStart.getYear() + 1900);
548
549         if (! Ext.isDate(dtStart)) {
550             dtStartField.markInvalid(this.app.i18n._('Start date is not valid'));
551             return false;
552         } else if  (! Tine.Tinebase.configManager.get('daysviewallowallevents', 'Calendar') && this.getForm().findField('is_all_day_event').checked === false && !! Tine.Tinebase.configManager.get('daysviewcroptime', 'Calendar') && dtStart < startTime) {
553             dtStartField.markInvalid(this.app.i18n._('End date is not allowed to be be lower than the configured time range.'));
554             return false;
555         } else {
556             dtStartField.clearInvalid();
557             return true;
558         }
559     },
560     
561     /**
562      * is called from onApplyChanges
563      * @param {Boolean} closeWindow
564      */
565     doApplyChanges: function(closeWindow) {
566         this.onRecordUpdate();
567         if (this.isValid()) {
568             this.fireEvent('update', Ext.util.JSON.encode(this.record.data));
569             this.onAfterApplyChanges(closeWindow);
570         } else {
571             this.saving = false;
572             this.loadMask.hide();
573             Ext.MessageBox.alert(_('Errors'), this.getValidationErrorMessage());
574         }
575     }
576 });
577
578 /**
579  * Opens a new event edit dialog window
580  * 
581  * @return {Ext.ux.Window}
582  */
583 Tine.Calendar.EventEditDialog.openWindow = function (config) {
584     // record is JSON encoded here...
585     var id = config.recordId ? config.recordId : 0;
586     var window = Tine.WindowFactory.getWindow({
587         width: 800,
588         height: 505,
589         name: Tine.Calendar.EventEditDialog.prototype.windowNamePrefix + id,
590         contentPanelConstructor: 'Tine.Calendar.EventEditDialog',
591         contentPanelConstructorConfig: config
592     });
593     return window;
594 };