prevent xDAV clients to change the container displayname to id/uuid
[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     /**
296      * mute first alert
297      * 
298      * @param {} button
299      * @param {} e
300      */
301     onMuteAlertOnce: function (button, e) {
302         this.record.set('mute', button.pressed);
303     },
304
305     initComponent: function() {
306         this.tbarItems.push(new Ext.Button(new Ext.Action({
307                     text: Tine.Tinebase.appMgr.get('Calendar').i18n._('Mute Alert'),
308                     handler: this.onMuteAlertOnce,
309                     iconCls: 'notes_noteIcon',
310                     disabled: false,
311                     scope: this,
312                     enableToggle: true
313                 })));
314
315         var organizerCombo;
316         this.attendeeGridPanel = new Tine.Calendar.AttendeeGridPanel({
317             bbar: [{
318                 xtype: 'label',
319                 html: Tine.Tinebase.appMgr.get('Calendar').i18n._('Organizer') + "&nbsp;"
320             }, organizerCombo = Tine.widgets.form.RecordPickerManager.get('Addressbook', 'Contact', {
321                 width: 300,
322                 name: 'organizer',
323                 userOnly: true,
324                 getValue: function() {
325                     var id = Tine.Addressbook.SearchCombo.prototype.getValue.apply(this, arguments),
326                         record = this.store.getById(id);
327                         
328                     return record ? record.data : id;
329                 }
330             })]
331         });
332         
333         // auto location
334         this.attendeeGridPanel.on('afteredit', function(o) {
335             if (o.field == 'user_id'
336                 && o.record.get('user_type') == 'resource'
337                 && o.record.get('user_id')
338                 && o.record.get('user_id').is_location
339             ) {
340                 this.getForm().findField('location').setValue(
341                     this.attendeeGridPanel.renderAttenderResourceName(o.record.get('user_id'))
342                 );
343             }
344         }, this);
345         
346         this.on('render', function() {this.getForm().add(organizerCombo);}, this);
347         
348         this.rrulePanel = new Tine.Calendar.RrulePanel({});
349         this.alarmPanel = new Tine.widgets.dialog.AlarmPanel({});
350         this.attendeeStore = this.attendeeGridPanel.getStore();
351         
352         // a combo with all attendee + origin/organizer
353         this.perspectiveCombo = new Tine.Calendar.PerspectiveCombo({
354             editDialog: this
355         });
356         
357         Tine.Calendar.EventEditDialog.superclass.initComponent.call(this);
358         
359         this.addAttendee();
360     },
361
362     /**
363      * if this addRelations is set, iterate and create attendee
364      */
365     addAttendee: function() {
366         var attendee = this.record.get('attendee');
367         var attendee = Ext.isArray(attendee) ? attendee : [];
368         
369         if (Ext.isArray(this.plugins)) {
370             for (var index = 0; index < this.plugins.length; index++) {
371                 if (this.plugins[index].hasOwnProperty('addRelations')) {
372
373                     var config = this.plugins[index].hasOwnProperty('relationConfig') ? this.plugins[index].relationConfig : {};
374                     
375                     for (var index2 = 0; index2 < this.plugins[index].addRelations.length; index2++) {
376                         var item = this.plugins[index].addRelations[index2];
377                         var attender = Ext.apply({
378                             user_type: 'user',
379                             role: 'REQ',
380                             quantity: 1,
381                             status: 'NEEDS-ACTION',
382                             user_id: item
383                         }, config);
384                         
385                         attendee.push(attender);
386                     }
387                 }
388             }
389         }
390         
391         this.record.set('attendee', attendee);
392     },
393     
394     /**
395      * checks if form data is valid
396      * 
397      * @return {Boolean}
398      */
399     isValid: function() {
400         var isValid = this.validateDtStart() && this.validateDtEnd();
401         
402         if (! this.rrulePanel.isValid()) {
403             isValid = false;
404             
405             this.rrulePanel.ownerCt.setActiveTab(this.rrulePanel);
406         }
407         
408         return isValid && Tine.Calendar.EventEditDialog.superclass.isValid.apply(this, arguments);
409     },
410      
411     onAllDayChange: function(checkbox, isChecked) {
412         var dtStartField = this.getForm().findField('dtstart');
413         var dtEndField = this.getForm().findField('dtend');
414         dtStartField.setDisabled(isChecked, 'time');
415         dtEndField.setDisabled(isChecked, 'time');
416         
417         if (isChecked) {
418             dtStartField.clearTime();
419             var dtend = dtEndField.getValue();
420             if (Ext.isDate(dtend) && dtend.format('H:i:s') != '23:59:59') {
421                 dtEndField.setValue(dtend.clearTime(true).add(Date.HOUR, 24).add(Date.SECOND, -1));
422             }
423             
424         } else {
425             dtStartField.undo();
426             dtEndField.undo();
427         }
428     },
429     
430     onDtEndChange: function(dtEndField, newValue, oldValue) {
431         this.validateDtEnd();
432     },
433     
434     /**
435      * on dt start change
436      * 
437      * @param {} dtStartField
438      * @param {} newValue
439      * @param {} oldValue
440      */
441     onDtStartChange: function(dtStartField, newValue, oldValue) {
442         if (this.validateDtStart() == false) {
443             return false;
444         }
445         
446         if (Ext.isDate(newValue) && Ext.isDate(oldValue)) {
447             var dtEndField = this.getForm().findField('dtend'),
448                 dtEnd = dtEndField.getValue();
449                 
450             if (Ext.isDate(dtEnd)) {
451                 var duration = dtEnd.getTime() - oldValue.getTime(),
452                     newDtEnd = newValue.add(Date.MILLI, duration);
453                 dtEndField.setValue(newDtEnd);
454                 this.validateDtEnd();
455             }
456         }
457     },
458     
459     /**
460      * copy record
461      * 
462      * TODO change attender status?
463      */
464     doCopyRecord: function() {
465         Tine.Calendar.EventEditDialog.superclass.doCopyRecord.call(this);
466         
467         // remove attender ids
468         Ext.each(this.record.data.attendee, function(attender) {
469             delete attender.id;
470         }, this);
471         
472         // Calendar is the only app with record based grants -> user gets edit grant for all fields when copying
473         this.record.set('editGrant', true);
474         
475         Tine.log.debug('Tine.Calendar.EventEditDialog::doCopyRecord() -> record:');
476         Tine.log.debug(this.record);
477     },
478     
479     /**
480      * is called after all subpanels have been loaded
481      */
482     onAfterRecordLoad: function() {
483         Tine.Calendar.EventEditDialog.superclass.onAfterRecordLoad.call(this);
484
485         // disable relations panel for non persistent exceptions till we have the baseEventId
486         if (this.record.isRecurInstance()) {
487             this.relationsPanel.setDisabled(true);
488         }
489         this.attendeeGridPanel.onRecordLoad(this.record);
490         this.rrulePanel.onRecordLoad(this.record);
491         this.alarmPanel.onRecordLoad(this.record);
492         
493         // apply grants
494         if (! this.record.get('editGrant')) {
495             this.getForm().items.each(function(f){
496                 if(f.isFormField && f.requiredGrant !== undefined){
497                     f.setDisabled(! this.record.get(f.requiredGrant));
498                 }
499             }, this);
500         }
501         
502         this.perspectiveCombo.loadPerspective();
503         // disable container selection combo if user has no right to edit
504         this.containerSelect.setDisabled.defer(20, this.containerSelect, [(! this.record.get('editGrant'))]);
505         
506         // disable time selectors if this is a whole day event
507         if (this.record.get('is_all_day_event')) {
508             this.onAllDayChange(null, true);
509         }
510     },
511     
512     onRecordUpdate: function() {
513         Tine.Calendar.EventEditDialog.superclass.onRecordUpdate.apply(this, arguments);
514         this.attendeeGridPanel.onRecordUpdate(this.record);
515         this.rrulePanel.onRecordUpdate(this.record);
516         this.alarmPanel.onRecordUpdate(this.record);
517         this.perspectiveCombo.updatePerspective();
518     },
519
520     setTabHeight: function() {
521         var eventTab = this.items.first().items.first();
522         var centerPanel = eventTab.items.first();
523         var tabPanel = centerPanel.items.last();
524         tabPanel.setHeight(centerPanel.getEl().getBottom() - tabPanel.getEl().getTop());
525     },
526     
527     validateDtEnd: function() {
528         var dtStart = this.getForm().findField('dtstart').getValue();
529         
530         var dtEndField = this.getForm().findField('dtend');
531         var dtEnd = dtEndField.getValue();
532         
533         var prefs = this.app.getRegistry().get('preferences'),
534             endTime = Date.parseDate(prefs.get('daysviewendtime'), 'H:i');
535         
536         if (endTime.format('H:i') == '00:00') {
537             endTime = endTime.add(Date.MINUTE, -1);
538         }
539         
540         // Update to the selected day
541         endTime.setDate(dtEnd.getDate());
542         endTime.setMonth(dtEnd.getMonth());
543         endTime.setYear(dtEnd.getYear() + 1900);
544
545         if (! Ext.isDate(dtEnd)) {
546             dtEndField.markInvalid(this.app.i18n._('End date is not valid'));
547             return false;
548         } else if (Ext.isDate(dtStart) && dtEnd.getTime() - dtStart.getTime() <= 0) {
549             dtEndField.markInvalid(this.app.i18n._('End date must be after start date'));
550             return false;
551         } 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) {
552             dtEndField.markInvalid(this.app.i18n._('End date is not allowed to be be higher than the configured time range.'));
553             return false;
554         } else {
555             dtEndField.clearInvalid();
556             return true;
557         }
558     },
559     
560     validateDtStart: function() {
561         var dtStartField = this.getForm().findField('dtstart');
562         var dtStart = dtStartField.getValue();
563         
564         var prefs = this.app.getRegistry().get('preferences'),
565             startTime = Date.parseDate(prefs.get('daysviewstarttime'), 'H:i');
566       
567         // Update to the selected day
568         startTime.setDate(dtStart.getDate());
569         startTime.setMonth(dtStart.getMonth());
570         startTime.setYear(dtStart.getYear() + 1900);
571
572         if (! Ext.isDate(dtStart)) {
573             dtStartField.markInvalid(this.app.i18n._('Start date is not valid'));
574             return false;
575         } 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) {
576             dtStartField.markInvalid(this.app.i18n._('End date is not allowed to be be lower than the configured time range.'));
577             return false;
578         } else {
579             dtStartField.clearInvalid();
580             return true;
581         }
582     },
583     
584     /**
585      * is called from onApplyChanges
586      * @param {Boolean} closeWindow
587      */
588     doApplyChanges: function(closeWindow) {
589         this.onRecordUpdate();
590         if (this.isValid()) {
591             this.fireEvent('update', Ext.util.JSON.encode(this.record.data));
592             this.onAfterApplyChanges(closeWindow);
593         } else {
594             this.saving = false;
595             this.loadMask.hide();
596             Ext.MessageBox.alert(_('Errors'), this.getValidationErrorMessage());
597         }
598     }
599 });
600
601 /**
602  * Opens a new event edit dialog window
603  * 
604  * @return {Ext.ux.Window}
605  */
606 Tine.Calendar.EventEditDialog.openWindow = function (config) {
607     // record is JSON encoded here...
608     var id = config.recordId ? config.recordId : 0;
609     var window = Tine.WindowFactory.getWindow({
610         width: 800,
611         height: 505,
612         name: Tine.Calendar.EventEditDialog.prototype.windowNamePrefix + id,
613         contentPanelConstructor: 'Tine.Calendar.EventEditDialog',
614         contentPanelConstructorConfig: config
615     });
616     return window;
617 };