avoid concurrency conflicts when merging records
[tine20] / tine20 / Tinebase / js / widgets / dialog / DuplicateMergeDialog.js
1 /*
2  * Tine 2.0
3  * 
4  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
5  * @author      Alexander Stintzing <a.stintzing@metaways.de>
6  * @copyright   Copyright (c) 2007-2012 Metaways Infosystems GmbH (http://www.metaways.de)
7  */
8 Ext.ns('Tine.widgets.dialog');
9
10 /**
11  * @namespace   Tine.widgets.dialog
12  * @class       Tine.widgets.dialog.DuplicateMergeDialog
13  * @extends     Ext.FormPanel
14  * @author      Alexander Stintzing <a.stintzing@metaways.de>
15  * 
16  * @param {Object} config The configuration options.
17  */
18
19 Tine.widgets.dialog.DuplicateMergeDialog = Ext.extend(Ext.FormPanel, {
20     
21     appName : null,
22     app: null,
23     modelName: null,
24     
25     recordClass: null,
26     recordProxy: null,
27     loadMask: null,
28     
29     layout : 'fit',
30     border : false,
31     cls : 'tw-editdialog',    
32
33     labelAlign : 'top',
34
35     anchor : '100% 100%',
36     deferredRender : false,
37     buttonAlign : null,
38     bufferResize : 500,
39     
40     /**
41      * init component
42      */
43     initComponent: function() {
44
45         if (!this.app) {
46             this.app = Tine.Tinebase.appMgr.get(this.appName);
47         }
48
49         this.recordClass = Tine.Tinebase.data.RecordMgr.get(this.appName, this.modelName);
50         this.selections = Ext.decode(this.selections);
51         this.records = [];
52         
53         this.recordProxy = Tine[this.appName][this.recordClass.getMeta('recordName').toLowerCase() + 'Backend'];
54         
55         if(!this.recordProxy) {
56             return;
57         }
58         
59         Ext.each(this.selections, function(rec) {
60             this.records.push(this.recordProxy.recordReader({responseText: Ext.encode(rec)}));
61         }, this );
62         
63         Tine.log.debug('initComponent: appName: ', this.appName);
64         Tine.log.debug('initComponent: app: ', this.app);
65
66         // init actions
67         this.initActions();
68
69         // get items for this dialog
70         this.items = this.getFormItems();
71
72         Tine.widgets.dialog.DuplicateMergeDialog.superclass.initComponent.call(this);
73     },
74     /**
75      * initializes the actions for the buttons
76      */
77     initActions: function() {
78         this.action_cancel = new Ext.Action({
79             text : _('Cancel'),
80             minWidth : 70,
81             scope : this,
82             handler : this.onCancel,
83             iconCls : 'action_cancel'
84         });
85
86         this.action_update = new Ext.Action({
87             text : _('OK'),
88             minWidth : 70,
89             scope : this,
90             handler : this.onUpdate,
91             iconCls : 'action_saveAndClose'
92         });
93         
94         this.fbar = [ '->', this.action_cancel, this.action_update ];
95     },
96     
97     /**
98      * is called to resolve the records
99      */
100     onUpdate: function() {
101         var store = this.gridPanel.getStore(),
102             updateRecord = store.getResolvedRecord(),
103             removeRecords = [],
104             allRecords = store.duplicates.concat([store.clientRecord]),
105             strategy = this.gridPanel.actionCombo.getValue();
106         
107         Tine.log.debug('Tine.widgets.dialog.DuplicateMergeDialog::onUpdate() - strategy:');
108         Tine.log.debug(this.gridPanel.actionCombo.getValue());
109         
110         Tine.log.debug('Tine.widgets.dialog.DuplicateMergeDialog::onUpdate() - allRecords:');
111         Tine.log.debug(allRecords);
112
113         if (strategy === 'mergeTheirs') {
114             // we need to use the first id and seq in update record
115             updateRecord.id = allRecords[0].id;
116             updateRecord.set('seq', allRecords[0].get('seq'));
117         } else {
118             // we need to use the second id + seq (from clientRecord) in update record
119             updateRecord.id = allRecords[1].id;
120             updateRecord.set('seq', allRecords[1].get('seq'));
121         }
122         updateRecord.data.id = updateRecord.id;
123
124         Tine.log.debug('Tine.widgets.dialog.DuplicateMergeDialog::onUpdate() - updateRecord:');
125         Tine.log.debug(updateRecord);
126         
127         Ext.each(allRecords, function(rec) {
128             if (rec.id != updateRecord.id) {
129                 removeRecords.push(rec);
130             }
131         });
132
133         Tine.log.debug('Tine.widgets.dialog.DuplicateMergeDialog::onUpdate() - removeRecords:');
134         Tine.log.debug(removeRecords);
135         
136         this.loadMask = new Ext.LoadMask(this.getEl(), {msg: _('Merging Records...')});
137         this.loadMask.show();
138         
139         this.recordProxy.saveRecord(updateRecord, {
140             scope: this,
141             success: function(newRecord) {
142                 this.removeDuplicates(removeRecords);
143             },
144             failure: function(failure) {
145                 this.onFailure('update', failure);
146             }
147         });
148     },
149     
150     /**
151      * remove duplicate records, when update of the new record succeeded
152      * @param {Array} removeRecords
153      */
154     removeDuplicates: function(removeRecords) {
155         this.recordProxy.deleteRecords(removeRecords, {
156             scope: this,
157             success: function() {
158                 this.onSuccess();
159             },
160             failure: function(failure) {
161                 this.onFailure('remove', failure);
162             }
163         });
164     },
165     
166     /**
167      * is called when a failure on saving the merge result or deleting the duplicate happens
168      * @param {} step
169      * @param {} failure
170      */
171     onFailure: function(step, failure) {
172         if(step == 'update') {
173              Tine.Tinebase.ExceptionHandler.handleRequestException(failure, function() { if (this.loadMask) this.loadMask.hide(); }, this);
174         } else {
175              Ext.MessageBox.alert(_('Merge Failed'), String.format(_('The merge succeeded, but the duplicate {0} could not be deleted.'), this.recordClass.getRecordName()), function() { Tine.Tinebase.ExceptionHandler.handleRequestException(failure); if (this.loadMask) this.loadMask.hide();}, this);
176         }
177     },
178     /**
179      * returns the form items of this panel
180      * @return {}
181      */
182     getFormItems: function() {
183         return {
184             layout: 'border',
185             items: [{
186                 region: 'center',
187                 layout: 'fit',
188                 items: [new Tine.widgets.dialog.DuplicateResolveGridPanel({
189                     ref: '../../gridPanel',
190                     app: this.app,
191                     recordClass: this.recordClass,
192                     store: new Tine.widgets.dialog.DuplicateResolveStore({
193                         recordClass: this.recordClass,
194                         defaultResolveStrategy: 'mergeMine',
195                         app: this.app,
196                         data: {
197                             clientRecord: this.records.shift(),
198                             duplicates: this.records
199                         },
200                         // TODO we might add some grants handling here to show the user if she can't edit, update or delete the records
201                         // strategy can't be set to 'keep' here
202                         checkEditGrant: Ext.emptyFn
203                     }),
204                     listeners: {
205                         afterrender: function() {
206                             var recordsName = this.recordClass.getRecordsName(),
207                                 recordName = this.recordClass.getRecordName();
208                 
209                             // change actionComboListEls
210                             this.actionCombo.store = new Ext.data.ArrayStore({
211                                 id: 0,
212                                 fields: ['value', 'text'],
213                                 data: [
214                                     ['mergeMine', String.format(_('Merge {0}, prefer First'), recordsName)],
215                                     ['mergeTheirs', String.format(_('Merge {0}, prefer Second'), recordsName)]
216                                 ]
217                             });
218                             this.actionCombo.setWidth(400);
219                             this.onStoreLoad();
220                 
221                             // change title
222                             this.setTitle(String.format(_('Merge {0}'), recordsName));
223                             
224                             // change column headers
225                             this.getColumnModel().setColumnHeader(this.getColumnModel().getIndexById('clientValue'), String.format(_('First {0}'), recordName));
226                             this.getColumnModel().setColumnHeader(this.getColumnModel().getIndexById('value0'), String.format(_('Second {0}'), recordName));
227                             this.getColumnModel().setColumnHeader(this.getColumnModel().getIndexById('finalValue'), String.format(_('Final {0}'), recordName));
228                         }
229                     }
230                     })]
231             }]
232         };
233     },
234     
235     onSuccess: function() {
236         this.window.fireEvent('contentschange');
237         this.onCancel();
238     },
239     
240     onCancel: function() {
241         this.window.purgeListeners();
242         this.window.close();
243     }
244     
245 });
246
247 Tine.widgets.dialog.DuplicateMergeDialog.getWindow = function(config) {
248     return Tine.WindowFactory.getWindow({
249         width: 800,
250         height: 600,
251         name: Tine.widgets.dialog.ImportDialog.windowNamePrefix + Ext.id(),
252         contentPanelConstructor: 'Tine.widgets.dialog.DuplicateMergeDialog',
253         contentPanelConstructorConfig: config
254     });
255 };