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