Merge branch '2013.03'
[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
341     /**
342      * checks if form data is valid
343      * 
344      * @return {Boolean}
345      */
346     isValid: function() {
347         var isValid = this.validateDtStart() && this.validateDtEnd();
348         
349         if (! this.rrulePanel.isValid()) {
350             isValid = false;
351             
352             this.rrulePanel.ownerCt.setActiveTab(this.rrulePanel);
353         }
354         
355         return isValid && Tine.Calendar.EventEditDialog.superclass.isValid.apply(this, arguments);
356     },
357      
358     onAllDayChange: function(checkbox, isChecked) {
359         var dtStartField = this.getForm().findField('dtstart');
360         var dtEndField = this.getForm().findField('dtend');
361         dtStartField.setDisabled(isChecked, 'time');
362         dtEndField.setDisabled(isChecked, 'time');
363         
364         if (isChecked) {
365             dtStartField.clearTime();
366             var dtend = dtEndField.getValue();
367             if (Ext.isDate(dtend) && dtend.format('H:i:s') != '23:59:59') {
368                 dtEndField.setValue(dtend.clearTime(true).add(Date.HOUR, 24).add(Date.SECOND, -1));
369             }
370             
371         } else {
372             dtStartField.undo();
373             dtEndField.undo();
374         }
375     },
376     
377     onDtEndChange: function(dtEndField, newValue, oldValue) {
378         this.validateDtEnd();
379     },
380     
381     /**
382      * on dt start change
383      * 
384      * @param {} dtStartField
385      * @param {} newValue
386      * @param {} oldValue
387      */
388     onDtStartChange: function(dtStartField, newValue, oldValue) {
389         if (Ext.isDate(newValue) && Ext.isDate(oldValue)) {
390             var dtEndField = this.getForm().findField('dtend'),
391                 dtEnd = dtEndField.getValue();
392                 
393             if (Ext.isDate(dtEnd)) {
394                 var duration = dtEnd.getTime() - oldValue.getTime(),
395                     newDtEnd = newValue.add(Date.MILLI, duration);
396                 
397                 dtEndField.setValue(newDtEnd);
398             }
399         }
400     },
401     
402     /**
403      * copy record
404      * 
405      * TODO change attender status?
406      */
407     doCopyRecord: function() {
408         Tine.Calendar.EventEditDialog.superclass.doCopyRecord.call(this);
409         
410         // remove attender ids
411         Ext.each(this.record.data.attendee, function(attender) {
412             delete attender.id;
413         }, this);
414         
415         // Calendar is the only app with record based grants -> user gets edit grant for all fields when copying
416         this.record.set('editGrant', true);
417         
418         Tine.log.debug('Tine.Calendar.EventEditDialog::doCopyRecord() -> record:');
419         Tine.log.debug(this.record);
420     },
421     
422     /**
423      * is called after all subpanels have been loaded
424      */
425     onAfterRecordLoad: function() {
426         Tine.Calendar.EventEditDialog.superclass.onAfterRecordLoad.call(this);
427         
428         this.attendeeGridPanel.onRecordLoad(this.record);
429         this.rrulePanel.onRecordLoad(this.record);
430         this.alarmPanel.onRecordLoad(this.record);
431         
432         // apply grants
433         if (! this.record.get('editGrant')) {
434             this.getForm().items.each(function(f){
435                 if(f.isFormField && f.requiredGrant !== undefined){
436                     f.setDisabled(! this.record.get(f.requiredGrant));
437                 }
438             }, this);
439         }
440         
441         this.perspectiveCombo.loadPerspective();
442         // disable container selection combo if user has no right to edit
443         this.containerSelect.setDisabled.defer(20, this.containerSelect, [(! this.record.get('editGrant'))]);
444     },
445     
446     onRecordUpdate: function() {
447         Tine.Calendar.EventEditDialog.superclass.onRecordUpdate.apply(this, arguments);
448         this.attendeeGridPanel.onRecordUpdate(this.record);
449         this.rrulePanel.onRecordUpdate(this.record);
450         this.alarmPanel.onRecordUpdate(this.record);
451         this.perspectiveCombo.updatePerspective();
452     },
453
454     setTabHeight: function() {
455         var eventTab = this.items.first().items.first();
456         var centerPanel = eventTab.items.first();
457         var tabPanel = centerPanel.items.last();
458         tabPanel.setHeight(centerPanel.getEl().getBottom() - tabPanel.getEl().getTop());
459     },
460     
461     validateDtEnd: function() {
462         var dtStart = this.getForm().findField('dtstart').getValue();
463         
464         var dtEndField = this.getForm().findField('dtend');
465         var dtEnd = dtEndField.getValue();
466         
467         if (! Ext.isDate(dtEnd)) {
468             dtEndField.markInvalid(this.app.i18n._('End date is not valid'));
469             return false;
470         } else if (Ext.isDate(dtStart) && dtEnd.getTime() - dtStart.getTime() <= 0) {
471             dtEndField.markInvalid(this.app.i18n._('End date must be after start date'));
472             return false;
473         } else {
474             dtEndField.clearInvalid();
475             return true;
476         }
477     },
478     
479     validateDtStart: function() {
480         var dtStartField = this.getForm().findField('dtstart');
481         var dtStart = dtStartField.getValue();
482         
483         if (! Ext.isDate(dtStart)) {
484             dtStartField.markInvalid(this.app.i18n._('Start date is not valid'));
485             return false;
486         } else {
487             dtStartField.clearInvalid();
488             return true;
489         }
490         
491     },
492     
493     /**
494      * is called from onApplyChanges
495      * @param {Boolean} closeWindow
496      */
497     doApplyChanges: function(closeWindow) {
498         this.onRecordUpdate();
499         if(this.isValid()) {
500             this.fireEvent('update', Ext.util.JSON.encode(this.record.data));
501             this.onAfterApplyChanges(closeWindow);
502         } else {
503             this.loadMask.hide();
504             Ext.MessageBox.alert(_('Errors'), this.getValidationErrorMessage());
505         }
506     }
507 });
508
509 /**
510  * Opens a new event edit dialog window
511  * 
512  * @return {Ext.ux.Window}
513  */
514 Tine.Calendar.EventEditDialog.openWindow = function (config) {
515     // record is JSON encoded here...
516     var id = config.recordId ? config.recordId : 0;
517     var window = Tine.WindowFactory.getWindow({
518         width: 800,
519         height: 505,
520         name: Tine.Calendar.EventEditDialog.prototype.windowNamePrefix + id,
521         contentPanelConstructor: 'Tine.Calendar.EventEditDialog',
522         contentPanelConstructorConfig: config
523     });
524     return window;
525 };