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