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