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