dc95a05c0cdeb037711e3d251aa7bd2e4bc13cdc
[tine20] / tine20 / Tinebase / js / widgets / dialog / EditDialog.js
1 /*
2  * Tine 2.0
3  * 
4  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
5  * @author      Cornelius Weiss <c.weiss@metaways.de>
6  * @copyright   Copyright (c) 2007-2013 Metaways Infosystems GmbH (http://www.metaways.de)
7  */
8 Ext.ns('Tine.widgets.dialog');
9
10 /**
11  * Generic 'Edit Record' dialog
12  * Base class for all 'Edit Record' dialogs
13  * 
14  * @namespace   Tine.widgets.dialog
15  * @class       Tine.widgets.dialog.EditDialog
16  * @extends     Ext.FormPanel
17  * @author      Cornelius Weiss <c.weiss@metaways.de>
18  * @constructor
19  * @param {Object} config The configuration options.
20  */
21
22 Tine.widgets.dialog.EditDialog = Ext.extend(Ext.FormPanel, {
23     /**
24      * @cfg {Tine.Tinebase.Application} app
25      * instance of the app object (required)
26      */
27     app: null,
28     /**
29      * @cfg {String} mode
30      * Set to 'local' if the EditDialog only operates on this.record (defaults to 'remote' which loads and saves using the recordProxy)
31      */
32     mode : 'remote',
33     /**
34      * @cfg {Array} tbarItems
35      * additional toolbar items (defaults to false)
36      */
37     tbarItems: false,
38     /**
39      * internal/untranslated app name (required)
40      * 
41      * @cfg {String} appName
42      */
43     appName: null,
44     /**
45      * the modelName (filled by application starter)
46      * 
47      * @type {String} modelName
48      */
49     modelName: null,
50     
51     /**
52      * record definition class  (required if no modelconfig! don't declare for modelconfig!!)
53      * 
54      * @cfg {Ext.data.Record} recordClass
55      */
56     recordClass: null,
57     /**
58      * @cfg {Ext.data.DataProxy} recordProxy
59      */
60     recordProxy: null,
61     /**
62      * @cfg {Bool} showContainerSelector
63      * show container selector in bottom area
64      */
65     showContainerSelector: null,
66     /**
67      * @cfg {Bool} evalGrants
68      * should grants of a grant-aware records be evaluated (defaults to true)
69      */
70     evalGrants: true,
71     /**
72      * @cfg {String} requiredSaveGrant
73      * required grant for apply/save
74      */
75     requiredSaveGrant: 'editGrant',
76
77     /**
78      * @cfg {Ext.data.Record} record
79      * record in edit process.
80      */
81     record: null,
82     
83     /**
84      * holds the modelConfig for the handled record (json-encoded object)
85      * will be decoded in initComponent
86      * 
87      * @type 
88      */
89     modelConfig: null,
90     
91     /**
92      * @cfg {String} saveAndCloseButtonText
93      * text of save and close button
94      */
95     saveAndCloseButtonText: '',
96     /**
97      * @cfg {String} cancelButtonText
98      * text of cancel button
99      */
100     cancelButtonText: '',
101     
102     /**
103      * @cfg {Boolean} copyRecord
104      * copy record
105      */
106     copyRecord: false,
107     
108     /**
109      * @cfg {Boolean} doDuplicateCheck
110      * do duplicate check when saving record (mode remote only)
111      */
112     doDuplicateCheck: true,
113
114     /**
115      * when a record has the relations-property the relations-panel can be disabled here
116      * @cfg {Boolean} hideRelationsPanel
117      */
118     hideRelationsPanel: false,
119     
120     /**
121      * when a record has the attachments-property the attachments-panel can be disabled here
122      * @cfg {Boolean} hideAttachmentsPanel
123      */
124     hideAttachmentsPanel: false,
125     
126     /**
127      * Registry for other relationgridpanels than the generic one,
128      * handling special types of relations the generic one will not.
129      * Panels registered here must have a store with the relation records.
130      * 
131      * @type {Array}
132      */
133     relationPanelRegistry: null,
134     
135     /**
136      * ignore relations to given php-class names in the relation grid
137      * @type {Array}
138      */
139     ignoreRelatedModels: null,
140     
141     /**
142      * dialog is currently saving data
143      * @type Boolean
144      */
145     saving: false,
146     
147     /**
148      * Disable adding cf tab even if model has support for customfields
149      * @type Boolean
150      */
151     disableCfs: false,
152     
153     /**
154      * @property window {Ext.Window|Ext.ux.PopupWindow|Ext.Air.Window}
155      */
156     /**
157      * @property {Number} loadRequest 
158      * transaction id of loadData request
159      */
160     /**
161      * @property loadMask {Ext.LoadMask}
162      */
163     
164     /**
165      * @property containerSelectCombo {Tine.widgets.container.SelectionComboBox}
166      */
167     containerSelectCombo: null,
168     
169     /**
170      * If set, these fields are readOnly (when called dependent to related record)
171      * 
172      * @type {Ext.util.MixedCollection}
173      */
174     fixedFields: null,
175
176     /**
177      * Plain Object with additional configuration (JSON-encoded)
178      * 
179      * @type {Object}
180      */
181     additionalConfig: null,
182
183     /**
184      * canonical name
185      * @cfg {String} canonicalName
186      */
187     canonicalName: 'EditDialog',
188     
189     // private
190     bodyStyle:'padding:5px',
191     layout: 'fit',
192     border: false,
193     cls: 'tw-editdialog',
194     anchor:'100% 100%',
195     deferredRender: false,
196     buttonAlign: null,
197     bufferResize: 500,
198
199     /**
200      * relations panel
201      * 
202      * @type Tine.widgets.relation.GenericPickerGridPanel
203      */
204     relationsPanel: null,
205     
206     // Array of Relation Pickers
207     relationPickers: null,
208     
209     /**
210      * attachments panel
211      * 
212      * @type Tine.widgets.dialog.AttachmentsGridPanel
213      */
214     attachmentsPanel: null,
215     
216     /**
217      * holds the loadMask
218      * set this to false, if no loadMask should be shown
219      * 
220      * @type {Ext.LoadMask}
221      */
222     loadMask: null,
223
224     /**
225      * hook notes panel into dialog
226      */
227     displayNotes: false,
228
229     useMultiple: false,
230
231     //private
232     initComponent: function() {
233         this.relationPanelRegistry = this.relationPanelRegistry ? this.relationPanelRegistry : [];
234         this.addEvents(
235             /**
236              * @event cancel
237              * Fired when user pressed cancel button
238              */
239             'cancel',
240             /**
241              * @event saveAndClose
242              * Fired when user pressed OK button
243              */
244             'saveAndClose',
245             /**
246              * @event update
247              * @desc  Fired when the record got updated
248              * @param {Json String} data data of the entry
249              * @pram  {String} this.mode
250              */
251             'update',
252             /**
253              * @event apply
254              * Fired when user pressed apply button
255              */
256             'apply',
257             /**
258              * @event load
259              * @param {Tine.widgets.dialog.EditDialog} this
260              * @param {Tine.data.Record} record which got loaded
261              * @param {Function} ticket function for async defer
262              * Fired when record is loaded
263              */
264             'load',
265             /**
266              * @event save
267              * @param {Tine.widgets.dialog.EditDialog} this
268              * @param {Tine.data.Record} record which got loaded
269              * @param {Function} ticket function for async defer
270              * Fired when remote record is saving
271              */
272             'save',
273             /**
274              * @event updateDependent
275              * Fired when a subpanel updates the record locally
276              */
277             'updateDependent',
278             /**
279              * @event change
280              * Fires just before the field blurs if the field value has changed.
281              * @param {Ext.form.Field} this
282              * @param {Mixed} newValue The new value
283              * @param {Mixed} oldValue The original value
284              */
285             'change'
286         );
287
288         if (Ext.isString(this.modelConfig)) {
289             this.modelConfig = Ext.decode(this.modelConfig);
290         }
291         
292         if (Ext.isString(this.additionalConfig)) {
293             Ext.apply(this, Ext.decode(this.additionalConfig));
294         }
295         
296         if (Ext.isString(this.fixedFields)) {
297             var decoded = Ext.decode(this.fixedFields);
298             this.fixedFields = new Ext.util.MixedCollection();
299             this.fixedFields.addAll(decoded);
300         }
301         
302         if (! this.recordClass && this.modelName) {
303             this.recordClass = Tine[this.appName].Model[this.modelName];
304         }
305         
306         if (this.recordClass) {
307             this.appName    = this.appName    ? this.appName    : this.recordClass.getMeta('appName');
308             this.modelName  = this.modelName  ? this.modelName  : this.recordClass.getMeta('modelName');
309         }
310
311         if (! this.app) {
312             this.app = Tine.Tinebase.appMgr.get(this.appName);
313         }
314         
315         if (! this.windowNamePrefix) {
316             this.windowNamePrefix = this.modelName + 'EditWindow_';
317         }
318         
319         Tine.log.debug('initComponent: appName: ', this.appName);
320         Tine.log.debug('initComponent: modelName: ', this.modelName);
321         Tine.log.debug('initComponent: app: ', this.app);
322         
323         // init some translations
324         if (this.app.i18n && this.recordClass !== null) {
325             this.i18nRecordName = this.app.i18n.n_hidden(this.recordClass.getMeta('recordName'), this.recordClass.getMeta('recordsName'), 1);
326             this.i18nRecordsName = this.app.i18n._hidden(this.recordClass.getMeta('recordsName'));
327         }
328
329         // auto record proxy
330         if (! this.recordProxy && this.recordClass) {
331             Tine.log.debug('no record proxy given, creating a new one...');
332             this.recordProxy = new Tine.Tinebase.data.RecordProxy({
333                 recordClass: this.recordClass
334             });
335         }
336
337         // auto eval grants
338         if (this.recordClass) {
339             var grantsField = this.recordClass.getMeta('grantsPath')
340                 .replace(/^data\./, '')
341                 .replace(/\..+/g, '');
342
343             this.evalGrants = this.evalGrants && (grantsField == 'data' || this.recordClass.hasField(grantsField));
344         }
345
346         // init plugins
347         this.plugins = Ext.isString(this.plugins) ? Ext.decode(this.plugins) : Ext.isArray(this.plugins) ? this.plugins.concat(Ext.decode(this.initialConfig.plugins)) : [];
348         
349         this.plugins.push(this.tokenModePlugin = new Tine.widgets.dialog.TokenModeEditDialogPlugin({}));
350         // added possibility to disable using customfield plugin
351         if (this.disableCfs !== true) {
352             this.plugins.push(new Tine.widgets.customfields.EditDialogPlugin({}));
353         }
354         
355         // init actions
356         this.initActions();
357         // init buttons and tbar
358         this.initButtons();
359         // init container selector
360         this.initContainerSelector();
361         // init record 
362         this.initRecord();
363         // get items for this dialog
364         this.items = this.getFormItems();
365
366         // init relations panel if relations are defined
367         this.initRelationsPanel();
368         // init attachments panel
369         this.initAttachmentsPanel();
370         // init notes panel
371         this.initNotesPanel();
372
373         Tine.widgets.dialog.EditDialog.superclass.initComponent.call(this);
374
375         // set fields readOnly if set
376         this.fixFields();
377         
378         // firefox fix: blur each item before tab changes, so no field  will be focused afterwards
379         if (Ext.isGecko) {
380             this.items.items[0].addListener('beforetabchange', function(tabpanel, newtab, oldtab) {
381                 if (! oldtab) {
382                     return;
383                 }
384                 var form = this.getForm();
385                 
386                 if (form && form.hasOwnProperty('items'))
387                     form.items.each(function(item, index) {
388                         item.blur();
389                     });
390             }, this);
391         }
392
393         if (Ext.isFunction(this.window.relayEvents) && Tine.Tinebase.featureEnabled('featureRememberPopupSize')) {
394             this.window.relayEvents(this, ['resize']);
395         }
396     },
397
398     /**
399      * returns canonical path part
400      * @returns {string}
401      */
402     getCanonicalPathSegment: function () {
403         if (this.recordClass) {
404             return ['',
405                 this.recordClass.getMeta('appName'),
406                 this.canonicalName,
407                 this.recordClass.getMeta('modelName'),
408             ].join(Tine.Tinebase.CanonicalPath.separator);
409         }
410     },
411
412     /**
413      * generic form layout
414      */
415     getFormItems: function() {
416         return {
417             xtype: 'tabpanel',
418             border: false,
419             plain:true,
420             activeTab: 0,
421             border: false,
422             defaults: {
423                 hideMode: 'offsets'
424             },
425             plugins: [{
426                 ptype : 'ux.tabpanelkeyplugin'
427             }],
428             items:[
429                 {
430                     title: this.i18nRecordName,
431                     autoScroll: true,
432                     border: false,
433                     frame: true,
434                     layout: 'border',
435                     defaults: { autoScroll: true },
436                     items: [Ext.applyIf(this.getRecordFormItems(), {
437                         region: 'center',
438                         xtype: 'columnform',
439                         labelAlign: 'top',
440                         formDefaults: {
441                             xtype:'textfield',
442                             anchor: '100%',
443                             labelSeparator: '',
444                             columnWidth: 1/2
445                         },
446                     })].concat(this.getEastPanel())
447                 }, new Tine.widgets.activities.ActivitiesTabPanel({
448                     app: this.appName,
449                     record_id: this.record.id,
450                     record_model: this.modelName
451                 })
452             ]
453         };
454     },
455
456     getEastPanel: function() {
457         var items = [];
458         if (this.recordClass.hasField('description')) {
459             items.push(new Ext.Panel({
460                 title: i18n._('Description'),
461                 iconCls: 'descriptionIcon',
462                 layout: 'form',
463                 labelAlign: 'top',
464                 border: false,
465                 items: [{
466                     style: 'margin-top: -4px; border 0px;',
467                     labelSeparator: '',
468                     xtype: 'textarea',
469                     name: 'description',
470                     hideLabel: true,
471                     grow: false,
472                     preventScrollbars: false,
473                     anchor: '100% 100%',
474                     emptyText: i18n._('Enter description'),
475                     requiredGrant: 'editGrant'
476                 }]
477             }));
478         }
479
480         if (this.recordClass.hasField('tags')) {
481             items.push(new Tine.widgets.tags.TagPanel({
482                 app: this.appName,
483                 border: false,
484                 bodyStyle: 'border:1px solid #B5B8C8;'
485             }));
486         }
487
488         return items.length ? {
489             layout: 'accordion',
490             animate: true,
491             region: 'east',
492             width: 210,
493             split: true,
494             collapsible: true,
495             collapseMode: 'mini',
496             header: false,
497             margins: '0 5 0 5',
498             border: true,
499             items: items
500         } : [];
501     },
502
503     getRecordFormItems: function() {
504         return new Tine.widgets.form.RecordForm({
505             recordClass: this.recordClass
506         });
507     },
508
509     /**
510      * fix fields (used for preselecting form fields when called in dependency to another record)
511      * @return {Boolean}
512      */
513     fixFields: function() {
514         if (this.fixedFields && this.fixedFields.getCount() > 0) {
515             if (! this.rendered) {
516                 this.fixFields.defer(100, this);
517                 return false;
518             }
519             
520             this.fixedFields.each(function(value, index) {
521                 var key = this.fixedFields.keys[index]; 
522                 
523                 var field = this.getForm().findField(key);
524                 
525                 if (field) {
526                     if (Ext.isFunction(this.recordClass.getField(key).type)) {
527                         var foreignRecordClass = this.recordClass.getField(key).type;
528                         var record = new foreignRecordClass(value);
529                         field.selectedRecord = record;
530                         field.setValue(value);
531                         field.fireEvent('select');
532                     } else {
533                         field.setValue(value);
534                     }
535                     field.disable();
536                 }
537             }, this);
538         }
539     },
540
541     /**
542      * call checkState for every field
543      */
544     checkStates: function() {
545         this.onRecordUpdate();
546         this.getForm().items.each(function (item) {
547             if (Ext.isFunction(item.checkState)) {
548                 item.checkState(this, this.record);
549             }
550         }, this)
551     },
552
553     /**
554      * Get available model for given application
555      *
556      *  @param {Mixed} application
557      *  @param {Boolean} customFieldModel
558      */
559     getApplicationModels: function (application, customFieldModel) {
560         var models      = [],
561             useModel,
562             appName     = Ext.isString(application) ? application : application.get('name'),
563             app         = Tine.Tinebase.appMgr.get(appName),
564             trans       = app && app.i18n ? app.i18n : i18n,
565             appModels   = Tine[appName].Model;
566
567         if (appModels) {
568             for (var model in appModels) {
569                 if (appModels.hasOwnProperty(model) && typeof appModels[model].getMeta === 'function') {
570                     if (customFieldModel && appModels[model].getField('customfields')) {
571                         useModel = appModels[model].getMeta('appName') + '_Model_' + appModels[model].getMeta('modelName');
572
573                         Tine.log.info('Found model with customfields property: ' + useModel);
574                         models.push([useModel, trans.n_(appModels[model].getMeta('recordName'), appModels[model].getMeta('recordsName'), 1)]);
575                     } else if (! customFieldModel) {
576                         useModel = 'Tine.' + appModels[model].getMeta('appName') + '.Model.' + appModels[model].getMeta('modelName');
577
578                         Tine.log.info('Found model: ' + useModel);
579                         models.push([useModel, trans.n_(appModels[model].getMeta('recordName'), appModels[model].getMeta('recordsName'), 1)]);
580                     }
581                 }
582             }
583         }
584         return models;
585     },
586
587     /**
588      * init actions
589      */
590     initActions: function() {
591         this.action_saveAndClose = new Ext.Action({
592             requiredGrant: this.requiredSaveGrant,
593             text: (this.saveAndCloseButtonText != '') ? this.app.i18n._(this.saveAndCloseButtonText) : i18n._('Ok'),
594             minWidth: 70,
595             ref: '../btnSaveAndClose',
596             scope: this,
597             // TODO: remove the defer when all subpanels use the deferByTicket mechanism
598             handler: function() { this.onSaveAndClose.defer(500, this); },
599             iconCls: 'action_saveAndClose'
600         });
601     
602         this.action_applyChanges = new Ext.Action({
603             requiredGrant: this.requiredSaveGrant,
604             text: i18n._('Apply'),
605             minWidth: 70,
606             ref: '../btnApplyChanges',
607             scope: this,
608             handler: this.onApplyChanges,
609             iconCls: 'action_applyChanges'
610         });
611         
612         this.action_cancel = new Ext.Action({
613             text: (this.cancelButtonText != '') ? this.app.i18n._(this.cancelButtonText) : i18n._('Cancel'),
614             minWidth: 70,
615             scope: this,
616             handler: this.onCancel,
617             iconCls: 'action_cancel'
618         });
619         
620         this.action_delete = new Ext.Action({
621             requiredGrant: 'deleteGrant',
622             text: i18n._('delete'),
623             minWidth: 70,
624             scope: this,
625             handler: this.onDelete,
626             iconCls: 'action_delete',
627             disabled: true
628         });
629
630         this.action_export = Tine.widgets.exportAction.getExportButton(this.recordClass, {
631             getExportOptions: this.getExportOptions.createDelegate(this)
632         }, Tine.widgets.exportAction.SCOPE_SINGLE);
633
634         // init actions
635         this.actionUpdater = new Tine.widgets.ActionUpdater({
636             recordClass: this.recordClass,
637             evalGrants: this.evalGrants
638         });
639
640         this.actionUpdater.addActions([
641             this.action_saveAndClose,
642             this.action_applyChanges,
643             this.action_cancel,
644             this.action_delete
645         ]);
646     },
647
648     /**
649      * get export options/data
650      */
651     getExportOptions: function() {
652         this.onRecordUpdate();
653         return {
654             recordData: this.record.data
655         };
656     },
657
658     /**
659      * init buttons
660      *
661      * use button order from preference
662      */
663     initButtons: function () {
664         this.fbar = [
665             '->'
666         ];
667
668         this.fbar.push(this.action_cancel, this.action_saveAndClose);
669
670         if (this.action_export) {
671             this.actionUpdater.addAction(this.action_export);
672             this.tbarItems = this.tbarItems || [];
673             this.tbarItems.push(this.action_export);
674         }
675
676         if (this.tbarItems && this.tbarItems.length) {
677             this.actionUpdater.addActions(this.tbarItems);
678             this.tbar = new Ext.Toolbar({
679                 items: this.tbarItems
680             });
681         }
682     },
683     
684     /**
685      * init container selector
686      */
687     initContainerSelector: function() {
688         if (this.showContainerSelector) {
689             this.containerSelectCombo = new Tine.widgets.container.SelectionComboBox({
690                 id: this.app.appName + 'EditDialogContainerSelector-' + Ext.id(),
691                 fieldLabel: i18n._('Saved in'),
692                 width: 300,
693                 listWidth: 300,
694                 name: this.recordClass.getMeta('containerProperty'),
695                 recordClass: this.recordClass,
696                 containerName: this.app.i18n.n_hidden(this.recordClass.getMeta('containerName'), this.recordClass.getMeta('containersName'), 1),
697                 containersName: this.app.i18n._hidden(this.recordClass.getMeta('containersName')),
698                 appName: this.app.appName,
699                 requiredGrant: this.evalGrants ? 'addGrant' : false,
700                 disabled: this.isContainerSelectorDisabled(),
701                 listeners: {
702                     scope: this,
703                     select: function() {    
704                         // enable or disable save button dependent to containers account grants
705                         // on edit: check editGrant, on add: check addGrant
706                         var grants = this.containerSelectCombo.selectedContainer
707                             ? this.containerSelectCombo.selectedContainer.account_grants : {},
708                             grantToCheck = (this.record.data.id) ? 'editGrant' : 'addGrant',
709                             enabled =  grants.hasOwnProperty(grantToCheck) && grants[grantToCheck]
710                                     || grants.hasOwnProperty('adminGrant') && grants.adminGrant ? true : false;
711
712                         this.action_saveAndClose.setDisabled(! enabled);
713                     }
714                 }
715             });
716             this.on('render', function() { this.getForm().add(this.containerSelectCombo); }, this);
717             
718             this.fbar = [
719                 i18n._('Saved in'),
720                 this.containerSelectCombo
721             ].concat(this.fbar);
722         }
723         
724     },
725     
726     /**
727      * checks if the container selector should be disabled (dependent on account grants of the container itself)
728      * @return {}
729      */
730     isContainerSelectorDisabled: function() {
731         if (this.record) {
732             var cp = this.recordClass.getMeta('containerProperty'),
733                 container = this.record.data[cp],
734                 grants = (container && container.hasOwnProperty('account_grants')) ? container.account_grants : null,
735                 cond = false;
736                 
737             // check grants if record already exists and grants should be evaluated
738             if(this.evalGrants && this.record.data.id && grants) {
739                 cond = ! (grants.hasOwnProperty('editGrant') && grants.editGrant);
740             }
741             
742             return cond;
743         } else {
744             return false;
745         }
746     },
747     
748     /**
749      * init record to edit
750      */
751     initRecord: function() {
752         Tine.log.debug('init record with mode: ' + this.mode);
753         if (! this.record) {
754             Tine.log.debug('creating new default data record');
755             this.record = new this.recordClass(this.recordClass.getDefaultData(), 0);
756         }
757         
758         if (this.mode !== 'local') {
759             if (this.record && this.record.id) {
760                 this.loadRemoteRecord();
761             } else {
762                 this.onRecordLoad.defer(10, this);
763             }
764         } else {
765             // note: in local mode we expect a valid record
766             if (! Ext.isFunction(this.record.beginEdit)) {
767                 this.record = this.recordProxy.recordReader({responseText: this.record});
768             }
769             this.onRecordLoad.defer(10, this);
770         }
771     },
772     
773     /**
774      * load record via record proxy
775      */
776     loadRemoteRecord: function() {
777         Tine.log.info('initiating record load via proxy');
778         this.loadRequest = this.recordProxy.loadRecord(this.record, {
779             scope: this,
780             success: function(record) {
781                 this.record = record;
782                 this.onRecordLoad();
783             }
784         });
785     },
786
787     /**
788      * copy this.record record
789      */
790     doCopyRecord: function() {
791         this.record = this.doCopyRecordToReturn(this.record);
792     },
793
794     /**
795      * Copy record and returns "new record with same settings"
796      *
797      * @param record
798      */
799     doCopyRecordToReturn: function(record) {
800         var omitFields = this.recordClass.getMeta('copyOmitFields') || [];
801         // always omit id + notes + attachments
802         omitFields = omitFields.concat(['id', 'notes', 'attachments', 'relations']);
803
804         var fieldsToCopy = this.recordClass.getFieldNames().diff(omitFields),
805             recordData = Ext.copyTo({}, record.data, fieldsToCopy);
806
807         var resetProperties = {
808             alarms:    ['id', 'record_id', 'sent_time', 'sent_message'],
809             relations: ['id', 'own_id', 'created_by', 'creation_time', 'last_modified_by', 'last_modified_time']
810         };
811
812         var setProperties = {alarms: {sent_status: 'pending'}};
813
814         Ext.iterate(resetProperties, function(property, properties) {
815             if (recordData.hasOwnProperty(property)) {
816                 var r = recordData[property];
817                 for (var index = 0; index < r.length; index++) {
818                     Ext.each(properties,
819                         function(prop) {
820                             r[index][prop] = null;
821                         }
822                     );
823                 }
824             }
825         });
826
827         Ext.iterate(setProperties, function(property, properties) {
828             if (recordData.hasOwnProperty(property)) {
829                 var r = recordData[property];
830                 for (var index = 0; index < r.length; index++) {
831                     Ext.iterate(properties,
832                         function(prop, value) {
833                             r[index][prop] = value;
834                         }
835                     );
836                 }
837             }
838         });
839
840         return new this.recordClass(recordData, 0);
841     },
842
843     
844     /**
845      * executed after record got updated from proxy
846      */
847     onRecordLoad: function() {
848         // interrupt process flow until dialog is rendered
849         if (! this.rendered) {
850             this.onRecordLoad.defer(250, this);
851             return;
852         }
853         Tine.log.debug('Tine.widgets.dialog.EditDialog::onRecordLoad() - Loading of the following record completed:');
854         Tine.log.debug(this.record);
855         
856         if (this.copyRecord) {
857             this.doCopyRecord();
858             this.window.setTitle(String.format(i18n._('Copy {0}'), this.i18nRecordName));
859         } else {
860             if (! this.record.id) {
861                 this.window.setTitle(String.format(i18n._('Add New {0}'), this.i18nRecordName));
862             } else {
863                 this.window.setTitle(String.format(i18n._('Edit {0} "{1}"'), this.i18nRecordName, this.record.getTitle()));
864             }
865         }
866         
867         var ticketFn = this.onAfterRecordLoad.deferByTickets(this),
868             wrapTicket = ticketFn();
869         
870         this.fireEvent('load', this, this.record, ticketFn);
871         wrapTicket();
872     },
873     
874     // finally load the record into the form
875     onAfterRecordLoad: function() {
876         var _ = window.lodash,
877             form = this.getForm();
878         
879         if (form) {
880             form.loadRecord(this.record);
881             form.clearInvalid();
882         }
883         
884         if (this.record && this.record.hasOwnProperty('data') && Ext.isObject(this.record.data[this.recordClass.getMeta('containerProperty')])) {
885             this.updateToolbars(this.record, this.recordClass.getMeta('containerProperty'));
886         }
887         
888         // add current timestamp as id, if this is a dependent record 
889         if (this.modelConfig && this.modelConfig.isDependent == true && this.record.id == 0) {
890             this.record.set('id', (new Date()).getTime());
891         }
892
893         // apply grants to fields with requiredGrant prop
894         if (this.evalGrants) {
895             this.getForm().items.each(function (f) {
896                 if (f.isFormField && f.requiredGrant !== undefined) {
897                     var hasRequiredGrant = _.get(this.record, this.recordClass.getMeta('grantsPath') + '.' + f.requiredGrant);
898                     f.setDisabled(!hasRequiredGrant);
899                 }
900             }, this);
901         }
902
903         this.checkStates.defer(100, this);
904
905         if (this.loadMask && !this.saving) {
906             this.loadMask.hide();
907         }
908     },
909     
910     /**
911      * executed when record gets updated from form
912      */
913     onRecordUpdate: function(callback, scope) {
914         var form = this.getForm();
915
916         // merge changes from form into record
917         form.updateRecord(this.record);
918
919         //TODO Use Promises instead of Tickets if async is needed
920         this.fireEvent('recordUpdate', this, this.record);
921     },
922     
923     /**
924      * @private
925      */
926     onRender : function(ct, position){
927         Tine.widgets.dialog.EditDialog.superclass.onRender.call(this, ct, position);
928         
929         // generalized keybord map for edit dlgs
930         new Ext.KeyMap(this.el, [
931             {
932                 key: [10,13], // ctrl + return
933                 ctrl: true,
934                 scope: this,
935                 fn: function() {
936                     if (this.getForm().hasOwnProperty('items')) {
937                         // force set last selected field
938                         this.getForm().items.each(function(item) {
939                             if (item.hasFocus) {
940                                 item.setValue(item.getRawValue());
941                             }
942                         }, this);
943                     }
944                     this.action_saveAndClose.execute();
945                 }
946             }
947         ]);
948         
949         if (this.loadMask !== false && this.i18nRecordName) {
950             this.loadMask = new Ext.LoadMask(ct, {msg: String.format(i18n._('Transferring {0}...'), this.i18nRecordName)});
951             this.loadMask.show();
952         }
953
954         // init change event
955         var form = this.getForm().items.each(function(item) {
956             this.relayEvents(item, ['change', 'select']);
957         }, this);
958         this.on('change', this.checkStates, this, {buffer: 100});
959         this.on('select', this.checkStates, this, {buffer: 100});
960     },
961     
962     /**
963      * update (action updateer) top and bottom toolbars
964      */
965     updateToolbars: function(record, containerField) {
966         if (! this.evalGrants) {
967             return;
968         }
969         
970         var actions = [
971             this.action_saveAndClose,
972             this.action_applyChanges,
973             this.action_delete,
974             this.action_cancel
975         ];
976         this.actionUpdater.updateActions(record);
977     },
978
979     /**
980      * get top toolbar
981      */
982     getToolbar: function() {
983         return this.getTopToolbar();
984     },
985     
986     /**
987      * is form valid?
988      * 
989      * @return {Boolean}
990      */
991     isValid: function() {
992         var me = this;
993         return new Promise(function (fulfill, reject) {
994             if (me.getForm().isValid()) {
995                 fulfill(true);
996             } else {
997                 reject(me.getValidationErrorMessage())
998             }
999         });
1000     },
1001
1002     /**
1003      * vaidates on multiple edit
1004      * 
1005      * @return {Boolean}
1006      */
1007     isMultipleValid: function() {
1008         return true;
1009     },
1010     
1011     /**
1012      * @private
1013      */
1014     onCancel : function(){
1015         this.fireEvent('cancel');
1016         this.purgeListeners();
1017         this.window.close();
1018     },
1019     
1020     /**
1021      * @private
1022      */
1023     onSaveAndClose: function() {
1024         this.fireEvent('saveAndClose');
1025         this.onApplyChanges(true);
1026     },
1027     
1028     /**
1029      * generic apply changes handler
1030      * @param {Boolean} closeWindow
1031      */
1032     onApplyChanges: function(closeWindow) {
1033         if (this.saving) {
1034             return;
1035         }
1036         this.saving = true;
1037
1038         this.loadMask.show();
1039
1040         var ticketFn = this.doApplyChanges.deferByTickets(this, [closeWindow]),
1041             wrapTicket = ticketFn();
1042
1043         this.fireEvent('save', this, this.record, ticketFn);
1044         wrapTicket();
1045     },
1046     
1047     /**
1048      * is called from onApplyChanges
1049      * @param {Boolean} closeWindow
1050      */
1051     doApplyChanges: function(closeWindow) {
1052         // we need to sync record before validating to let (sub) panels have 
1053         // current data of other panels
1054         this.onRecordUpdate();
1055
1056         // quit copy mode
1057         this.copyRecord = false;
1058
1059         var isValid = this.isValid(),
1060             vBool = !! isValid,
1061             me = this;
1062
1063         if (Ext.isDefined(isValid) && ! Ext.isFunction(isValid.then)) {
1064             // convert legacy isValid into promise
1065             isValid = new Promise(function (fulfill, reject) {;
1066                 return vBool ? fulfill(true) : reject(me.getValidationErrorMessage());
1067             });
1068         }
1069
1070         isValid.then(function () {
1071             if (me.mode !== 'local') {
1072                 me.recordProxy.saveRecord(me.record, {
1073                     scope: me,
1074                     success: function (record) {
1075                         // override record with returned data
1076                         me.record = record;
1077                         if (!Ext.isFunction(me.window.cascade)) {
1078                             // update form with this new data
1079                             // NOTE: We update the form also when window should be closed,
1080                             //       cause sometimes security restrictions might prevent
1081                             //       closing of native windows
1082                             me.onRecordLoad();
1083                         }
1084                         var ticketFn = me.onAfterApplyChanges.deferByTickets(me, [closeWindow]),
1085                             wrapTicket = ticketFn();
1086
1087                         me.fireEvent('update', Ext.util.JSON.encode(me.record.data), me.mode, me, ticketFn);
1088                         wrapTicket();
1089                     },
1090                     failure: me.onRequestFailed,
1091                     timeout: 300000 // 5 minutes
1092                 }, me.getAdditionalSaveParams(me));
1093             } else {
1094                 me.onRecordLoad();
1095                 var ticketFn = me.onAfterApplyChanges.deferByTickets(me, [closeWindow]),
1096                     wrapTicket = ticketFn();
1097
1098                 me.fireEvent('update', Ext.util.JSON.encode(me.record.data), me.mode, me, ticketFn);
1099                 wrapTicket();
1100             }
1101         }, function (message) {
1102             me.saving = false;
1103             me.loadMask.hide();
1104             Ext.MessageBox.alert(i18n._('Errors'), message);
1105         });
1106     },
1107
1108     /**
1109      * returns additional save params
1110      *
1111      * @param {EditDialog} me
1112      * @returns {{duplicateCheck: boolean}}
1113      */
1114     getAdditionalSaveParams: function(me) {
1115         return {
1116             duplicateCheck: me.doDuplicateCheck
1117         };
1118     },
1119     
1120     onAfterApplyChanges: function(closeWindow) {
1121         this.window.rename(this.windowNamePrefix + this.record.id);
1122         this.saving = false;
1123         
1124         if (closeWindow) {
1125             this.window.fireEvent('saveAndClose');
1126             this.purgeListeners();
1127             this.window.close();
1128         } else {
1129             this.loadMask.hide();
1130         }
1131     },
1132     
1133     /**
1134      * get validation error message
1135      * 
1136      * @return {String}
1137      */
1138     getValidationErrorMessage: function() {
1139         return i18n._('Please fix the errors noted.');
1140     },
1141     
1142     /**
1143      * generic delete handler
1144      */
1145     onDelete: function(btn, e) {
1146         Ext.MessageBox.confirm(i18n._('Confirm'), String.format(i18n._('Do you really want to delete this {0}?'), this.i18nRecordName), function(_button) {
1147             if(btn == 'yes') {
1148                 var deleteMask = new Ext.LoadMask(this.getEl(), {msg: String.format(i18n._('Deleting {0}'), this.i18nRecordName)});
1149                 deleteMask.show();
1150                 
1151                 this.recordProxy.deleteRecords(this.record, {
1152                     scope: this,
1153                     success: function() {
1154                         this.purgeListeners();
1155                         this.window.close();
1156                     },
1157                     failure: function () {
1158                         Ext.MessageBox.alert(i18n._('Failed'), String.format(i18n._('Could not delete {0}.'), this.i18nRecordName));
1159                         Ext.MessageBox.hide();
1160                     }
1161                 });
1162             }
1163         });
1164     },
1165     
1166     /**
1167      * duplicate(s) found exception handler
1168      * 
1169      * @param {Object} exception
1170      */
1171     onDuplicateException: function(exception) {
1172         var resolveGridPanel = new Tine.widgets.dialog.DuplicateResolveGridPanel({
1173             app: this.app,
1174             store: new Tine.widgets.dialog.DuplicateResolveStore({
1175                 app: this.app,
1176                 recordClass: this.recordClass,
1177                 recordProxy: this.recordProxy,
1178                 data: {
1179                     clientRecord: exception.clientRecord,
1180                     duplicates: exception.duplicates
1181                 }
1182             }),
1183             fbar: [
1184                 '->',
1185                 this.action_cancel,
1186                 this.action_saveAndClose
1187             ]
1188         });
1189         
1190         // intercept save handler
1191         resolveGridPanel.btnSaveAndClose.setHandler(function(btn, e) {
1192             var resolveStrategy = resolveGridPanel.store.resolveStrategy;
1193             
1194             // action discard -> close window
1195             if (resolveStrategy == 'discard') {
1196                 return this.onCancel();
1197             }
1198             
1199             this.record = resolveGridPanel.store.getResolvedRecord();
1200             
1201             // quit copy mode before populating form with resolved data
1202             this.copyRecord = false;
1203             this.onRecordLoad();
1204             
1205             mainCardPanel.layout.setActiveItem(this.id);
1206             resolveGridPanel.doLayout();
1207             
1208             this.doDuplicateCheck = false;
1209             this.onSaveAndClose();
1210         }, this);
1211         
1212         // place in viewport
1213         this.window.setTitle(String.format(i18n._('Resolve Duplicate {0} Suspicion'), this.i18nRecordName));
1214         var mainCardPanel = this.findParentBy(function(p) {return p.isWindowMainCardPanel });
1215         mainCardPanel.add(resolveGridPanel);
1216         mainCardPanel.layout.setActiveItem(resolveGridPanel.id);
1217         resolveGridPanel.doLayout();
1218     },
1219     
1220     /**
1221      * generic request exception handler
1222      * 
1223      * @param {Object} exception
1224      */
1225     onRequestFailed: function(exception) {
1226         this.saving = false;
1227
1228         if (this.exceptionHandlingMap && this.exceptionHandlingMap[exception.code] && typeof this.exceptionHandlingMap[exception.code] === 'function') {
1229             this.exceptionHandlingMap[exception.code](exception);
1230
1231         } else if (exception.code == 629) {
1232             this.onDuplicateException.apply(this, arguments);
1233
1234         } else {
1235             Tine.Tinebase.ExceptionHandler.handleRequestException(exception);
1236         }
1237
1238         this.loadMask.hide();
1239     },
1240     
1241     /**
1242      * creates the relations panel, if relations are defined
1243      */
1244     initRelationsPanel: function() {
1245         if (! this.hideRelationsPanel && this.recordClass && this.recordClass.hasField('relations')) {
1246             // init relations panel before onRecordLoad
1247             if (! this.relationsPanel) {
1248                 this.relationsPanel = new Tine.widgets.relation.GenericPickerGridPanel({ anchor: '100% 100%', editDialog: this });
1249             }
1250             // interrupt process flow until dialog is rendered
1251             if (! this.rendered) {
1252                 this.initRelationsPanel.defer(250, this);
1253                 return;
1254             }
1255             // add relations panel if this is rendered
1256             if (this.items.items[0]) {
1257                 this.items.items[0].add(this.relationsPanel);
1258             }
1259             
1260             Tine.log.debug('Tine.widgets.dialog.EditDialog::initRelationsPanel() - Initialized relations panel and added to dialog tab items.');
1261         }
1262     },
1263
1264     /**
1265      * create notes panel
1266      */
1267     initNotesPanel: function() {
1268         // This dialog is pretty generic but for some cases it's used in a differend way
1269         if(this.displayNotes == true) {
1270             this.items.items.push(new Tine.widgets.activities.ActivitiesGridPanel({
1271                 anchor: '100% 100%',
1272                 editDialog: this
1273             }));
1274         }
1275     },
1276
1277     /**
1278      * creates attachments panel
1279      */
1280     initAttachmentsPanel: function() {
1281         if (! this.attachmentsPanel && ! this.hideAttachmentsPanel && this.recordClass && this.recordClass.hasField('attachments') && Tine.Tinebase.registry.get('filesystemAvailable')) {
1282             this.attachmentsPanel = new Tine.widgets.dialog.AttachmentsGridPanel({ anchor: '100% 100%', editDialog: this }); 
1283             this.items.items.push(this.attachmentsPanel);
1284         }
1285     }
1286 });