merge.js (9602B)
1 /* 2 ***** BEGIN LICENSE BLOCK ***** 3 4 Copyright © 2009 Center for History and New Media 5 George Mason University, Fairfax, Virginia, USA 6 http://zotero.org 7 8 This file is part of Zotero. 9 10 Zotero is free software: you can redistribute it and/or modify 11 it under the terms of the GNU Affero General Public License as published by 12 the Free Software Foundation, either version 3 of the License, or 13 (at your option) any later version. 14 15 Zotero is distributed in the hope that it will be useful, 16 but WITHOUT ANY WARRANTY; without even the implied warranty of 17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 GNU Affero General Public License for more details. 19 20 You should have received a copy of the GNU Affero General Public License 21 along with Zotero. If not, see <http://www.gnu.org/licenses/>. 22 23 ***** END LICENSE BLOCK ***** 24 */ 25 26 var Zotero_Merge_Window = new function () { 27 var _wizard = null; 28 var _wizardPage = null; 29 var _mergeGroup = null; 30 var _numObjects = null; 31 32 var _io = null; 33 var _conflicts = null; 34 var _merged = []; 35 var _pos = -1; 36 37 this.init = function () { 38 _wizard = document.getElementsByTagName('wizard')[0]; 39 _wizardPage = document.getElementsByTagName('wizardpage')[0]; 40 _mergeGroup = document.getElementsByTagName('zoteromergegroup')[0]; 41 42 _wizard.setAttribute('width', Math.min(980, screen.width - 20)); 43 _wizard.setAttribute('height', Math.min(718, screen.height - 30)); 44 45 // Set font size from pref 46 Zotero.setFontSize(_wizardPage); 47 48 _wizard.getButton('cancel').setAttribute('label', Zotero.getString('sync.cancel')); 49 50 _io = window.arguments[0]; 51 // Not totally clear when this is necessary 52 if (window.arguments[0].wrappedJSObject) { 53 _io = window.arguments[0].wrappedJSObject; 54 } 55 _conflicts = _io.dataIn.conflicts; 56 if (!_conflicts.length) { 57 // TODO: handle no conflicts 58 return; 59 } 60 61 if (_io.dataIn.type) { 62 _mergeGroup.type = _io.dataIn.type; 63 } 64 _mergeGroup.leftCaption = _io.dataIn.captions[0]; 65 _mergeGroup.rightCaption = _io.dataIn.captions[1]; 66 _mergeGroup.mergeCaption = _io.dataIn.captions[2]; 67 68 _resolveAllCheckbox = document.getElementById('resolve-all'); 69 if (_conflicts.length == 1) { 70 _resolveAllCheckbox.hidden = true; 71 } 72 else { 73 _mergeGroup.onSelectionChange = _updateResolveAllCheckbox; 74 } 75 76 _numObjects = document.getElementById('zotero-merge-num-objects'); 77 document.getElementById('zotero-merge-total-objects').value = _conflicts.length; 78 79 this.onNext(); 80 } 81 82 83 this.onBack = function () { 84 _merged[_pos] = _getCurrentMergeInfo(); 85 86 _pos--; 87 88 if (_pos == 0) { 89 _wizard.canRewind = false; 90 } 91 92 _updateGroup(); 93 94 var nextButton = _wizard.getButton("next"); 95 96 if (Zotero.isMac) { 97 nextButton.setAttribute("hidden", "false"); 98 _wizard.getButton("finish").setAttribute("hidden", "true"); 99 } 100 else { 101 var buttons = document.getAnonymousElementByAttribute(_wizard, "anonid", "Buttons"); 102 var deck = document.getAnonymousElementByAttribute(buttons, "anonid", "WizardButtonDeck"); 103 deck.selectedIndex = 1; 104 } 105 106 _setInstructionsString(nextButton.label); 107 } 108 109 110 this.onNext = function () { 111 // At end or resolving all 112 if (_pos + 1 == _conflicts.length || _resolveAllCheckbox.checked) { 113 return true; 114 } 115 116 // First page 117 if (_pos == -1) { 118 _wizard.canRewind = false; 119 } 120 // Subsequent pages 121 else { 122 _wizard.canRewind = true; 123 _merged[_pos] = _getCurrentMergeInfo(); 124 } 125 126 _pos++; 127 128 try { 129 _updateGroup(); 130 } 131 catch (e) { 132 _error(e); 133 return; 134 } 135 136 _updateResolveAllCheckbox(); 137 138 if (_isLastConflict()) { 139 _showFinishButton(); 140 } 141 else { 142 _showNextButton(); 143 } 144 145 return false; 146 } 147 148 149 this.onFinish = function () { 150 // If using one side for all remaining, update merge object 151 if (!_isLastConflict() && _resolveAllCheckbox.checked) { 152 let side = _mergeGroup.rightpane.getAttribute("selected") == "true" ? 'right' : 'left' 153 for (let i = _pos; i < _conflicts.length; i++) { 154 _merged[i] = { 155 data: _getMergeDataWithSide(i, side), 156 selected: side 157 }; 158 } 159 } 160 else { 161 _merged[_pos] = _getCurrentMergeInfo(); 162 } 163 164 _merged.forEach(function (x, i, a) { 165 // Add key 166 x.data.key = _conflicts[i].left.key || _conflicts[i].right.key; 167 // Add back version 168 if (x.data) { 169 x.data.version = _conflicts[i][x.selected].version; 170 } 171 }) 172 173 _io.dataOut = _merged; 174 return true; 175 } 176 177 178 this.onCancel = function () { 179 // if already merged, ask 180 } 181 182 183 this.onResolveAllChange = function (resolveAll) { 184 if (resolveAll || _isLastConflict()) { 185 _showFinishButton(); 186 } 187 else { 188 _showNextButton(); 189 } 190 } 191 192 193 function _updateGroup() { 194 // Adjust counter 195 _numObjects.value = _pos + 1; 196 197 let data = {}; 198 Object.assign(data, _conflicts[_pos]); 199 var mergeInfo = _getMergeInfo(_pos); 200 data.merge = mergeInfo.data; 201 data.selected = mergeInfo.selected; 202 if (!_conflicts[_pos].libraryID) { 203 throw new Error("libraryID not provided in conflict object"); 204 } 205 _mergeGroup.libraryID = _conflicts[_pos].libraryID; 206 _mergeGroup.data = data; 207 208 _updateResolveAllCheckbox(); 209 } 210 211 212 function _getCurrentMergeInfo() { 213 return { 214 data: _mergeGroup.merged, 215 selected: _mergeGroup.leftpane.getAttribute("selected") == "true" ? "left" : "right" 216 }; 217 } 218 219 220 /** 221 * Get the default or previously chosen merge info for a given position 222 * 223 * @param {Integer} pos 224 * @return {Object} - Object with 'data' (JSON field data) and 'selected' ('left', 'right') properties 225 */ 226 function _getMergeInfo(pos) { 227 // If data already selected, use that 228 if (_merged[pos]) { 229 return _merged[pos]; 230 } 231 // If either side was deleted, use other side 232 if (_conflicts[pos].left.deleted) { 233 let mergeInfo = { 234 data: {}, 235 selected: 'right' 236 }; 237 Object.assign(mergeInfo.data, _conflicts[pos].right); 238 return mergeInfo; 239 } 240 if (_conflicts[pos].right.deleted) { 241 let mergeInfo = { 242 data: {}, 243 selected: 'left' 244 }; 245 Object.assign(mergeInfo.data, _conflicts[pos].left); 246 return mergeInfo; 247 } 248 // Apply changes from each side and pick most recent version for conflicting fields 249 var mergeInfo = { 250 data: {} 251 }; 252 Object.assign(mergeInfo.data, _conflicts[pos].left) 253 Zotero.DataObjectUtilities.applyChanges(mergeInfo.data, _conflicts[pos].changes); 254 if (_conflicts[pos].left.dateModified > _conflicts[pos].right.dateModified) { 255 var side = 0; 256 } 257 // Use remote if remote Date Modified is later or same 258 else { 259 var side = 1; 260 } 261 Zotero.DataObjectUtilities.applyChanges( 262 mergeInfo.data, _conflicts[pos].conflicts.map(x => x[side]) 263 ); 264 mergeInfo.selected = side ? 'right' : 'left'; 265 return mergeInfo; 266 } 267 268 269 /** 270 * Get the merge data using a given side at a given position 271 * 272 * @param {Integer} pos 273 * @param {String} side - 'left' or 'right' 274 * @return {Object} - JSON field data 275 */ 276 function _getMergeDataWithSide(pos, side) { 277 if (!side) { 278 throw new Error("Side not provided"); 279 } 280 281 if (_conflicts[pos].left.deleted || _conflicts[pos].right.deleted) { 282 return _conflicts[pos][side]; 283 } 284 285 var data = {}; 286 Object.assign(data, _conflicts[pos].left) 287 Zotero.DataObjectUtilities.applyChanges(data, _conflicts[pos].changes); 288 Zotero.DataObjectUtilities.applyChanges( 289 data, _conflicts[pos].conflicts.map(x => x[side == 'left' ? 0 : 1]) 290 ); 291 return data; 292 } 293 294 295 function _updateResolveAllCheckbox() { 296 if (_mergeGroup.type == 'file') { 297 if (_mergeGroup.rightpane.getAttribute("selected") == 'true') { 298 var label = 'resolveAllRemote'; 299 } 300 else { 301 var label = 'resolveAllLocal'; 302 } 303 } 304 else { 305 if (_mergeGroup.rightpane.getAttribute("selected") == 'true') { 306 var label = 'resolveAllRemoteFields'; 307 } 308 else { 309 var label = 'resolveAllLocalFields'; 310 } 311 } 312 _resolveAllCheckbox.label = Zotero.getString('sync.conflict.' + label); 313 } 314 315 316 function _isLastConflict() { 317 return (_pos + 1) == _conflicts.length; 318 } 319 320 321 function _showNextButton() { 322 var nextButton = _wizard.getButton("next"); 323 324 if (Zotero.isMac) { 325 nextButton.setAttribute("hidden", "false"); 326 _wizard.getButton("finish").setAttribute("hidden", "true"); 327 } 328 else { 329 var buttons = document.getAnonymousElementByAttribute(_wizard, "anonid", "Buttons"); 330 var deck = document.getAnonymousElementByAttribute(buttons, "anonid", "WizardButtonDeck"); 331 deck.selectedIndex = 1; 332 } 333 334 _setInstructionsString(nextButton.label); 335 } 336 337 338 function _showFinishButton() { 339 var finishButton = _wizard.getButton("finish"); 340 341 if (Zotero.isMac) { 342 _wizard.getButton("next").setAttribute("hidden", "true"); 343 finishButton.setAttribute("hidden", "false"); 344 } 345 // Windows uses a deck to switch between the Next and Finish buttons 346 // TODO: check Linux 347 else { 348 var buttons = document.getAnonymousElementByAttribute(_wizard, "anonid", "Buttons"); 349 var deck = document.getAnonymousElementByAttribute(buttons, "anonid", "WizardButtonDeck"); 350 deck.selectedIndex = 0; 351 } 352 353 _setInstructionsString(finishButton.label); 354 } 355 356 357 function _setInstructionsString(buttonName) { 358 switch (_mergeGroup.type) { 359 case 'file': 360 var msg = 'fileChanged'; 361 break; 362 363 default: 364 // TODO: maybe don't always call it 'item' 365 var msg = 'itemChanged'; 366 } 367 368 msg = Zotero.getString('sync.conflict.' + msg, buttonName) 369 document.getElementById('zotero-merge-instructions').value = msg; 370 } 371 372 373 function _error(e) { 374 Zotero.debug(e); 375 _io.error = e; 376 _wizard.getButton('cancel').click(); 377 } 378 }