Support for automatically merging collections and searches
This commit is contained in:
parent
02cd71ebb5
commit
cd4d084dd9
3 changed files with 395 additions and 196 deletions
|
@ -120,7 +120,7 @@ Zotero.DataObjectUtilities = {
|
||||||
case 'collections':
|
case 'collections':
|
||||||
case 'tags':
|
case 'tags':
|
||||||
case 'relations':
|
case 'relations':
|
||||||
changed = this["_" + field + "Equals"](val1, val2);
|
changed = this["_" + field + "Changed"](val1, val2);
|
||||||
if (changed) {
|
if (changed) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -154,36 +154,17 @@ Zotero.DataObjectUtilities = {
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
_creatorsEquals: function (data1, data2) {
|
_creatorsChanged: function (data1, data2) {
|
||||||
if (!data2 || data1.length != data2.length) return true;
|
if (!data2 || data1.length != data2.length) return true;
|
||||||
for (let i = 0; i < data1.length; i++) {
|
for (let i = 0; i < data1.length; i++) {
|
||||||
if (!Zotero.Creators.equals(data1[i], data2[i])) {
|
if (!Zotero.Creators.equals(data1[i], data2[i])) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
},
|
}
|
||||||
|
}
|
||||||
_collectionsEquals: function (data1, data2) {
|
|
||||||
if (!data2 || data1.length != data2.length) return true;
|
|
||||||
let c1 = data1.concat();
|
|
||||||
let c2 = data2.concat();
|
|
||||||
c1.sort();
|
|
||||||
c2.sort();
|
|
||||||
return Zotero.Utilities.arrayEquals(c1, c2);
|
|
||||||
},
|
|
||||||
|
|
||||||
_tagsEquals: function (data1, data2) {
|
|
||||||
if (!data2 || data1.length != data2.length) return true;
|
|
||||||
for (let i = 0; i < data1.length; i++) {
|
|
||||||
if (!Zotero.Tags.equals(data1[i], data2[i])) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_relationsEquals: function (data1, data2) {
|
_conditionsChanged: function (data1, data2) {
|
||||||
if (!data2) return true;
|
if (!data2) return true;
|
||||||
var pred1 = Object.keys(data1);
|
var pred1 = Object.keys(data1);
|
||||||
pred1.sort();
|
pred1.sort();
|
||||||
|
@ -192,10 +173,44 @@ Zotero.DataObjectUtilities = {
|
||||||
if (!Zotero.Utilities.arrayEquals(pred1, pred2)) return false;
|
if (!Zotero.Utilities.arrayEquals(pred1, pred2)) return false;
|
||||||
for (let i in pred1) {
|
for (let i in pred1) {
|
||||||
if (!Zotero.Utilities.arrayEquals(pred1[i], pred2[i])) {
|
if (!Zotero.Utilities.arrayEquals(pred1[i], pred2[i])) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
_collectionsChanged: function (data1, data2) {
|
||||||
|
if (!data2 || data1.length != data2.length) return true;
|
||||||
|
let c1 = data1.concat();
|
||||||
|
let c2 = data2.concat();
|
||||||
|
c1.sort();
|
||||||
|
c2.sort();
|
||||||
|
return !Zotero.Utilities.arrayEquals(c1, c2);
|
||||||
|
},
|
||||||
|
|
||||||
|
_tagsChanged: function (data1, data2) {
|
||||||
|
if (!data2 || data1.length != data2.length) return true;
|
||||||
|
for (let i = 0; i < data1.length; i++) {
|
||||||
|
if (!Zotero.Tags.equals(data1[i], data2[i])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
_relationsChanged: function (data1, data2) {
|
||||||
|
if (!data2) return true;
|
||||||
|
var pred1 = Object.keys(data1);
|
||||||
|
pred1.sort();
|
||||||
|
var pred2 = Object.keys(data2);
|
||||||
|
pred2.sort();
|
||||||
|
if (!Zotero.Utilities.arrayEquals(pred1, pred2)) return true;
|
||||||
|
for (let i in pred1) {
|
||||||
|
if (!Zotero.Utilities.arrayEquals(pred1[i], pred2[i])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
@ -231,6 +246,7 @@ Zotero.DataObjectUtilities = {
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case 'creators':
|
case 'creators':
|
||||||
case 'collections':
|
case 'collections':
|
||||||
|
case 'conditions':
|
||||||
case 'relations':
|
case 'relations':
|
||||||
case 'tags':
|
case 'tags':
|
||||||
let changes = this["_" + field + "Diff"](val1, val2);
|
let changes = this["_" + field + "Diff"](val1, val2);
|
||||||
|
@ -277,7 +293,7 @@ Zotero.DataObjectUtilities = {
|
||||||
// All remaining fields don't exist in data1
|
// All remaining fields don't exist in data1
|
||||||
|
|
||||||
let val = data2[field];
|
let val = data2[field];
|
||||||
if (val === false || val === ""
|
if (val === false || val === "" || val === null
|
||||||
|| (typeof val == 'object' && Object.keys(val).length == 0)) {
|
|| (typeof val == 'object' && Object.keys(val).length == 0)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -305,7 +321,7 @@ Zotero.DataObjectUtilities = {
|
||||||
op: "delete"
|
op: "delete"
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
if (!this._creatorsEquals(data1, data2)) {
|
if (this._creatorsChanged(data1, data2)) {
|
||||||
return [{
|
return [{
|
||||||
field: "creators",
|
field: "creators",
|
||||||
op: "modify",
|
op: "modify",
|
||||||
|
@ -315,8 +331,7 @@ Zotero.DataObjectUtilities = {
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
|
|
||||||
_collectionsDiff: function (data1, data2) {
|
_collectionsDiff: function (data1, data2 = []) {
|
||||||
data2 = data2 || [];
|
|
||||||
var changeset = [];
|
var changeset = [];
|
||||||
var removed = Zotero.Utilities.arrayDiff(data1, data2);
|
var removed = Zotero.Utilities.arrayDiff(data1, data2);
|
||||||
for (let i = 0; i < removed.length; i++) {
|
for (let i = 0; i < removed.length; i++) {
|
||||||
|
@ -337,8 +352,38 @@ Zotero.DataObjectUtilities = {
|
||||||
return changeset;
|
return changeset;
|
||||||
},
|
},
|
||||||
|
|
||||||
_tagsDiff: function (data1, data2) {
|
_conditionsDiff: function (data1, data2 = {}) {
|
||||||
data2 = data2 || [];
|
var changeset = [];
|
||||||
|
outer:
|
||||||
|
for (let i = 0; i < data1.length; i++) {
|
||||||
|
for (let j = 0; j < data2.length; j++) {
|
||||||
|
if (Zotero.SearchConditions.equals(data1[i], data2[j])) {
|
||||||
|
continue outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changeset.push({
|
||||||
|
field: "conditions",
|
||||||
|
op: "member-remove",
|
||||||
|
value: data1[i]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
outer:
|
||||||
|
for (let i = 0; i < data2.length; i++) {
|
||||||
|
for (let j = 0; j < data1.length; j++) {
|
||||||
|
if (Zotero.SearchConditions.equals(data2[i], data1[j])) {
|
||||||
|
continue outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changeset.push({
|
||||||
|
field: "conditions",
|
||||||
|
op: "member-add",
|
||||||
|
value: data2[i]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return changeset;
|
||||||
|
},
|
||||||
|
|
||||||
|
_tagsDiff: function (data1, data2 = []) {
|
||||||
var changeset = [];
|
var changeset = [];
|
||||||
outer:
|
outer:
|
||||||
for (let i = 0; i < data1.length; i++) {
|
for (let i = 0; i < data1.length; i++) {
|
||||||
|
@ -369,8 +414,7 @@ Zotero.DataObjectUtilities = {
|
||||||
return changeset;
|
return changeset;
|
||||||
},
|
},
|
||||||
|
|
||||||
_relationsDiff: function (data1, data2) {
|
_relationsDiff: function (data1, data2 = {}) {
|
||||||
data2 = data2 || {};
|
|
||||||
var changeset = [];
|
var changeset = [];
|
||||||
for (let pred in data1) {
|
for (let pred in data1) {
|
||||||
let vals1 = typeof data1[pred] == 'string' ? [data1[pred]] : data1[pred];
|
let vals1 = typeof data1[pred] == 'string' ? [data1[pred]] : data1[pred];
|
||||||
|
@ -448,10 +492,12 @@ Zotero.DataObjectUtilities = {
|
||||||
throw new Error("Unimplemented");
|
throw new Error("Unimplemented");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'conditions':
|
||||||
case 'tags':
|
case 'tags':
|
||||||
let found = false;
|
let found = false;
|
||||||
|
let f = c.field == 'conditions' ? Zotero.SearchConditions : Zotero.Tags;
|
||||||
for (let i = 0; i < json[c.field].length; i++) {
|
for (let i = 0; i < json[c.field].length; i++) {
|
||||||
if (Zotero.Tags.equals(json[c.field][i], c.value)) {
|
if (f.equals(json[c.field][i], c.value)) {
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -479,9 +525,11 @@ Zotero.DataObjectUtilities = {
|
||||||
throw new Error("Unimplemented");
|
throw new Error("Unimplemented");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'conditions':
|
||||||
case 'tags':
|
case 'tags':
|
||||||
|
let f = c.field == 'conditions' ? Zotero.SearchConditions : Zotero.Tags;
|
||||||
for (let i = 0; i < json[c.field].length; i++) {
|
for (let i = 0; i < json[c.field].length; i++) {
|
||||||
if (Zotero.Tags.equals(json[c.field][i], c.value)) {
|
if (f.equals(json[c.field][i], c.value)) {
|
||||||
json[c.field].splice(i, 1);
|
json[c.field].splice(i, 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2286,6 +2286,16 @@ Zotero.SearchConditions = new function(){
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two API JSON condition objects
|
||||||
|
*/
|
||||||
|
this.equals = function (data1, data2) {
|
||||||
|
return data1.condition === data2.condition
|
||||||
|
&& data1.operator === data2.operator
|
||||||
|
&& data1.value === data2.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Parses a search into words and "double-quoted phrases"
|
* Parses a search into words and "double-quoted phrases"
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
describe("Zotero.DataObjectUtilities", function() {
|
describe("Zotero.DataObjectUtilities", function() {
|
||||||
|
describe("#diff()", function () {
|
||||||
// This is mostly covered by syncLocal::_reconcileChanges() tests, but we test some
|
// This is mostly covered by syncLocal::_reconcileChanges() tests, but we test some
|
||||||
// additional things here
|
// additional things here
|
||||||
describe("#diff()", function () {
|
describe("items", function () {
|
||||||
//
|
//
|
||||||
// Fields
|
// Fields
|
||||||
//
|
//
|
||||||
|
@ -181,6 +182,62 @@ describe("Zotero.DataObjectUtilities", function() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
// Searches
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Search conditions
|
||||||
|
//
|
||||||
|
describe("searches", function () {
|
||||||
|
describe("conditions", function () {
|
||||||
|
it("should not show an empty conditions object and a missing one as different", function () {
|
||||||
|
var json1 = {
|
||||||
|
conditions: {}
|
||||||
|
};
|
||||||
|
var json2 = {
|
||||||
|
};
|
||||||
|
var changes = Zotero.DataObjectUtilities.diff(json1, json2);
|
||||||
|
Zotero.debug(changes);
|
||||||
|
assert.lengthOf(changes, 0);
|
||||||
|
|
||||||
|
var json1 = {};
|
||||||
|
var json2 = {
|
||||||
|
conditions: {}
|
||||||
|
};
|
||||||
|
var changes = Zotero.DataObjectUtilities.diff(json1, json2);
|
||||||
|
Zotero.debug(changes);
|
||||||
|
assert.lengthOf(changes, 0);
|
||||||
|
})
|
||||||
|
|
||||||
|
/*it("should not show an empty conditions object and a missing one as different", function () {
|
||||||
|
var json1 = {
|
||||||
|
conditions: []
|
||||||
|
};
|
||||||
|
var json2 = {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
condition: 'title',
|
||||||
|
operator: 'contains',
|
||||||
|
value: 'test'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
var changes = Zotero.DataObjectUtilities.diff(json1, json2);
|
||||||
|
Zotero.debug(changes);
|
||||||
|
assert.lengthOf(changes, 0);
|
||||||
|
|
||||||
|
var json1 = {};
|
||||||
|
var json2 = {
|
||||||
|
conditions: {}
|
||||||
|
};
|
||||||
|
var changes = Zotero.DataObjectUtilities.diff(json1, json2);
|
||||||
|
Zotero.debug(changes);
|
||||||
|
assert.lengthOf(changes, 0);
|
||||||
|
})*/
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
describe("#applyChanges()", function () {
|
describe("#applyChanges()", function () {
|
||||||
//
|
//
|
||||||
|
@ -538,5 +595,89 @@ describe("Zotero.DataObjectUtilities", function() {
|
||||||
assert.lengthOf(json.tags, 0);
|
assert.lengthOf(json.tags, 0);
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Search conditions
|
||||||
|
//
|
||||||
|
describe("conditions", function () {
|
||||||
|
it("should add a condition", function () {
|
||||||
|
var json = {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
condition: "title",
|
||||||
|
op: "contains",
|
||||||
|
value: "A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
var changes = [
|
||||||
|
{
|
||||||
|
field: "conditions",
|
||||||
|
op: "member-add",
|
||||||
|
value: {
|
||||||
|
condition: "title",
|
||||||
|
op: "contains",
|
||||||
|
value: "B"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
Zotero.DataObjectUtilities.applyChanges(json, changes);
|
||||||
|
assert.sameDeepMembers(
|
||||||
|
json.conditions,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: "title",
|
||||||
|
op: "contains",
|
||||||
|
value: "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: "title",
|
||||||
|
op: "contains",
|
||||||
|
value: "B"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should remove a condition", function () {
|
||||||
|
var json = {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
condition: "title",
|
||||||
|
op: "contains",
|
||||||
|
value: "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: "title",
|
||||||
|
op: "contains",
|
||||||
|
value: "B"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
var changes = [
|
||||||
|
{
|
||||||
|
field: "conditions",
|
||||||
|
op: "member-remove",
|
||||||
|
value: {
|
||||||
|
condition: "title",
|
||||||
|
op: "contains",
|
||||||
|
value: "B"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
Zotero.DataObjectUtilities.applyChanges(json, changes);
|
||||||
|
assert.sameDeepMembers(
|
||||||
|
json.conditions,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: "title",
|
||||||
|
op: "contains",
|
||||||
|
value: "A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue