Update conflict resolution for API syncing
This will appear much less frequently, since non-conflicting field changes on both sides can be resolved automatically, but genuine field conflicts still require manual conflict resolution. The merge pane is no longer editable, since the itembox code to do that is async and can't run in a modal window, but it's not really necessary, particularly with conflicts happening less frequently. TODO: - Remote item deletions - File conflicts - Maybe handle some edge cases where the conflicted items fail to save
This commit is contained in:
parent
7075300a17
commit
0aecaad761
17 changed files with 839 additions and 550 deletions
|
@ -82,19 +82,6 @@
|
|||
this.blurHandler = this.hideEditor;
|
||||
break;
|
||||
|
||||
case 'merge':
|
||||
this.clickByItem = true;
|
||||
break;
|
||||
|
||||
case 'mergeedit':
|
||||
this.clickable = true;
|
||||
this.editable = true;
|
||||
this.saveOnEdit = false;
|
||||
this.showTypeMenu = true;
|
||||
this.clickHandler = this.showEditor;
|
||||
this.blurHandler = this.hideEditor;
|
||||
break;
|
||||
|
||||
case 'fieldmerge':
|
||||
this.hideEmptyFields = true;
|
||||
this._fieldAlternatives = {};
|
||||
|
@ -1909,9 +1896,7 @@
|
|||
if(field === 'creator') {
|
||||
// Reset creator mode settings here so that flex attribute gets reset
|
||||
this.switchCreatorMode(row, (otherFields.fieldMode ? 1 : 0), true);
|
||||
Zotero.debug("HERE");
|
||||
if(Zotero.ItemTypes.getName(this.item.itemTypeID) === "bookSection") {
|
||||
Zotero.debug("YES");
|
||||
var creatorTypeLabels = document.getAnonymousNodes(this)[0].getElementsByClassName("creator-type-label");
|
||||
Zotero.debug(creatorTypeLabels[creatorTypeLabels.length-1] + "");
|
||||
document.getElementById("zotero-author-guidance").show({
|
||||
|
|
|
@ -44,73 +44,36 @@
|
|||
]]>
|
||||
</constructor>
|
||||
|
||||
<field name="_type"/>
|
||||
<property name="type" onget="return this._type;">
|
||||
<field name="_data"/>
|
||||
<property name="data" onget="return this._data;">
|
||||
<setter>
|
||||
<![CDATA[
|
||||
this._type = val;
|
||||
var hbox = document.getAnonymousNodes(this)[0];
|
||||
hbox.setAttribute('mergetype', val);
|
||||
]]>
|
||||
</setter>
|
||||
</property>
|
||||
|
||||
<property name="left" onget="return this._leftpane.ref;">
|
||||
<setter>
|
||||
<![CDATA[
|
||||
// TODO: Make sure object is the correct type
|
||||
|
||||
if (val == 'deleted') {
|
||||
this._leftpane.ref = 'deleted';
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for note or attachment
|
||||
if (val instanceof Zotero.Item) {
|
||||
this.type = this._getTypeFromItem(val);
|
||||
}
|
||||
|
||||
// Clone object so changes in merge pane don't affect it
|
||||
this._leftpane.ref = val.copy();
|
||||
this._leftpane.original = val;
|
||||
]]>
|
||||
</setter>
|
||||
</property>
|
||||
|
||||
<property name="right" onget="return this._rightpane.ref;">
|
||||
<setter>
|
||||
<![CDATA[
|
||||
// TODO: make sure left is set
|
||||
if (!this._leftpane.ref) {
|
||||
throw ("Left object must be set before setting <zoteromergegroup>.right");
|
||||
}
|
||||
|
||||
if (val == 'deleted') {
|
||||
this._rightpane.ref = 'deleted';
|
||||
}
|
||||
else {
|
||||
// TODO: Make sure object is the correct type
|
||||
|
||||
// Check for note or attachment if not already set
|
||||
if (this._leftpane.ref == 'deleted' && val instanceof Zotero.Item) {
|
||||
this.type = this._getTypeFromItem(val);
|
||||
}
|
||||
|
||||
// Clone object so changes in merge pane don't affect it
|
||||
this._rightpane.ref = val.copy();
|
||||
this._rightpane.original = val;
|
||||
}
|
||||
|
||||
this._data = val;
|
||||
this.refresh();
|
||||
]]>
|
||||
</setter>
|
||||
</property>
|
||||
|
||||
<property name="merge" onget="return this._mergepane.ref">
|
||||
<property name="merged" onget="return this._mergepane.data"/>
|
||||
|
||||
<field name="_type"/>
|
||||
<property name="type" onget="return this._type;">
|
||||
<setter>
|
||||
<![CDATA[
|
||||
// TODO: Make sure object is the correct type
|
||||
this._mergepane.ref = val;
|
||||
switch (val) {
|
||||
case 'item':
|
||||
case 'attachment':
|
||||
case 'note':
|
||||
case 'file':
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception(`Unsupported merge object type '${type}'`);
|
||||
}
|
||||
|
||||
this._type = val;
|
||||
var hbox = document.getAnonymousNodes(this)[0];
|
||||
hbox.setAttribute('mergetype', val);
|
||||
]]>
|
||||
</setter>
|
||||
</property>
|
||||
|
@ -119,51 +82,43 @@
|
|||
<property name="rightCaption" onset="this._rightpane.caption.label = val"/>
|
||||
<property name="mergeCaption" onset="this._mergepane.caption.label = val"/>
|
||||
|
||||
|
||||
<field name="_leftpane"/>
|
||||
<property name="leftpane" onget="return this._leftpane"/>
|
||||
<field name="_rightpane"/>
|
||||
<property name="rightpane" onget="return this._rightpane"/>
|
||||
<field name="_mergepane"/>
|
||||
<property name="mergepane" onget="return this._mergepane"/>
|
||||
|
||||
<property name="onSelectionChange"/>
|
||||
|
||||
<field name="_leftpane"/>
|
||||
<field name="_rightpane"/>
|
||||
<field name="_mergepane"/>
|
||||
|
||||
<method name="refresh">
|
||||
<body>
|
||||
<![CDATA[
|
||||
// Set merge pane to most recently changed object
|
||||
// If one object was deleted, set merge pane to other
|
||||
// TODO: use delete timestamp
|
||||
|
||||
if (this._leftpane.ref != 'deleted'
|
||||
&& this._rightpane.ref != 'deleted') {
|
||||
|
||||
var dm1 = this._leftpane.ref.getField('dateModified');
|
||||
if (dm1) {
|
||||
dm1 = Zotero.Date.sqlToDate(dm1);
|
||||
}
|
||||
|
||||
var dm2 = this._rightpane.ref.getField('dateModified');
|
||||
if (dm2) {
|
||||
dm2 = Zotero.Date.sqlToDate(dm2);
|
||||
}
|
||||
if (this._data.left.deleted && this._data.right.deleted) {
|
||||
throw new Exception("'left' and 'right' cannot both be deleted");
|
||||
}
|
||||
|
||||
if (this._leftpane.ref == 'deleted' || dm2 > dm1) {
|
||||
var mergeItem = this._rightpane.original;
|
||||
this._leftpane.removeAttribute("selected");
|
||||
this._rightpane.setAttribute("selected", "true");
|
||||
// Check for note or attachment
|
||||
this.type = this._getTypeFromObject(
|
||||
this._data.left.deleted ? this._data.right : this._data.left
|
||||
);
|
||||
|
||||
var showButton = this.type != 'item';
|
||||
|
||||
this._leftpane.showButton = showButton;
|
||||
this._rightpane.showButton = showButton;
|
||||
this._leftpane.data = this._data.left;
|
||||
this._rightpane.data = this._data.right;
|
||||
this._mergepane.type = this.type;
|
||||
this._mergepane.data = this._data.merge;
|
||||
|
||||
if (this._data.selected == 'left') {
|
||||
this.choosePane(this._leftpane);
|
||||
}
|
||||
else {
|
||||
var mergeItem = this._leftpane.original;
|
||||
this._rightpane.removeAttribute("selected");
|
||||
this._leftpane.setAttribute("selected", "true");
|
||||
this.choosePane(this._rightpane);
|
||||
}
|
||||
|
||||
this._mergepane.ref = mergeItem;
|
||||
|
||||
/*
|
||||
|
||||
Code to display only the different values -- not used
|
||||
|
@ -224,22 +179,51 @@
|
|||
</body>
|
||||
</method>
|
||||
|
||||
<method name="_getTypeFromItem">
|
||||
|
||||
<method name="choosePane">
|
||||
<parameter name="pane"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
if (pane.getAttribute('anonid') == 'leftpane') {
|
||||
var position = 'left';
|
||||
var otherPane = this._rightpane;
|
||||
}
|
||||
else {
|
||||
var position = 'right';
|
||||
var otherPane = this._leftpane;
|
||||
}
|
||||
|
||||
pane.removeAttribute("selected");
|
||||
otherPane.removeAttribute("selected");
|
||||
pane.setAttribute("selected", "true");
|
||||
|
||||
if (pane.deleted) {
|
||||
this._mergepane.deleted = true;
|
||||
}
|
||||
else {
|
||||
this._mergepane.data = pane.data;
|
||||
}
|
||||
|
||||
if (this.onSelectionChange) {
|
||||
this.onSelectionChange();
|
||||
}
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
|
||||
<method name="_getTypeFromObject">
|
||||
<parameter name="obj"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
if (!(obj instanceof Zotero.Item)) {
|
||||
throw ("obj is not a Zotero.Item in merge.xml");
|
||||
if (!obj.itemType) {
|
||||
Zotero.debug(obj, 1);
|
||||
throw new Error("obj is not item JSON");
|
||||
}
|
||||
if (obj.isAttachment()) {
|
||||
return 'attachment';
|
||||
}
|
||||
else if (obj.isNote()) {
|
||||
return 'note';
|
||||
}
|
||||
else {
|
||||
return 'item';
|
||||
if (obj.itemType == 'attachment' || obj.itemType == 'note') {
|
||||
return obj.itemType;
|
||||
}
|
||||
return 'item';
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
@ -248,7 +232,7 @@
|
|||
<parameter name="id"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
return document.getAnonymousNodes(this)[0].getElementsByAttribute('id',id)[0];
|
||||
return document.getAnonymousNodes(this)[0].getElementsByAttribute('anonid',id)[0];
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
@ -256,9 +240,9 @@
|
|||
|
||||
<content>
|
||||
<xul:hbox id="merge-group" flex="1">
|
||||
<xul:zoteromergepane id="leftpane" flex="1"/>
|
||||
<xul:zoteromergepane id="rightpane" flex="1"/>
|
||||
<xul:zoteromergepane id="mergepane" flex="1"/>
|
||||
<xul:zoteromergepane anonid="leftpane" flex="1"/>
|
||||
<xul:zoteromergepane anonid="rightpane" flex="1"/>
|
||||
<xul:zoteromergepane anonid="mergepane" flex="1"/>
|
||||
</xul:hbox>
|
||||
</content>
|
||||
</binding>
|
||||
|
@ -273,11 +257,20 @@
|
|||
<constructor>
|
||||
<![CDATA[
|
||||
this.parent = document.getBindingParent(this.parentNode);
|
||||
this.isMergePane = this.getAttribute('anonid') == 'mergepane';
|
||||
|
||||
if (!this.isMergePane) {
|
||||
this.pane.onclick = function () {
|
||||
this.parent.choosePane(this);
|
||||
}.bind(this);
|
||||
}
|
||||
]]>
|
||||
</constructor>
|
||||
|
||||
<property name="type" onget="return this.parent.type" readonly="true"/>
|
||||
<property name="caption" onget="return this._id('caption')" readonly="true"/>
|
||||
<field name="showButton"/>
|
||||
<property name="pane" onget="return document.getAnonymousNodes(this)[0]"/>
|
||||
<property name="objectbox" onget="return this._id('objectbox')" readonly="true"/>
|
||||
|
||||
<field name="_deleted"/>
|
||||
|
@ -291,7 +284,7 @@
|
|||
placeholder.hidden = !!val;
|
||||
}
|
||||
else {
|
||||
this._id('objectbox').hidden = !!true;
|
||||
this._id('objectbox').hidden = true;
|
||||
}
|
||||
var deleteBox = this._id('delete-box');
|
||||
deleteBox.hidden = !val;
|
||||
|
@ -299,10 +292,13 @@
|
|||
</setter>
|
||||
</property>
|
||||
|
||||
<property name="ref" onget="return this._deleted ? 'deleted' : this.objectbox.ref;">
|
||||
<field name="_data"/>
|
||||
<property name="data" onget="return this._data">
|
||||
<setter>
|
||||
<![CDATA[
|
||||
if (val == 'deleted') {
|
||||
this._data = val;
|
||||
|
||||
if (val.deleted) {
|
||||
this.deleted = true;
|
||||
return;
|
||||
}
|
||||
|
@ -324,7 +320,7 @@
|
|||
elementName = 'zoteronoteeditor';
|
||||
break;
|
||||
|
||||
case 'storagefile':
|
||||
case 'file':
|
||||
elementName = 'zoterostoragefilebox';
|
||||
break;
|
||||
|
||||
|
@ -344,149 +340,64 @@
|
|||
oldObjBox.parentNode.replaceChild(objbox, oldObjBox);
|
||||
}
|
||||
|
||||
objbox.setAttribute("id", "objectbox");
|
||||
objbox.setAttribute("anonid", "objectbox");
|
||||
objbox.setAttribute("flex", "1");
|
||||
|
||||
if (this.getAttribute('id') == 'mergepane') {
|
||||
objbox.mode = 'mergeedit';
|
||||
objbox.mode = 'view';
|
||||
|
||||
var button = this._id('choose-button');
|
||||
if (this.showButton) {
|
||||
button.label = Zotero.getString('sync.conflict.chooseThisVersion');
|
||||
button.onclick = function () {
|
||||
this.parent.choosePane(this);
|
||||
}.bind(this);
|
||||
button.hidden = false;
|
||||
}
|
||||
else {
|
||||
objbox.mode = 'merge';
|
||||
objbox.clickHandler = this.chooseObj;
|
||||
button.hidden = true;
|
||||
}
|
||||
|
||||
// Type-specific settings
|
||||
switch (this.type) {
|
||||
case 'attachment':
|
||||
case 'note':
|
||||
case 'storagefile':
|
||||
objbox.buttonCaption = Zotero.getString('sync.conflict.chooseThisVersion');
|
||||
break;
|
||||
}
|
||||
// Store JSON
|
||||
this._data = val;
|
||||
|
||||
objbox.ref = val;
|
||||
// Create item from JSON for metadata box
|
||||
var item = new Zotero.Item(val.itemType);
|
||||
item.fromJSON(val);
|
||||
objbox.ref = item;
|
||||
]]>
|
||||
</setter>
|
||||
</property>
|
||||
|
||||
<field name="original"/> <!-- original object -->
|
||||
<field name="parent"/>
|
||||
|
||||
<method name="chooseObj">
|
||||
<parameter name="obj"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
var pane = Zotero.getAncestorByTagName(obj, 'zoteromergepane');
|
||||
var mergegroup = document.getBindingParent(pane);
|
||||
var mergepane = mergegroup.mergepane;
|
||||
|
||||
if (pane.getAttribute('id') == 'leftpane') {
|
||||
var position = 'left';
|
||||
var otherPane = mergegroup.rightpane;
|
||||
}
|
||||
else {
|
||||
var position = 'right';
|
||||
var otherPane = mergegroup.leftpane;
|
||||
}
|
||||
|
||||
pane.removeAttribute("selected");
|
||||
otherPane.removeAttribute("selected");
|
||||
pane.setAttribute("selected", "true");
|
||||
|
||||
if (pane.ref == 'deleted') {
|
||||
mergepane.deleted = true;
|
||||
}
|
||||
else {
|
||||
mergepane.ref = pane.original;
|
||||
}
|
||||
|
||||
if (mergegroup.onSelectionChange) {
|
||||
mergegroup.onSelectionChange();
|
||||
}
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<!-- Unused -->
|
||||
<method name="chooseField">
|
||||
<parameter name="row"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
// If used, has to be updated to handle original item
|
||||
|
||||
var fieldName = row.firstChild.getAttribute('fieldname');
|
||||
// TODO: creator/date
|
||||
var value = row.lastChild.firstChild.nodeValue
|
||||
|
||||
var mergegroup = document.getBindingParent(this.parentNode).parent;
|
||||
var mergepane = mergegroup.mergepane;
|
||||
var pane = document.getBindingParent(this.parentNode);
|
||||
|
||||
if (pane.getAttribute('id') == 'leftpane') {
|
||||
var position = 'left';
|
||||
var otherPane = mergegroup.rightpane;
|
||||
}
|
||||
else {
|
||||
var position = 'right';
|
||||
var otherPane = mergegroup.leftpane;
|
||||
}
|
||||
|
||||
// Changing item type
|
||||
if (fieldName == 'itemType') {
|
||||
fieldName = 'itemTypeID';
|
||||
value = row.lastChild.getAttribute('itemTypeID');
|
||||
|
||||
if (!mergepane.ref) {
|
||||
mergepane.ref = new Zotero.Item(value);
|
||||
}
|
||||
else {
|
||||
mergepane.objectbox.changeTypeTo(value, true);
|
||||
}
|
||||
|
||||
pane.objectbox.clickableFields = [];
|
||||
pane.objectbox.clickable = true;
|
||||
var fieldIDs = Zotero.ItemFields.getItemTypeFields(value);
|
||||
var fieldNames = ['itemType'];
|
||||
for each(var field in fieldIDs) {
|
||||
fieldNames.push(Zotero.ItemFields.getName(field));
|
||||
}
|
||||
otherPane.objectbox.clickableFields = fieldNames;
|
||||
otherPane.objectbox.clickable = false;
|
||||
pane.objectbox.refresh();
|
||||
otherPane.objectbox.refresh();
|
||||
}
|
||||
// Changing another field
|
||||
else {
|
||||
mergepane.ref.setField(fieldName, value);
|
||||
}
|
||||
|
||||
mergepane.objectbox.refresh();
|
||||
]]>
|
||||
</body>
|
||||
<method name="click">
|
||||
<body><![CDATA[
|
||||
this.pane.click();
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
<method name="_id">
|
||||
<parameter name="id"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
if (!document.getAnonymousNodes(this)[0].getElementsByAttribute('id', id).length) {
|
||||
return false;
|
||||
}
|
||||
return document.getAnonymousNodes(this)[0].getElementsByAttribute('id', id)[0];
|
||||
var elems = document.getAnonymousNodes(this)[0].getElementsByAttribute('anonid', id);
|
||||
return elems.length ? elems[0] : false;
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
</implementation>
|
||||
|
||||
<content>
|
||||
<xul:groupbox id="merge-pane" flex="1">
|
||||
<xul:caption id="caption"/>
|
||||
<xul:box id="object-placeholder"/>
|
||||
<xul:hbox id="delete-box" hidden="true" flex="1"
|
||||
onclick="document.getBindingParent(this).chooseObj(this)">
|
||||
<xul:label value="&zotero.merge.deleted;"/>
|
||||
</xul:hbox>
|
||||
</xul:groupbox>
|
||||
<xul:vbox flex="1">
|
||||
<xul:groupbox anonid="merge-pane" flex="1">
|
||||
<xul:caption anonid="caption"/>
|
||||
<xul:box anonid="object-placeholder"/>
|
||||
<xul:hbox anonid="delete-box" hidden="true" flex="1">
|
||||
<xul:label value="&zotero.merge.deleted;"/>
|
||||
</xul:hbox>
|
||||
</xul:groupbox>
|
||||
<xul:button anonid="choose-button" hidden="true"/>
|
||||
</xul:vbox>
|
||||
</content>
|
||||
</binding>
|
||||
</bindings>
|
||||
|
|
|
@ -76,17 +76,6 @@
|
|||
this.displayRelated = true;
|
||||
break;
|
||||
|
||||
case 'merge':
|
||||
this.displayButton = true;
|
||||
break;
|
||||
|
||||
case 'mergeedit':
|
||||
// Not currently implemented
|
||||
// (editing works, but value isn't saved)
|
||||
//this.editable = true;
|
||||
this.keyDownHandler = this.handleKeyDown;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw ("Invalid mode '" + val + "' in noteeditor.xml");
|
||||
}
|
||||
|
@ -111,24 +100,20 @@
|
|||
|
||||
<field name="_item"/>
|
||||
<property name="item" onget="return this._item;">
|
||||
<setter>
|
||||
<![CDATA[
|
||||
Zotero.spawn(function* () {
|
||||
this._item = val;
|
||||
// TODO: use clientDateModified instead
|
||||
this._mtime = val.getField('dateModified');
|
||||
|
||||
var parentKey = this.item.parentKey;
|
||||
if (parentKey) {
|
||||
this.parentItem = Zotero.Items.getByLibraryAndKey(this.item.libraryID, parentKey);
|
||||
}
|
||||
|
||||
this._id('links').item = this.item;
|
||||
|
||||
yield this.refresh();
|
||||
}, this);
|
||||
]]>
|
||||
</setter>
|
||||
<setter><![CDATA[
|
||||
this._item = val;
|
||||
// TODO: use clientDateModified instead
|
||||
this._mtime = val.getField('dateModified');
|
||||
|
||||
var parentKey = this.item.parentKey;
|
||||
if (parentKey) {
|
||||
this.parentItem = Zotero.Items.getByLibraryAndKey(this.item.libraryID, parentKey);
|
||||
}
|
||||
|
||||
this._id('links').item = this.item;
|
||||
|
||||
this.refresh();
|
||||
]]></setter>
|
||||
</property>
|
||||
|
||||
<property name="linksOnTop">
|
||||
|
@ -187,61 +172,58 @@
|
|||
|
||||
<method name="refresh">
|
||||
<body><![CDATA[
|
||||
return Zotero.spawn(function* () {
|
||||
Zotero.debug('Refreshing note editor');
|
||||
|
||||
var textbox = this._id('noteField');
|
||||
var textboxReadOnly = this._id('noteFieldReadOnly');
|
||||
var button = this._id('goButton');
|
||||
|
||||
if (this.editable) {
|
||||
textbox.hidden = false;
|
||||
textboxReadOnly.hidden = true;
|
||||
}
|
||||
else {
|
||||
textbox.hidden = true;
|
||||
textboxReadOnly.hidden = false;
|
||||
textbox = textboxReadOnly;
|
||||
}
|
||||
|
||||
//var scrollPos = textbox.inputField.scrollTop;
|
||||
if (this.item) {
|
||||
yield this.item.loadNote();
|
||||
textbox.value = this.item.getNote();
|
||||
}
|
||||
else {
|
||||
textbox.value = '';
|
||||
}
|
||||
//textbox.inputField.scrollTop = scrollPos;
|
||||
|
||||
this._id('linksbox').hidden = !(this.displayTags && this.displayRelated);
|
||||
|
||||
if (this.keyDownHandler) {
|
||||
textbox.setAttribute('onkeydown',
|
||||
'document.getBindingParent(this).handleKeyDown(event)');
|
||||
}
|
||||
else {
|
||||
textbox.removeAttribute('onkeydown');
|
||||
}
|
||||
|
||||
if (this.commandHandler) {
|
||||
textbox.setAttribute('oncommand',
|
||||
'document.getBindingParent(this).commandHandler()');
|
||||
}
|
||||
else {
|
||||
textbox.removeAttribute('oncommand');
|
||||
}
|
||||
|
||||
if (this.displayButton) {
|
||||
button.label = this.buttonCaption;
|
||||
button.hidden = false;
|
||||
button.setAttribute('oncommand',
|
||||
'document.getBindingParent(this).clickHandler(this)');
|
||||
}
|
||||
else {
|
||||
button.hidden = true;
|
||||
}
|
||||
}, this);
|
||||
Zotero.debug('Refreshing note editor');
|
||||
|
||||
var textbox = this._id('noteField');
|
||||
var textboxReadOnly = this._id('noteFieldReadOnly');
|
||||
var button = this._id('goButton');
|
||||
|
||||
if (this.editable) {
|
||||
textbox.hidden = false;
|
||||
textboxReadOnly.hidden = true;
|
||||
}
|
||||
else {
|
||||
textbox.hidden = true;
|
||||
textboxReadOnly.hidden = false;
|
||||
textbox = textboxReadOnly;
|
||||
}
|
||||
|
||||
//var scrollPos = textbox.inputField.scrollTop;
|
||||
if (this.item) {
|
||||
textbox.value = this.item.getNote();
|
||||
}
|
||||
else {
|
||||
textbox.value = '';
|
||||
}
|
||||
//textbox.inputField.scrollTop = scrollPos;
|
||||
|
||||
this._id('linksbox').hidden = !(this.displayTags && this.displayRelated);
|
||||
|
||||
if (this.keyDownHandler) {
|
||||
textbox.setAttribute('onkeydown',
|
||||
'document.getBindingParent(this).handleKeyDown(event)');
|
||||
}
|
||||
else {
|
||||
textbox.removeAttribute('onkeydown');
|
||||
}
|
||||
|
||||
if (this.commandHandler) {
|
||||
textbox.setAttribute('oncommand',
|
||||
'document.getBindingParent(this).commandHandler()');
|
||||
}
|
||||
else {
|
||||
textbox.removeAttribute('oncommand');
|
||||
}
|
||||
|
||||
if (this.displayButton) {
|
||||
button.label = this.buttonCaption;
|
||||
button.hidden = false;
|
||||
button.setAttribute('oncommand',
|
||||
'document.getBindingParent(this).clickHandler(this)');
|
||||
}
|
||||
else {
|
||||
button.hidden = true;
|
||||
}
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
|
|
|
@ -95,7 +95,8 @@
|
|||
var r = "";
|
||||
|
||||
if (this.item) {
|
||||
yield this.item.loadTags();
|
||||
yield this.item.loadTags()
|
||||
.tap(() => Zotero.Promise.check(this.mode));
|
||||
var tags = this.item.getTags();
|
||||
if (tags) {
|
||||
for(var i = 0; i < tags.length; i++)
|
||||
|
@ -210,7 +211,8 @@
|
|||
return Zotero.spawn(function* () {
|
||||
Zotero.debug('Reloading tags box');
|
||||
|
||||
yield this.item.loadTags();
|
||||
yield this.item.loadTags()
|
||||
.tap(() => Zotero.Promise.check(this.mode));
|
||||
|
||||
// Cancel field focusing while we're updating
|
||||
this._reloading = true;
|
||||
|
@ -218,6 +220,7 @@
|
|||
this.id('addButton').hidden = !this.editable;
|
||||
|
||||
this._tagColors = yield Zotero.Tags.getColors(this.item.libraryID)
|
||||
.tap(() => Zotero.Promise.check(this.mode));
|
||||
|
||||
var rows = this.id('tagRows');
|
||||
while(rows.hasChildNodes()) {
|
||||
|
|
|
@ -24,24 +24,17 @@
|
|||
*/
|
||||
|
||||
var Zotero_Merge_Window = new function () {
|
||||
this.init = init;
|
||||
this.onBack = onBack;
|
||||
this.onNext = onNext;
|
||||
this.onFinish = onFinish;
|
||||
this.onCancel = onCancel;
|
||||
|
||||
var _wizard = null;
|
||||
var _wizardPage = null;
|
||||
var _mergeGroup = null;
|
||||
var _numObjects = null;
|
||||
|
||||
var _initialized = false;
|
||||
var _io = null;
|
||||
var _objects = null;
|
||||
var _conflicts = null;
|
||||
var _merged = [];
|
||||
var _pos = -1;
|
||||
|
||||
function init() {
|
||||
this.init = function () {
|
||||
_wizard = document.getElementsByTagName('wizard')[0];
|
||||
_wizardPage = document.getElementsByTagName('wizardpage')[0];
|
||||
_mergeGroup = document.getElementsByTagName('zoteromergegroup')[0];
|
||||
|
@ -54,69 +47,42 @@ var Zotero_Merge_Window = new function () {
|
|||
|
||||
_wizard.getButton('cancel').setAttribute('label', Zotero.getString('sync.cancel'));
|
||||
|
||||
_io = window.arguments[0];
|
||||
_objects = _io.dataIn.objects;
|
||||
if (!_objects.length) {
|
||||
// TODO: handle no objects
|
||||
_io = window.arguments[0].wrappedJSObject;
|
||||
_conflicts = _io.dataIn.conflicts;
|
||||
if (!_conflicts.length) {
|
||||
// TODO: handle no conflicts
|
||||
return;
|
||||
}
|
||||
|
||||
_mergeGroup.type = _io.dataIn.type;
|
||||
_mergeGroup.onSelectionChange = _updateResolveAllCheckbox;
|
||||
|
||||
switch (_mergeGroup.type) {
|
||||
case 'item':
|
||||
case 'storagefile':
|
||||
break;
|
||||
|
||||
default:
|
||||
_error("Unsupported merge object type '" + _mergeGroup.type
|
||||
+ "' in Zotero_Merge_Window.init()");
|
||||
return;
|
||||
}
|
||||
|
||||
_mergeGroup.leftCaption = _io.dataIn.captions[0];
|
||||
_mergeGroup.rightCaption = _io.dataIn.captions[1];
|
||||
_mergeGroup.mergeCaption = _io.dataIn.captions[2];
|
||||
|
||||
_resolveAllCheckbox = document.getElementById('resolve-all');
|
||||
if (_conflicts.length == 1) {
|
||||
_resolveAllCheckbox.hidden = true;
|
||||
}
|
||||
else {
|
||||
_mergeGroup.onSelectionChange = _updateResolveAllCheckbox;
|
||||
}
|
||||
|
||||
_numObjects = document.getElementById('zotero-merge-num-objects');
|
||||
document.getElementById('zotero-merge-total-objects').value = _objects.length;
|
||||
document.getElementById('zotero-merge-total-objects').value = _conflicts.length;
|
||||
|
||||
this.onNext();
|
||||
}
|
||||
|
||||
|
||||
function onBack() {
|
||||
this.onBack = function () {
|
||||
_merged[_pos] = _getCurrentMergeInfo();
|
||||
|
||||
_pos--;
|
||||
|
||||
if (_pos == 0) {
|
||||
_wizard.canRewind = false;
|
||||
}
|
||||
|
||||
_merged[_pos + 1] = _getCurrentMergeObject();
|
||||
|
||||
_numObjects.value = _pos + 1;
|
||||
|
||||
_mergeGroup.left = _objects[_pos][0];
|
||||
_mergeGroup.right = _objects[_pos][1];
|
||||
|
||||
// Restore previously merged object into merge pane
|
||||
_mergeGroup.merge = _merged[_pos].ref;
|
||||
if (_merged[_pos].id == _mergeGroup.left.id) {
|
||||
_mergeGroup.leftpane.setAttribute("selected", "true");
|
||||
_mergeGroup.rightpane.removeAttribute("selected");
|
||||
}
|
||||
else {
|
||||
_mergeGroup.leftpane.removeAttribute("selected");
|
||||
_mergeGroup.rightpane.setAttribute("selected", "true");
|
||||
}
|
||||
_updateResolveAllCheckbox();
|
||||
|
||||
if (_mergeGroup.type == 'item') {
|
||||
_updateChangedCreators();
|
||||
}
|
||||
_updateGroup();
|
||||
|
||||
var nextButton = _wizard.getButton("next");
|
||||
|
||||
|
@ -134,36 +100,26 @@ var Zotero_Merge_Window = new function () {
|
|||
}
|
||||
|
||||
|
||||
function onNext() {
|
||||
if (_pos + 1 == _objects.length || _resolveAllCheckbox.checked) {
|
||||
this.onNext = function () {
|
||||
// At end or resolving all
|
||||
if (_pos + 1 == _conflicts.length || _resolveAllCheckbox.checked) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// First page
|
||||
if (_pos == -1) {
|
||||
_wizard.canRewind = false;
|
||||
}
|
||||
// Subsequent pages
|
||||
else {
|
||||
_wizard.canRewind = true;
|
||||
_merged[_pos] = _getCurrentMergeInfo();
|
||||
}
|
||||
|
||||
_pos++;
|
||||
|
||||
if (_pos == 0) {
|
||||
_wizard.canRewind = false;
|
||||
}
|
||||
else {
|
||||
_wizard.canRewind = true;
|
||||
|
||||
// Save merged object to return array
|
||||
_merged[_pos - 1] = _getCurrentMergeObject();
|
||||
}
|
||||
|
||||
// Adjust counter
|
||||
_numObjects.value = _pos + 1;
|
||||
|
||||
try {
|
||||
_mergeGroup.left = _objects[_pos][0];
|
||||
_mergeGroup.right = _objects[_pos][1];
|
||||
|
||||
// Restore previously merged object into merge pane
|
||||
if (_merged[_pos]) {
|
||||
_mergeGroup.merge = _merged[_pos].ref;
|
||||
_mergeGroup.leftpane.removeAttribute("selected");
|
||||
_mergeGroup.rightpane.removeAttribute("selected");
|
||||
}
|
||||
_updateGroup();
|
||||
}
|
||||
catch (e) {
|
||||
_error(e);
|
||||
|
@ -172,10 +128,6 @@ var Zotero_Merge_Window = new function () {
|
|||
|
||||
_updateResolveAllCheckbox();
|
||||
|
||||
if (_mergeGroup.type == 'item') {
|
||||
_updateChangedCreators();
|
||||
}
|
||||
|
||||
if (_isLastConflict()) {
|
||||
_showFinishButton();
|
||||
}
|
||||
|
@ -187,28 +139,40 @@ var Zotero_Merge_Window = new function () {
|
|||
}
|
||||
|
||||
|
||||
function onFinish() {
|
||||
this.onFinish = function () {
|
||||
// If using one side for all remaining, update merge object
|
||||
if (!_isLastConflict() && _resolveAllCheckbox.checked) {
|
||||
let useRemote = _mergeGroup.rightpane.getAttribute("selected") == "true";
|
||||
for (let i = _pos; i < _objects.length; i++) {
|
||||
_merged[i] = _getMergeObject(
|
||||
_objects[i][useRemote ? 1 : 0],
|
||||
_objects[i][0],
|
||||
_objects[i][1]
|
||||
);
|
||||
let side = _mergeGroup.rightpane.getAttribute("selected") == "true" ? 'right' : 'left'
|
||||
for (let i = _pos; i < _conflicts.length; i++) {
|
||||
_merged[i] = {
|
||||
data: _getMergeDataWithSide(i, side),
|
||||
selected: side
|
||||
};
|
||||
}
|
||||
}
|
||||
else {
|
||||
_merged[_pos] = _getCurrentMergeObject();
|
||||
_merged[_pos] = _getCurrentMergeInfo();
|
||||
}
|
||||
|
||||
_merged.forEach(function (x, i, a) {
|
||||
// Add key
|
||||
x.data.key = _conflicts[i].left.key || _conflicts[i].right.key;
|
||||
// If selecting right item, add back version
|
||||
if (x.data && x.selected == 'right') {
|
||||
x.data.version = _conflicts[i].right.version;
|
||||
}
|
||||
else {
|
||||
delete x.data.version;
|
||||
}
|
||||
a[i] = x.data;
|
||||
})
|
||||
|
||||
_io.dataOut = _merged;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function onCancel() {
|
||||
this.onCancel = function () {
|
||||
// if already merged, ask
|
||||
}
|
||||
|
||||
|
@ -222,19 +186,117 @@ var Zotero_Merge_Window = new function () {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function _updateGroup() {
|
||||
// Adjust counter
|
||||
_numObjects.value = _pos + 1;
|
||||
|
||||
let data = {};
|
||||
Object.assign(data, _conflicts[_pos]);
|
||||
var mergeInfo = _getMergeInfo(_pos);
|
||||
data.merge = mergeInfo.data;
|
||||
data.selected = mergeInfo.selected;
|
||||
_mergeGroup.data = data;
|
||||
|
||||
_updateResolveAllCheckbox();
|
||||
}
|
||||
|
||||
|
||||
function _getCurrentMergeInfo() {
|
||||
return {
|
||||
data: _mergeGroup.merged,
|
||||
selected: _mergeGroup.leftpane.getAttribute("selected") == "true" ? "left" : "right"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the default or previously chosen merge info for a given position
|
||||
*
|
||||
* @param {Integer} pos
|
||||
* @return {Object} - Object with 'data' (JSON field data) and 'selected' ('left', 'right') properties
|
||||
*/
|
||||
function _getMergeInfo(pos) {
|
||||
// If data already selected, use that
|
||||
if (_merged[pos]) {
|
||||
return _merged[pos];
|
||||
}
|
||||
// If either side was deleted, use other side
|
||||
if (_conflicts[pos].left.deleted) {
|
||||
let mergeInfo = {
|
||||
data: {},
|
||||
selected: 'right'
|
||||
};
|
||||
Object.assign(mergeInfo.data, _conflicts[pos].right);
|
||||
return mergeInfo;
|
||||
}
|
||||
if (_conflicts[pos].right.deleted) {
|
||||
let mergeInfo = {
|
||||
data: {},
|
||||
selected: 'left'
|
||||
};
|
||||
Object.assign(mergeInfo.data, _conflicts[pos].left);
|
||||
return mergeInfo;
|
||||
}
|
||||
// Apply changes from each side and pick most recent version for conflicting fields
|
||||
var mergeInfo = {
|
||||
data: {}
|
||||
};
|
||||
Object.assign(mergeInfo.data, _conflicts[pos].left)
|
||||
Zotero.DataObjectUtilities.applyChanges(mergeInfo.data, _conflicts[pos].changes);
|
||||
if (_conflicts[pos].left.dateModified > _conflicts[pos].right.dateModified) {
|
||||
var side = 0;
|
||||
}
|
||||
// Use remote if remote Date Modified is later or same
|
||||
else {
|
||||
var side = 1;
|
||||
}
|
||||
Zotero.DataObjectUtilities.applyChanges(mergeInfo.data, _conflicts[pos].conflicts.map(x => x[side]));
|
||||
mergeInfo.selected = side ? 'right' : 'left';
|
||||
return mergeInfo;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the merge data using a given side at a given position
|
||||
*
|
||||
* @param {Integer} pos
|
||||
* @param {String} side - 'left' or 'right'
|
||||
* @return {Object} - JSON field data
|
||||
*/
|
||||
function _getMergeDataWithSide(pos, side) {
|
||||
if (!side) {
|
||||
throw new Error("Side not provided");
|
||||
}
|
||||
|
||||
if (_conflicts[pos].left.deleted || _conflicts[pos].right.deleted) {
|
||||
return _conflicts[pos][side];
|
||||
}
|
||||
|
||||
var data = {};
|
||||
Object.assign(data, _conflicts[pos].left)
|
||||
Zotero.DataObjectUtilities.applyChanges(data, _conflicts[pos].changes);
|
||||
Zotero.DataObjectUtilities.applyChanges(
|
||||
data, _conflicts[pos].conflicts.map(x => x[side == 'left' ? 0 : 1])
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
function _updateResolveAllCheckbox() {
|
||||
if (_mergeGroup.rightpane.getAttribute("selected") == 'true') {
|
||||
var label = 'sync.merge.resolveAllRemote';
|
||||
var label = 'resolveAllRemoteFields';
|
||||
}
|
||||
else {
|
||||
var label = 'sync.merge.resolveAllLocal';
|
||||
var label = 'resolveAllLocalFields';
|
||||
}
|
||||
_resolveAllCheckbox.label = Zotero.getString(label);
|
||||
// TODO: files
|
||||
_resolveAllCheckbox.label = Zotero.getString('sync.conflict.' + label);
|
||||
}
|
||||
|
||||
|
||||
function _isLastConflict() {
|
||||
return (_pos + 1) == _objects.length;
|
||||
return (_pos + 1) == _conflicts.length;
|
||||
}
|
||||
|
||||
|
||||
|
@ -274,70 +336,18 @@ var Zotero_Merge_Window = new function () {
|
|||
}
|
||||
|
||||
|
||||
function _getMergeObject(ref, left, right) {
|
||||
var id = ref == 'deleted'
|
||||
? (left == 'deleted' ? right.id : left.id)
|
||||
: ref.id;
|
||||
|
||||
return {
|
||||
id: id,
|
||||
ref: ref,
|
||||
left: left,
|
||||
right: right
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function _getCurrentMergeObject() {
|
||||
return _getMergeObject(_mergeGroup.merge, _mergeGroup.left, _mergeGroup.right);
|
||||
}
|
||||
|
||||
|
||||
// Hack to support creator reconciliation via item view
|
||||
function _updateChangedCreators() {
|
||||
if (_mergeGroup.type != 'item') {
|
||||
_error("_updateChangedCreators called on non-item object in "
|
||||
+ "Zotero_Merge_Window._updateChangedCreators()");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_io.dataIn.changedCreators) {
|
||||
var originalCreators = _mergeGroup.rightpane.original.getCreators();
|
||||
var clonedCreators = _mergeGroup.rightpane.ref.getCreators();
|
||||
var refresh = false;
|
||||
for (var i in originalCreators) {
|
||||
var changedCreator = _io.dataIn.changedCreators[Zotero.Creators.getLibraryKeyHash(originalCreators[i].ref)];
|
||||
if (changedCreator) {
|
||||
_mergeGroup.rightpane.original.setCreator(
|
||||
i, changedCreator, originalCreators[i].creatorTypeID
|
||||
);
|
||||
clonedCreators[i].ref = changedCreator;
|
||||
refresh = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (refresh) {
|
||||
_mergeGroup.rightpane.objectbox.refresh();
|
||||
_mergeGroup.mergepane.objectbox.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TEMP
|
||||
function _setInstructionsString(buttonName) {
|
||||
switch (_mergeGroup.type) {
|
||||
case 'storagefile':
|
||||
var msg = Zotero.getString('sync.conflict.fileChanged');
|
||||
case 'file':
|
||||
var msg = 'fileChanged';
|
||||
break;
|
||||
|
||||
default:
|
||||
// TODO: cf. localization: maybe not always call it 'item'
|
||||
var msg = Zotero.getString('sync.conflict.itemChanged');
|
||||
// TODO: maybe don't always call it 'item'
|
||||
var msg = 'itemChanged';
|
||||
}
|
||||
|
||||
msg += " " + Zotero.getString('sync.conflict.chooseVersionToKeep', buttonName);
|
||||
|
||||
msg = Zotero.getString('sync.conflict.' + msg, buttonName)
|
||||
document.getElementById('zotero-merge-instructions').value = msg;
|
||||
}
|
||||
|
||||
|
|
|
@ -27,13 +27,6 @@ var noteEditor;
|
|||
var notifierUnregisterID;
|
||||
|
||||
function onLoad() {
|
||||
Zotero.spawn(function* () {
|
||||
Zotero.debug('=-=-=');
|
||||
var bar = yield Zotero.Promise.delay(1000).return('DONE');
|
||||
Zotero.debug(bar);
|
||||
Zotero.debug('-----');
|
||||
});
|
||||
|
||||
noteEditor = document.getElementById('zotero-note-editor');
|
||||
noteEditor.mode = 'edit';
|
||||
noteEditor.focus();
|
||||
|
|
|
@ -631,7 +631,7 @@ Zotero.Item.prototype.setField = function(field, value, loadIn) {
|
|||
this._disabledCheck();
|
||||
|
||||
if (value === undefined) {
|
||||
throw new Error("Value cannot be undefined");
|
||||
throw new Error(`'${field}' value cannot be undefined`);
|
||||
}
|
||||
|
||||
if (typeof value == 'string') {
|
||||
|
|
|
@ -2209,17 +2209,18 @@ Zotero.Schema = new function(){
|
|||
|
||||
yield Zotero.DB.queryAsync("UPDATE syncDeleteLog SET libraryID=1 WHERE libraryID=0");
|
||||
yield Zotero.DB.queryAsync("ALTER TABLE syncDeleteLog RENAME TO syncDeleteLogOld");
|
||||
yield Zotero.DB.queryAsync("CREATE TABLE syncDeleteLog (\n syncObjectTypeID INT NOT NULL,\n libraryID INT NOT NULL,\n key TEXT NOT NULL,\n synced INT NOT NULL DEFAULT 0,\n UNIQUE (syncObjectTypeID, libraryID, key),\n FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID),\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
|
||||
yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO syncDeleteLog SELECT * FROM syncDeleteLogOld");
|
||||
yield Zotero.DB.queryAsync("CREATE TABLE syncDeleteLog (\n syncObjectTypeID INT NOT NULL,\n libraryID INT NOT NULL,\n key TEXT NOT NULL,\n dateDeleted TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n synced INT NOT NULL DEFAULT 0,\n UNIQUE (syncObjectTypeID, libraryID, key),\n FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID),\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
|
||||
yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO syncDeleteLog SELECT syncObjectTypeID, libraryID, key, timestamp, 0 FROM syncDeleteLogOld");
|
||||
yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS syncDeleteLog_timestamp");
|
||||
yield Zotero.DB.queryAsync("CREATE INDEX syncDeleteLog_synced ON syncDeleteLog(synced)");
|
||||
// TODO: Something special for tag deletions?
|
||||
//yield Zotero.DB.queryAsync("DELETE FROM syncDeleteLog WHERE syncObjectTypeID IN (2, 5, 6)");
|
||||
//yield Zotero.DB.queryAsync("DELETE FROM syncObjectTypes WHERE syncObjectTypeID IN (2, 5, 6)");
|
||||
|
||||
yield Zotero.DB.queryAsync("UPDATE storageDeleteLog SET libraryID=1 WHERE libraryID=0");
|
||||
yield Zotero.DB.queryAsync("ALTER TABLE storageDeleteLog RENAME TO storageDeleteLogOld");
|
||||
yield Zotero.DB.queryAsync("CREATE TABLE storageDeleteLog (\n libraryID INT NOT NULL,\n key TEXT NOT NULL,\n synced INT NOT NULL DEFAULT 0,\n PRIMARY KEY (libraryID, key),\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
|
||||
yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO storageDeleteLog SELECT * FROM storageDeleteLogOld");
|
||||
yield Zotero.DB.queryAsync("CREATE TABLE storageDeleteLog (\n libraryID INT NOT NULL,\n key TEXT NOT NULL,\n dateDeleted TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n synced INT NOT NULL DEFAULT 0,\n PRIMARY KEY (libraryID, key),\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
|
||||
yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO storageDeleteLog SELECT libraryID, key, timestamp, 0 FROM storageDeleteLogOld");
|
||||
yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS storageDeleteLog_timestamp");
|
||||
yield Zotero.DB.queryAsync("CREATE INDEX storageDeleteLog_synced ON storageDeleteLog(synced)");
|
||||
|
||||
|
|
|
@ -37,7 +37,8 @@ Zotero.Sync.EventListeners.ChangeListener = new function () {
|
|||
return;
|
||||
}
|
||||
|
||||
var syncSQL = "REPLACE INTO syncDeleteLog VALUES (?, ?, ?, 0)";
|
||||
var syncSQL = "REPLACE INTO syncDeleteLog (syncObjectTypeID, libraryID, key, synced) "
|
||||
+ "VALUES (?, ?, ?, 0)";
|
||||
|
||||
if (type == 'item' && Zotero.Sync.Storage.WebDAV.includeUserFiles) {
|
||||
var storageSQL = "REPLACE INTO storageDeleteLog VALUES (?, ?, 0)";
|
||||
|
|
|
@ -222,6 +222,7 @@ Zotero.Sync.Data.Local = {
|
|||
|
||||
Zotero.debug("Processing " + objectTypePlural + " in sync cache for " + libraryName);
|
||||
|
||||
var conflicts = [];
|
||||
var numSaved = 0;
|
||||
var numSkipped = 0;
|
||||
|
||||
|
@ -340,18 +341,6 @@ Zotero.Sync.Data.Local = {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Ignore conflicts from Quick Start Guide, and just use remote version
|
||||
/*if (objectType == 'item'
|
||||
&& jsonDataLocal.key == "ABCD2345"
|
||||
&& jsonDataLocal.url.indexOf('quick_start_guide') != -1
|
||||
&& jsonData.url.indexOf('quick_start_guide') != -1) {
|
||||
Zotero.debug("Ignoring conflict for item '" + jsonData.title + "' "
|
||||
+ "-- using remote version");
|
||||
let saved = yield this._saveObjectFromJSON(obj, jsonData, options);
|
||||
if (saved) numSaved++;
|
||||
continue;
|
||||
}*/
|
||||
|
||||
// If no conflicts, apply remote changes automatically
|
||||
if (!result.conflicts.length) {
|
||||
Zotero.DataObjectUtilities.applyChanges(
|
||||
|
@ -362,21 +351,17 @@ Zotero.Sync.Data.Local = {
|
|||
continue;
|
||||
}
|
||||
|
||||
Zotero.debug('======DIFF========');
|
||||
Zotero.debug(cachedJSON);
|
||||
Zotero.debug(jsonDataLocal);
|
||||
Zotero.debug(jsonData);
|
||||
Zotero.debug(result);
|
||||
throw new Error("Conflict");
|
||||
if (objectType != 'item') {
|
||||
throw new Error(`Unexpected conflict on ${objectType} object`);
|
||||
}
|
||||
|
||||
|
||||
// TODO
|
||||
|
||||
// reconcile changes automatically if we can
|
||||
|
||||
// if we can't:
|
||||
// if it's a search or collection, use most recent version
|
||||
// if it's an item,
|
||||
conflicts.push({
|
||||
left: jsonDataLocal,
|
||||
right: jsonData,
|
||||
changes: result.changes,
|
||||
conflicts: result.conflicts
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let saved = yield this._saveObjectFromJSON(obj, jsonData, options);
|
||||
|
@ -387,11 +372,20 @@ Zotero.Sync.Data.Local = {
|
|||
isNewObject = true;
|
||||
|
||||
// Check if object has been deleted locally
|
||||
if (yield this.objectInDeleteLog(objectType, libraryID, objectKey)) {
|
||||
let dateDeleted = yield this.getDateDeleted(
|
||||
objectType, libraryID, objectKey
|
||||
);
|
||||
if (dateDeleted) {
|
||||
switch (objectType) {
|
||||
case 'item':
|
||||
throw new Error("Unimplemented");
|
||||
break;
|
||||
conflicts.push({
|
||||
left: {
|
||||
deleted: true,
|
||||
dateDeleted: Zotero.Date.dateToSQL(dateDeleted, true)
|
||||
},
|
||||
right: jsonData
|
||||
});
|
||||
continue;
|
||||
|
||||
// Auto-restore some locally deleted objects that have changed remotely
|
||||
case 'collection':
|
||||
|
@ -433,6 +427,63 @@ Zotero.Sync.Data.Local = {
|
|||
yield this.processSyncCacheForObjectType(libraryID, objectType, options);
|
||||
}
|
||||
|
||||
if (conflicts.length) {
|
||||
conflicts.sort(function (a, b) {
|
||||
var d1 = a.left.dateDeleted || a.left.dateModified;
|
||||
var d2 = b.left.dateDeleted || b.left.dateModified;
|
||||
if (d1 > d2) {
|
||||
return 1
|
||||
}
|
||||
if (d1 < d2) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
|
||||
var mergeData = this.resolveConflicts(conflicts);
|
||||
if (mergeData) {
|
||||
let mergeOptions = {};
|
||||
Object.assign(mergeOptions, options);
|
||||
// Tell _saveObjectFromJSON not to save with 'synced' set to true
|
||||
mergeOptions.saveAsChanged = true;
|
||||
let concurrentObjects = 50;
|
||||
yield Zotero.Utilities.Internal.forEachChunkAsync(
|
||||
mergeData,
|
||||
concurrentObjects,
|
||||
function (chunk) {
|
||||
return Zotero.DB.executeTransaction(function* () {
|
||||
for (let json of chunk) {
|
||||
let obj = yield objectsClass.getByLibraryAndKeyAsync(
|
||||
libraryID, json.key, { noCache: true }
|
||||
);
|
||||
// Update object with merge data
|
||||
if (obj) {
|
||||
if (json.deleted) {
|
||||
yield obj.erase();
|
||||
}
|
||||
else {
|
||||
yield this._saveObjectFromJSON(obj, json, mergeOptions);
|
||||
}
|
||||
}
|
||||
// Recreate deleted object
|
||||
else if (!json.deleted) {
|
||||
obj = new Zotero[ObjectType];
|
||||
obj.libraryID = libraryID;
|
||||
obj.key = json.key;
|
||||
yield obj.loadPrimaryData();
|
||||
|
||||
let saved = yield this._saveObjectFromJSON(obj, json, options, {
|
||||
// Don't cache new items immediately, which skips reloading after save
|
||||
skipCache: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
data = yield this._getUnwrittenData(libraryID, objectType);
|
||||
Zotero.debug("Skipping " + data.length + " "
|
||||
+ (data.length == 1 ? objectType : objectTypePlural)
|
||||
|
@ -441,6 +492,41 @@ Zotero.Sync.Data.Local = {
|
|||
}),
|
||||
|
||||
|
||||
resolveConflicts: function (conflicts) {
|
||||
var io = {
|
||||
dataIn: {
|
||||
captions: [
|
||||
Zotero.getString('sync.conflict.localItem'),
|
||||
Zotero.getString('sync.conflict.remoteItem'),
|
||||
Zotero.getString('sync.conflict.mergedItem')
|
||||
],
|
||||
conflicts
|
||||
}
|
||||
};
|
||||
|
||||
var url = 'chrome://zotero/content/merge.xul';
|
||||
|
||||
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
||||
.getService(Components.interfaces.nsIWindowMediator);
|
||||
var lastWin = wm.getMostRecentWindow("navigator:browser");
|
||||
if (lastWin) {
|
||||
lastWin.openDialog(url, '', 'chrome,modal,centerscreen', io);
|
||||
}
|
||||
else {
|
||||
// When using nsIWindowWatcher, the object has to be wrapped here
|
||||
// https://developer.mozilla.org/en-US/docs/Working_with_windows_in_chrome_code#Example_5_Using_nsIWindowWatcher_for_passing_an_arbritrary_JavaScript_object
|
||||
io.wrappedJSObject = io;
|
||||
let ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
|
||||
.getService(Components.interfaces.nsIWindowWatcher);
|
||||
ww.openWindow(null, url, '', 'chrome,modal,centerscreen,dialog', io);
|
||||
}
|
||||
if (io.error) {
|
||||
throw io.error;
|
||||
}
|
||||
return io.dataOut;
|
||||
},
|
||||
|
||||
|
||||
//
|
||||
// Classic sync
|
||||
//
|
||||
|
@ -460,8 +546,10 @@ Zotero.Sync.Data.Local = {
|
|||
_saveObjectFromJSON: Zotero.Promise.coroutine(function* (obj, json, options) {
|
||||
try {
|
||||
yield obj.fromJSON(json);
|
||||
obj.version = json.version;
|
||||
obj.synced = true;
|
||||
if (!options.saveAsChanged) {
|
||||
obj.version = json.version;
|
||||
obj.synced = true;
|
||||
}
|
||||
Zotero.debug("SAVING " + json.key + " WITH SYNCED");
|
||||
Zotero.debug(obj.version);
|
||||
yield obj.save({
|
||||
|
@ -699,14 +787,14 @@ Zotero.Sync.Data.Local = {
|
|||
|
||||
|
||||
/**
|
||||
* @return {Promise<Boolean>}
|
||||
* @return {Promise<Date|false>}
|
||||
*/
|
||||
objectInDeleteLog: Zotero.Promise.coroutine(function* (objectType, libraryID, key) {
|
||||
getDateDeleted: Zotero.Promise.coroutine(function* (objectType, libraryID, key) {
|
||||
var syncObjectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType);
|
||||
var sql = "SELECT COUNT(*) FROM syncDeleteLog WHERE libraryID=? AND key=? "
|
||||
var sql = "SELECT dateDeleted FROM syncDeleteLog WHERE libraryID=? AND key=? "
|
||||
+ "AND syncObjectTypeID=?";
|
||||
var count = yield Zotero.DB.valueQueryAsync(sql, [libraryID, key, syncObjectTypeID]);
|
||||
return !!count;
|
||||
var date = yield Zotero.DB.valueQueryAsync(sql, [libraryID, key, syncObjectTypeID]);
|
||||
return date ? Zotero.Date.sqlToDate(date, true) : false;
|
||||
}),
|
||||
|
||||
|
||||
|
|
|
@ -789,12 +789,6 @@ sync.cancel = Cancel Sync
|
|||
sync.openSyncPreferences = Open Sync Preferences
|
||||
sync.resetGroupAndSync = Reset Group and Sync
|
||||
sync.removeGroupsAndSync = Remove Groups and Sync
|
||||
sync.localObject = Local Object
|
||||
sync.remoteObject = Remote Object
|
||||
sync.mergedObject = Merged Object
|
||||
sync.merge.resolveAllLocal = Use the local version for all remaining conflicts
|
||||
sync.merge.resolveAllRemote = Use the remote version for all remaining conflicts
|
||||
|
||||
|
||||
sync.error.usernameNotSet = Username not set
|
||||
sync.error.usernameNotSet.text = You must enter your zotero.org username and password in the Zotero preferences to sync with the Zotero server.
|
||||
|
@ -845,9 +839,17 @@ sync.conflict.tagItemMerge.log = The Zotero tag '%S' has been added to and/or
|
|||
sync.conflict.tag.addedToRemote = It has been added to the following remote items:
|
||||
sync.conflict.tag.addedToLocal = It has been added to the following local items:
|
||||
|
||||
sync.conflict.fileChanged = The following file has been changed in multiple locations.
|
||||
sync.conflict.itemChanged = The following item has been changed in multiple locations.
|
||||
sync.conflict.chooseVersionToKeep = Choose the version you would like to keep, and then click %S.
|
||||
sync.conflict.localItem = Local Item
|
||||
sync.conflict.remoteItem = Remote Item
|
||||
sync.conflict.mergedItem = Merged Item
|
||||
sync.conflict.localFile = Local File
|
||||
sync.conflict.remoteFile = Remote File
|
||||
sync.conflict.resolveAllLocal = Use the local version for all remaining conflicts
|
||||
sync.conflict.resolveAllRemote = Use the remote version for all remaining conflicts
|
||||
sync.conflict.resolveAllLocalFields = Use local fields for all remaining conflicts
|
||||
sync.conflict.resolveAllRemoteFields = Use remote fields for all remaining conflicts
|
||||
sync.conflict.itemChanged = The following item has been changed in multiple locations. Click the version to use for resolving conflicting fields, and then click %S.
|
||||
sync.conflict.fileChanged = The following file has been changed in multiple locations. Choose the version you would like to keep, and then click %S.
|
||||
sync.conflict.chooseThisVersion = Choose this version
|
||||
|
||||
sync.status.notYetSynced = Not yet synced
|
||||
|
|
|
@ -68,11 +68,10 @@ zoteromergegroup {
|
|||
overflow-y: auto;
|
||||
}
|
||||
|
||||
zoteromergepane #trash-box, zoteromergepane #delete-box {
|
||||
zoteromergepane *[anonid="delete-box"] {
|
||||
min-width: 15em;
|
||||
-moz-box-align: center;
|
||||
-moz-box-pack: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
zoteromergepane[selected=true] groupbox caption {
|
||||
|
@ -80,11 +79,6 @@ zoteromergepane[selected=true] groupbox caption {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
zoteromergepane[id=leftpane]:not([selected=true]):hover groupbox caption,
|
||||
zoteromergepane[id=rightpane]:not([selected=true]):hover groupbox caption {
|
||||
/* font-weight: bold; */
|
||||
}
|
||||
|
||||
hbox:not([mergetype=note]) zoteromergepane:active[id=leftpane] groupbox caption,
|
||||
hbox:not([mergetype=note]) zoteromergepane:active[id=rightpane] groupbox caption {
|
||||
color: red;
|
||||
|
|
|
@ -309,6 +309,7 @@ CREATE TABLE syncDeleteLog (
|
|||
syncObjectTypeID INT NOT NULL,
|
||||
libraryID INT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
dateDeleted TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
synced INT NOT NULL DEFAULT 0,
|
||||
UNIQUE (syncObjectTypeID, libraryID, key),
|
||||
FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID),
|
||||
|
@ -319,6 +320,7 @@ CREATE INDEX syncDeleteLog_synced ON syncDeleteLog(synced);
|
|||
CREATE TABLE storageDeleteLog (
|
||||
libraryID INT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
dateDeleted TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
synced INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (libraryID, key),
|
||||
FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE
|
||||
|
|
|
@ -86,6 +86,8 @@ function waitForWindow(uri, callback) {
|
|||
}
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
win.close();
|
||||
deferred.reject(e);
|
||||
return;
|
||||
}
|
||||
|
@ -113,7 +115,7 @@ function waitForWindow(uri, callback) {
|
|||
* @return {Promise}
|
||||
*/
|
||||
function waitForDialog(onOpen, button='accept', url) {
|
||||
return waitForWindow(url || "chrome://global/content/commonDialog.xul", Zotero.Promise.method(function (dialog, deferred) {
|
||||
return waitForWindow(url || "chrome://global/content/commonDialog.xul", Zotero.Promise.method(function (dialog) {
|
||||
var failure = false;
|
||||
if (onOpen) {
|
||||
try {
|
||||
|
@ -272,6 +274,8 @@ var createGroup = Zotero.Promise.coroutine(function* (props) {
|
|||
* @param {String} [params.parentKey]
|
||||
* @param {Boolean} [params.synced]
|
||||
* @param {Integer} [params.version]
|
||||
* @param {Integer} [params.dateAdded] - Allowed for items
|
||||
* @param {Integer} [params.dateModified] - Allowed for items
|
||||
*/
|
||||
function createUnsavedDataObject(objectType, params = {}) {
|
||||
if (!objectType) {
|
||||
|
@ -298,6 +302,9 @@ function createUnsavedDataObject(objectType, params = {}) {
|
|||
break;
|
||||
}
|
||||
var allowedParams = ['parentID', 'parentKey', 'synced', 'version'];
|
||||
if (objectType == 'item') {
|
||||
allowedParams.push('dateAdded', 'dateModified');
|
||||
}
|
||||
allowedParams.forEach(function (param) {
|
||||
if (params[param] !== undefined) {
|
||||
obj[param] = params[param];
|
||||
|
@ -316,7 +323,7 @@ function getNameProperty(objectType) {
|
|||
return objectType == 'item' ? 'title' : 'name';
|
||||
}
|
||||
|
||||
var modifyDataObject = Zotero.Promise.coroutine(function* (obj, params = {}) {
|
||||
var modifyDataObject = Zotero.Promise.coroutine(function* (obj, params = {}, saveOptions) {
|
||||
switch (obj.objectType) {
|
||||
case 'item':
|
||||
yield obj.loadItemData();
|
||||
|
@ -329,7 +336,7 @@ var modifyDataObject = Zotero.Promise.coroutine(function* (obj, params = {}) {
|
|||
default:
|
||||
obj.name = params.name !== undefined ? params.name : Zotero.Utilities.randomString();
|
||||
}
|
||||
return obj.saveTx();
|
||||
return obj.saveTx(saveOptions);
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -110,7 +110,7 @@ describe("Zotero.Item", function () {
|
|||
|
||||
it("should throw if value is undefined", function () {
|
||||
var item = new Zotero.Item('book');
|
||||
assert.throws(() => item.setField('title'), "Value cannot be undefined");
|
||||
assert.throws(() => item.setField('title'), "'title' value cannot be undefined");
|
||||
})
|
||||
|
||||
it("should not mark an empty field set to an empty string as changed", function () {
|
||||
|
|
|
@ -661,16 +661,16 @@ describe("Zotero.Sync.Data.Engine", function () {
|
|||
assert.isFalse(Zotero.Items.exists(itemID));
|
||||
|
||||
// Make sure objects weren't added to sync delete log
|
||||
assert.isFalse(yield Zotero.Sync.Data.Local.objectInDeleteLog(
|
||||
assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted(
|
||||
'setting', userLibraryID, 'tagColors'
|
||||
));
|
||||
assert.isFalse(yield Zotero.Sync.Data.Local.objectInDeleteLog(
|
||||
assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted(
|
||||
'collection', userLibraryID, collectionKey
|
||||
));
|
||||
assert.isFalse(yield Zotero.Sync.Data.Local.objectInDeleteLog(
|
||||
assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted(
|
||||
'search', userLibraryID, searchKey
|
||||
));
|
||||
assert.isFalse(yield Zotero.Sync.Data.Local.objectInDeleteLog(
|
||||
assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted(
|
||||
'item', userLibraryID, itemKey
|
||||
));
|
||||
})
|
||||
|
@ -984,7 +984,7 @@ describe("Zotero.Sync.Data.Engine", function () {
|
|||
|
||||
// JSON objects 3 should be deleted and not in the delete log
|
||||
assert.isFalse(objectsClass.getByLibraryAndKey(userLibraryID, objects[type][2].key));
|
||||
assert.isFalse(yield Zotero.Sync.Data.Local.objectInDeleteLog(
|
||||
assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted(
|
||||
type, userLibraryID, objects[type][2].key
|
||||
));
|
||||
}
|
||||
|
|
|
@ -31,6 +31,316 @@ describe("Zotero.Sync.Data.Local", function() {
|
|||
})
|
||||
})
|
||||
|
||||
describe("Conflict Resolution", function () {
|
||||
beforeEach(function* () {
|
||||
yield Zotero.DB.queryAsync("DELETE FROM syncCache");
|
||||
})
|
||||
|
||||
after(function* () {
|
||||
yield Zotero.DB.queryAsync("DELETE FROM syncCache");
|
||||
})
|
||||
|
||||
it("should show conflict resolution window on item conflicts", function* () {
|
||||
var libraryID = Zotero.Libraries.userLibraryID;
|
||||
|
||||
var type = 'item';
|
||||
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
||||
|
||||
var objects = [];
|
||||
var values = [];
|
||||
var dateAdded = Date.now() - 86400000;
|
||||
for (let i = 0; i < 2; i++) {
|
||||
values.push({
|
||||
left: {},
|
||||
right: {}
|
||||
});
|
||||
|
||||
// Create object in cache
|
||||
let obj = objects[i] = yield createDataObject(
|
||||
type,
|
||||
{
|
||||
version: 10,
|
||||
dateAdded: Zotero.Date.dateToSQL(new Date(dateAdded), true),
|
||||
// Set Date Modified values one minute apart to enforce order
|
||||
dateModified: Zotero.Date.dateToSQL(
|
||||
new Date(dateAdded + (i * 60000)), true
|
||||
)
|
||||
}
|
||||
);
|
||||
let jsonData = yield obj.toJSON();
|
||||
jsonData.key = obj.key;
|
||||
jsonData.version = 10;
|
||||
let json = {
|
||||
key: obj.key,
|
||||
version: jsonData.version,
|
||||
data: jsonData
|
||||
};
|
||||
yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
|
||||
|
||||
// Create new version in cache, simulating a download
|
||||
json.version = jsonData.version = 15;
|
||||
values[i].right.title = jsonData.title = Zotero.Utilities.randomString();
|
||||
yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
|
||||
|
||||
// Modify object locally
|
||||
yield modifyDataObject(obj, undefined, { skipDateModifiedUpdate: true });
|
||||
values[i].left.title = obj.getField('title');
|
||||
}
|
||||
|
||||
waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
|
||||
var doc = dialog.document;
|
||||
var wizard = doc.documentElement;
|
||||
var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0];
|
||||
|
||||
// 1 (remote)
|
||||
// Remote version should be selected by default
|
||||
assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
|
||||
wizard.getButton('next').click();
|
||||
|
||||
// 2 (local)
|
||||
assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
|
||||
// Select local object
|
||||
mergeGroup.leftpane.click();
|
||||
assert.equal(mergeGroup.leftpane.getAttribute('selected'), 'true');
|
||||
assert.isTrue(wizard.getButton('next').hidden);
|
||||
assert.isFalse(wizard.getButton('finish').hidden);
|
||||
wizard.getButton('finish').click();
|
||||
})
|
||||
yield Zotero.Sync.Data.Local.processSyncCacheForObjectType(
|
||||
libraryID, type, { stopOnError: true }
|
||||
);
|
||||
|
||||
assert.equal(objects[0].getField('title'), values[0].right.title);
|
||||
assert.equal(objects[1].getField('title'), values[1].left.title);
|
||||
})
|
||||
|
||||
it("should resolve all remaining conflicts with one side", function* () {
|
||||
var libraryID = Zotero.Libraries.userLibraryID;
|
||||
|
||||
var type = 'item';
|
||||
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
||||
|
||||
var objects = [];
|
||||
var values = [];
|
||||
var dateAdded = Date.now() - 86400000;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
values.push({
|
||||
left: {},
|
||||
right: {}
|
||||
});
|
||||
|
||||
// Create object in cache
|
||||
let obj = objects[i] = yield createDataObject(
|
||||
type,
|
||||
{
|
||||
version: 10,
|
||||
dateAdded: Zotero.Date.dateToSQL(new Date(dateAdded), true),
|
||||
// Set Date Modified values one minute apart to enforce order
|
||||
dateModified: Zotero.Date.dateToSQL(
|
||||
new Date(dateAdded + (i * 60000)), true
|
||||
)
|
||||
}
|
||||
);
|
||||
let jsonData = yield obj.toJSON();
|
||||
jsonData.key = obj.key;
|
||||
jsonData.version = 10;
|
||||
let json = {
|
||||
key: obj.key,
|
||||
version: jsonData.version,
|
||||
data: jsonData
|
||||
};
|
||||
yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
|
||||
|
||||
// Create new version in cache, simulating a download
|
||||
json.version = jsonData.version = 15;
|
||||
values[i].right.title = jsonData.title = Zotero.Utilities.randomString();
|
||||
yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
|
||||
|
||||
// Modify object locally
|
||||
yield modifyDataObject(obj, undefined, { skipDateModifiedUpdate: true });
|
||||
values[i].left.title = obj.getField('title');
|
||||
}
|
||||
|
||||
waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
|
||||
var doc = dialog.document;
|
||||
var wizard = doc.documentElement;
|
||||
var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0];
|
||||
var resolveAll = doc.getElementById('resolve-all');
|
||||
|
||||
// 1 (remote)
|
||||
// Remote version should be selected by default
|
||||
assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
|
||||
assert.equal(
|
||||
resolveAll.label,
|
||||
Zotero.getString('sync.conflict.resolveAllRemoteFields')
|
||||
);
|
||||
wizard.getButton('next').click();
|
||||
|
||||
// 2 (local and Resolve All checkbox)
|
||||
assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
|
||||
mergeGroup.leftpane.click();
|
||||
assert.equal(
|
||||
resolveAll.label,
|
||||
Zotero.getString('sync.conflict.resolveAllLocalFields')
|
||||
);
|
||||
resolveAll.click();
|
||||
|
||||
assert.isTrue(wizard.getButton('next').hidden);
|
||||
assert.isFalse(wizard.getButton('finish').hidden);
|
||||
wizard.getButton('finish').click();
|
||||
})
|
||||
yield Zotero.Sync.Data.Local.processSyncCacheForObjectType(
|
||||
libraryID, type, { stopOnError: true }
|
||||
);
|
||||
|
||||
assert.equal(objects[0].getField('title'), values[0].right.title);
|
||||
assert.equal(objects[1].getField('title'), values[1].left.title);
|
||||
assert.equal(objects[2].getField('title'), values[2].left.title);
|
||||
})
|
||||
|
||||
it("should handle local item deletion, keeping deletion", function* () {
|
||||
var libraryID = Zotero.Libraries.userLibraryID;
|
||||
|
||||
var type = 'item';
|
||||
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
||||
|
||||
// Create object in cache
|
||||
var obj = yield createDataObject(type, { version: 10 });
|
||||
var jsonData = yield obj.toJSON();
|
||||
var key = jsonData.key = obj.key;
|
||||
jsonData.version = 10;
|
||||
let json = {
|
||||
key: obj.key,
|
||||
version: jsonData.version,
|
||||
data: jsonData
|
||||
};
|
||||
yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
|
||||
|
||||
// Create new version in cache, simulating a download
|
||||
json.version = jsonData.version = 15;
|
||||
jsonData.title = Zotero.Utilities.randomString();
|
||||
yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
|
||||
|
||||
// Delete object locally
|
||||
yield obj.eraseTx();
|
||||
|
||||
waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
|
||||
var doc = dialog.document;
|
||||
var wizard = doc.documentElement;
|
||||
var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0];
|
||||
|
||||
// Remote version should be selected by default
|
||||
assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
|
||||
assert.ok(mergeGroup.leftpane.pane.onclick);
|
||||
mergeGroup.leftpane.pane.click();
|
||||
wizard.getButton('finish').click();
|
||||
})
|
||||
yield Zotero.Sync.Data.Local.processSyncCacheForObjectType(
|
||||
libraryID, type, { stopOnError: true }
|
||||
);
|
||||
|
||||
obj = objectsClass.getByLibraryAndKey(libraryID, key);
|
||||
assert.isFalse(obj);
|
||||
})
|
||||
|
||||
it("should handle restore locally deleted item", function* () {
|
||||
var libraryID = Zotero.Libraries.userLibraryID;
|
||||
|
||||
var type = 'item';
|
||||
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
||||
|
||||
// Create object in cache
|
||||
var obj = yield createDataObject(type, { version: 10 });
|
||||
var jsonData = yield obj.toJSON();
|
||||
var key = jsonData.key = obj.key;
|
||||
jsonData.version = 10;
|
||||
let json = {
|
||||
key: obj.key,
|
||||
version: jsonData.version,
|
||||
data: jsonData
|
||||
};
|
||||
yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
|
||||
|
||||
// Create new version in cache, simulating a download
|
||||
json.version = jsonData.version = 15;
|
||||
jsonData.title = Zotero.Utilities.randomString();
|
||||
yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
|
||||
|
||||
// Delete object locally
|
||||
yield obj.eraseTx();
|
||||
|
||||
waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
|
||||
var doc = dialog.document;
|
||||
var wizard = doc.documentElement;
|
||||
var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0];
|
||||
|
||||
assert.isTrue(doc.getElementById('resolve-all').hidden);
|
||||
|
||||
// Remote version should be selected by default
|
||||
assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
|
||||
wizard.getButton('finish').click();
|
||||
})
|
||||
yield Zotero.Sync.Data.Local.processSyncCacheForObjectType(
|
||||
libraryID, type, { stopOnError: true }
|
||||
);
|
||||
|
||||
obj = objectsClass.getByLibraryAndKey(libraryID, key);
|
||||
assert.ok(obj);
|
||||
yield obj.loadItemData();
|
||||
assert.equal(obj.getField('title'), jsonData.title);
|
||||
})
|
||||
|
||||
it("should handle note conflict", function* () {
|
||||
var libraryID = Zotero.Libraries.userLibraryID;
|
||||
|
||||
var type = 'item';
|
||||
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
||||
|
||||
var noteText1 = "<p>A</p>";
|
||||
var noteText2 = "<p>B</p>";
|
||||
|
||||
// Create object in cache
|
||||
var obj = new Zotero.Item('note');
|
||||
obj.setNote("");
|
||||
obj.version = 10;
|
||||
yield obj.saveTx();
|
||||
var jsonData = yield obj.toJSON();
|
||||
var key = jsonData.key = obj.key;
|
||||
let json = {
|
||||
key: obj.key,
|
||||
version: jsonData.version,
|
||||
data: jsonData
|
||||
};
|
||||
yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
|
||||
|
||||
// Create new version in cache, simulating a download
|
||||
json.version = jsonData.version = 15;
|
||||
json.data.note = noteText2;
|
||||
yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
|
||||
|
||||
// Delete object locally
|
||||
obj.setNote(noteText1);
|
||||
|
||||
waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
|
||||
var doc = dialog.document;
|
||||
var wizard = doc.documentElement;
|
||||
var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0];
|
||||
|
||||
// Remote version should be selected by default
|
||||
assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
|
||||
wizard.getButton('finish').click();
|
||||
})
|
||||
yield Zotero.Sync.Data.Local.processSyncCacheForObjectType(
|
||||
libraryID, type, { stopOnError: true }
|
||||
);
|
||||
|
||||
obj = objectsClass.getByLibraryAndKey(libraryID, key);
|
||||
assert.ok(obj);
|
||||
yield obj.loadNote();
|
||||
assert.equal(obj.getNote(), noteText2);
|
||||
})
|
||||
})
|
||||
|
||||
describe("#_reconcileChanges()", function () {
|
||||
describe("items", function () {
|
||||
it("should ignore non-conflicting local changes and return remote changes", function () {
|
||||
|
|
Loading…
Reference in a new issue