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