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