- Add automatic merging of collection and tag metadata and associated items, with warning alerts (eventually to be converted to logged notifications)

- Switch to using only keys for deleted items
- Fix various tag-related problems
- Probably other things
This commit is contained in:
Dan Stillman 2008-12-27 05:42:52 +00:00
parent 104d39bfa6
commit c7b2c84869
6 changed files with 389 additions and 213 deletions

View file

@ -398,7 +398,7 @@ Zotero.Collection.prototype.save = function () {
currentIDs = [];
}
if (this._previousData.childCollections) {
if (this._previousData) {
for each(var id in this._previousData.childCollections) {
if (currentIDs.indexOf(id) == -1) {
removed.push(id);
@ -406,7 +406,7 @@ Zotero.Collection.prototype.save = function () {
}
}
for each(var id in currentIDs) {
if (this._previousData.childCollections &&
if (this._previousData &&
this._previousData.childCollections.indexOf(id) != -1) {
continue;
}
@ -442,7 +442,7 @@ Zotero.Collection.prototype.save = function () {
currentIDs = [];
}
if (this._previousData.childItems) {
if (this._previousData) {
for each(var id in this._previousData.childItems) {
if (currentIDs.indexOf(id) == -1) {
removed.push(id);
@ -450,7 +450,7 @@ Zotero.Collection.prototype.save = function () {
}
}
for each(var id in currentIDs) {
if (this._previousData.childItems &&
if (this._previousData &&
this._previousData.childItems.indexOf(id) != -1) {
continue;
}
@ -801,6 +801,8 @@ Zotero.Collection.prototype.toArray = function() {
Zotero.Collection.prototype.serialize = function(nested) {
var childCollections = this.getChildCollections(true);
var childItems = this.getChildItems(true);
var obj = {
primary: {
collectionID: this.id,
@ -811,8 +813,8 @@ Zotero.Collection.prototype.serialize = function(nested) {
name: this.name,
parent: this.parent,
},
childCollections: this.getChildCollections(true),
childItems: this.getChildItems(true),
childCollections: childCollections ? childCollections : [],
childItems: childItems ? childItems : [],
descendents: this.id ? this.getDescendents(nested) : []
};
return obj;

View file

@ -143,8 +143,8 @@ Zotero.Tag.prototype.loadFromRow = function (row) {
/**
* Returns items linked to this tag
*
* @param bool asIDs Return as itemIDs
* @return array Array of Zotero.Item instances or itemIDs,
* @param {Boolean} asIDs Return as itemIDs
* @return {Array} Array of Zotero.Item instances or itemIDs,
* or FALSE if none
*/
Zotero.Tag.prototype.getLinkedItems = function (asIDs) {
@ -211,7 +211,7 @@ Zotero.Tag.prototype.removeItem = function (itemID) {
}
Zotero.Tag.prototype.save = function () {
Zotero.Tag.prototype.save = function (full) {
// Default to manual tag
if (!this.type) {
this.type = 0;
@ -307,7 +307,7 @@ Zotero.Tag.prototype.save = function () {
// Linked items
if (this._changed.linkedItems) {
if (full || this._changed.linkedItems) {
var removed = [];
var newids = [];
var currentIDs = this.getLinkedItems(true);
@ -315,19 +315,31 @@ Zotero.Tag.prototype.save = function () {
currentIDs = [];
}
if (this._previousData.linkedItems) {
for each(var id in this._previousData.linkedItems) {
if (currentIDs.indexOf(id) == -1) {
removed.push(id);
}
// Use the database for comparison instead of relying on the cache
// This is necessary for a syncing edge case (described in sync.js).
if (full) {
var sql = "SELECT itemID FROM itemTags WHERE tagID=?";
var dbItemIDs = Zotero.DB.columnQuery(sql, tagID);
if (dbItemIDs) {
removed = Zotero.Utilities.prototype.arrayDiff(currentIDs, dbItemIDs);
newids = Zotero.Utilities.prototype.arrayDiff(dbItemIDs, currentIDs);
}
else {
newids = currentIDs;
}
}
for each(var id in currentIDs) {
if (this._previousData.linkedItems &&
this._previousData.linkedItems.indexOf(id) != -1) {
continue;
else {
if (this._previousData.linkedItems) {
removed = Zotero.Utilities.prototype.arrayDiff(
currentIDs, this._previousData.linkedItems
);
newids = Zotero.Utilities.prototype.arrayDiff(
this._previousData.linkedItems, currentIDs
);
}
else {
newids = currentIDs;
}
newids.push(id);
}
if (removed.length) {
@ -414,8 +426,17 @@ Zotero.Tag.prototype.diff = function (tag, includeMatches, ignoreOnlyDateModifie
var d2 = Zotero.Utilities.prototype.arrayDiff(
otherData.linkedItems, thisData.linkedItems
);
numDiffs += d1.length;
numDiffs += d2.length;
numDiffs += d1.length + d2.length;
if (d1.length || d2.length) {
numDiffs += d1.length + d2.length;
diff[0].linkedItems = d1;
diff[1].linkedItems = d2;
}
else {
diff[0].linkedItems = [];
diff[1].linkedItems = [];
}
// DEBUG: ignoreOnlyDateModified wouldn't work if includeMatches was set?
if (numDiffs == 0 ||
@ -429,6 +450,8 @@ Zotero.Tag.prototype.diff = function (tag, includeMatches, ignoreOnlyDateModifie
Zotero.Tag.prototype.serialize = function () {
var linkedItems = this.getLinkedItems(true);
var obj = {
primary: {
tagID: this.id,
@ -439,7 +462,7 @@ Zotero.Tag.prototype.serialize = function () {
name: this.name,
type: this.type,
},
linkedItems: this.getLinkedItems(true),
linkedItems: linkedItems ? linkedItems : []
};
return obj;
}

View file

@ -2144,7 +2144,15 @@ Zotero.Schema = new function(){
}
}
//
// // 1.5 Sync Preview 3.6
if (i==47) {
Zotero.DB.query("ALTER TABLE syncDeleteLog RENAME TO syncDeleteLogOld");
Zotero.DB.query("DROP INDEX syncDeleteLog_timestamp");
Zotero.DB.query("CREATE TABLE syncDeleteLog (\n syncObjectTypeID INT NOT NULL,\n key TEXT NOT NULL UNIQUE,\n timestamp INT NOT NULL,\n FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID)\n);");
Zotero.DB.query("CREATE INDEX syncDeleteLog_timestamp ON syncDeleteLog(timestamp);");
Zotero.DB.query("INSERT INTO syncDeleteLog SELECT syncObjectTypeID, key, timestamp FROM syncDeleteLogOld");
Zotero.DB.query("DROP TABLE syncDeleteLogOld");
}
}
_updateDBVersion('userdata', toVersion);

View file

@ -136,10 +136,10 @@ Zotero.Sync = new function() {
/**
* @param object lastSyncDate JS Date object
* @return mixed Returns object with deleted ids
* @return mixed
* {
* items: [ { id: 123, key: ABCD1234 }, ... ]
* creators: [ { id: 123, key: ABCD1234 }, ... ],
* items: [ 'ABCD1234', 'BCDE2345', ... ]
* creators: [ 'ABCD1234', 'BCDE2345', ... ],
* ...
* }
* or FALSE if none or -1 if last sync time is before start of log
@ -162,7 +162,7 @@ Zotero.Sync = new function() {
}
var param = false;
var sql = "SELECT syncObjectTypeID, objectID, key FROM syncDeleteLog";
var sql = "SELECT syncObjectTypeID, key FROM syncDeleteLog";
if (lastSyncDate) {
param = Zotero.Date.toUnixTimestamp(lastSyncDate);
sql += " WHERE timestamp>?";
@ -174,20 +174,17 @@ Zotero.Sync = new function() {
return false;
}
var deletedIDs = {};
var deletedKeys = {};
for each(var syncObject in this.syncObjects) {
deletedIDs[syncObject.plural.toLowerCase()] = [];
deletedKeys[syncObject.plural.toLowerCase()] = [];
}
for each(var row in rows) {
var type = this.getObjectTypeName(row.syncObjectTypeID);
type = this.syncObjects[type].plural.toLowerCase()
deletedIDs[type].push({
id: row.objectID,
key: row.key
});
deletedKeys[type].push(row.key);
}
return deletedIDs;
return deletedKeys;
}
@ -297,7 +294,7 @@ Zotero.Sync.EventListener = new function () {
Zotero.DB.beginTransaction();
if (event == 'delete') {
var sql = "INSERT INTO syncDeleteLog VALUES (?, ?, ?, ?)";
var sql = "INSERT INTO syncDeleteLog VALUES (?, ?, ?)";
var syncStatement = Zotero.DB.getStatement(sql);
if (isItem && Zotero.Sync.Storage.active) {
@ -326,9 +323,8 @@ Zotero.Sync.EventListener = new function () {
}
syncStatement.bindInt32Parameter(0, objectTypeID);
syncStatement.bindInt32Parameter(1, ids[i]);
syncStatement.bindStringParameter(2, key);
syncStatement.bindInt32Parameter(3, ts);
syncStatement.bindStringParameter(1, key);
syncStatement.bindInt32Parameter(2, ts);
if (storageEnabled &&
oldItem.primary.itemType == 'attachment' &&
@ -652,7 +648,7 @@ Zotero.Sync.Server = new function () {
});
this.nextLocalSyncDate = false;
this.apiVersion = 2;
this.apiVersion = 3;
default xml namespace = '';
@ -715,7 +711,7 @@ Zotero.Sync.Server = new function () {
}
Zotero.debug('Got session ID ' + _sessionID + ' from server');
//Zotero.debug('Got session ID ' + _sessionID + ' from server');
if (callback) {
callback();
@ -1318,10 +1314,10 @@ Zotero.BufferedInputListener.prototype = {
* },
* deleted: {
* items: [
* { id: 1234, key: ABCDEFGHIJKMNPQRSTUVWXYZ23456789 }, ...
* 'ABCD1234', 'BCDE2345', ...
* ],
* creators: [
* { id: 1234, key: ABCDEFGHIJKMNPQRSTUVWXYZ23456789 }, ...
* 'ABCD1234', 'BCDE2345', ...
* ]
* }
* };
@ -1370,26 +1366,21 @@ Zotero.Sync.Server.Session.prototype.removeFromUpdated = function (syncObjectTyp
}
Zotero.Sync.Server.Session.prototype.addToDeleted = function (syncObjectTypeName, id, key) {
Zotero.Sync.Server.Session.prototype.addToDeleted = function (syncObjectTypeName, key) {
var pluralType = Zotero.Sync.syncObjects[syncObjectTypeName].plural.toLowerCase();
var deleted = this.uploadIDs.deleted[pluralType];
// DEBUG: inefficient
for each(var pair in deleted) {
if (pair.id == id) {
return;
}
if (deleted.indexOf(key) != -1) {
return;
}
deleted.push({ id: id, key: key});
deleted.push(key);
}
Zotero.Sync.Server.Session.prototype.removeFromDeleted = function (syncObjectTypeName, id, key) {
Zotero.Sync.Server.Session.prototype.removeFromDeleted = function (syncObjectTypeName, key) {
var pluralType = Zotero.Sync.syncObjects[syncObjectTypeName].plural.toLowerCase();
var deleted = this.uploadIDs.deleted[pluralType];
for (var i=0; i<deleted.length; i++) {
if (deleted[i].id == id && deleted[i].key == key) {
if (deleted[i] == key) {
deleted.splice(i, 1);
i--;
}
@ -1488,16 +1479,16 @@ Zotero.Sync.Server.Data = new function() {
* Pull out collections from delete queue in XML
*
* @param {XML} xml
* @return {Integer[]} Array of collection ids
* @return {String[]} Array of collection keys
*/
function _getDeletedCollections(xml) {
var ids = [];
function _getDeletedCollectionKeys(xml) {
var keys = [];
if (xml.deleted && xml.deleted.collections) {
for each(var xmlNode in xml.deleted.collections.collection) {
ids.push(parseInt(xmlNode.@id));
keys.push(xmlNode.@key.toString());
}
}
return ids;
return keys;
}
@ -1510,7 +1501,7 @@ Zotero.Sync.Server.Data = new function() {
Zotero.DB.beginTransaction();
xml = _preprocessUpdatedXML(xml);
var deletedCollections = _getDeletedCollections(xml);
var deletedCollectionKeys = _getDeletedCollectionKeys(xml);
var remoteCreatorStore = {};
var relatedItemsStore = {};
@ -1644,85 +1635,18 @@ Zotero.Sync.Server.Data = new function() {
break;
case 'collection':
var diff = obj.diff(remoteObj, false, true);
if (diff) {
var fieldsChanged = false;
for (var field in diff[0].primary) {
if (field != 'dateModified') {
fieldsChanged = true;
break;
}
}
for (var field in diff[0].fields) {
fieldsChanged = true;
break;
}
if (fieldsChanged) {
// Check for collection hierarchy change
if (diff[0].childCollections.length) {
// TODO
}
if (diff[1].childCollections.length) {
// TODO
}
// Check for item membership change
if (diff[0].childItems.length) {
var childItems = remoteObj.getChildItems(true);
remoteObj.childItems = childItems.concat(diff[0].childItems);
}
if (diff[1].childItems.length) {
var childItems = obj.getChildItems(true);
obj.childItems = childItems.concat(diff[1].childItems);
}
// TODO: log
// TEMP: uncomment once supported
//reconcile = true;
}
// No CR necessary
else {
var save = false;
// Check for child collections in the remote object
// that aren't in the local one
if (diff[1].childCollections.length) {
// TODO: log
// TODO: add
save = true;
}
// Check for items in the remote object
// that aren't in the local one
if (diff[1].childItems.length) {
var childItems = obj.getChildItems(true);
obj.childItems = childItems.concat(diff[1].childItems);
var msg = _logCollectionItemMerge(obj.name, diff[1].childItems);
// TODO: log rather than alert
alert(msg);
save = true;
}
if (save) {
obj.save();
}
continue;
}
}
else {
var changed = _mergeCollection(obj, remoteObj);
if (!changed) {
syncSession.removeFromUpdated(type, obj.id);
continue;
}
break;
continue;
case 'tag':
var diff = obj.diff(remoteObj, false, true);
if (!diff) {
var changed = _mergeTag(obj, remoteObj);
if (!changed) {
syncSession.removeFromUpdated(type, obj.id);
continue;
}
break;
continue;
}
if (!reconcile) {
@ -1775,13 +1699,6 @@ Zotero.Sync.Server.Data = new function() {
syncSession.uploadIDs.updated[types][index] = newID;
}
// Update id in local deletions array
for (var i in syncSession.uploadIDs.deleted[types]) {
if (syncSession.uploadIDs.deleted[types][i].id == oldID) {
syncSession.uploadIDs.deleted[types][i] = newID;
}
}
// Add items linked to creators to updated array,
// since their timestamps will be set to the
// transaction timestamp
@ -1808,21 +1725,35 @@ Zotero.Sync.Server.Data = new function() {
else {
isNewObject = true;
Zotero.debug(syncSession.uploadIDs.deleted);
// Check if object has been deleted locally
for each(var pair in syncSession.uploadIDs.deleted[types]) {
if (pair.id != parseInt(xmlNode.@id) ||
pair.key != xmlNode.@key.toString()) {
for each(var key in syncSession.uploadIDs.deleted[types]) {
if (key != xmlNode.@key.toString()) {
continue;
}
// TODO: non-merged items
if (type != 'item') {
alert('Delete reconciliation unimplemented for ' + types);
throw ('Delete reconciliation unimplemented for ' + types);
switch (type) {
case 'item':
localDelete = true;
break;
// Auto-restore locally deleted tags that have
// changed remotely
case 'tag':
syncSession.removeFromDeleted(type, key);
var msg = _generateAutoChangeMessage(
type, null, xmlNode.@name.toString()
);
alert(msg);
continue;
default:
alert('Delete reconciliation unimplemented for ' + types);
throw ('Delete reconciliation unimplemented for ' + types);
}
localDelete = true;
}
// If key already exists on a different item, change local key
@ -1858,11 +1789,17 @@ Zotero.Sync.Server.Data = new function() {
? parseInt(xmlNode.@type) : 0;
var linkedItems = _deleteConflictingTag(syncSession, tagName, tagType);
if (linkedItems) {
obj.dateModified = Zotero.DB.transactionDateTime;
var mod = false;
for each(var id in linkedItems) {
obj.addItem(id);
var added = obj.addItem(id);
if (added) {
mod = true;
}
}
if (mod) {
obj.dateModified = Zotero.DB.transactionDateTime;
syncSession.addToUpdated('tag', parseInt(xmlNode.@id));
}
syncSession.addToUpdated('tag', parseInt(xmlNode.@id));
}
}
@ -1889,11 +1826,7 @@ Zotero.Sync.Server.Data = new function() {
if (obj.isRegularItem()) {
var creators = obj.getCreators();
for each(var creator in creators) {
syncSession.removeFromDeleted(
'creator',
creator.ref.id,
creator.ref.key
);
syncSession.removeFromDeleted('creator', creator.ref.key);
}
}
else if (obj.isAttachment() &&
@ -1925,10 +1858,14 @@ Zotero.Sync.Server.Data = new function() {
Zotero.debug("Processing remotely deleted " + types);
for each(var xmlNode in xml.deleted[types][type]) {
var id = parseInt(xmlNode.@id);
var obj = Zotero[Types].get(id);
var key = xmlNode.@key.toString();
var obj = Zotero[Types].getByKey(key);
// Object can't be found
if (!obj || obj.key != xmlNode.@key) {
if (!obj) {
// Since it's already deleted remotely, don't include
// the object in the deleted array if something else
// caused its deletion during the sync
syncSession.removeFromDeleted(type, xmlNode.@key.toString());
continue;
}
@ -1940,7 +1877,7 @@ Zotero.Sync.Server.Data = new function() {
}
// Local object hasn't been modified -- delete
else {
toDelete.push(id);
toDelete.push(obj.id);
}
}
}
@ -1995,7 +1932,20 @@ Zotero.Sync.Server.Data = new function() {
}
}
for each(var obj in toSave) {
obj.save();
// Use a special saving mode for tags to avoid an issue that
// occurs if a tag has changed names remotely but another tag
// conflicts with the local version after the first tag has been
// updated in memory, causing a deletion of the local tag.
// Using the normal save mode, when the first remote tag then
// goes to save, the linked items aren't saved, since as far
// as the in-memory object is concerned, they haven't changed,
// even though they've been deleted from the DB.
//
// To replicate, add an item, add a tag, sync both sides,
// rename the tag, add a new one with the old name, and sync.
var full = type == 'tag';
obj.save(full);
}
// Add back related items (which now exist)
@ -2012,7 +1962,7 @@ Zotero.Sync.Server.Data = new function() {
// Add back subcollections
else if (type == 'collection') {
for each(var collection in collections) {
if (collection.childCollections) {
if (collection.childCollections.length) {
collection.obj.childCollections = collection.childCollections;
collection.obj.save();
}
@ -2042,8 +1992,8 @@ Zotero.Sync.Server.Data = new function() {
// collections so that any deleted items within them don't
// update them, which would trigger erroneous conflicts
var collections = [];
for each(var colID in deletedCollections) {
var col = Zotero.Collections.get(colID);
for each(var colKey in deletedCollectionKeys) {
var col = Zotero.Collections.getByKey(colKey);
col.lockDateModified();
collections.push(col);
}
@ -2154,10 +2104,9 @@ Zotero.Sync.Server.Data = new function() {
Zotero.debug('Processing locally deleted ' + types);
for each(var obj in ids.deleted[types]) {
for each(var key in ids.deleted[types]) {
var deletexml = new XML('<' + type + '/>');
deletexml.@id = obj.id;
deletexml.@key = obj.key;
deletexml.@key = key;
xml.deleted[types][type] += deletexml;
}
}
@ -2171,6 +2120,215 @@ Zotero.Sync.Server.Data = new function() {
}
function _mergeCollection(localObj, remoteObj) {
var diff = localObj.diff(remoteObj, false, true);
if (!diff) {
return false;
}
Zotero.debug("COLLECTION HAS CHANGED");
Zotero.debug(diff);
// Local is newer
if (diff[0].primary.dateModified >
diff[1].primary.dateModified) {
var remoteIsTarget = false;
var targetObj = localObj;
var targetDiff = diff[0];
var otherDiff = diff[1];
}
// Remote is newer
else {
var remoteIsTarget = true;
var targetObj = remoteObj;
var targetDiff = diff[1];
var otherDiff = diff[0];
}
if (targetDiff.fields.name) {
var msg = _generateAutoChangeMessage(
'collection', diff[0].fields.name, diff[1].fields.name, remoteIsTarget
);
// TODO: log rather than alert
alert(msg);
}
// Check for child collections in the other object
// that aren't in the target one
if (otherDiff.childCollections.length) {
// TODO: log
// TODO: add
throw ("Collection hierarchy conflict resolution is unimplemented");
}
// Add items in other object to target one
if (otherDiff.childItems.length) {
var childItems = targetObj.getChildItems(true);
targetObj.childItems = childItems.concat(otherDiff.childItems);
var msg = _generateCollectionItemMergeMessage(
targetObj.name,
otherDiff.childItems,
remoteIsTarget
);
// TODO: log rather than alert
alert(msg);
}
targetObj.save();
return true;
}
function _mergeTag(localObj, remoteObj) {
var diff = localObj.diff(remoteObj, false, true);
if (!diff) {
return false;
}
Zotero.debug("TAG HAS CHANGED");
Zotero.debug(diff);
// Local is newer
if (diff[0].primary.dateModified >
diff[1].primary.dateModified) {
var remoteIsTarget = false;
var targetObj = localObj;
var targetDiff = diff[0];
var otherDiff = diff[1];
}
// Remote is newer
else {
var remoteIsTarget = true;
var targetObj = remoteObj;
var targetDiff = diff[1];
var otherDiff = diff[0];
}
// TODO: log old name
if (targetDiff.fields.name) {
var msg = _generateAutoChangeMessage(
'tag', diff[0].fields.name, diff[1].fields.name, remoteIsTarget
);
alert(msg);
}
// Add linked items in the other object to the target one
if (otherDiff.linkedItems.length) {
// need to handle changed items
var linkedItems = targetObj.getLinkedItems(true);
targetObj.linkedItems = linkedItems.concat(otherDiff.linkedItems);
var msg = _generateTagItemMergeMessage(
targetObj.name,
otherDiff.linkedItems,
remoteIsTarget
);
// TODO: log rather than alert
alert(msg);
}
targetObj.save();
return true;
}
/**
* @param {String} itemType
* @param {String} localName
* @param {String} remoteName
* @param {Boolean} [remoteMoreRecent=false]
*/
function _generateAutoChangeMessage(itemType, localName, remoteName, remoteMoreRecent) {
if (localName === null) {
// TODO: localize
localName = "[deleted]";
var localDelete = true;
}
// TODO: localize
var msg = "A " + itemType + " has changed both locally and "
+ "remotely since the last sync:";
msg += "\n\n";
msg += "Local version: " + localName + "\n";
msg += "Remote version: " + remoteName + "\n";
msg += "\n";
if (localDelete) {
msg += "The remote version has been kept.";
}
else {
var moreRecent = remoteMoreRecent ? remoteName : localName;
msg += "The most recent version, '" + moreRecent + "', has been kept.";
}
return msg;
}
/**
* @param {String} collectionName
* @param {Integer[]} addedItemIDs
* @param {Boolean} remoteIsTarget
*/
function _generateCollectionItemMergeMessage(collectionName, addedItemIDs, remoteIsTarget) {
// TODO: localize
var introMsg = "Items in the collection '" + collectionName + "' have been "
+ "added and/or removed in multiple locations."
introMsg += " ";
if (remoteIsTarget) {
introMsg += "The following items have been added to the remote collection:";
}
else {
introMsg += "The following items have been added to the local collection:";
}
var itemText = [];
for each(var id in addedItemIDs) {
var item = Zotero.Items.get(id);
var title = item.getField('title');
var text = " - " + title;
var firstCreator = item.getField('firstCreator');
if (firstCreator) {
text += " (" + firstCreator + ")";
}
itemText.push(text);
}
return introMsg + "\n\n" + itemText.join("\n");
}
/**
* @param {String} tagName
* @param {Integer[]} addedItemIDs
* @param {Boolean} remoteIsTarget
*/
function _generateTagItemMergeMessage(tagName, addedItemIDs, remoteIsTarget) {
// TODO: localize
var introMsg = "The tag '" + tagName + "' has been "
+ "added to and/or removed from items in multiple locations."
introMsg += " ";
if (remoteIsTarget) {
introMsg += "It has been added to the following remote items:";
}
else {
introMsg += "It has been added to the following local items:";
}
var itemText = [];
for each(var id in addedItemIDs) {
var item = Zotero.Items.get(id);
var title = item.getField('title');
var text = " - " + title;
var firstCreator = item.getField('firstCreator');
if (firstCreator) {
text += " (" + firstCreator + ")";
}
itemText.push(text);
}
return introMsg + "\n\n" + itemText.join("\n");
}
/**
* Open a conflict resolution window and return the results
*
@ -2223,7 +2381,7 @@ Zotero.Sync.Server.Data = new function() {
delete relatedItems[obj.id];
}
syncSession.addToDeleted(type, obj.id, obj.left.key);
syncSession.addToDeleted(type, obj.left.key);
}
continue;
}
@ -2236,7 +2394,7 @@ Zotero.Sync.Server.Data = new function() {
// Item had been deleted locally, so remove from
// deleted array
if (obj.left == 'deleted') {
syncSession.removeFromDeleted(type, obj.id, obj.ref.key);
syncSession.removeFromDeleted(type, obj.ref.key);
}
// TODO: only upload if the local item was chosen
@ -2583,26 +2741,6 @@ Zotero.Sync.Server.Data = new function() {
}
function _logCollectionItemMerge(collectionName, remoteItemIDs) {
// TODO: localize
var introMsg = "Items in the collection '" + collectionName + "' have been "
+ "added and/or removed in multiple locations. The following remote "
+ "items have been added to the local collection:";
var itemText = [];
for each(var id in remoteItemIDs) {
var item = Zotero.Items.get(id);
var title = item.getField('title');
var text = " - " + title;
var firstCreator = item.getField('firstCreator');
if (firstCreator) {
text += " (" + firstCreator + ")";
}
itemText.push(text);
}
return introMsg + "\n\n" + itemText.join("\n");
}
/**
* Converts a Zotero.Creator object to an E4X <creator> object
*/
@ -2871,16 +3009,16 @@ Zotero.Sync.Server.Data = new function() {
function _deleteConflictingTag(syncSession, name, type) {
var tagID = Zotero.Tags.getID(name, type);
if (tagID) {
Zotero.debug("Deleting conflicting local tag " + tagID);
var tag = Zotero.Tags.get(tagID);
var linkedItems = tag.getLinkedItems(true);
Zotero.Tags.erase(tagID);
// DEBUG: should purge() be called by Tags.erase()
Zotero.Tags.purge();
syncSession.removeFromUpdated('tag', tagID);
syncSession.addToDeleted('tag', tagID, tag.key);
//syncSession.addToDeleted('tag', tag.key);
return linkedItems;
return linkedItems ? linkedItems : [];
}
return false;

View file

@ -292,27 +292,33 @@ Zotero.Utilities.prototype.isInt = function(x) {
/**
* Compares an array with another (comparator) and returns an array with
* the values from comparator that don't exist in vector
* Compares an array with another and returns an array with
* the values from array2 that don't exist in array1
*
* Code by Carlos R. L. Rodrigues
* From http://jsfromhell.com/array/diff [rev. #1]
*
* @param {Array} v Array that will be checked
* @param {Array} c Array that will be compared
* @param {Boolean} useIndex If true, returns an array containing just
* @param {Array} array1 Array that will be checked
* @param {Array} array2 Array that will be compared
* @param {Boolean} useIndex If true, return an array containing just
* the index of the comparator's elements;
* otherwise returns the values
* otherwise return the values
*/
Zotero.Utilities.prototype.arrayDiff = function(v, c, m) {
var d = [], e = -1, h, i, j, k;
for(i = c.length, k = v.length; i--;){
for(j = k; j && (h = c[i] !== v[--j]););
h && (d[++e] = m ? i : c[i]);
}
return d;
};
Zotero.Utilities.prototype.arrayDiff = function(array1, array2, useIndex) {
if (array1.constructor.name != 'Array') {
throw ("array1 is not an array in Zotero.Utilities.arrayDiff() (" + array1 + ")");
}
if (array2.constructor.name != 'Array') {
throw ("array2 is not an array in Zotero.Utilities.arrayDiff() (" + array2 + ")");
}
var val, pos, vals = [];
for (var i=0; i<array2.length; i++) {
val = array2[i];
pos = array1.indexOf(val);
if (pos == -1) {
vals.push(useIndex ? pos : val);
}
}
return vals;
}
/**

View file

@ -1,4 +1,4 @@
-- 46
-- 47
-- This file creates tables containing user-specific data -- any changes made
-- here must be mirrored in transition steps in schema.js::_migrateSchema()
@ -198,7 +198,6 @@ CREATE INDEX fulltextItemWords_itemID ON fulltextItemWords(itemID);
CREATE TABLE syncDeleteLog (
syncObjectTypeID INT NOT NULL,
objectID INT NOT NULL,
key TEXT NOT NULL UNIQUE,
timestamp INT NOT NULL,
FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID)